@carbon/react
Version:
React components for the Carbon Design System
1,245 lines (1,196 loc) • 43.8 kB
JavaScript
/**
* 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.
*/
;
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;