Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Tabs component #1068

Merged
merged 59 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
240f73b
Scaffold and basic styling
dlnr Feb 6, 2024
887f636
Tab story
dlnr Feb 6, 2024
564aa29
Working on interaction
dlnr Feb 7, 2024
3a9eed2
Switching tabs with context
dlnr Feb 8, 2024
ab8462e
Context in external file, cleanup
dlnr Feb 8, 2024
6fc764c
Keyboard navigation
dlnr Feb 8, 2024
e7cbbbd
Home and End button
dlnr Feb 8, 2024
13b4558
Mobile overflow
dlnr Feb 9, 2024
ef97f36
Hover effect and some docs
dlnr Feb 9, 2024
dafeb4d
Typo
dlnr Feb 9, 2024
f08368e
Single tab
dlnr Feb 9, 2024
503e5b9
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 12, 2024
a72cfe5
typography and underline fixes
dlnr Feb 12, 2024
67ff1ca
Overflow not for desktop
dlnr Feb 12, 2024
8007243
Testing rendering
dlnr Feb 12, 2024
f5a94c9
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 14, 2024
149b955
Refactor focus navigation in Tabs component
dlnr Feb 14, 2024
5867785
Move useFocusWithArrows to common folder
dlnr Feb 14, 2024
89f2e4e
comments
dlnr Feb 15, 2024
191aca5
Resolved some comments and added test
dlnr Feb 15, 2024
e396ee7
getByRole tests
dlnr Feb 15, 2024
4f4f19d
Panel padding dev rules removed
dlnr Feb 15, 2024
238deac
Refactor TabsProps type definition
dlnr Feb 15, 2024
c06bf22
Update copyright year to 2024
dlnr Feb 19, 2024
e84026b
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 19, 2024
f976245
Refactor tab button hover styles
dlnr Feb 19, 2024
a3d56c5
Update styling in Tabs.Panel
dlnr Feb 19, 2024
8e6a80a
Merge branch 'develop' into feature/DES-607-Tabs
VincentSmedinga Feb 19, 2024
3a3fe7a
Keyboard navigation test
dlnr Feb 20, 2024
eefdf1d
Plop template updates
dlnr Feb 20, 2024
e4c2f8a
Tabs use unique ID
dlnr Feb 20, 2024
6d07910
Keyboard nav options
dlnr Feb 20, 2024
8858980
Horizontal nav
dlnr Feb 20, 2024
44918e4
Refactor tabs focus styles and update Story title
dlnr Feb 20, 2024
6d72933
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 20, 2024
7c3fb2d
Show scrollbar
dlnr Feb 21, 2024
b4378c4
Resolved various comments
dlnr Feb 22, 2024
a0211e7
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 22, 2024
1b4af04
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Feb 27, 2024
d0b3972
Resolved comments
dlnr Feb 27, 2024
2984fe8
Fix table of contents and a documentation sentence
VincentSmedinga Feb 27, 2024
d03c52a
Disabled control
dlnr Feb 27, 2024
6cb80a0
Merge branch 'feature/DES-607-Tabs' of https://github.com/Amsterdam/d…
dlnr Feb 27, 2024
023414b
Tab set to 0 to show selected state
dlnr Feb 27, 2024
8283ee5
Remove link-appearance tokens
dlnr Feb 27, 2024
19d8bdc
License header
dlnr Feb 27, 2024
f2e74a0
Licensing
dlnr Feb 27, 2024
3bdac54
Refactor SlowTab
alimpens Feb 27, 2024
e94f714
Update proprietary/tokens/src/components/amsterdam/tabs.tokens.json
dlnr Feb 27, 2024
40efe3a
ChildComponent tests
dlnr Feb 28, 2024
9e29083
Refactor Tabs component structure
dlnr Feb 28, 2024
9342fd4
Add TabsButton tests
alimpens Feb 28, 2024
49e337e
Fix string literal
alimpens Feb 28, 2024
9117745
Add tabspanel test
alimpens Feb 28, 2024
e2872f2
Add test for changing panels
alimpens Feb 28, 2024
281613e
Merge branch 'feature/DES-607-Tabs' of https://github.com/Amsterdam/d…
alimpens Feb 28, 2024
437b253
Use children instead of label
alimpens Feb 28, 2024
3df2331
Omit default param
alimpens Feb 28, 2024
66936be
Merge branch 'develop' into feature/DES-607-Tabs
VincentSmedinga Feb 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./tabs/tabs";
@import "./column/column";
@import "./margin/margin";
@import "./gap/gap";
Expand Down
30 changes: 30 additions & 0 deletions packages/css/src/components/tabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Tabs
alimpens marked this conversation as resolved.
Show resolved Hide resolved

Tabs are used to bundle related content in a compact overview within a page. Each tab has a short name, and these names indicate the relationship between the information displayed in each tab.

## How to Use

- The content of each tab is viewable independently, not in the context of one another.
- The content within each tab should have a similar structure.
- Use when there is limited visual space and content needs to be divided into sections.

