@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
JavaScript
"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';
}