Skip to content

Commit

Permalink
feat(community-tabs): hr-1595 controlled mode
Browse files Browse the repository at this point in the history
  • Loading branch information
cianfoley-nearform authored and jraff committed Dec 9, 2020
1 parent a587f56 commit 9752181
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 78 deletions.
78 changes: 75 additions & 3 deletions packages/Tabs/Tabs.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { useRef, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import FlexGrid from '@tds/core-flex-grid'

import { safeRest } from '@tds/util-helpers'
import { ChevronRight, ChevronLeft } from '@tds/core-interactive-icon'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import HairlineDivider from '@tds/core-hairline-divider'
import DimpleDivider from '@tds/core-dimple-divider'
import {
TabsContainer,
TabBorder,
Expand Down Expand Up @@ -40,6 +43,53 @@ const Tabs = props => {
const [current, setCurrent] = useState(0)
const { children, leftArrowLabel, rightArrowLabel, ...rest } = props

useEffect(() => {
// if open is null or undefined it is uncontrolled
// empty string may be a valid input to select no tabs (this case is required)
if (props.open === null || props.open === undefined) return
if (!props.children.length) return
const tabIndex = props.children.findIndex(child => child.props.id === props.open)

if (tabIndex >= 0) {
setCurrent(tabIndex)
return
}
// if tabIndex === null set to -1 to keep tabs contolled, but select no tab
setCurrent(-1)
}, [props.open])

const handleBlur = () => {
// on blur in controlled mode, we set the index back to prop value
if (props.open === null || props.open === undefined) return
const tabIndex = props.children.findIndex(child => child.props.id === props.open)
if (tabIndex !== current) {
setCurrent(tabIndex)
}
}

const handleClick = index => {
if (!props.open) {
setCurrent(index) // set internally if not-controlled
return
}
// raise to controlling component to set on click if controlled
props.onOpen(props.children[index].props.id)
}

const handleSelect = (index, previousIndex) => {
// this is for setting the focus in controlled mode
// we need to temporarily set the index (f will undo)
// only if both the newTab and previous are the same, was the tab actually clicked
// and we can raise up the event.
setCurrent(index)
const newTab = props.children[index]
const previousTab = props.children[previousIndex]
if (newTab === previousTab) {
// this is on a tab switch
props.onOpen(newTab.props.id)
}
}

const getTabsWidth = () => {
let tabsWidthValue = 0
const tabsArray =
Expand Down Expand Up @@ -86,12 +136,14 @@ const Tabs = props => {

const handleTabsKeyUp = (e, i) => {
if (e.keyCode === ENTER_KEY || e.keyCode === SPACE_BAR_KEY) {
setCurrent(i)
handleClick(i)
}
if (e.target.offsetLeft <= MARGIN_BUFFER) {
// eslint-disable-next-line consistent-return
return setTabsTranslatePosition(0)
}
setTabsTranslatePosition(-e.target.offsetLeft + MARGIN_BUFFER)
// eslint-disable-next-line consistent-return
return getTabsWidth()
}

Expand Down Expand Up @@ -132,10 +184,14 @@ const Tabs = props => {
return props.children.map((tab, i) => {
return (
<Tab
id={tab.props.id}
key={hash(i)}
onKeyUp={e => handleTabsKeyUp(e, i)}
onClick={() => setCurrent(i)}
onClick={() => {
handleClick(i)
}}
aria-label={tab.props.heading}
onBlur={handleBlur}
>
<TabLabel>{tab.props.heading}</TabLabel>
</Tab>
Expand Down Expand Up @@ -169,6 +225,7 @@ const Tabs = props => {
setTimeout(() => getTabsWidth(), 100)
}
}, [])

return (
<TabsContainer {...safeRest(rest)} ref={tabsRoot}>
<FlexGrid gutter={false}>
Expand All @@ -187,12 +244,17 @@ const Tabs = props => {
</ArrowInner>
</TabArrows>
)}
<ReactTabs>
<ReactTabs
selectedIndex={props.open && current}
onSelect={props.onOpen && handleSelect}
>
<TabBorder>
<TabListContainer ref={tabRef} positionToMove={tabsTranslatePosition}>
<TabList style={{ width: tabsContainerWidth }}>{mapTabs()}</TabList>
</TabListContainer>
</TabBorder>
<HairlineDivider />
<DimpleDivider />
{mapTabContent()}
</ReactTabs>
{isRightArrowVisible && (
Expand Down Expand Up @@ -222,11 +284,21 @@ Tabs.propTypes = {
children: PropTypes.node.isRequired,
leftArrowLabel: PropTypes.string,
rightArrowLabel: PropTypes.string,
/**
* Set the selected tab by id
*/
open: PropTypes.string,
/**
* Event raised on tab click
*/
onOpen: PropTypes.func,
}

Tabs.defaultProps = {
leftArrowLabel: 'Move menu to the left',
rightArrowLabel: 'Move menu to the right',
open: null,
onOpen: null,
}

Tabs.Panel = Panel
Expand Down
55 changes: 55 additions & 0 deletions packages/Tabs/Tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,58 @@ Note that the `copy` prop must be provided at all times for the correct accessib
</Tabs.Panel>
</Tabs>
```

### Controlled Example

Use `open` and `onOpen` to control the component externally.

Specify an `id` (string) on each `<Tab.Panel>` and pass the `id` of the panel with you wish to have opened programmatically to the `<Tabs>` component.

On selection of a tab, `onOpen` will be called with the first argument containing the `id` of the tab clicked.

```jsx
const ControlledTabsExample = () => {
const [open, setOpen] = React.useState('a-la-carte')

const handleOpen = id => {
setOpen(id)
}

return (
<>
<Tabs copy="en" open={open} onOpen={handleOpen}>
<Tabs.Panel id="themepacks" heading="Themepacks" />
<Tabs.Panel id="premium" heading="Premium" />
<Tabs.Panel id="a-la-carte" heading="A-la-carte" />
<Tabs.Panel id="essentials" heading="Essentials" />
<Tabs.Panel id="more" heading="More Content" />
<Tabs.Panel id="more-again" heading="More content again" />
<Tabs.Panel id="even-more" heading="Even more content" />
</Tabs>
<FlexGrid>
<FlexGrid.Row>
<FlexGrid.Col xs={12}>
<Box below={3}>
<Text>
You selected tab: <Strong>{open}</Strong>
</Text>
</Box>
</FlexGrid.Col>
</FlexGrid.Row>
<FlexGrid.Row>
<FlexGrid.Col xs={12}>
<Button onClick={() => handleOpen('no tab')}>Select no tab</Button>
</FlexGrid.Col>
</FlexGrid.Row>
</FlexGrid>
</>
)
}
;<ControlledTabsExample />
```

### Accessibility

- When using Tabs, the consuming application should allow hashes in the url to automatically load a tab. Eg. `https://t.com#premium` should load the Premium tab.

- The application should also change the page url to include the hash as tabs change
Loading

0 comments on commit 9752181

Please sign in to comment.