You can navigate tabs with your keyboard:

| key | element |
| :------------- | :--------------------------------------------- |
| Enter or Space | Open or close the tab that has the focus |
| Tab | Go to the next element that can have focus |
| Shift + Tab | Go to the next element that can have focus |
| Left arrow | If the tabs have focus: go to the previous tab |
| Right arrow | If the tabs have focus: go to the next tab |
| Home | If the tabs have focus: go to the first tab |
| End | If the tabs have focus, go to the last tab |

### Caution

Do not use tabs if the content in each tab functions just as well on separate pages.

## References

- [W3C - Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
- [MDN - Tab Aria Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role)
41 changes: 41 additions & 0 deletions packages/css/src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@import "../../common/breakpoint";

.amsterdam-tabs__list {
border-bottom: var(--amsterdam-tabs-list-border-bottom);
display: flex;
overflow-x: auto;
}

.amsterdam-tabs__button {
background-color: var(--amsterdam-tabs-button-background-color);
border: var(--amsterdam-tabs-button-border);
color: var(--amsterdam-tabs-button-color);
cursor: var(--amsterdam-tabs-button-cursor);
font-family: var(--amsterdam-tabs-button-font-family);
font-size: var(--amsterdam-tabs-button-font-size);
font-weight: var(--amsterdam-tabs-button-font-weight);
line-height: var(--amsterdam-tabs-button-line-height);
outline-offset: var(--amsterdam-tabs-button-outline-offset);
padding-block: var(--amsterdam-tabs-button-padding-block);
padding-inline: var(--amsterdam-tabs-button-padding-inline);

&:disabled {
color: var(--amsterdam-tabs-button-disabled-color);
cursor: var(--amsterdam-tab-button-disabled-cursor);
}

&:hover:not([aria-selected="true"], [disabled]) {
box-shadow: var(--amsterdam-tabs-button-hover-box-shadow);
color: var(--amsterdam-tabs-button-hover-color);
}

&[aria-selected="true"] {
background-color: var(--amsterdam-tabs-button-selected-background-color);
color: var(--amsterdam-tabs-button-selected-color);
}
}
3 changes: 3 additions & 0 deletions packages/react/src/Tabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# React Tabs component
alimpens marked this conversation as resolved.
Show resolved Hide resolved

[Tabs documentation](../../../css/src/components/tabs/README.md)
76 changes: 76 additions & 0 deletions packages/react/src/Tabs/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Tabs } from './Tabs'
import '@testing-library/jest-dom'

