@carbon/react
Version:
React components for the Carbon Design System
775 lines (773 loc) • 28.7 kB
JavaScript
/**
* 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 };