UNPKG

@accessible/tabs

Version:

🅰 An accessible and versatile tabs component for React with keyboard navigation and labeling features taught in w3.org's WAI-ARIA tabs example

332 lines (282 loc) • 11.7 kB
"use strict"; exports.__esModule = true; exports.Tabs = Tabs; exports.useTab = useTab; exports.Tab = Tab; exports.TabList = TabList; exports.Panel = Panel; exports.TabsConsumer = exports.useTabs = exports.TabsContext = void 0; var React = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("react")); var _button = /*#__PURE__*/require("@accessible/button"); var _useKey = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-key")); var _useConditionalFocus = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-conditional-focus")); var _useId = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-id")); var _mergedRef = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/merged-ref")); var _passiveLayoutEffect = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/passive-layout-effect")); var _change = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/change")); var _clsx = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("clsx")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { 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; } const __reactCreateElement__ = React.createElement; // An optimized function for adding an `index` prop to elements of a Tab or // Panel type. All tabs must be on the same child depth level as other tabs, // same with panels. Once one tab or panel is found, it will not traverse // deeper into the tree. Using this in favor of something more generalized // in order to not hurt render performance on large trees. const cloneChildrenWithIndex = (elements, type) => { let index = 0; let didUpdate = false; const children = React.Children.map(elements, child => { // bails out if not an element object if (! /*#__PURE__*/React.isValidElement(child)) return child; // bails out if certainly the wrong type if (type === Panel && (child.type === TabList || child.type === Tab) || type === Tab && child.type === Panel) return child; // found a match if (child.type === type) { // bail out if the indexes are user-provided if (child.props.index !== void 0) { index = child.props.index + 1; return child; } else { didUpdate = true; return /*#__PURE__*/React.cloneElement(child, { index: index++ }); } } // only checks the children if we're not on a depth with tabs/panels if (index === 0) { const nextChildren = cloneChildrenWithIndex(child.props.children, type); if (nextChildren === child.props.children) return child;else { didUpdate = true; return /*#__PURE__*/React.cloneElement(child, void 0, nextChildren); } } return child; }); return !didUpdate ? elements : (children === null || children === void 0 ? void 0 : children.length) === 1 ? children[0] : children; }; function noop() {} const TabsContext = /*#__PURE__*/React.createContext({ tabs: [], registerTab: () => noop, active: void 0, activate: noop, manualActivation: false, preventScroll: false }), { Consumer: TabsConsumer } = TabsContext, useTabs = () => React.useContext(TabsContext); exports.TabsConsumer = TabsConsumer; exports.useTabs = useTabs; exports.TabsContext = TabsContext; function _ref(state, action) { const { index } = action; if (action.type === 'register') { state = state.slice(0); state[index] = { element: action.element, id: action.id, disabled: action.disabled }; } else if (action.type === 'unregister') { state = state.slice(0); state[index] = void 0; } return state; } function Tabs({ active, defaultActive = 0, manualActivation = false, preventScroll = false, onChange = noop, children }) { const [tabs, dispatchTabs] = React.useReducer(_ref, []); const [userActive, setActive] = React.useState(defaultActive); (0, _change.default)(userActive, onChange); const nextActive = active !== null && active !== void 0 ? active : userActive; function _ref2(index, element, id, disabled) { dispatchTabs({ type: 'register', index, element, id, disabled }); return () => dispatchTabs({ type: 'unregister', index }); } function _ref3(index) { var _tabs; if ((_tabs = tabs[index || -1]) === null || _tabs === void 0 ? void 0 : _tabs.disabled) return; setActive(index); } const context = React.useMemo(() => ({ tabs, registerTab: _ref2, active: nextActive, activate: _ref3, preventScroll, manualActivation }), [tabs, nextActive, manualActivation, preventScroll]); return /*#__PURE__*/__reactCreateElement__(TabsContext.Provider, { value: context }, cloneChildrenWithIndex(cloneChildrenWithIndex(children, Tab), Panel)); } function useTab(index) { const { tabs, activate, active } = React.useContext(TabsContext); function _ref4() { var _tabs$index3; return !((_tabs$index3 = tabs[index]) === null || _tabs$index3 === void 0 ? void 0 : _tabs$index3.disabled) && activate(index); } return React.useMemo(() => { var _tabs$index, _tabs$index2, _tabs$index4; return { id: (_tabs$index = tabs[index]) === null || _tabs$index === void 0 ? void 0 : _tabs$index.id, tabRef: (_tabs$index2 = tabs[index]) === null || _tabs$index2 === void 0 ? void 0 : _tabs$index2.element, index: index, activate: _ref4, isActive: index === active, disabled: ((_tabs$index4 = tabs[index]) === null || _tabs$index4 === void 0 ? void 0 : _tabs$index4.disabled) || false }; }, [tabs, index, active, activate]); } function Tab({ id, index, disabled = false, activeClass, inactiveClass, activeStyle, inactiveStyle, onDelete = noop, children }) { id = (0, _useId.default)(id); const { registerTab } = useTabs(); const triggerRef = React.useRef(null); const { tabs, manualActivation } = useTabs(); const { isActive, activate } = useTab(index); const ref = (0, _mergedRef.default)( // @ts-ignore children.ref, triggerRef); (0, _useKey.default)(triggerRef, { // right arrow ArrowRight: () => focusNext(tabs, index), // left arrow ArrowLeft: () => focusPrev(tabs, index), // home Home: () => { var _tabs$, _tabs$$element; return (_tabs$ = tabs[0]) === null || _tabs$ === void 0 ? void 0 : (_tabs$$element = _tabs$.element) === null || _tabs$$element === void 0 ? void 0 : _tabs$$element.focus(); }, // end End: () => { var _tabs2, _tabs2$element; return (_tabs2 = tabs[tabs.length - 1]) === null || _tabs2 === void 0 ? void 0 : (_tabs2$element = _tabs2.element) === null || _tabs2$element === void 0 ? void 0 : _tabs2$element.focus(); }, // delete Delete: onDelete }); React.useEffect(() => registerTab(index, triggerRef.current, id, disabled), // eslint-disable-next-line react-hooks/exhaustive-deps [id, disabled, index]); return /*#__PURE__*/__reactCreateElement__(_button.Button, null, /*#__PURE__*/React.cloneElement(children, { 'aria-controls': id, 'aria-selected': '' + isActive, 'aria-disabled': '' + (isActive || disabled), role: 'tab', className: (0, _clsx.default)(children.props.className, isActive ? activeClass : inactiveClass) || void 0, style: Object.assign({}, children.props.style, isActive ? activeStyle : inactiveStyle), tabIndex: children.props.hasOwnProperty('tabIndex') ? children.props.tabIndex : isActive ? 0 : -1, onFocus: e => { var _children$props$onFoc, _children$props; if (!manualActivation) activate(); (_children$props$onFoc = (_children$props = children.props).onFocus) === null || _children$props$onFoc === void 0 ? void 0 : _children$props$onFoc.call(_children$props, e); }, onClick: e => { var _children$props$onCli, _children$props2; activate(); (_children$props$onCli = (_children$props2 = children.props).onClick) === null || _children$props$onCli === void 0 ? void 0 : _children$props$onCli.call(_children$props2, e); }, ref })); } function focusNext(tabs, currentIndex) { var _tabs$2, _tabs$2$element, _tabs3, _tabs3$element; if (currentIndex === tabs.length - 1) (_tabs$2 = tabs[0]) === null || _tabs$2 === void 0 ? void 0 : (_tabs$2$element = _tabs$2.element) === null || _tabs$2$element === void 0 ? void 0 : _tabs$2$element.focus();else (_tabs3 = tabs[currentIndex + 1]) === null || _tabs3 === void 0 ? void 0 : (_tabs3$element = _tabs3.element) === null || _tabs3$element === void 0 ? void 0 : _tabs3$element.focus(); } function focusPrev(tabs, currentIndex) { var _tabs4, _tabs4$element, _tabs5, _tabs5$element; if (currentIndex === 0) (_tabs4 = tabs[tabs.length - 1]) === null || _tabs4 === void 0 ? void 0 : (_tabs4$element = _tabs4.element) === null || _tabs4$element === void 0 ? void 0 : _tabs4$element.focus();else (_tabs5 = tabs[currentIndex - 1]) === null || _tabs5 === void 0 ? void 0 : (_tabs5$element = _tabs5.element) === null || _tabs5$element === void 0 ? void 0 : _tabs5$element.focus(); } function TabList({ children }) { return /*#__PURE__*/React.cloneElement(children, { role: 'tablist' }); } function Panel({ index, activeClass, inactiveClass, activeStyle, inactiveStyle, children }) { const { isActive, id } = useTab(index); const { manualActivation, preventScroll } = useTabs(); const prevActive = React.useRef(isActive); const panelRef = React.useRef(null); const ref = (0, _mergedRef.default)( // @ts-ignore children.ref, panelRef); (0, _useConditionalFocus.default)(panelRef, manualActivation && !prevActive.current && isActive, { includeRoot: true, preventScroll }); // ensures the tab panel won't be granted the window's focus // by default, but receives focus when the visual state changes to // active (0, _passiveLayoutEffect.default)(() => { prevActive.current = isActive; }, [isActive, index]); return /*#__PURE__*/React.cloneElement(children, { 'aria-hidden': `${!isActive}`, id, className: (0, _clsx.default)(children.props.className, isActive ? activeClass : inactiveClass) || void 0, style: Object.assign({ visibility: isActive ? 'visible' : 'hidden' }, children.props.style, isActive ? activeStyle : inactiveStyle), tabIndex: children.props.hasOwnProperty('tabIndex') ? children.props.tabIndex : isActive ? 0 : -1, ref }); } /* istanbul ignore next */ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { Tabs.displayName = 'Tabs'; TabList.displayName = 'TabList'; Tab.displayName = 'Tab'; Panel.displayName = 'Panel'; }