Skip to content

Commit

Permalink
fix: make #tabs horizontal scrollable (#699)
Browse files Browse the repository at this point in the history
* fix: make #tabs not wrap anymore, but add a horizontal scroll experience

* docs: add scrollable #tabs example and tests

* chore: update outdated snapshots

* fix: only use edged scroll nav buttons if #tabs is used from browser edge to edge
  • Loading branch information
tujoworker authored Nov 12, 2020
1 parent c2d0096 commit c4b9ddb
Show file tree
Hide file tree
Showing 17 changed files with 1,176 additions and 427 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,59 @@
showTabs: true
---

import TabsExamples from 'Pages/uilib/components/tabs/Examples'
import {
TabsExampleContentObject,
TabsExampleScrollable,
TabsExampleLeftAligned,
TabsExampleUsingData,
TabsExampleRightAligned,
TabsExampleReachRouterNavigation,
TabsExampleReactRouterNavigation,
} from 'Pages/uilib/components/tabs/Examples'

## Demos

<TabsExamples />
### Tabs using 'data' property and content object

<TabsExampleContentObject />

### Tabs using 'data' property only

<TabsExampleUsingData />

### Tabs using React Components only

<TabsExampleLeftAligned />

### Right aligned tabs

<TabsExampleRightAligned />

### Tabs optimized for mobile

Depending on your setup, you may have to align your Tabs all the way to the edge of the browser window. E.g. with a negative margin:

```css
@media screen and (min-width: 40em) {
.dnb-tabs .dnb-tabs__tabs {
margin: 0 -4rem;
}
}
```

<TabsExampleScrollable />

### Router navigation with Reach Router

This demo uses `@reach/router`. More [examples on CodeSandbox](https://codesandbox.io/embed/8z8xov7xyj).

<TabsExampleReachRouterNavigation />

### Router navigation with react-router-dom

This demo uses `react-router-dom`. More [examples on CodeSandbox](https://codesandbox.io/embed/8z8xov7xyj).

<TabsExampleReactRouterNavigation />

## Example Content

Expand Down
184 changes: 150 additions & 34 deletions packages/dnb-ui-lib/src/components/tabs/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createSkeletonClass,
skeletonDOMAttributes
} from '../skeleton/SkeletonHelper'
import Button from '../button/Button'

export default class Tabs extends React.PureComponent {
static tagName = 'dnb-tabs'
Expand Down Expand Up @@ -264,6 +265,8 @@ export default class Tabs extends React.PureComponent {
}

this.state = {
hasScrollbar: false,
atEdge: false,
_listenForPropChanges: true,
selected_key,
_selected_key: selected_key,
Expand All @@ -274,8 +277,51 @@ export default class Tabs extends React.PureComponent {
this._tablistRef = React.createRef()
}

componentDidMount() {
this.addScrollBehaviour()
this.scrollToTab()
}

onScrollHandler = () => {
const hasScrollbar = this.hasScrollbar()
if (hasScrollbar !== this.state.hasScrollbar) {
this.setState({
hasScrollbar
})
}
this.setState({
atEdge: this.isAtEdge()
})
}

hasScrollbar() {
return (
this._tablistRef.current.scrollWidth >
this._tablistRef.current.offsetWidth
)
}

isAtEdge() {
if (typeof window === 'undefined') {
return false
}
return this._tablistRef.current.offsetWidth >= window.innerWidth - 32
}

componentWillUnmount() {
clearTimeout(this._setFocusOnTablistId)
clearTimeout(this._scrollToTabTimeout)
clearTimeout(this._setFocusOnTablistFirst)
clearTimeout(this._setFocusOnTablistSecond)
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.onScrollHandler)
}
}

addScrollBehaviour() {
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.onScrollHandler)
}
this.onScrollHandler()
}

