UNPKG

@carbon/react

Version:

React components for the Carbon Design System

1,245 lines (1,196 loc) 43.8 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var iconsReact = require('@carbon/icons-react'); var layout = require('@carbon/layout'); var cx = require('classnames'); var PropTypes = require('prop-types'); var React = require('react'); require('../Grid/FlexGrid.js'); var Grid = require('../Grid/Grid.js'); require('../Grid/Row.js'); require('../Grid/Column.js'); require('../Grid/ColumnHang.js'); require('../Grid/GridContext.js'); var reactIs = require('react-is'); require('../Tooltip/DefinitionTooltip.js'); var Tooltip = require('../Tooltip/Tooltip.js'); var useControllableState = require('../../internal/useControllableState.js'); var useId = require('../../internal/useId.js'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); var useNoInteractiveChildren = require('../../internal/useNoInteractiveChildren.js'); var usePrefix = require('../../internal/usePrefix.js'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var usePressable = require('./usePressable.js'); var deprecate = require('../../prop-types/deprecate.js'); var useEvent = require('../../internal/useEvent.js'); var useMatchMedia = require('../../internal/useMatchMedia.js'); require('../Text/index.js'); var index = require('../BadgeIndicator/index.js'); var debounce = require('../../node_modules/es-toolkit/dist/compat/function/debounce.mjs.js'); var Text = require('../Text/Text.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var _ChevronLeft, _ChevronRight, _BadgeIndicator; const verticalTabHeight = 64; // Used to manage the overall state of the Tabs const TabsContext = /*#__PURE__*/React__default["default"].createContext({ baseId: '', activeIndex: 0, defaultSelectedIndex: 0, dismissable: false, onTabCloseRequest() {}, setActiveIndex() {}, selectedIndex: 0, setSelectedIndex() {} }); // Used to keep track of position in a tablist const TabContext = /*#__PURE__*/React__default["default"].createContext({ index: 0, hasSecondaryLabel: false }); const lgMediaQuery = `(min-width: ${layout.breakpoints.lg.width})`; const smMediaQuery = `(max-width: ${layout.breakpoints.md.width})`; // Used to keep track of position in a list of tab panels const TabPanelContext = /*#__PURE__*/React__default["default"].createContext(0); /** * Tabs */ function Tabs({ children, defaultSelectedIndex = 0, onChange, selectedIndex: controlledSelectedIndex, dismissable, onTabCloseRequest }) { const baseId = useId.useId('ccs'); if (dismissable && !onTabCloseRequest) { console.error('dismissable property specified without also providing an onTabCloseRequest property.'); } // The active index is used to track the element which has focus in our tablist const [activeIndex, setActiveIndex] = React.useState(defaultSelectedIndex); // The selected index is used for the tab/panel pairing which is "visible" const [selectedIndex, setSelectedIndex] = useControllableState.useControllableState({ value: controlledSelectedIndex, defaultValue: defaultSelectedIndex, onChange: value => onChange?.({ selectedIndex: value }) }); const value = { baseId, activeIndex, defaultSelectedIndex, dismissable, onTabCloseRequest, setActiveIndex, selectedIndex, setSelectedIndex }; return /*#__PURE__*/React__default["default"].createElement(TabsContext.Provider, { value: value }, children); } Tabs.propTypes = { /** * Provide child elements to be rendered inside the `Tabs`. * These elements should render either `TabsList` or `TabsPanels` */ children: PropTypes__default["default"].node, /** * Specify which content tab should be initially selected when the component * is first rendered */ defaultSelectedIndex: PropTypes__default["default"].number, /** * Whether the render Tab children should be dismissable. */ dismissable: PropTypes__default["default"].bool, /** * Provide an optional function which is called whenever the state of the * `Tabs` changes */ onChange: PropTypes__default["default"].func, /** * If specifying the `onTabCloseRequest` prop, provide a callback function * responsible for removing the tab when close button is pressed on one of the Tab elements */ onTabCloseRequest: props => { if (props.dismissable && !props.onTabCloseRequest) { return new Error('dismissable property specified without also providing an onTabCloseRequest property.'); } return undefined; }, /** * Control which content panel is currently selected. This puts the component * in a controlled mode and should be used along with `onChange` */ selectedIndex: PropTypes__default["default"].number }; function TabsVertical({ children, height, defaultSelectedIndex = 0, onChange, selectedIndex: controlledSelectedIndex, ...rest }) { const [selectedIndex, setSelectedIndex] = useControllableState.useControllableState({ value: controlledSelectedIndex, defaultValue: defaultSelectedIndex, onChange: value => onChange?.({ selectedIndex: value }) }); const props = { ...rest, selectedIndex, onChange: ({ selectedIndex }) => setSelectedIndex(selectedIndex) }; const isSm = useMatchMedia.useMatchMedia(smMediaQuery); if (!isSm) { return ( /*#__PURE__*/ // eslint-disable-next-line react/forbid-component-props React__default["default"].createElement(Grid.Grid, { style: { height: height } }, /*#__PURE__*/React__default["default"].createElement(Tabs, props, children)) ); } return /*#__PURE__*/React__default["default"].createElement(Tabs, props, children); } TabsVertical.propTypes = { /** * Provide child elements to be rendered inside the `TabsVertical`. * These elements should render either `TabsListVertical` or `TabsPanels` */ children: PropTypes__default["default"].node, /** * Specify which content tab should be initially selected when the component * is first rendered */ defaultSelectedIndex: PropTypes__default["default"].number, /** * Option to set a height style only if using vertical variation */ height: PropTypes__default["default"].string, /** * Provide an optional function which is called whenever the state of the * `Tabs` changes */ onChange: PropTypes__default["default"].func, /** * Control which content panel is currently selected. This puts the component * in a controlled mode and should be used along with `onChange` */ selectedIndex: PropTypes__default["default"].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.match(event, keys.ArrowRight): return (index + 1) % total; case match.match(event, keys.ArrowLeft): return (total + index - 1) % total; case match.match(event, keys.Home): return 0; case match.match(event, keys.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.match(event, keys.ArrowDown): return (index + 1) % total; case match.match(event, keys.ArrowUp): return (total + index - 1) % total; case match.match(event, keys.Home): return 0; case match.match(event, keys.End): return total - 1; default: return index; } } /** * TabList */ 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__default["default"].useContext(TabsContext); const prefix = usePrefix.usePrefix(); const ref = React.useRef(null); const previousButton = React.useRef(null); const nextButton = React.useRef(null); const [isScrollable, setIsScrollable] = React.useState(false); const [scrollLeft, setScrollLeft] = React.useState(0); let hasSecondaryLabelTabs = false; if (contained) { hasSecondaryLabelTabs = React__default["default"].Children.toArray(children).some(child => { const _child = child; return /*#__PURE__*/React__default["default"].isValidElement(child) && !!_child.props.secondaryLabel; }); } const isLg = useMatchMedia.useMatchMedia(lgMediaQuery); const distributeWidth = fullWidth && contained && isLg && React__default["default"].Children.toArray(children).length < 9; const className = cx__default["default"](`${prefix}--tabs`, { [`${prefix}--tabs--contained`]: contained, [`${prefix}--tabs--light`]: light, [`${prefix}--tabs__icon--default`]: iconSize === 'default', [`${prefix}--tabs__icon--lg`]: iconSize === 'lg', // TODO: V12 - Remove this class [`${prefix}--layout--size-lg`]: iconSize === 'lg', [`${prefix}--tabs--tall`]: hasSecondaryLabelTabs, [`${prefix}--tabs--full-width`]: distributeWidth, [`${prefix}--tabs--dismissable`]: dismissable }, customClassName); // Previous Button // VISIBLE IF: // SCROLLABLE // AND SCROLL_LEFT > 0 const buttonWidth = 44; // Next Button // VISIBLE IF: // SCROLLABLE // AND SCROLL_LEFT + CLIENT_WIDTH < SCROLL_WIDTH const [isNextButtonVisible, setIsNextButtonVisible] = React.useState(ref.current ? scrollLeft + buttonWidth + ref.current.clientWidth < ref.current.scrollWidth : false); const isPreviousButtonVisible = ref.current ? isScrollable && scrollLeft > 0 : false; const previousButtonClasses = cx__default["default"](`${prefix}--tab--overflow-nav-button`, `${prefix}--tab--overflow-nav-button--previous`, { [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible }); const nextButtonClasses = cx__default["default"](`${prefix}--tab--overflow-nav-button`, `${prefix}--tab--overflow-nav-button--next`, { [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible }); const tabs = React.useRef([]); const debouncedOnScroll = React.useCallback(() => { const updateScroll = debounce.debounce(() => { if (ref.current) { setScrollLeft(ref.current.scrollLeft); } }, scrollDebounceWait); updateScroll(); }, [scrollDebounceWait]); function onKeyDown(event) { if (match.matches(event, [keys.ArrowRight, keys.ArrowLeft, keys.Home, keys.End])) { event.preventDefault(); const filteredTabs = tabs.current.filter(tab => tab !== null); const activeTabs = filteredTabs.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; } // reset active index to selected tab index for manual activation 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) { // The width of the "scroll buttons" const { width: tabWidth } = tab.getBoundingClientRect(); // The start and end position of the selected tab const start = tab.offsetLeft; const end = tab.offsetLeft + tabWidth; // The start and end of the visible area for the tabs const visibleStart = ref.current.scrollLeft + buttonWidth; const visibleEnd = ref.current.scrollLeft + ref.current.clientWidth - buttonWidth; // The beginning of the tab is clipped and not visible if (start < visibleStart) { setScrollLeft(start - buttonWidth); } // The end of the tab is clipped and not visible if (end > visibleEnd) { setScrollLeft(end + buttonWidth - ref.current.clientWidth); } } } React.useEffect(() => { const tab = tabs.current[selectedIndex]; if (scrollIntoView && tab) { tab.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } }, []); React.useEffect(() => { //adding 1 in calculation for firefox support setIsNextButtonVisible(ref.current ? scrollLeft + buttonWidth + ref.current.clientWidth + 1 < ref.current.scrollWidth : false); if (dismissable) { if (ref.current) { setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth); } } }, [scrollLeft, children, dismissable, isScrollable]); React.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["default"](() => { if (ref.current) { // adding 1 in calculation for firefox support setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth + 1); } function handler() { if (ref.current) { // adding 1 in calculation for firefox support setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth + 1); } } const debouncedHandler = debounce.debounce(handler, 200); window.addEventListener('resize', debouncedHandler); return () => { debouncedHandler.cancel(); window.removeEventListener('resize', debouncedHandler); }; }, []); // updates scroll location for all scroll behavior. useIsomorphicEffect["default"](() => { if (scrollLeft !== null && ref.current) { ref.current.scrollLeft = scrollLeft; } }, [scrollLeft]); // scroll manual tabs when active index changes (focus outline movement) useIsomorphicEffect["default"](() => { const tab = activation === 'manual' ? tabs.current[activeIndex] : tabs.current[selectedIndex]; scrollTabIntoView(tab); }, [activation, activeIndex]); // scroll tabs when selected index changes useIsomorphicEffect["default"](() => { const tab = tabs.current[selectedIndex]; scrollTabIntoView(tab); }, [selectedIndex, isScrollable, children]); usePressable.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.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__*/React__default["default"].createElement("div", { className: className }, /*#__PURE__*/React__default["default"].createElement("button", _rollupPluginBabelHelpers["extends"]({ "aria-hidden": "true", tabIndex: -1, "aria-label": "Scroll left", ref: previousButton, className: previousButtonClasses, type: "button" }, leftOverflowButtonProps), _ChevronLeft || (_ChevronLeft = /*#__PURE__*/React__default["default"].createElement(iconsReact.ChevronLeft, null))), /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-label": label, ref: ref, role: "tablist", className: `${prefix}--tab--list`, onScroll: debouncedOnScroll, onKeyDown: onKeyDown, onBlur: handleBlur }), React__default["default"].Children.map(children, (child, index) => { return !reactIs.isElement(child) ? null : /*#__PURE__*/React__default["default"].createElement(TabContext.Provider, { value: { index, hasSecondaryLabel: hasSecondaryLabelTabs, contained } }, /*#__PURE__*/React__default["default"].cloneElement(child, { ref: node => { tabs.current[index] = node; } })); })), /*#__PURE__*/React__default["default"].createElement("button", _rollupPluginBabelHelpers["extends"]({ "aria-hidden": "true", tabIndex: -1, "aria-label": "Scroll right", ref: nextButton, className: nextButtonClasses, type: "button" }, rightOverflowButtonProps), _ChevronRight || (_ChevronRight = /*#__PURE__*/React__default["default"].createElement(iconsReact.ChevronRight, null)))); } TabList.propTypes = { /** * Specify whether the content tab should be activated automatically or * manually */ activation: PropTypes__default["default"].oneOf(['automatic', 'manual']), /** * Provide an accessible label to be read when a user interacts with this * component */ 'aria-label': PropTypes__default["default"].string, /** * Provide child elements to be rendered inside `ContentTabs`. * These elements should render a `ContentTab` */ children: PropTypes__default["default"].node, /** * Specify an optional className to be added to the container node */ className: PropTypes__default["default"].string, /** * Specify whether component is contained type */ contained: PropTypes__default["default"].bool, /** * Used for tabs within a grid, this makes it so tabs span the full container width and have the same width. Only available on contained tabs with <9 children */ fullWidth: PropTypes__default["default"].bool, /** * If using `IconTab`, specify the size of the icon being used. */ iconSize: PropTypes__default["default"].oneOf(['default', 'lg']), /** * Provide the props that describe the left overflow button */ leftOverflowButtonProps: PropTypes__default["default"].object, /** * Specify whether to use the light component variant */ light: deprecate["default"](PropTypes__default["default"].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.'), /** * Provide the props that describe the right overflow button */ rightOverflowButtonProps: PropTypes__default["default"].object, /** * Optionally provide a delay (in milliseconds) passed to the lodash * debounce of the onScroll handler. This will impact the responsiveness * of scroll arrow buttons rendering when scrolling to the first or last tab. */ scrollDebounceWait: PropTypes__default["default"].number, /** * Choose whether to automatically scroll * to newly selected tabs on component rerender */ scrollIntoView: PropTypes__default["default"].bool }; /** * TabListVertical */ // type TabElement = HTMLElement & { disabled?: boolean }; function TabListVertical({ activation = 'automatic', 'aria-label': label, children, className: customClassName, scrollIntoView, ...rest }) { const { activeIndex, selectedIndex, setSelectedIndex, setActiveIndex } = React__default["default"].useContext(TabsContext); const prefix = usePrefix.usePrefix(); const ref = React.useRef(null); const [isOverflowingBottom, setIsOverflowingBottom] = React.useState(false); const [isOverflowingTop, setIsOverflowingTop] = React.useState(false); const isSm = useMatchMedia.useMatchMedia(smMediaQuery); const className = cx__default["default"](`${prefix}--tabs`, `${prefix}--tabs--vertical`, `${prefix}--tabs--contained`, customClassName); const tabs = React.useRef([]); function onKeyDown(event) { if (match.matches(event, [keys.ArrowDown, keys.ArrowUp, keys.Home, keys.End])) { event.preventDefault(); const filteredTabs = tabs.current.filter(tab => tab !== null); const activeTabs = filteredTabs.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; } // reset active index to selected tab index for manual activation if (activation === 'manual') { setActiveIndex(selectedIndex); } } React.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)); } } }, []); React.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) { // scrolls so selected tab is in view if (selectedPositionTop - halfTabHeight < containerTop || selectedPositionTop - containerTop + verticalTabHeight + halfTabHeight > containerHeight) { ref.current && ref.current.scrollTo({ top: (selectedIndex - 1) * verticalTabHeight, behavior: 'smooth' }); } } } window.addEventListener('resize', handler); handler(); return () => { window.removeEventListener('resize', handler); }; }, [selectedIndex, scrollIntoView]); React.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__*/React__default["default"].createElement(TabList, _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-label": label, contained: true }), children); } return /*#__PURE__*/React__default["default"].createElement("div", { className: className }, isOverflowingTop && /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tab--list-gradient_top` }), /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-label": label, ref: ref, role: "tablist", className: `${prefix}--tab--list`, onKeyDown: onKeyDown, onBlur: handleBlur }), React__default["default"].Children.map(children, (child, index) => { return !reactIs.isElement(child) ? null : /*#__PURE__*/React__default["default"].createElement(TabContext.Provider, { value: { index, hasSecondaryLabel: false } }, /*#__PURE__*/React__default["default"].cloneElement(child, { ref: node => { tabs.current[index] = node; } })); })), isOverflowingBottom && /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tab--list-gradient_bottom` })); } TabListVertical.propTypes = { /** * Specify whether the content tab should be activated automatically or * manually */ activation: PropTypes__default["default"].oneOf(['automatic', 'manual']), /** * Provide an accessible label to be read when a user interacts with this * component */ 'aria-label': PropTypes__default["default"].string, /** * Provide child elements to be rendered inside `ContentTabs`. * These elements should render a `ContentTab` */ children: PropTypes__default["default"].node, /** * Specify an optional className to be added to the container node */ className: PropTypes__default["default"].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 () => {}; } // We manually override the scroll behavior to be "auto". // If it is set as smooth, this animation does not update correctly 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 () => { // Restore the previous scroll behavior node.style['scroll-behavior'] = defaultScrollBehavior; // Make sure that our `scrollLeft` value is in sync with the existing // `ref` after our requestAnimationFrame loop above setScrollLeft(node.scrollLeft); if (frameId) { cancelAnimationFrame(frameId); } }; } /** * Tab */ const Tab = /*#__PURE__*/React.forwardRef(function Tab({ as = 'button', children, className: customClassName, disabled, onClick, onKeyDown, secondaryLabel, renderIcon: Icon, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const { selectedIndex, setSelectedIndex, baseId, dismissable, onTabCloseRequest } = React__default["default"].useContext(TabsContext); const { index: index$1, hasSecondaryLabel, contained } = React__default["default"].useContext(TabContext); const { badgeIndicator } = React__default["default"].useContext(IconTabContext) || {}; const dismissIconRef = React.useRef(null); const tabRef = React.useRef(null); const ref = useMergedRefs.useMergedRefs([forwardRef, tabRef]); const [ignoreHover, setIgnoreHover] = React.useState(false); const id = `${baseId}-tab-${index$1}`; const panelId = `${baseId}-tabpanel-${index$1}`; const [isEllipsisApplied, setIsEllipsisApplied] = React.useState(false); const isEllipsisActive = element => { setIsEllipsisApplied(element.offsetHeight < element.scrollHeight); return element.offsetHeight < element.scrollHeight; }; const className = cx__default["default"](`${prefix}--tabs__nav-item`, `${prefix}--tabs__nav-link`, { [`${prefix}--tabs__nav-item--selected`]: selectedIndex === index$1, [`${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.useEvent(dismissIconRef, 'mouseover', onDismissIconMouseEnter); useEvent.useEvent(dismissIconRef, 'mouseleave', onDismissIconMouseLeave); useIsomorphicEffect["default"](() => { 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$1); // set focus after removing tab if (tabRef.current && tabRef.current.parentElement) { // determine number of tabs, excluding disabled const tabCount = Array.from(tabRef.current.parentElement.childNodes).filter(node => { const element = node; return element.classList.contains(`${prefix}--tabs__nav-link`) && !element.classList.contains(`${prefix}--tabs__nav-item--disabled`); }).length; // if not removing last tab focus on next tab if (tabRef.current && index$1 + 1 !== tabCount) { tabRef.current.focus(); } // if removing last tab focus on previous tab else { const prevTabIndex = (tabCount - 2) * 2; tabRef.current.parentElement.childNodes[prevTabIndex]?.focus(); } } }; const handleKeyDown = event => { if (dismissable && match.match(event, keys.Delete)) { handleClose(event); } onKeyDown?.(event); }; const DismissIcon = /*#__PURE__*/React__default["default"].createElement("div", { className: cx__default["default"]({ [`${prefix}--tabs__nav-item--close`]: dismissable, [`${prefix}--tabs__nav-item--close--hidden`]: !dismissable }) }, /*#__PURE__*/React__default["default"].createElement("button", { type: "button", tabIndex: selectedIndex === index$1 && dismissable ? 0 : -1, "aria-disabled": disabled, "aria-hidden": selectedIndex === index$1 && dismissable ? 'false' : 'true', disabled: disabled, className: cx__default["default"]({ [`${prefix}--tabs__nav-item--close-icon`]: dismissable, [`${prefix}--visually-hidden`]: !dismissable, [`${prefix}--tabs__nav-item--close-icon--selected`]: selectedIndex === index$1, [`${prefix}--tabs__nav-item--close-icon--disabled`]: disabled }), onClick: handleClose, title: `Remove ${typeof children === 'string' ? children : ''} tab`, ref: dismissIconRef }, /*#__PURE__*/React__default["default"].createElement(iconsReact.Close, { "aria-hidden": selectedIndex === index$1 && dismissable ? 'false' : 'true', "aria-label": `Press delete to remove ${typeof children === 'string' ? children : ''} tab` }))); const hasIcon = Icon ?? dismissable; // should only happen for vertical variation, so no dismissable icon is needed here if (isEllipsisApplied) { return /*#__PURE__*/React__default["default"].createElement(Tooltip.Tooltip, { label: children, align: "top", leaveDelayMs: 0, autoAlign: true, onMouseEnter: () => false, closeOnActivation: true }, /*#__PURE__*/React__default["default"].createElement(BaseComponent, _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-controls": panelId, "aria-disabled": disabled, "aria-selected": selectedIndex === index$1, ref: ref, id: id, role: "tab", className: className, disabled: disabled, title: children, onClick: evt => { if (disabled) { return; } setSelectedIndex(index$1); onClick?.(evt); }, onKeyDown: handleKeyDown, tabIndex: selectedIndex === index$1 ? '0' : '-1', type: "button" }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tabs__nav-item-label-wrapper` }, /*#__PURE__*/React__default["default"].createElement(Text.Text, { className: `${prefix}--tabs__nav-item-label` }, children)), hasSecondaryLabel && secondaryLabel && /*#__PURE__*/React__default["default"].createElement(Text.Text, { as: "div", className: `${prefix}--tabs__nav-item-secondary-label`, title: secondaryLabel }, secondaryLabel))); } return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(BaseComponent, _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-controls": panelId, "aria-disabled": disabled, "aria-selected": selectedIndex === index$1, ref: ref, id: id, role: "tab", className: className, disabled: disabled, onClick: evt => { if (disabled) { return; } setSelectedIndex(index$1); onClick?.(evt); }, onKeyDown: handleKeyDown, tabIndex: selectedIndex === index$1 ? '0' : '-1', type: "button" }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tabs__nav-item-label-wrapper` }, dismissable && Icon && /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--tabs__nav-item--icon-left` }, /*#__PURE__*/React__default["default"].createElement(Icon, { size: 16 })), /*#__PURE__*/React__default["default"].createElement(Text.Text, { className: `${prefix}--tabs__nav-item-label` }, children), !dismissable && Icon && /*#__PURE__*/React__default["default"].createElement("div", { className: cx__default["default"](`${prefix}--tabs__nav-item--icon`, { [`${prefix}--visually-hidden`]: !hasIcon }) }, !dismissable && Icon && /*#__PURE__*/React__default["default"].createElement(Icon, { size: 16 }))), hasSecondaryLabel && secondaryLabel && /*#__PURE__*/React__default["default"].createElement(Text.Text, { as: "div", className: `${prefix}--tabs__nav-item-secondary-label`, title: secondaryLabel }, secondaryLabel), !disabled && badgeIndicator && (_BadgeIndicator || (_BadgeIndicator = /*#__PURE__*/React__default["default"].createElement(index.BadgeIndicator, null)))), DismissIcon); }); Tab.propTypes = { /** * Provide a custom element to render instead of the default button */ as: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].elementType]), /** * Provide child elements to be rendered inside `Tab`. */ children: PropTypes__default["default"].node, /** * Specify an optional className to be added to your Tab */ className: PropTypes__default["default"].string, /** * Whether your Tab is disabled. */ disabled: PropTypes__default["default"].bool, /** * Provide a handler that is invoked when a user clicks on the control */ onClick: PropTypes__default["default"].func, /** * Provide a handler that is invoked on the key down event for the control */ onKeyDown: PropTypes__default["default"].func, /** * An optional parameter to allow overriding the anchor rendering. * Useful for using Tab along with react-router or other client * side router libraries. */ renderButton: PropTypes__default["default"].func, /** * A component used to render an icon. */ renderIcon: PropTypes__default["default"].oneOfType([PropTypes__default["default"].func, PropTypes__default["default"].object]), /** * An optional label to render under the primary tab label. * Only useful for contained tabs. */ secondaryLabel: PropTypes__default["default"].string }; /** * IconTab */ const IconTabContext = /*#__PURE__*/React.createContext(false); const IconTab = /*#__PURE__*/React__default["default"].forwardRef(function IconTab({ badgeIndicator, children, className: customClassName, defaultOpen = false, enterDelayMs, leaveDelayMs, label, ...rest }, ref) { const prefix = usePrefix.usePrefix(); const value = React.useMemo(() => ({ badgeIndicator }), [badgeIndicator]); const hasSize20 = /*#__PURE__*/React.isValidElement(children) && children.props.size === 20; const classNames = cx__default["default"](`${prefix}--tabs__nav-item--icon-only`, customClassName, { [`${prefix}--tabs__nav-item--icon-only__20`]: hasSize20 }); return /*#__PURE__*/React__default["default"].createElement(IconTabContext.Provider, { value: value }, /*#__PURE__*/React__default["default"].createElement(Tooltip.Tooltip, { align: "bottom", defaultOpen: defaultOpen, className: `${prefix}--icon-tooltip`, enterDelayMs: enterDelayMs, label: label, leaveDelayMs: leaveDelayMs }, /*#__PURE__*/React__default["default"].createElement(Tab, _rollupPluginBabelHelpers["extends"]({ className: classNames, ref: ref }, rest), children))); }); IconTab.propTypes = { /** * **Experimental**: Display an empty dot badge on the Tab. */ badgeIndicator: PropTypes__default["default"].bool, /** * Provide an icon to be rendered inside `IconTab` as the visual label for Tab. */ children: PropTypes__default["default"].node, /** * Specify an optional className to be added to your Tab */ className: PropTypes__default["default"].string, /** * Specify whether the tooltip for the icon should be open when it first renders */ defaultOpen: PropTypes__default["default"].bool, /** * Specify the duration in milliseconds to delay before displaying the tooltip for the icon. */ enterDelayMs: PropTypes__default["default"].number, /** * Provide the label to be rendered inside the Tooltip. The label will use * `aria-labelledby` and will fully describe the child node that is provided. * This means that if you have text in the child node it will not be * announced to the screen reader. If using the badgeIndicator then provide a * label with describing that there is a new notification. */ label: PropTypes__default["default"].node.isRequired, /** * Specify the duration in milliseconds to delay before hiding the tooltip */ leaveDelayMs: PropTypes__default["default"].number }; /** * TabPanel */ const TabPanel = /*#__PURE__*/React__default["default"].forwardRef(function TabPanel({ children, className: customClassName, ...rest }, forwardRef) { const prefix = usePrefix.usePrefix(); const panel = React.useRef(null); const ref = useMergedRefs.useMergedRefs([forwardRef, panel]); const [tabIndex, setTabIndex] = React.useState(0); const [interactiveContent, setInteractiveContent] = React.useState(false); const { selectedIndex, baseId } = React__default["default"].useContext(TabsContext); const index = React__default["default"].useContext(TabPanelContext); const id = `${baseId}-tabpanel-${index}`; const tabId = `${baseId}-tab-${index}`; const className = cx__default["default"](`${prefix}--tab-content`, customClassName, { [`${prefix}--tab-content--interactive`]: interactiveContent }); React.useEffect(() => { if (!panel.current) { return; } const content = useNoInteractiveChildren.getInteractiveContent(panel.current); if (content) { setInteractiveContent(true); setTabIndex(-1); } }, []); // tabindex should only be 0 if no interactive content in children React.useEffect(() => { const node = panel.current; if (!node) { return; } function callback() { const content = useNoInteractiveChildren.getInteractiveContent(node); if (content) { setInteractiveContent(true); setTabIndex(-1); } else { setInteractiveContent(false); setTabIndex(0); } } const observer = new MutationObserver(callback); observer.observe(node, { childList: true, subtree: true }); return () => observer.disconnect(); }, []); return /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({}, rest, { "aria-labelledby": tabId, id: id, className: className, ref: ref, role: "tabpanel", tabIndex: tabIndex, hidden: selectedIndex !== index }), children); }); TabPanel.propTypes = { /** * Provide child elements to be rendered inside `TabPanel`. */ children: PropTypes__default["default"].node, /** * Specify an optional className to be added to TabPanel. */ className: PropTypes__default["default"].string }; /** * TabPanels */ function TabPanels({ children }) { const prefix = usePrefix.usePrefix(); const refs = React.useRef([]); const hiddenStates = React.useRef([]); useIsomorphicEffect["default"](() => { const tabContainer = refs.current[0]?.previousElementSibling; const isVertical = tabContainer?.classList.contains(`${prefix}--tabs--vertical`); const parentHasHeight = tabContainer?.parentElement?.style.height; // Should only apply same height to vertical Tab Panels without a given height if (isVertical && !parentHasHeight) { hiddenStates.current = refs.current.map(ref => ref?.hidden || false); // un-hide hidden Tab Panels to get heights refs.current.forEach(ref => { if (ref) { ref.hidden = false; } }); // set max height to TabList const heights = refs.current.map(ref => ref?.offsetHeight || 0); const max = Math.max(...heights); tabContainer.style.height = max + 'px'; // re-hide hidden Tab Panels refs.current.forEach((ref, index) => { if (ref) { ref.hidden = hiddenStates.current[index]; } }); } }); return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, React__default["default"].Children.map(children, (child, index) => { return !reactIs.isElement(child) ? null : /*#__PURE__*/React__default["default"].createElement(TabPanelContext.Provider, { value: index }, /*#__PURE__*/React__default["default"].cloneElement(child, { ref: element => { refs.current[index] = element; } })); })); } TabPanels.propTypes = { /** * Provide child elements to be rendered inside `TabPanels`. */ children: PropTypes__default["default"].node }; exports.IconTab = IconTab; exports.Tab = Tab; exports.TabList = TabList; exports.TabListVertical = TabListVertical; exports.TabPanel = TabPanel; exports.TabPanels = TabPanels; exports.Tabs = Tabs; exports.TabsVertical = TabsVertical;