UNPKG

@carbon/react

Version:

React components for the Carbon Design System

775 lines (773 loc) 28.7 kB
/** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import { usePrefix } from "../../internal/usePrefix.js"; import { Text } from "../Text/Text.js"; import { ArrowDown, ArrowLeft, ArrowRight as ArrowRight$1, ArrowUp as ArrowUp$1, Delete, End, Home } from "../../internal/keyboard/keys.js"; import { match, matches } from "../../internal/keyboard/match.js"; import useIsomorphicEffect from "../../internal/useIsomorphicEffect.js"; import { useId } from "../../internal/useId.js"; import { deprecate } from "../../prop-types/deprecate.js"; import { isComponentElement } from "../../internal/utils.js"; import { useMergedRefs } from "../../internal/useMergedRefs.js"; import { useEvent } from "../../internal/useEvent.js"; import { Tooltip } from "../Tooltip/Tooltip.js"; import BadgeIndicator from "../BadgeIndicator/index.js"; import { useControllableState } from "../../internal/useControllableState.js"; import { GridAsGridComponent } from "../Grid/Grid.js"; import { useMatchMedia } from "../../internal/useMatchMedia.js"; import { usePressable } from "./usePressable.js"; import classNames from "classnames"; import React, { Children, cloneElement, createContext, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import PropTypes from "prop-types"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { ChevronLeft, ChevronRight, Close } from "@carbon/icons-react"; import { debounce } from "es-toolkit/compat"; import { breakpoints } from "@carbon/layout"; //#region src/components/Tabs/Tabs.tsx /** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const buttonWidth = 44; const verticalTabHeight = 64; const TabsContext = React.createContext({ baseId: "", activeIndex: 0, defaultSelectedIndex: 0, dismissable: false, onTabCloseRequest() {}, setActiveIndex() {}, selectedIndex: 0, setSelectedIndex() {} }); const TabContext = React.createContext({ index: 0, hasSecondaryLabel: false }); const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`; const smMediaQuery = `(max-width: ${breakpoints.md.width})`; const TabPanelContext = React.createContext(0); function Tabs({ children, defaultSelectedIndex = 0, onChange, selectedIndex: controlledSelectedIndex, dismissable, onTabCloseRequest }) { const baseId = useId("ccs"); if (dismissable && !onTabCloseRequest) console.error("dismissable property specified without also providing an onTabCloseRequest property."); const [activeIndex, setActiveIndex] = useState(defaultSelectedIndex); const [selectedIndex, setSelectedIndex] = useControllableState({ value: controlledSelectedIndex, defaultValue: defaultSelectedIndex, onChange: (value) => onChange?.({ selectedIndex: value }) }); const value = { baseId, activeIndex, defaultSelectedIndex, dismissable, onTabCloseRequest, setActiveIndex, selectedIndex, setSelectedIndex }; return /* @__PURE__ */ jsx(TabsContext.Provider, { value, children }); } Tabs.propTypes = { children: PropTypes.node, defaultSelectedIndex: PropTypes.number, dismissable: PropTypes.bool, onChange: PropTypes.func, onTabCloseRequest: (props) => { if (props.dismissable && !props.onTabCloseRequest) return /* @__PURE__ */ new Error("dismissable property specified without also providing an onTabCloseRequest property."); }, selectedIndex: PropTypes.number }; function TabsVertical({ children, height, defaultSelectedIndex = 0, onChange, selectedIndex: controlledSelectedIndex, ...rest }) { const [selectedIndex, setSelectedIndex] = useControllableState({ value: controlledSelectedIndex, defaultValue: defaultSelectedIndex, onChange: (value) => onChange?.({ selectedIndex: value }) }); const props = { ...rest, selectedIndex, onChange: ({ selectedIndex }) => setSelectedIndex(selectedIndex) }; if (!useMatchMedia(smMediaQuery)) return /* @__PURE__ */ jsx(GridAsGridComponent, { style: { height }, children: /* @__PURE__ */ jsx(Tabs, { ...props, children }) }); return /* @__PURE__ */ jsx(Tabs, { ...props, children }); } TabsVertical.propTypes = { children: PropTypes.node, defaultSelectedIndex: PropTypes.number, height: PropTypes.string, onChange: PropTypes.func, selectedIndex: PropTypes.number }; /** * Get the next index for a given keyboard event * given a count of the total items and the current index */ function getNextIndex(event, total, index) { switch (true) { case match(event, ArrowRight$1): return (index + 1) % total; case match(event, ArrowLeft): return (total + index - 1) % total; case match(event, Home): return 0; case match(event, End): return total - 1; default: return index; } } /** * Get the next index for a given keyboard event * given a count of the total items and the current index */ function getNextIndexVertical(event, total, index) { switch (true) { case match(event, ArrowDown): return (index + 1) % total; case match(event, ArrowUp$1): return (total + index - 1) % total; case match(event, Home): return 0; case match(event, End): return total - 1; default: return index; } } function TabList({ activation = "automatic", "aria-label": label, children, className: customClassName, contained = false, fullWidth = false, iconSize, leftOverflowButtonProps, light, rightOverflowButtonProps, scrollDebounceWait = 200, scrollIntoView, ...rest }) { const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex, dismissable } = React.useContext(TabsContext); const prefix = usePrefix(); const ref = useRef(null); const previousButton = useRef(null); const nextButton = useRef(null); const [isScrollable, setIsScrollable] = useState(false); const [scrollLeft, setScrollLeft] = useState(0); const hasSecondaryLabelTabs = contained && Children.toArray(children).some((child) => isComponentElement(child, Tab) && typeof child.props.secondaryLabel !== "undefined"); const isLg = useMatchMedia(lgMediaQuery); const distributeWidth = fullWidth && contained && isLg && React.Children.toArray(children).length < 9; const className = classNames(`${prefix}--tabs`, { [`${prefix}--tabs--contained`]: contained, [`${prefix}--tabs--light`]: light, [`${prefix}--tabs__icon--default`]: iconSize === "default", [`${prefix}--tabs__icon--lg`]: iconSize === "lg", [`${prefix}--layout--size-lg`]: iconSize === "lg", [`${prefix}--tabs--tall`]: hasSecondaryLabelTabs, [`${prefix}--tabs--full-width`]: distributeWidth, [`${prefix}--tabs--dismissable`]: dismissable }, customClassName); const [isNextButtonVisible, setIsNextButtonVisible] = useState(ref.current ? scrollLeft + buttonWidth + ref.current.clientWidth < ref.current.scrollWidth : false); const isPreviousButtonVisible = ref.current ? isScrollable && scrollLeft > 0 : false; const previousButtonClasses = classNames(`${prefix}--tab--overflow-nav-button`, `${prefix}--tab--overflow-nav-button--previous`, { [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible }); const nextButtonClasses = classNames(`${prefix}--tab--overflow-nav-button`, `${prefix}--tab--overflow-nav-button--next`, { [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible }); const tabs = useRef([]); const debouncedOnScroll = useCallback(() => { debounce(() => { if (ref.current) setScrollLeft(ref.current.scrollLeft); }, scrollDebounceWait)(); }, [scrollDebounceWait]); function onKeyDown(event) { if (matches(event, [ ArrowRight$1, ArrowLeft, Home, End ])) { event.preventDefault(); const activeTabs = tabs.current.filter((tab) => tab !== null).filter((tab) => !tab.disabled); const currentIndex = activeTabs.indexOf(tabs.current[activation === "automatic" ? selectedIndex : activeIndex]); const nextIndex = tabs.current.indexOf(activeTabs[getNextIndex(event, activeTabs.length, currentIndex)]); if (activation === "automatic") setSelectedIndex(nextIndex); else if (activation === "manual") setActiveIndex(nextIndex); tabs.current[nextIndex]?.focus(); } } function handleBlur({ relatedTarget: currentActiveNode }) { if (ref.current?.contains(currentActiveNode)) return; if (activation === "manual") setActiveIndex(selectedIndex); } /** * Scroll the tab into view if it is not already visible * @param tab - The tab to scroll into view * @returns {void} */ function scrollTabIntoView(tab) { if (!isScrollable || !ref.current) return; if (tab) { const { width: tabWidth } = tab.getBoundingClientRect(); const start = tab.offsetLeft; const end = tab.offsetLeft + tabWidth; const visibleStart = ref.current.scrollLeft + buttonWidth; const visibleEnd = ref.current.scrollLeft + ref.current.clientWidth - buttonWidth; if (start < visibleStart) setScrollLeft(start - buttonWidth); if (end > visibleEnd) setScrollLeft(end + buttonWidth - ref.current.clientWidth); } } useEffect(() => { const tab = tabs.current[selectedIndex]; if (scrollIntoView && tab) tab.scrollIntoView({ block: "nearest", inline: "nearest" }); }, []); useEffect(() => { setIsNextButtonVisible(ref.current ? scrollLeft + buttonWidth + ref.current.clientWidth + 1 < ref.current.scrollWidth : false); if (dismissable && ref.current) setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth + 1); }, [ children, dismissable, scrollLeft ]); useEffect(() => { if (tabs.current[selectedIndex]?.disabled) { const activeTabs = tabs.current.filter((tab) => { return !tab.disabled; }); if (activeTabs.length > 0) { const tab = activeTabs[0]; setSelectedIndex(tabs.current.indexOf(tab)); } } }, []); useIsomorphicEffect(() => { if (ref.current) setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth + 1); function handler() { if (ref.current) setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth + 1); } const debouncedHandler = debounce(handler, 200); window.addEventListener("resize", debouncedHandler); return () => { debouncedHandler.cancel(); window.removeEventListener("resize", debouncedHandler); }; }, []); useIsomorphicEffect(() => { if (scrollLeft !== null && ref.current) ref.current.scrollLeft = scrollLeft; }, [scrollLeft]); useIsomorphicEffect(() => { scrollTabIntoView(activation === "manual" ? tabs.current[activeIndex] : tabs.current[selectedIndex]); }, [activation, activeIndex]); useIsomorphicEffect(() => { const tab = tabs.current[selectedIndex]; scrollTabIntoView(tab); }, [ selectedIndex, isScrollable, children ]); usePressable(previousButton, { onPress({ longPress }) { if (!longPress && ref.current) setScrollLeft(Math.max(scrollLeft - ref.current.scrollWidth / tabs.current.length * 1.5, 0)); }, onLongPress() { return createLongPressBehavior(ref, "backward", setScrollLeft); } }); usePressable(nextButton, { onPress({ longPress }) { if (!longPress && ref.current) setScrollLeft(Math.min(scrollLeft + ref.current.scrollWidth / tabs.current.length * 1.5, ref.current.scrollWidth - ref.current.clientWidth)); }, onLongPress() { return createLongPressBehavior(ref, "forward", setScrollLeft); } }); return /* @__PURE__ */ jsxs("div", { className, children: [ /* @__PURE__ */ jsx("button", { "aria-hidden": "true", tabIndex: -1, "aria-label": "Scroll left", ref: previousButton, className: previousButtonClasses, type: "button", ...leftOverflowButtonProps, children: /* @__PURE__ */ jsx(ChevronLeft, {}) }), /* @__PURE__ */ jsx("div", { ...rest, "aria-label": label, ref, role: "tablist", className: `${prefix}--tab--list`, onScroll: debouncedOnScroll, onKeyDown, onBlur: handleBlur, children: Children.map(children, (child, index) => { return !isValidElement(child) ? null : /* @__PURE__ */ jsx(TabContext.Provider, { value: { index, hasSecondaryLabel: hasSecondaryLabelTabs, contained }, children: cloneElement(child, { ref: (node) => { if (!node) return; tabs.current[index] = node; } }) }); }) }), /* @__PURE__ */ jsx("button", { "aria-hidden": "true", tabIndex: -1, "aria-label": "Scroll right", ref: nextButton, className: nextButtonClasses, type: "button", ...rightOverflowButtonProps, children: /* @__PURE__ */ jsx(ChevronRight, {}) }) ] }); } TabList.propTypes = { activation: PropTypes.oneOf(["automatic", "manual"]), "aria-label": PropTypes.string, children: PropTypes.node, className: PropTypes.string, contained: PropTypes.bool, fullWidth: PropTypes.bool, iconSize: PropTypes.oneOf(["default", "lg"]), leftOverflowButtonProps: PropTypes.object, light: deprecate(PropTypes.bool, "The `light` prop for `TabList` has been deprecated in favor of the new `Layer` component. It will be removed in the next major release."), rightOverflowButtonProps: PropTypes.object, scrollDebounceWait: PropTypes.number, scrollIntoView: PropTypes.bool }; function TabListVertical({ activation = "automatic", "aria-label": label, children, className: customClassName, scrollIntoView, ...rest }) { const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex } = React.useContext(TabsContext); const prefix = usePrefix(); const ref = useRef(null); const [isOverflowingBottom, setIsOverflowingBottom] = useState(false); const [isOverflowingTop, setIsOverflowingTop] = useState(false); const isSm = useMatchMedia(smMediaQuery); const className = classNames(`${prefix}--tabs`, `${prefix}--tabs--vertical`, `${prefix}--tabs--contained`, customClassName); const tabs = useRef([]); function onKeyDown(event) { if (matches(event, [ ArrowDown, ArrowUp$1, Home, End ])) { event.preventDefault(); const activeTabs = tabs.current.filter((tab) => tab !== null).filter((tab) => !tab.disabled); const currentIndex = activeTabs.indexOf(tabs.current[activation === "automatic" ? selectedIndex : activeIndex]); const nextIndex = tabs.current.indexOf(activeTabs[getNextIndexVertical(event, activeTabs.length, currentIndex)]); if (activation === "automatic") setSelectedIndex(nextIndex); else if (activation === "manual") setActiveIndex(nextIndex); tabs.current[nextIndex]?.focus(); } } function handleBlur({ relatedTarget: currentActiveNode }) { if (ref.current?.contains(currentActiveNode)) return; if (activation === "manual") setActiveIndex(selectedIndex); } useEffect(() => { if (tabs.current[selectedIndex]?.disabled) { const activeTabs = tabs.current.filter((tab) => { return !tab.disabled; }); if (activeTabs.length > 0) { const tab = activeTabs[0]; setSelectedIndex(tabs.current.indexOf(tab)); } } }, []); useEffect(() => { function handler() { const containerHeight = ref.current?.offsetHeight; const containerTop = ref.current?.getBoundingClientRect().top; const selectedPositionTop = tabs.current[selectedIndex]?.getBoundingClientRect().top; const halfTabHeight = verticalTabHeight / 2; if (containerTop && containerHeight) { if (selectedPositionTop - halfTabHeight < containerTop || selectedPositionTop - containerTop + verticalTabHeight + halfTabHeight > containerHeight) ref.current?.scrollTo({ top: (selectedIndex - 1) * verticalTabHeight, behavior: "smooth" }); } } window.addEventListener("resize", handler); handler(); return () => { window.removeEventListener("resize", handler); }; }, [selectedIndex, scrollIntoView]); useEffect(() => { const element = ref.current; if (!element) return; const handler = () => { const halfTabHeight = verticalTabHeight / 2; setIsOverflowingBottom(element.scrollTop + element.clientHeight + halfTabHeight <= element.scrollHeight); setIsOverflowingTop(element.scrollTop > halfTabHeight); }; const resizeObserver = new ResizeObserver(() => handler()); resizeObserver.observe(element); element.addEventListener("scroll", handler); return () => { resizeObserver.disconnect(); element.removeEventListener("scroll", handler); }; }); if (isSm) return /* @__PURE__ */ jsx(TabList, { ...rest, "aria-label": label, contained: true, children }); return /* @__PURE__ */ jsxs("div", { className, children: [ isOverflowingTop && /* @__PURE__ */ jsx("div", { className: `${prefix}--tab--list-gradient_top` }), /* @__PURE__ */ jsx("div", { ...rest, "aria-label": label, ref, role: "tablist", className: `${prefix}--tab--list`, onKeyDown, onBlur: handleBlur, children: Children.map(children, (child, index) => { return !isValidElement(child) ? null : /* @__PURE__ */ jsx(TabContext.Provider, { value: { index, hasSecondaryLabel: false }, children: cloneElement(child, { ref: (node) => { if (!node) return; tabs.current[index] = node; } }) }); }) }), isOverflowingBottom && /* @__PURE__ */ jsx("div", { className: `${prefix}--tab--list-gradient_bottom` }) ] }); } TabListVertical.propTypes = { activation: PropTypes.oneOf(["automatic", "manual"]), "aria-label": PropTypes.string, children: PropTypes.node, className: PropTypes.string }; /** * Helper function to set up the behavior when a button is "long pressed". * This function will take a ref to the tablist, a direction, and a setter * for scrollLeft and will update the scroll position within a requestAnimationFrame. * * It returns a cleanup function to be run * when the long press is deactivated */ function createLongPressBehavior(ref, direction, setScrollLeft) { const node = ref.current; if (!node) return () => {}; const defaultScrollBehavior = node?.style["scroll-behavior"]; node.style["scroll-behavior"] = "auto"; const scrollDelta = direction === "forward" ? 5 : -5; let frameId = null; function tick() { if (!node) return; node.scrollLeft = node.scrollLeft + scrollDelta; frameId = requestAnimationFrame(tick); } frameId = requestAnimationFrame(tick); return () => { node.style["scroll-behavior"] = defaultScrollBehavior; setScrollLeft(node.scrollLeft); if (frameId) cancelAnimationFrame(frameId); }; } const Tab = forwardRef(({ as = "button", children, className: customClassName, disabled, onClick, onKeyDown, secondaryLabel, renderIcon: Icon, ...rest }, forwardRef) => { const prefix = usePrefix(); const { selectedIndex, setSelectedIndex, baseId, dismissable, onTabCloseRequest } = React.useContext(TabsContext); const { index, hasSecondaryLabel, contained } = React.useContext(TabContext); const { badgeIndicator } = React.useContext(IconTabContext) || {}; const dismissIconRef = useRef(null); const tabRef = useRef(null); const ref = useMergedRefs([forwardRef, tabRef]); const [ignoreHover, setIgnoreHover] = useState(false); const id = `${baseId}-tab-${index}`; const panelId = `${baseId}-tabpanel-${index}`; const [isEllipsisApplied, setIsEllipsisApplied] = useState(false); const isEllipsisActive = (element) => { setIsEllipsisApplied(element.offsetHeight < element.scrollHeight); return element.offsetHeight < element.scrollHeight; }; const className = classNames(`${prefix}--tabs__nav-item`, `${prefix}--tabs__nav-link`, { [`${prefix}--tabs__nav-item--selected`]: selectedIndex === index, [`${prefix}--tabs__nav-item--disabled`]: disabled, [`${prefix}--tabs__nav-item--hover-off`]: ignoreHover }, customClassName); const BaseComponent = as; const onDismissIconMouseEnter = (evt) => { if (contained && tabRef.current) { evt.stopPropagation(); setIgnoreHover(true); tabRef.current.classList.add(`${prefix}--tabs__nav-item--hover-off`); } }; const onDismissIconMouseLeave = () => { if (contained && tabRef.current) { tabRef.current.classList.remove(`${prefix}--tabs__nav-item--hover-off`); setIgnoreHover(false); } }; useEvent(dismissIconRef, "mouseover", onDismissIconMouseEnter); useEvent(dismissIconRef, "mouseleave", onDismissIconMouseLeave); useIsomorphicEffect(() => { function handler() { const elementTabId = document.getElementById(`${id}`) || tabRef.current; if (elementTabId?.closest(`.${prefix}--tabs--vertical`)) { const newElement = elementTabId?.getElementsByClassName(`${prefix}--tabs__nav-item-label`)[0]; isEllipsisActive(newElement); } } handler(); window.addEventListener("resize", handler); return () => { window.removeEventListener("resize", handler); }; }, [prefix, id]); const handleClose = (evt) => { evt.stopPropagation(); onTabCloseRequest?.(index); if (tabRef.current && tabRef.current.parentElement) { const tabCount = Array.from(tabRef.current.parentElement.childNodes).filter((node) => { if (!(node instanceof HTMLElement)) return false; return node.classList.contains(`${prefix}--tabs__nav-link`) && !node.classList.contains(`${prefix}--tabs__nav-item--disabled`); }).length; if (tabRef.current && index + 1 !== tabCount) tabRef.current.focus(); else { const prevTabIndex = (tabCount - 2) * 2; const previousTab = tabRef.current.parentElement.childNodes[prevTabIndex]; if (previousTab instanceof HTMLElement) previousTab.focus(); } } }; const handleKeyDown = (event) => { if (dismissable && match(event, Delete)) handleClose(event); onKeyDown?.(event); }; const DismissIcon = /* @__PURE__ */ jsx("div", { className: classNames({ [`${prefix}--tabs__nav-item--close`]: dismissable, [`${prefix}--tabs__nav-item--close--hidden`]: !dismissable }), children: /* @__PURE__ */ jsx("button", { type: "button", tabIndex: -1, "aria-disabled": disabled, "aria-hidden": selectedIndex === index && dismissable ? "false" : "true", disabled, className: classNames({ [`${prefix}--tabs__nav-item--close-icon`]: dismissable, [`${prefix}--visually-hidden`]: !dismissable, [`${prefix}--tabs__nav-item--close-icon--selected`]: selectedIndex === index, [`${prefix}--tabs__nav-item--close-icon--disabled`]: disabled }), onClick: handleClose, title: `Remove ${typeof children === "string" ? children : ""} tab`, ref: dismissIconRef, children: /* @__PURE__ */ jsx(Close, { "aria-hidden": selectedIndex === index && dismissable ? "false" : "true", "aria-label": `Press delete to remove ${typeof children === "string" ? children : ""} tab` }) }) }); const hasIcon = Icon ?? dismissable; if (isEllipsisApplied) return /* @__PURE__ */ jsx(Tooltip, { label: children, align: "top", leaveDelayMs: 0, autoAlign: true, onMouseEnter: () => false, closeOnActivation: true, children: /* @__PURE__ */ jsxs(BaseComponent, { ...rest, "aria-controls": panelId, "aria-disabled": disabled, "aria-selected": selectedIndex === index, ref, id, role: "tab", className, disabled, title: children, onClick: (evt) => { if (disabled) return; setSelectedIndex(index); onClick?.(evt); }, onKeyDown: handleKeyDown, tabIndex: selectedIndex === index ? "0" : "-1", type: "button", children: [/* @__PURE__ */ jsx("div", { className: `${prefix}--tabs__nav-item-label-wrapper`, children: /* @__PURE__ */ jsx(Text, { className: `${prefix}--tabs__nav-item-label`, children }) }), hasSecondaryLabel && secondaryLabel && /* @__PURE__ */ jsx(Text, { as: "div", className: `${prefix}--tabs__nav-item-secondary-label`, title: secondaryLabel, children: secondaryLabel })] }) }); return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(BaseComponent, { ...rest, "aria-controls": panelId, "aria-disabled": disabled, "aria-selected": selectedIndex === index, ref, id, role: "tab", className, disabled, onClick: (evt) => { if (disabled) return; setSelectedIndex(index); onClick?.(evt); }, onKeyDown: handleKeyDown, tabIndex: selectedIndex === index ? "0" : "-1", type: "button", children: [ /* @__PURE__ */ jsxs("div", { className: `${prefix}--tabs__nav-item-label-wrapper`, children: [ dismissable && Icon && /* @__PURE__ */ jsx("div", { className: `${prefix}--tabs__nav-item--icon-left`, children: /* @__PURE__ */ jsx(Icon, { size: 16 }) }), /* @__PURE__ */ jsx(Text, { className: `${prefix}--tabs__nav-item-label`, children }), !dismissable && Icon && /* @__PURE__ */ jsx("div", { className: classNames(`${prefix}--tabs__nav-item--icon`, { [`${prefix}--visually-hidden`]: !hasIcon }), children: !dismissable && Icon && /* @__PURE__ */ jsx(Icon, { size: 16 }) }) ] }), hasSecondaryLabel && secondaryLabel && /* @__PURE__ */ jsx(Text, { as: "div", className: `${prefix}--tabs__nav-item-secondary-label`, title: secondaryLabel, children: secondaryLabel }), !disabled && badgeIndicator && /* @__PURE__ */ jsx(BadgeIndicator, {}) ] }), DismissIcon] }); }); Tab.propTypes = { as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), children: PropTypes.node, className: PropTypes.string, disabled: PropTypes.bool, onClick: PropTypes.func, onKeyDown: PropTypes.func, renderButton: PropTypes.func, renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), secondaryLabel: PropTypes.string }; /** * IconTab */ const IconTabContext = createContext(false); const IconTab = React.forwardRef(({ badgeIndicator, children, className: customClassName, defaultOpen = false, enterDelayMs, leaveDelayMs, label, ...rest }, ref) => { const prefix = usePrefix(); const value = useMemo(() => ({ badgeIndicator }), [badgeIndicator]); const hasSize20 = isValidElement(children) && (children.props.size === 20 || children.props.size === "20"); const classNames$1 = classNames(`${prefix}--tabs__nav-item--icon-only`, customClassName, { [`${prefix}--tabs__nav-item--icon-only__20`]: hasSize20 }); return /* @__PURE__ */ jsx(IconTabContext.Provider, { value, children: /* @__PURE__ */ jsx(Tooltip, { align: "bottom", defaultOpen, className: `${prefix}--icon-tooltip`, enterDelayMs, label, leaveDelayMs, children: /* @__PURE__ */ jsx(Tab, { className: classNames$1, ref, ...rest, children }) }) }); }); IconTab.propTypes = { badgeIndicator: PropTypes.bool, children: PropTypes.node, className: PropTypes.string, defaultOpen: PropTypes.bool, enterDelayMs: PropTypes.number, label: PropTypes.node.isRequired, leaveDelayMs: PropTypes.number }; const TabPanel = React.forwardRef(({ children, className: customClassName, ...rest }, forwardRef) => { const prefix = usePrefix(); const { selectedIndex, baseId } = React.useContext(TabsContext); const index = React.useContext(TabPanelContext); const id = `${baseId}-tabpanel-${index}`; const tabId = `${baseId}-tab-${index}`; const className = classNames(`${prefix}--tab-content`, customClassName); return /* @__PURE__ */ jsx("div", { ...rest, "aria-labelledby": tabId, id, className, ref: forwardRef, role: "tabpanel", hidden: selectedIndex !== index, children }); }); TabPanel.propTypes = { children: PropTypes.node, className: PropTypes.string }; function TabPanels({ children }) { const prefix = usePrefix(); const refs = useRef([]); const hiddenStates = useRef([]); useIsomorphicEffect(() => { const tabContainer = refs.current[0]?.previousElementSibling; const isVertical = tabContainer?.classList.contains(`${prefix}--tabs--vertical`); const parentHasHeight = tabContainer?.parentElement?.style.height; if (isVertical && !parentHasHeight) { hiddenStates.current = refs.current.map((ref) => ref?.hidden || false); refs.current.forEach((ref) => { if (ref) ref.hidden = false; }); const heights = refs.current.map((ref) => ref?.offsetHeight || 0); const max = Math.max(...heights); if (tabContainer instanceof HTMLElement) tabContainer.style.height = max + "px"; refs.current.forEach((ref, index) => { if (ref) ref.hidden = hiddenStates.current[index]; }); } }); return /* @__PURE__ */ jsx(Fragment, { children: Children.map(children, (child, index) => { return !isValidElement(child) ? null : /* @__PURE__ */ jsx(TabPanelContext.Provider, { value: index, children: cloneElement(child, { ref: (element) => { refs.current[index] = element; } }) }); }) }); } TabPanels.propTypes = { children: PropTypes.node }; //#endregion export { IconTab, Tab, TabList, TabListVertical, TabPanel, TabPanels, Tabs, TabsVertical };