onKeyDownHandler = (e) => {
Expand Down Expand Up @@ -304,7 +350,34 @@ export default class Tabs extends React.PureComponent {
this.openTab(+1, e, 'step')
}

scrollToTop() {
scrollToTab() {
clearTimeout(this._scrollToTabTimeout)
this._scrollToTabTimeout = setTimeout(() => {
if (this.state.hasScrollbar) {
try {
const isFirst = this._tablistRef.current
.querySelector('.dnb-tabs__button__snap:first-of-type button')
.classList.contains('selected')
const isLast = this._tablistRef.current
.querySelector('.dnb-tabs__button__snap:last-of-type button')
.classList.contains('selected')

this.setState({
isFirst,
isLast
})

const elem = this._tablistRef.current.querySelector(
'.dnb-tabs__button.selected'
)
this._tablistRef.current.scrollLeft =
elem && !isFirst ? elem.offsetLeft : 0
} catch (e) {
warn(e)
}
}
}, 1) // delay because chrome does not react during click

if (
isTrue(this.props.scroll) &&
this._tablistRef.current &&
Expand All @@ -319,13 +392,14 @@ export default class Tabs extends React.PureComponent {

setFocusOnTablist = () => {
if (typeof document !== 'undefined') {
setTimeout(() => {
clearTimeout(this._setFocusOnTablistFirst)
this._setFocusOnTablistFirst = setTimeout(() => {
if (this._tablistRef.current) {
this._tablistRef.current.focus()
}
}, 1) // to make sure we don't "flicker"
clearTimeout(this._setFocusOnTablistId)
this._setFocusOnTablistId = setTimeout(() => {
clearTimeout(this._setFocusOnTablistSecond)
this._setFocusOnTablistSecond = setTimeout(() => {
if (this._tablistRef.current) {
this._tablistRef.current.focus()
}
Expand Down Expand Up @@ -382,10 +456,15 @@ export default class Tabs extends React.PureComponent {
}

if (selected_key) {
this.setState({
selected_key,
_listenForPropChanges: false
})
this.setState(
{
selected_key,
_listenForPropChanges: false
},
() => {
this.scrollToTab()
}
)
}

dispatchCustomElementEvent(this, 'on_change', {
Expand All @@ -404,8 +483,6 @@ export default class Tabs extends React.PureComponent {
warn('Tabs Error:', e)
}
}

this.scrollToTop()
}

isSelected(tabKey) {
Expand Down Expand Up @@ -538,6 +615,7 @@ export default class Tabs extends React.PureComponent {

TabsListHandler = ({ children, className }) => {
const { align, section_style, section_spacing } = this.props
const { hasScrollbar, atEdge } = this.state

return (
<div
Expand All @@ -552,10 +630,28 @@ export default class Tabs extends React.PureComponent {
isTrue(section_spacing) ? 'default' : section_spacing
}`
: null,
hasScrollbar && 'dnb-tabs--has-scrollbar',
atEdge && 'dnb-tabs--at-edge',
className
)}
>
<ScrollNavButton
onMouseDown={this.prevTab}
icon="chevron_left"
className={classnames(
hasScrollbar && 'dnb-tabs__scroll-nav-button--visible',
this.state.isFirst && 'dnb-tabs__scroll-nav-button--hide'
)}
/>
{children}
<ScrollNavButton
onMouseDown={this.nextTab}
icon="chevron_right"
className={classnames(
hasScrollbar && 'dnb-tabs__scroll-nav-button--visible',
this.state.isLast && 'dnb-tabs__scroll-nav-button--hide'
)}
/>
</div>
)
}
Expand Down Expand Up @@ -595,32 +691,32 @@ export default class Tabs extends React.PureComponent {
skeletonDOMAttributes(itemParams, skeleton, this.context)

return (
<button
type="button"
role="tab"
tabIndex="-1"
id={`${this._id}-tab-${key}`}
aria-selected={isSelected}
className={classnames(
'dnb-tabs__button',
// createSkeletonClass('font', skeleton, this.context),
isSelected && 'selected'
)}
onClick={this.openTabByDOM}
key={`tab-${key}`}
data-tab-key={key}
{...itemParams}
>
<span
<div className="dnb-tabs__button__snap" key={`tab-${key}`}>
<button
type="button"
role="tab"
tabIndex="-1"
id={`${this._id}-tab-${key}`}
aria-selected={isSelected}
className={classnames(
'dnb-tabs__button__title',
createSkeletonClass('font', skeleton, this.context)
'dnb-tabs__button',
isSelected && 'selected'
)}
onClick={this.openTabByDOM}
data-tab-key={key}
{...itemParams}
>
{title}
</span>
<Dummy>{title}</Dummy>
</button>
<span
className={classnames(
'dnb-tabs__button__title',
createSkeletonClass('font', skeleton, this.context)
)}
>
{title}
</span>
<Dummy>{title}</Dummy>
</button>
</div>
)
}
)
Expand Down Expand Up @@ -800,3 +896,23 @@ export const Dummy = ({ children }) => {
Dummy.propTypes = {
children: PropTypes.node.isRequired
}

const ScrollNavButton = (props) => {
return (
<Button
size="medium"
variant="primary"
tabIndex="-1"
bounding
aria-hidden
{...props}
className={classnames(
'dnb-tabs__scroll-nav-button',
props.className
)}
/>
)
}
ScrollNavButton.propTypes = {
className: PropTypes.node.isRequired
}
Loading

0 comments on commit c4b9ddb

Please sign in to comment.