react-ally
Version:
Accessible react components
122 lines (107 loc) • 3.13 kB
JavaScript
import React, { Fragment, createContext, useContext, useEffect, useRef, useState } from 'react';
import { bool, string } from 'prop-types';
const TabsContext = createContext({});
export const Tabs = ({ initialActiveIndex = 0, id, ...props }) => {
const [activeIndex, setActiveIndex] = useState(initialActiveIndex);
return (
<TabsContext.Provider value={{ activeIndex, tabsId: id, setActiveIndex }}>
<div id={id} {...props} />
</TabsContext.Provider>
);
};
Tabs.propTypes = { id: string.isRequired };
export const TabList = ({ manual = false, ...props }) => {
const [focusIndex, setFocusIndex] = useState(-1);
return (
<div {...props} role="tablist">
{React.Children.map(props.children, (child, index) =>
React.cloneElement(child, {
count: props.children.length,
focusIndex,
index,
manual,
setFocusIndex
})
)}
</div>
);
};
TabList.propTypes = {
manual: bool
};
const tabId = (tabsId, index) => `${tabsId}-${index}th-tab`;
const panelId = (tabsId, index) => `${tabsId}-${index}th-panel`;
export const Tab = ({ count, focusIndex, index, manual, setFocusIndex, ...props }) => {
const ref = useRef(null);
const mounted = useRef(false);
const { activeIndex, setActiveIndex, tabsId } = useContext(TabsContext);
const active = index === activeIndex;
const focused = focusIndex > -1 ? index === focusIndex : index === activeIndex;
useEffect(() => {
if (mounted.current && focused) {
ref.current.focus();
}
}, [activeIndex, focusIndex]);
useEffect(() => {
mounted.current = true;
}, []);
return (
<button
ref={ref}
id={tabId(tabsId, index)}
{...props}
aria-controls={panelId(tabsId, index)}
aria-selected={active}
onClick={() => setActiveIndex(index)}
onKeyDown={e => {
const func = manual ? setFocusIndex : setActiveIndex;
const next = () => func((index + 1) % count);
const prev = () => func((index - 1 + count) % count);
switch (e.keyCode) {
case 35:
func(count - 1);
break;
case 36:
func(0);
break;
case 37:
prev();
break;
case 38:
prev();
break;
case 39:
next();
break;
case 40:
next();
break;
default:
break;
}
}}
role="tab"
tabIndex={active - 1}
/>
);
};
export const TabPanels = ({ children }) => (
<Fragment>
{React.Children.map(children, (child, index) => React.cloneElement(child, { index }))}
</Fragment>
);
export const TabPanel = ({ index, ...props }) => {
const { activeIndex, tabsId } = useContext(TabsContext);
const active = index === activeIndex;
return (
<div
id={panelId(tabsId, index)}
{...props}
aria-labelledby={tabId(tabsId, index)}
aria-selected={active}
role="tabpanel"
style={{ display: active ? 'inherit' : 'none' }}
tabIndex={0}
/>
);
};