UNPKG

@chakra-xui/tabs

Version:

Accessible Tabs component for React and Chakra xui

436 lines (378 loc) 14.2 kB
"use strict"; exports.__esModule = true; exports.useTabs = useTabs; exports.useTabList = useTabList; exports.useTab = useTab; exports.useTabPanels = useTabPanels; exports.useTabPanel = useTabPanel; exports.useTabIndicator = useTabIndicator; exports.TabsProvider = void 0; var _clickable = require("@chakra-xui/clickable"); var _descendant = require("@chakra-xui/descendant"); var _hooks = require("@chakra-xui/hooks"); var _utils = require("@chakra-xui/utils"); var React = _interopRequireWildcard(require("react")); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /** * Tabs hooks that provides all the states, and accessibility * helpers to keep all things working properly. * * Its returned object will be passed unto a Context Provider * so all child components can read from it. * There is no document link yet * @see Docs https://chakra-xui.com/docs/components/useTabs */ function useTabs(props) { var defaultIndex = props.defaultIndex, onChange = props.onChange, index = props.index, isManual = props.isManual, isLazy = props.isLazy, _props$orientation = props.orientation, orientation = _props$orientation === void 0 ? "horizontal" : _props$orientation, htmlProps = _objectWithoutPropertiesLoose(props, ["defaultIndex", "onChange", "index", "isManual", "isLazy", "orientation"]); /** * We use this to keep track of the index of the focused tab. * * Tabs can be automatically activated, this means selection follows focus. * When we navigate with the arrow keys, we move focus and selection to next/prev tab * * Tabs can also be manually activated, this means selection does not follow focus. * When we navigate with the arrow keys, we only move focus NOT selection. The user * will need not manually activate the tab using `Enter` or `Space`. * * This is why we need to keep track of the `focusedIndex` and `selectedIndex` */ var _React$useState = React.useState(defaultIndex != null ? defaultIndex : 0), focusedIndex = _React$useState[0], setFocusedIndex = _React$useState[1]; var _useControllableState = (0, _hooks.useControllableState)({ defaultValue: defaultIndex != null ? defaultIndex : 0, value: index, onChange: onChange, propsMap: { value: "index", defaultValue: "defaultIndex", onChange: "onChange" } }), selectedIndex = _useControllableState[0], setSelectedIndex = _useControllableState[1]; /** * Sync focused `index` with controlled `selectedIndex` (which is the `props.index`) */ React.useEffect(function () { if (!(0, _utils.isUndefined)(index)) { setFocusedIndex(index); } }, [index]); /** * Think of `useDescendants` as a register for the tab nodes. * * This manager is used to store only the tab nodes that are not disabled, and focusable. * If we have the following code * * ```jsx * <Tab>Tab 1</Tab> * <Tab isDisabled>Tab 2</Tab> * <Tab>Tab 3</Tab> * ``` * * The manager will only hold references to "Tab 1" and "Tab 3", since `Tab 2` is disabled */ var enabledDomContext = (0, _descendant.useDescendants)(); /** * This manager is used to store all tab nodes whether disabled or not. * If we have the following code * * ```jsx * <Tab>Tab 1</Tab> * <Tab isDisabled>Tab 2</Tab> * <Tab>Tab 3</Tab> * ``` * * The manager will only hold references to "Tab 1", "Tab 2" "Tab 3". * * We need this for correct indexing of tabs in event a tab is disabled */ var domContext = (0, _descendant.useDescendants)(); /** * generate a unique id or use user-provided id for * the tabs widget */ var id = (0, _hooks.useId)(props.id, "tabs"); return { id: id, selectedIndex: selectedIndex, focusedIndex: focusedIndex, setSelectedIndex: setSelectedIndex, setFocusedIndex: setFocusedIndex, isManual: isManual, isLazy: isLazy, orientation: orientation, enabledDomContext: enabledDomContext, domContext: domContext, htmlProps: htmlProps }; } var _createContext = (0, _utils.createContext)({ name: "TabsContext", errorMessage: "useTabsContext: `context` is undefined. Seems you forgot to wrap all tabs components within <Tabs />" }), TabsProvider = _createContext[0], useTabsContext = _createContext[1]; exports.TabsProvider = TabsProvider; /** * Tabs hook to manage multiple tab buttons, * and ensures only one tab is selected per time. * * @param props props object for the tablist */ function useTabList(props) { var _useTabsContext = useTabsContext(), setFocusedIndex = _useTabsContext.setFocusedIndex, focusedIndex = _useTabsContext.focusedIndex, orientation = _useTabsContext.orientation, enabledDomContext = _useTabsContext.enabledDomContext; var count = enabledDomContext.descendants.length; /** * Function to update the selected tab index */ var setIndex = React.useCallback(function (index) { var tab = enabledDomContext.descendants[index]; if (tab != null && tab.element) { tab.element.focus(); setFocusedIndex(index); } }, [enabledDomContext.descendants, setFocusedIndex]); var onKeyDown = React.useCallback(function (event) { var nextTab = function nextTab() { return setIndex((focusedIndex + 1) % count); }; var prevTab = function prevTab() { return setIndex((focusedIndex - 1 + count) % count); }; var firstTab = function firstTab() { return setIndex(0); }; var lastTab = function lastTab() { return setIndex(count - 1); }; var isHorizontal = orientation === "horizontal"; var isVertical = orientation === "vertical"; var eventKey = (0, _utils.normalizeEventKey)(event); var keyMap = { ArrowRight: function ArrowRight() { return isHorizontal && nextTab(); }, ArrowLeft: function ArrowLeft() { return isHorizontal && prevTab(); }, ArrowDown: function ArrowDown() { return isVertical && nextTab(); }, ArrowUp: function ArrowUp() { return isVertical && prevTab(); }, Home: firstTab, End: lastTab }; var action = keyMap[eventKey]; if (action) { event.preventDefault(); action(event); } }, [count, focusedIndex, orientation, setIndex]); return _extends({}, props, { role: "tablist", "aria-orientation": orientation, onKeyDown: (0, _utils.callAllHandlers)(props.onKeyDown, onKeyDown) }); } /** * Tabs hook to manage each tab button. * * A tab can be disabled and focusable, or both, * hence the use of `useClickable` to handle this scenario */ function useTab(props) { var isDisabled = props.isDisabled, isFocusable = props.isFocusable, htmlProps = _objectWithoutPropertiesLoose(props, ["isDisabled", "isFocusable"]); var _useTabsContext2 = useTabsContext(), setSelectedIndex = _useTabsContext2.setSelectedIndex, isManual = _useTabsContext2.isManual, id = _useTabsContext2.id, setFocusedIndex = _useTabsContext2.setFocusedIndex, enabledDomContext = _useTabsContext2.enabledDomContext, domContext = _useTabsContext2.domContext, selectedIndex = _useTabsContext2.selectedIndex; var ref = React.useRef(null); /** * Think of `useDescendant` as the function that registers tab node * to the `enabledDomContext`, and returns its index. * * Tab is registered if it is enabled or focusable */ var enabledIndex = (0, _descendant.useDescendant)({ disabled: Boolean(isDisabled), focusable: Boolean(isFocusable), context: enabledDomContext, element: ref.current }); /** * Registers all tabs (whether disabled or not) */ var index = (0, _descendant.useDescendant)({ context: domContext, element: ref.current }); var isSelected = index === selectedIndex; var onClick = function onClick() { setFocusedIndex(enabledIndex); setSelectedIndex(index); }; var onFocus = function onFocus() { var isDisabledButFocusable = isDisabled && isFocusable; var shouldSelect = !isManual && !isDisabledButFocusable; if (shouldSelect) { setSelectedIndex(index); } }; var clickableProps = (0, _clickable.useClickable)(_extends({}, htmlProps, { ref: (0, _utils.mergeRefs)(ref, props.ref), isDisabled: isDisabled, isFocusable: isFocusable, onClick: (0, _utils.callAllHandlers)(props.onClick, onClick) })); var type = "button"; return _extends({}, clickableProps, { id: makeTabId(id, index), role: "tab", tabIndex: isSelected ? 0 : -1, type: type, "aria-selected": (0, _utils.ariaAttr)(isSelected), "aria-controls": makeTabPanelId(id, index), onFocus: isDisabled ? undefined : (0, _utils.callAllHandlers)(props.onFocus, onFocus) }); } /** * Tabs hook for managing the visibility of multiple tab panels. * * Since only one panel can be show at a time, we use `cloneElement` * to inject `selected` panel to each TabPanel. * * It returns a cloned version of its children with * all functionality included. */ function useTabPanels(props) { var context = useTabsContext(); var id = context.id, selectedIndex = context.selectedIndex; var validChildren = (0, _utils.getValidChildren)(props.children); var children = validChildren.map(function (child, index) { return /*#__PURE__*/React.cloneElement(child, { isSelected: index === selectedIndex, id: makeTabPanelId(id, index) }); }); return _extends({}, props, { children: children }); } /** * Tabs hook for managing the visible/hidden states * of the tab panel. * * @param props props object for the tab panel */ function useTabPanel(props) { var isSelected = props.isSelected, id = props.id, htmlProps = _objectWithoutPropertiesLoose(props, ["isSelected", "id"]); var _useTabsContext3 = useTabsContext(), isLazy = _useTabsContext3.isLazy; return _extends({}, htmlProps, { children: !isLazy || isSelected ? props.children : null, role: "tabpanel", hidden: !isSelected, id: id }); } /** * Tabs hook to show an animated indicators that * follows the active tab. * * The way we do it is by measuring the DOM Rect (or dimensions) * of the active tab, and return that as CSS style for * the indicator. */ function useTabIndicator() { var context = useTabsContext(); var selectedIndex = context.selectedIndex, orientation = context.orientation, domContext = context.domContext; var isHorizontal = orientation === "horizontal"; var isVertical = orientation === "vertical"; // Get the clientRect of the selected tab var _React$useState2 = React.useState(function () { if (isHorizontal) return { left: 0, width: 0 }; if (isVertical) return { top: 0, height: 0 }; return undefined; }), rect = _React$useState2[0], setRect = _React$useState2[1]; var _React$useState3 = React.useState(false), hasMeasured = _React$useState3[0], setHasMeasured = _React$useState3[1]; // Update the selected tab rect when the selectedIndex changes (0, _hooks.useSafeLayoutEffect)(function () { var _tab$element; if ((0, _utils.isUndefined)(selectedIndex)) return undefined; var tab = domContext.descendants[selectedIndex]; var tabRect = tab == null ? void 0 : (_tab$element = tab.element) == null ? void 0 : _tab$element.getBoundingClientRect(); // Horizontal Tab: Calculate width and left distance if (isHorizontal && tabRect) { var left = tabRect.left, width = tabRect.width; setRect({ left: left, width: width }); } // Vertical Tab: Calculate height and top distance if (isVertical && tabRect) { var top = tabRect.top, height = tabRect.height; setRect({ top: top, height: height }); } // Prevent unwanted transition from 0 to measured rect // by setting the measured state in the next tick var id = requestAnimationFrame(function () { setHasMeasured(true); }); return function () { if (id) { cancelAnimationFrame(id); } }; }, [selectedIndex, isHorizontal, isVertical, domContext.descendants]); return _extends({ position: "absolute", transition: hasMeasured ? "all 200ms cubic-bezier(0, 0, 0.2, 1)" : "none" }, rect); } function makeTabId(id, index) { return id + "--tab-" + index; } function makeTabPanelId(id, index) { return id + "--tabpanel-" + index; } //# sourceMappingURL=use-tabs.js.map