Last active
December 3, 2021 09:50
-
-
Save reecelucas/6f9e2e79be35a66e67030ccb705bf412 to your computer and use it in GitHub Desktop.
React version of Heydon Pickering's "Tabbed Interface" inclusive component. https://inclusive-components.design/tabbed-interfaces/.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const whiteListAttributePatterns = /^(aria-|data-|role|name|tabIndex|className|autoFocus)/; | |
export default (props = {}) => | |
Object.keys(props) | |
.filter(prop => whiteListAttributePatterns.test(prop)) | |
.reduce((acc, key) => { | |
acc[key] = props[key]; | |
return acc; | |
}, {}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Tab from './Tab'; | |
import TabList from './TabList'; | |
import TabPanel from './TabPanel'; | |
import TabPanels from './TabPanels'; | |
import Tabs from './Tabs'; | |
export { Tabs, TabList, Tab, TabPanels, TabPanel }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useContext, useEffect, useRef } from 'react'; | |
import PropTypes from 'prop-types'; | |
import TabContext from './TabContext'; | |
import getAttributeProps from './getAttributeProps'; | |
const propTypes = { | |
children: PropTypes.oneOfType([ | |
PropTypes.arrayOf(PropTypes.node), | |
PropTypes.node | |
]).isRequired, | |
/** | |
* `id` is injected by `TabList`, using `React.cloneElement`. | |
* Don't pass it in manually. This PropType is here to keep | |
* eslint happy. | |
*/ | |
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |
active: PropTypes.bool | |
}; | |
const Tab = ({ children, id, active, ...rest }) => { | |
const domRef = useRef(); | |
const { activeId, setActiveId } = useContext(TabContext); | |
const attributes = getAttributeProps(rest); | |
useEffect(() => { | |
if (active) { | |
setActiveId(id); | |
} | |
}, []); | |
useEffect( | |
() => { | |
if (isActive()) { | |
domRef.current.focus(); | |
} | |
}, | |
[activeId] | |
); | |
const onClick = event => { | |
event.preventDefault(); | |
setActiveId(id); | |
}; | |
const isActive = () => id === activeId; | |
return ( | |
<li role="presentation" {...attributes}> | |
<a | |
ref={domRef} | |
href={`#section-${id}`} | |
id={`tab-${id}`} | |
role="tab" | |
tabIndex={isActive() ? null : '-1'} | |
aria-controls={`section-${id}`} | |
aria-selected={isActive() ? 'true' : null} | |
onClick={onClick} | |
> | |
{children} | |
</a> | |
</li> | |
); | |
}; | |
Tab.propTypes = propTypes; | |
export default Tab; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
const TabContext = React.createContext({}); | |
export default TabContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useContext, useEffect, useState } from 'react'; | |
import PropTypes from 'prop-types'; | |
import TabContext from './TabContext'; | |
import getAttributeProps from '../../helpers/getAttributeProps'; | |
const propTypes = { | |
children: PropTypes.oneOfType([ | |
PropTypes.arrayOf(PropTypes.node), | |
PropTypes.node | |
]).isRequired | |
}; | |
const TabList = ({ children, ...rest }) => { | |
const [tabsLength, setTabsLength] = useState(0); | |
const { activeId, setActiveId } = useContext(TabContext); | |
useEffect(() => { | |
setTabsLength(React.Children.count(children) - 1); // 0-based `Children` length | |
}, []); | |
const onKeyDown = ({ key }) => { | |
const isLeftArrow = key === 'ArrowLeft'; | |
const isRightArrow = key === 'ArrowRight'; | |
if (!isLeftArrow && !isRightArrow) return; | |
const nextId = isLeftArrow ? activeId - 1 : activeId + 1; | |
const nextIdLeft = nextId >= 0 ? nextId : 0; | |
const nextIdRight = nextId <= tabsLength ? nextId : tabsLength; | |
setActiveId(isLeftArrow ? nextIdLeft : nextIdRight); | |
}; | |
return ( | |
<ul role="tablist" onKeyDown={onKeyDown} {...getAttributeProps(rest)}> | |
{React.Children.map(children, (child, index) => | |
// Inject`id` prop into each child component | |
React.cloneElement(child, { id: index }) | |
)} | |
</ul> | |
); | |
}; | |
TabList.propTypes = propTypes; | |
export default TabList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useContext } from 'react'; | |
import PropTypes from 'prop-types'; | |
import TabContext from './TabContext'; | |
const propTypes = { | |
children: PropTypes.oneOfType([ | |
PropTypes.arrayOf(PropTypes.node), | |
PropTypes.node | |
]).isRequired, | |
/** | |
* `id` is injected by `TabPanels`, using `React.cloneElement`. | |
* Don't pass it in manually. This PropType is here to keep | |
* eslint happy. | |
*/ | |
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) | |
}; | |
const TabPanel = ({ id, children }) => { | |
const { activeId } = useContext(TabContext); | |
const show = () => id === activeId; | |
return ( | |
<section | |
id={`section-${id}`} | |
role="tabpanel" | |
tabIndex="-1" | |
aria-labelledby={`tab-${id}`} | |
hidden={show() ? false : true} | |
> | |
{children} | |
</section> | |
); | |
}; | |
TabPanel.propTypes = propTypes; | |
export default TabPanel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import PropTypes from 'prop-types'; | |
import React from 'react'; | |
const propTypes = { | |
children: PropTypes.oneOfType([ | |
PropTypes.arrayOf(PropTypes.node), | |
PropTypes.node | |
]).isRequired | |
}; | |
const TabPanels = ({ children }) => { | |
return React.Children.map(children, (child, index) => | |
// Inject`id` prop into each child component | |
React.cloneElement(child, { id: index }) | |
); | |
}; | |
TabPanels.propTypes = propTypes; | |
export default TabPanels; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react'; | |
import PropTypes from 'prop-types'; | |
import TabContext from './TabContext'; | |
const propTypes = { | |
children: PropTypes.oneOfType([ | |
PropTypes.arrayOf(PropTypes.node), | |
PropTypes.node | |
]).isRequired | |
}; | |
const Tabs = ({ children }) => { | |
const [currentId, setCurrentId] = useState(0); | |
const setActiveId = id => { | |
setCurrentId(id); | |
}; | |
return ( | |
<TabContext.Provider | |
value={{ | |
activeId: currentId, | |
setActiveId | |
}} | |
> | |
{children} | |
</TabContext.Provider> | |
); | |
}; | |
Tabs.propTypes = propTypes; | |
export default Tabs; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment