From 8bd973f4aa0459f26a57d27c50352f29f5de9b68 Mon Sep 17 00:00:00 2001 From: Johan Reitan Date: Tue, 10 Mar 2020 10:47:53 +0100 Subject: [PATCH] add tabs typings --- libraries/core-react/src/Tabs/Tab.jsx | 21 ++- libraries/core-react/src/Tabs/TabList.jsx | 158 ++++++++++-------- libraries/core-react/src/Tabs/TabPanel.jsx | 28 +++- libraries/core-react/src/Tabs/TabPanels.jsx | 45 +++-- libraries/core-react/src/Tabs/Tabs.context.js | 3 +- libraries/core-react/src/Tabs/Tabs.jsx | 54 ++++-- 6 files changed, 196 insertions(+), 113 deletions(-) diff --git a/libraries/core-react/src/Tabs/Tab.jsx b/libraries/core-react/src/Tabs/Tab.jsx index c5b56ecf15..32ae194f84 100644 --- a/libraries/core-react/src/Tabs/Tab.jsx +++ b/libraries/core-react/src/Tabs/Tab.jsx @@ -78,9 +78,24 @@ const StyledTab = styled.button.attrs(({ active, disabled }) => ({ } ` -export const Tab = forwardRef(function Tab(props, ref) { - return -}) +/** + * @typedef {object} Props + * @prop {boolean} [active] If `true`, the tab will be active. + * @prop {boolean} [disabled] If `true`, the tab will be disabled. + * @prop {string} [className] + * @prop {React.ReactNode} children + */ + +export const Tab = forwardRef( + /** + * @param {Props} props + * @param {React.Ref} ref + * @returns {React.ReactElement} + */ + function Tab(props, ref) { + return + }, +) Tab.propTypes = { /** If `true`, the tab will be active. */ diff --git a/libraries/core-react/src/Tabs/TabList.jsx b/libraries/core-react/src/Tabs/TabList.jsx index 526ddf0966..92793981db 100644 --- a/libraries/core-react/src/Tabs/TabList.jsx +++ b/libraries/core-react/src/Tabs/TabList.jsx @@ -25,81 +25,98 @@ const StyledTabList = styled.div.attrs(() => ({ grid-auto-columns: ${({ variant }) => variants[variant]}; ` -const TabList = forwardRef(function TabsList({ children, ...props }, ref) { - const { activeTab, handleChange, tabsId, variant } = useContext(TabsContext) +/** + * @typedef {object} Props + * @prop {string} [className] + * @prop {import('./Tabs').TabsVariant} [variant] Sets the width of the tabs + * @prop {Tab | Tab[]} children + */ + +const TabList = forwardRef( + /** + * @param {Props} props + * @param {React.Ref} ref + * @returns {React.ReactElement} + */ + function TabsList({ children, ...rest }, ref) { + const { activeTab, handleChange, tabsId, variant } = useContext(TabsContext) + + const currentTab = useRef(activeTab) + + const [focusVisible, setFocusVisible] = useState(false) + + const selectedTabRef = useCallback((node) => { + if (node !== null) { + node.focus() + } + }, []) + + useEffect(() => { + currentTab.current = activeTab + }, [activeTab]) + + const Tabs = React.Children.map(children, (child, index) => { + const tabRef = + index === activeTab + // @ts-ignore + ? useCombinedRefs(child.ref, selectedTabRef) + // @ts-ignore + : child.ref + + // @ts-ignore + return React.cloneElement(child, { + id: `${tabsId}-tab-${index + 1}`, + 'aria-controls': `${tabsId}-panel-${index + 1}`, + active: index === activeTab, + index, + onClick: () => handleChange(index), + ref: tabRef, + focusVisible, + }) + }) - const currentTab = useRef(activeTab) + const focusableChildren = Tabs.filter((child) => !child.props.disabled).map( + (child) => child.props.index, + ) - const [focusVisible, setFocusVisible] = useState(false) + const firstFocusableChild = focusableChildren[0] + const lastFocusableChild = focusableChildren[focusableChildren.length - 1] - const selectedTabRef = useCallback((node) => { - if (node !== null) { - node.focus() + const handleTabsChange = (direction, fallbackTab) => { + const i = direction === 'left' ? 1 : -1 + const nextTab = + focusableChildren[focusableChildren.indexOf(currentTab.current) - i] + handleChange(nextTab === undefined ? fallbackTab : nextTab) } - }, []) - - useEffect(() => { - currentTab.current = activeTab - }, [activeTab]) - - const Tabs = React.Children.map(children, (child, index) => { - const tabRef = - index === activeTab - ? useCombinedRefs(child.ref, selectedTabRef) - : child.ref - - return React.cloneElement(child, { - id: `${tabsId}-tab-${index + 1}`, - 'aria-controls': `${tabsId}-panel-${index + 1}`, - active: index === activeTab, - index, - onClick: () => handleChange(index), - ref: tabRef, - focusVisible, - }) - }) - - const focusableChildren = Tabs.filter((child) => !child.props.disabled).map( - (child) => child.props.index, - ) - - const firstFocusableChild = focusableChildren[0] - const lastFocusableChild = focusableChildren[focusableChildren.length - 1] - - const handleTabsChange = (direction, fallbackTab) => { - const i = direction === 'left' ? 1 : -1 - const nextTab = - focusableChildren[focusableChildren.indexOf(currentTab.current) - i] - handleChange(nextTab === undefined ? fallbackTab : nextTab) - } - - const handleKeyPress = (event) => { - const { key } = event - setFocusVisible(true) - if (key === 'ArrowLeft') { - handleTabsChange('left', lastFocusableChild) + + const handleKeyPress = (event) => { + const { key } = event + setFocusVisible(true) + if (key === 'ArrowLeft') { + handleTabsChange('left', lastFocusableChild) + } + if (key === 'ArrowRight') { + handleTabsChange('right', firstFocusableChild) + } } - if (key === 'ArrowRight') { - handleTabsChange('right', firstFocusableChild) + + const handleMouseDown = () => { + setFocusVisible(false) } - } - - const handleMouseDown = () => { - setFocusVisible(false) - } - - return ( - - {Tabs} - - ) -}) + + return ( + + {Tabs} + + ) + }, +) const tabType = PropTypes.shape({ type: PropTypes.oneOf([Tab]), @@ -109,14 +126,17 @@ TabList.propTypes = { /** @ignore */ className: PropTypes.string, /** Sets the width of the tabs */ + // @ts-ignore variant: PropTypes.oneOf(['fullWidth', 'minWidth']), /** @ignore */ + // @ts-ignore children: PropTypes.oneOfType([PropTypes.arrayOf(tabType), tabType]) .isRequired, } TabList.defaultProps = { className: null, + // @ts-ignore variant: 'minWidth', } diff --git a/libraries/core-react/src/Tabs/TabPanel.jsx b/libraries/core-react/src/Tabs/TabPanel.jsx index d097f30601..ac382a4e89 100644 --- a/libraries/core-react/src/Tabs/TabPanel.jsx +++ b/libraries/core-react/src/Tabs/TabPanel.jsx @@ -15,13 +15,27 @@ const StyledTabPanel = styled.div.attrs(() => ({ paddingBottom, }) -const TabPanel = forwardRef(function TabPanel({ ...props }, ref) { - return ( - - {props.children} - - ) -}) +/** + * @typedef {object} Props + * @prop {React.ReactNode} children + * @prop {string} [className] + * @prop {boolean} [hidden] If `true`, the panel will be hidden. + */ + +const TabPanel = forwardRef( + /** + * @param {Props} props + * @param {React.Ref} ref + * @returns {React.ReactElement} + */ + function TabPanel({ ...props }, ref) { + return ( + + {props.children} + + ) + }, +) TabPanel.propTypes = { /** @ignore */ diff --git a/libraries/core-react/src/Tabs/TabPanels.jsx b/libraries/core-react/src/Tabs/TabPanels.jsx index d74a2cf695..8ceb69236f 100644 --- a/libraries/core-react/src/Tabs/TabPanels.jsx +++ b/libraries/core-react/src/Tabs/TabPanels.jsx @@ -3,22 +3,36 @@ import PropTypes from 'prop-types' import { TabsContext } from './Tabs.context' import { TabPanel } from './TabPanel' -const TabPanels = forwardRef(function TabPanels({ children, ...props }, ref) { - const { activeTab, tabsId } = useContext(TabsContext) +/** + * @typedef {object} Props + * @prop {string} [className] + * @prop {TabPanel | TabPanel[]} children + */ - const Panels = React.Children.map(children, (child, index) => - React.cloneElement(child, { - id: `${tabsId}-panel-${index + 1}`, - 'aria-labelledby': `${tabsId}-tab-${index + 1}`, - hidden: activeTab !== index, - }), - ) - return ( -
- {Panels} -
- ) -}) +const TabPanels = forwardRef( + /** + * @param {Props} props + * @param {React.Ref} ref + * @returns {React.ReactElement} + */ + function TabPanels({ children, ...rest }, ref) { + const { activeTab, tabsId } = useContext(TabsContext) + + const Panels = React.Children.map(children, (child, index) => + // @ts-ignore + React.cloneElement(child, { + id: `${tabsId}-panel-${index + 1}`, + 'aria-labelledby': `${tabsId}-tab-${index + 1}`, + hidden: activeTab !== index, + }), + ) + return ( +
+ {Panels} +
+ ) + }, +) const panelType = PropTypes.shape({ type: PropTypes.oneOf([TabPanel]), @@ -28,6 +42,7 @@ TabPanels.propTypes = { /** @ignore */ className: PropTypes.string, /** @ignore */ + // @ts-ignore children: PropTypes.oneOfType([PropTypes.arrayOf(panelType), panelType]) .isRequired, } diff --git a/libraries/core-react/src/Tabs/Tabs.context.js b/libraries/core-react/src/Tabs/Tabs.context.js index 8b61312c8a..6b4c739fb6 100644 --- a/libraries/core-react/src/Tabs/Tabs.context.js +++ b/libraries/core-react/src/Tabs/Tabs.context.js @@ -2,7 +2,8 @@ import React, { createContext } from 'react' const TabsContext = createContext({ variant: '', - handleChange: () => {}, + /** @param {number} index */ + handleChange: (index) => {}, activeTab: 0, tabsId: '', }) diff --git a/libraries/core-react/src/Tabs/Tabs.jsx b/libraries/core-react/src/Tabs/Tabs.jsx index f8149f947c..6cfab0384f 100644 --- a/libraries/core-react/src/Tabs/Tabs.jsx +++ b/libraries/core-react/src/Tabs/Tabs.jsx @@ -3,25 +3,41 @@ import PropTypes from 'prop-types' import createId from 'lodash.uniqueid' import { TabsProvider } from './Tabs.context' -const Tabs = forwardRef(function Tabs( - { activeTab, onChange, variant, ...props }, - ref, -) { - const tabsId = useMemo(() => createId('tabs-'), []) +/** + * @typedef {'fullWidth' | 'minWidth'} TabsVariant + */ - return ( - -
- - ) -}) +/** + * @typedef {object} Props + * @prop {number} [activeTab] The index of the active tab + * @prop {() => void} [onChange] The callback function for selecting a tab + * @prop {TabsVariant} [variant] Sets the width of the tabs + * @prop {React.ReactNode} children + */ + +const Tabs = forwardRef( + /** + * @param {Props} props + * @param {React.Ref} ref + * @returns {React.ReactElement} + */ + function Tabs({ activeTab, onChange, variant, ...rest }, ref) { + const tabsId = useMemo(() => createId('tabs-'), []) + + return ( + +
+ + ) + }, +) Tabs.propTypes = { /** The index of the active tab */ @@ -29,6 +45,7 @@ Tabs.propTypes = { /** The callback function for selecting a tab */ onChange: PropTypes.func, /** Sets the width of the tabs */ + // @ts-ignore variant: PropTypes.oneOf(['fullWidth', 'minWidth']), /** @ignore */ children: PropTypes.node.isRequired, @@ -37,6 +54,7 @@ Tabs.propTypes = { Tabs.defaultProps = { activeTab: 0, onChange: () => {}, + // @ts-ignore variant: 'minWidth', }