Skip to content

Commit

Permalink
add tabs typings
Browse files Browse the repository at this point in the history
  • Loading branch information
Johan Reitan authored and joharei committed Apr 7, 2020
1 parent 19533a6 commit b2fa92b
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 113 deletions.
21 changes: 18 additions & 3 deletions libraries/core-react/src/Tabs/Tab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,24 @@ const StyledTab = styled.button.attrs(({ active, disabled }) => ({
}
`

export const Tab = forwardRef(function Tab(props, ref) {
return <StyledTab ref={ref} {...props} />
})
/**
* @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<any>} ref
* @returns {React.ReactElement}
*/
function Tab(props, ref) {
return <StyledTab ref={ref} {...props} />
},
)

Tab.propTypes = {
/** If `true`, the tab will be active. */
Expand Down
158 changes: 89 additions & 69 deletions libraries/core-react/src/Tabs/TabList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>} 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 (
<StyledTabList
onKeyDown={handleKeyPress}
onMouseDown={handleMouseDown}
ref={ref}
{...props}
variant={variant}
>
{Tabs}
</StyledTabList>
)
})

return (
<StyledTabList
onKeyDown={handleKeyPress}
onMouseDown={handleMouseDown}
ref={ref}
{...rest}
variant={variant}
>
{Tabs}
</StyledTabList>
)
},
)

const tabType = PropTypes.shape({
type: PropTypes.oneOf([Tab]),
Expand All @@ -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',
}

Expand Down
28 changes: 21 additions & 7 deletions libraries/core-react/src/Tabs/TabPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,27 @@ const StyledTabPanel = styled.div.attrs(() => ({
paddingBottom,
})

const TabPanel = forwardRef(function TabPanel({ ...props }, ref) {
return (
<StyledTabPanel ref={ref} {...props}>
{props.children}
</StyledTabPanel>
)
})
/**
* @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<any>} ref
* @returns {React.ReactElement}
*/
function TabPanel({ ...props }, ref) {
return (
<StyledTabPanel ref={ref} {...props}>
{props.children}
</StyledTabPanel>
)
},
)

TabPanel.propTypes = {
/** @ignore */
Expand Down
45 changes: 30 additions & 15 deletions libraries/core-react/src/Tabs/TabPanels.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div ref={ref} {...props}>
{Panels}
</div>
)
})
const TabPanels = forwardRef(
/**
* @param {Props} props
* @param {React.Ref<any>} 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 (
<div ref={ref} {...rest}>
{Panels}
</div>
)
},
)

const panelType = PropTypes.shape({
type: PropTypes.oneOf([TabPanel]),
Expand All @@ -28,6 +42,7 @@ TabPanels.propTypes = {
/** @ignore */
className: PropTypes.string,
/** @ignore */
// @ts-ignore
children: PropTypes.oneOfType([PropTypes.arrayOf(panelType), panelType])
.isRequired,
}
Expand Down
3 changes: 2 additions & 1 deletion libraries/core-react/src/Tabs/Tabs.context.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, { createContext } from 'react'

const TabsContext = createContext({
variant: '',
handleChange: () => {},
/** @param {number} index */
handleChange: (index) => {},
activeTab: 0,
tabsId: '',
})
Expand Down
54 changes: 36 additions & 18 deletions libraries/core-react/src/Tabs/Tabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,49 @@ 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 (
<TabsProvider
value={{
activeTab,
handleChange: onChange,
tabsId,
variant,
}}
>
<div ref={ref} {...props} />
</TabsProvider>
)
})
/**
* @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<any>} ref
* @returns {React.ReactElement}
*/
function Tabs({ activeTab, onChange, variant, ...rest }, ref) {
const tabsId = useMemo(() => createId('tabs-'), [])

return (
<TabsProvider
value={{
activeTab,
handleChange: onChange,
tabsId,
variant,
}}
>
<div ref={ref} {...rest} />
</TabsProvider>
)
},
)

Tabs.propTypes = {
/** The index of the active tab */
activeTab: PropTypes.number,
/** 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,
Expand All @@ -37,6 +54,7 @@ Tabs.propTypes = {
Tabs.defaultProps = {
activeTab: 0,
onChange: () => {},
// @ts-ignore
variant: 'minWidth',
}

Expand Down

0 comments on commit b2fa92b

Please sign in to comment.