describe('Tabs', () => {
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
it('renders', () => {
render(<Tabs />)

const component = screen.getByRole('tabs')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
render(<Tabs />)

const component = screen.getByRole('tabs')

expect(component).toHaveClass('amsterdam-tabs')
})

it('renders an additional class name', () => {
render(<Tabs className="extra" />)

const component = screen.getByRole('tabs')

expect(component).toHaveClass('amsterdam-tabs extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLDivElement>()

const { container } = render(<Tabs ref={ref} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})

it('supports children', () => {
render(
<Tabs>
<Tabs.List>
<Tabs.Button tab={0} label="Tab 1" />
<Tabs.Button tab={1} label="Tab 2" />
</Tabs.List>
<Tabs.Panel tab={0}>Content 1</Tabs.Panel>
<Tabs.Panel tab={2}>Content 2</Tabs.Panel>
</Tabs>,
)

expect(screen.getByRole('tabs')).toBeInTheDocument()
expect(screen.getByRole('tablist')).toBeInTheDocument()
expect(screen.getByRole('tab', { selected: true })).toBeInTheDocument()
expect(screen.getByRole('tabpanel')).toBeInTheDocument()
})

it('should render its subcomponents', () => {
render(
<Tabs>
<Tabs.List>
<Tabs.Button tab={0} label="Tab 1" />
<Tabs.Button tab={1} label="Tab 2" />
</Tabs.List>
<Tabs.Panel tab={0}>Content 1</Tabs.Panel>
<Tabs.Panel tab={1}>Content 2</Tabs.Panel>
</Tabs>,
)

expect(screen.getByRole('tab', { name: 'Tab 1', selected: true })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Tab 2', selected: false })).toBeInTheDocument()
expect(screen.getByRole('tabpanel')).toBeInTheDocument()
})
})
alimpens marked this conversation as resolved.
Show resolved Hide resolved
65 changes: 65 additions & 0 deletions packages/react/src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef, useId, useImperativeHandle, useRef, useState } from 'react'
import type { ForwardedRef, ForwardRefExoticComponent, HTMLAttributes, PropsWithChildren, RefAttributes } from 'react'
import { TabsButton } from './TabsButton'
import { TabsContext } from './TabsContext'
import { TabsList } from './TabsList'
import { TabsPanel } from './TabsPanel'
import useFocusWithArrows from '../common/useFocusWithArrows'

export type TabsProps = PropsWithChildren<HTMLAttributes<HTMLDivElement>>

type TabsComponent = {
/** Always use a TabList to hold the Tab Buttons */
List: typeof TabsList
/** Use a TabButton for each tab */
Button: typeof TabsButton
/** A TabsPanel will only return its contents when the corresponding TabsButton is activated */
Panel: typeof TabsPanel
} & ForwardRefExoticComponent<TabsProps & RefAttributes<HTMLDivElement>>

export const Tabs = forwardRef(
({ children, className, ...restProps }: TabsProps, ref: ForwardedRef<HTMLDivElement>) => {
const tabsId = useId()
const [activeTab, setActiveTab] = useState(0)
const innerRef = useRef<HTMLDivElement>(null)

const updateTab = (tab: number) => {
setActiveTab(tab)
}

// use a passed ref if it's there, otherwise use innerRef
useImperativeHandle(ref, () => innerRef.current as HTMLDivElement)

const { keyDown } = useFocusWithArrows({
ref: innerRef,
rotating: true,
directChildrenOnly: false,
alimpens marked this conversation as resolved.
Show resolved Hide resolved
horizontally: true,
})

return (
<TabsContext.Provider value={{ activeTab, updateTab, tabsId }}>
<div
{...restProps}
role="tabs"
ref={innerRef}
onKeyDown={keyDown}
className={clsx('amsterdam-tabs', className)}
>
{children}
</div>
</TabsContext.Provider>
)
},
) as TabsComponent

Tabs.List = TabsList
Tabs.Button = TabsButton
Tabs.Panel = TabsPanel
Tabs.displayName = 'Tabs'
42 changes: 42 additions & 0 deletions packages/react/src/Tabs/TabsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef, startTransition, useContext } from 'react'
import type { ButtonHTMLAttributes, ForwardedRef, PropsWithChildren } from 'react'
import { TabsContext } from './TabsContext'

export type TabsButtonProps = {
label: string
tab: number
} & PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>

export const TabsButton = forwardRef(
({ label, tab = 0, className, ...restProps }: TabsButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
const { activeTab, updateTab, tabsId } = useContext(TabsContext)

return (
<button
{...restProps}
role="tab"
id={`{tabsId}-tab-${tab}`}
aria-controls={`${tabsId}-panel-${tab}`}
aria-selected={activeTab === tab}
tabIndex={activeTab === tab ? 0 : -1}
ref={ref}
onClick={() => {
startTransition(() => {
updateTab(tab)
})
}}
className={clsx('amsterdam-tabs__button', className)}
>
{label}
alimpens marked this conversation as resolved.
Show resolved Hide resolved
</button>
)
},
)

TabsButton.displayName = 'Tabs.Button'
21 changes: 21 additions & 0 deletions packages/react/src/Tabs/TabsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { createContext } from 'react'
alimpens marked this conversation as resolved.
Show resolved Hide resolved

export type TabsContextValue = {
activeTab: number
// eslint-disable-next-line no-unused-vars
updateTab: (tab: number) => void
tabsId: string
}

const defaultValues: TabsContextValue = {
activeTab: 0,
updateTab: () => {},
tabsId: '',
}

export const TabsContext = createContext(defaultValues)
20 changes: 20 additions & 0 deletions packages/react/src/Tabs/TabsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'

export type TabsListProps = PropsWithChildren<HTMLAttributes<HTMLDivElement>>

export const TabsList = forwardRef(
({ children, className, ...restProps }: TabsListProps, ref: ForwardedRef<HTMLDivElement>) => (
<div {...restProps} role="tablist" ref={ref} className={clsx('amsterdam-tabs__list', className)}>
{children}
</div>
),
)

TabsList.displayName = 'Tabs.List'
39 changes: 39 additions & 0 deletions packages/react/src/Tabs/TabsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef, useContext } from 'react'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'
import { TabsContext } from './TabsContext'

export type TabsPanelProps = {
tab: number
} & PropsWithChildren<HTMLAttributes<HTMLDivElement>>

export const TabsPanel = forwardRef(
({ tab, children, className, ...restProps }: TabsPanelProps, ref: ForwardedRef<HTMLDivElement>) => {
const { activeTab, tabsId } = useContext(TabsContext)

if (tab !== activeTab) {
return null
}

return (
<div
{...restProps}
role="tabpanel"
aria-labelledby={`${tabsId}-tab-${tab}`}
alimpens marked this conversation as resolved.
Show resolved Hide resolved
id={`${tabsId}-panel-${tab}`}
tabIndex={0}
ref={ref}
className={clsx('amsterdam-tabs__panel', className)}
>
{children}
</div>
)
},
)

TabsPanel.displayName = 'Tabs.Panel'
5 changes: 5 additions & 0 deletions packages/react/src/Tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { Tabs } from './Tabs'
export type { TabsProps } from './Tabs'
alimpens marked this conversation as resolved.
Show resolved Hide resolved
export type { TabsPanelProps } from './TabsPanel'
export type { TabsListProps } from './TabsList'
export type { TabsButtonProps } from './TabsButton'
Loading
Loading