diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index 2253eccb5a..5715be61d6 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./tabs/tabs"; @import "./column/column"; @import "./margin/margin"; @import "./gap/gap"; diff --git a/packages/css/src/components/tabs/README.md b/packages/css/src/components/tabs/README.md new file mode 100644 index 0000000000..f1173d97a5 --- /dev/null +++ b/packages/css/src/components/tabs/README.md @@ -0,0 +1,32 @@ + + +# Tabs + +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) diff --git a/packages/css/src/components/tabs/tabs.scss b/packages/css/src/components/tabs/tabs.scss new file mode 100644 index 0000000000..e23466f721 --- /dev/null +++ b/packages/css/src/components/tabs/tabs.scss @@ -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); + } +} diff --git a/packages/react/src/Tabs/README.md b/packages/react/src/Tabs/README.md new file mode 100644 index 0000000000..d817326bf1 --- /dev/null +++ b/packages/react/src/Tabs/README.md @@ -0,0 +1,5 @@ + + +# React Tabs component + +[Tabs documentation](../../../css/src/components/tabs/README.md) diff --git a/packages/react/src/Tabs/Tabs.test.tsx b/packages/react/src/Tabs/Tabs.test.tsx new file mode 100644 index 0000000000..aaafbf7d1e --- /dev/null +++ b/packages/react/src/Tabs/Tabs.test.tsx @@ -0,0 +1,103 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { Tabs } from './Tabs' +import '@testing-library/jest-dom' + +describe('Tabs', () => { + it('renders', () => { + render() + + const component = screen.getByRole('tabs') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + render() + + const component = screen.getByRole('tabs') + + expect(component).toHaveClass('amsterdam-tabs') + }) + + it('renders an additional class name', () => { + render() + + const component = screen.getByRole('tabs') + + expect(component).toHaveClass('amsterdam-tabs extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(ref.current).toBe(component) + }) + + it('supports children', () => { + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ) + + expect(screen.getByRole('tabs')).toBeInTheDocument() + expect(screen.getByRole('tablist')).toBeInTheDocument() + expect(screen.getByRole('tab', { selected: true })).toBeInTheDocument() + expect(screen.getByRole('tabpanel')).toBeInTheDocument() + }) + + it('should select a tab when clicked', async () => { + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ) + + const tabOne = screen.getByRole('tab', { name: 'Tab 1' }) + const tabTwo = screen.getByRole('tab', { name: 'Tab 2' }) + + expect(tabOne).toHaveAttribute('aria-selected', 'true') + expect(tabOne).toHaveAttribute('tabindex', '0') + + expect(tabTwo).toHaveAttribute('aria-selected', 'false') + expect(tabTwo).toHaveAttribute('tabindex', '-1') + + expect(screen.getByRole('tabpanel')).toHaveTextContent('Content 1') + + if (tabTwo) { + fireEvent.click(tabTwo) + } + + expect(tabOne).toHaveAttribute('aria-selected', 'false') + expect(tabOne).toHaveAttribute('tabindex', '-1') + + expect(tabTwo).toHaveAttribute('aria-selected', 'true') + expect(tabTwo).toHaveAttribute('tabindex', '0') + + expect(screen.getByRole('tabpanel')).toHaveTextContent('Content 2') + }) + + it.skip('should forward the onClick event on the Tab', () => { + // This feature has not been implemented yet + }) + + it.skip('should be able to set the active initial tab', () => { + // This feature has not been implemented yet + }) +}) diff --git a/packages/react/src/Tabs/Tabs.tsx b/packages/react/src/Tabs/Tabs.tsx new file mode 100644 index 0000000000..5ad9519c48 --- /dev/null +++ b/packages/react/src/Tabs/Tabs.tsx @@ -0,0 +1,64 @@ +/** + * @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> + +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> + +export const Tabs = forwardRef( + ({ children, className, ...restProps }: TabsProps, ref: ForwardedRef) => { + const tabsId = useId() + const [activeTab, setActiveTab] = useState(0) + const innerRef = useRef(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, + horizontally: true, + }) + + return ( + +
+ {children} +
+
+ ) + }, +) as TabsComponent + +Tabs.List = TabsList +Tabs.Button = TabsButton +Tabs.Panel = TabsPanel +Tabs.displayName = 'Tabs' diff --git a/packages/react/src/Tabs/TabsButton.test.tsx b/packages/react/src/Tabs/TabsButton.test.tsx new file mode 100644 index 0000000000..eb6faa3e11 --- /dev/null +++ b/packages/react/src/Tabs/TabsButton.test.tsx @@ -0,0 +1,63 @@ +import { render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { TabsButton } from './TabsButton' +import '@testing-library/jest-dom' + +describe('Tabs button', () => { + it('renders', () => { + render() + + const component = screen.getByRole('tab') + + expect(component).toBeInTheDocument() + }) + + it('renders a design system BEM class name', () => { + render() + + const component = screen.getByRole('tab') + + expect(component).toHaveClass('amsterdam-tabs__button') + }) + + it('renders an additional class name', () => { + render() + + const component = screen.getByRole('tab') + + expect(component).toHaveClass('amsterdam-tabs__button extra') + }) + + it('renders a label', () => { + render(Label) + + const component = screen.getByRole('tab', { name: 'Label' }) + + expect(component).toBeInTheDocument() + }) + + it('renders the correct id based on the tabs prop', () => { + const { container } = render() + + const component = container.querySelector('#-tab-123') + + expect(component).toBeInTheDocument() + }) + + it('should associate the button with the correct tab', () => { + render() + + const component = screen.getByRole('tab') + + expect(component).toHaveAttribute('aria-controls', '-panel-0') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + render() + + const component = screen.getByRole('tab') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/Tabs/TabsButton.tsx b/packages/react/src/Tabs/TabsButton.tsx new file mode 100644 index 0000000000..18a5ad93cc --- /dev/null +++ b/packages/react/src/Tabs/TabsButton.tsx @@ -0,0 +1,41 @@ +/** + * @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 = { + tab: number +} & PropsWithChildren> + +export const TabsButton = forwardRef( + ({ children, className, tab = 0, ...restProps }: TabsButtonProps, ref: ForwardedRef) => { + const { activeTab, updateTab, tabsId } = useContext(TabsContext) + + return ( + + ) + }, +) + +TabsButton.displayName = 'Tabs.Button' diff --git a/packages/react/src/Tabs/TabsContext.tsx b/packages/react/src/Tabs/TabsContext.tsx new file mode 100644 index 0000000000..ef295b7a34 --- /dev/null +++ b/packages/react/src/Tabs/TabsContext.tsx @@ -0,0 +1,21 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { createContext } from 'react' + +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) diff --git a/packages/react/src/Tabs/TabsList.test.tsx b/packages/react/src/Tabs/TabsList.test.tsx new file mode 100644 index 0000000000..faaac45a2f --- /dev/null +++ b/packages/react/src/Tabs/TabsList.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { TabsList } from './TabsList' +import '@testing-library/jest-dom' + +describe('Tabs list', () => { + it('renders', () => { + render() + + const component = screen.getByRole('tablist') + + expect(component).toBeInTheDocument() + }) + + it('renders a design system BEM class name', () => { + render() + + const component = screen.getByRole('tablist') + + expect(component).toHaveClass('amsterdam-tabs__list') + }) + + it('renders an additional class name', () => { + render() + + const component = screen.getByRole('tablist') + + expect(component).toHaveClass('amsterdam-tabs__list extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const tabsList = container.querySelector('.amsterdam-tabs__list') + + expect(ref.current).toBe(tabsList) + }) +}) diff --git a/packages/react/src/Tabs/TabsList.tsx b/packages/react/src/Tabs/TabsList.tsx new file mode 100644 index 0000000000..f2b69f22ca --- /dev/null +++ b/packages/react/src/Tabs/TabsList.tsx @@ -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> + +export const TabsList = forwardRef( + ({ children, className, ...restProps }: TabsListProps, ref: ForwardedRef) => ( +
+ {children} +
+ ), +) + +TabsList.displayName = 'Tabs.List' diff --git a/packages/react/src/Tabs/TabsPanel.test.tsx b/packages/react/src/Tabs/TabsPanel.test.tsx new file mode 100644 index 0000000000..accfffc186 --- /dev/null +++ b/packages/react/src/Tabs/TabsPanel.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { TabsPanel } from './TabsPanel' +import '@testing-library/jest-dom' + +describe('Tabs panel', () => { + it('renders', () => { + render() + + const component = screen.getByRole('tabpanel') + + expect(component).toBeInTheDocument() + }) + + it('renders a design system BEM class name', () => { + render() + + const component = screen.getByRole('tabpanel') + + expect(component).toHaveClass('amsterdam-tabs__panel') + }) + + it('renders an additional class name', () => { + render() + + const component = screen.getByRole('tabpanel') + + expect(component).toHaveClass('amsterdam-tabs__panel extra') + }) + + it('renders the correct id based on the tabs prop', () => { + const { container } = render() + + const component = container.querySelector('#-panel-0') + + expect(component).toBeInTheDocument() + }) + + it('should associate the tab with the correct button', () => { + render() + + const component = screen.getByRole('tabpanel') + + expect(component).toHaveAttribute('aria-labelledby', '-tab-0') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const tabsPanel = container.querySelector('.amsterdam-tabs__panel') + + expect(ref.current).toBe(tabsPanel) + }) +}) diff --git a/packages/react/src/Tabs/TabsPanel.tsx b/packages/react/src/Tabs/TabsPanel.tsx new file mode 100644 index 0000000000..08fed826a7 --- /dev/null +++ b/packages/react/src/Tabs/TabsPanel.tsx @@ -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> + +export const TabsPanel = forwardRef( + ({ tab, children, className, ...restProps }: TabsPanelProps, ref: ForwardedRef) => { + const { activeTab, tabsId } = useContext(TabsContext) + + if (tab !== activeTab) { + return null + } + + return ( +
+ {children} +
+ ) + }, +) + +TabsPanel.displayName = 'Tabs.Panel' diff --git a/packages/react/src/Tabs/index.ts b/packages/react/src/Tabs/index.ts new file mode 100644 index 0000000000..390086223c --- /dev/null +++ b/packages/react/src/Tabs/index.ts @@ -0,0 +1,5 @@ +export { Tabs } from './Tabs' +export type { TabsProps } from './Tabs' +export type { TabsPanelProps } from './TabsPanel' +export type { TabsListProps } from './TabsList' +export type { TabsButtonProps } from './TabsButton' diff --git a/packages/react/src/common/useFocusWithArrows.test.tsx b/packages/react/src/common/useFocusWithArrows.test.tsx new file mode 100644 index 0000000000..7a46b48ad1 --- /dev/null +++ b/packages/react/src/common/useFocusWithArrows.test.tsx @@ -0,0 +1,85 @@ +import { fireEvent, render } from '@testing-library/react' +import { useRef } from 'react' +import useFocusWithArrows, { KeyboardKeys } from './useFocusWithArrows' + +describe('use focus with arrows', () => { + const onFocusOneMock = jest.fn() + const onFocusTwoMock = jest.fn() + const onFocusThreeMock = jest.fn() + const getComponent = (rotate: boolean | undefined = undefined) => + // eslint-disable-next-line react/display-name + function () { + const ref = useRef(null) + const { keyDown } = useFocusWithArrows({ + ref: ref, + rotating: rotate, + }) + return ( +
+ + + +
+ ) + } + + afterEach(() => { + onFocusOneMock.mockReset() + onFocusTwoMock.mockReset() + onFocusThreeMock.mockReset() + }) + + it('sets focus when using arrow keys', () => { + const Component = getComponent() + const { container } = render() + const firstChild = container.firstChild as HTMLElement + expect(onFocusOneMock).not.toHaveBeenCalled() + + // 4 times, so we can check if there are no other elements focused after reaching the last one + Array.from(Array(4).keys()).forEach(() => { + fireEvent.keyDown(firstChild, { + key: KeyboardKeys.ArrowDown, + }) + }) + + expect(onFocusOneMock).toHaveBeenCalledTimes(1) + expect(onFocusTwoMock).toHaveBeenCalledTimes(1) + expect(onFocusThreeMock).toHaveBeenCalledTimes(1) + + // Same here + Array.from(Array(4).keys()).forEach(() => { + fireEvent.keyDown(firstChild, { + key: KeyboardKeys.ArrowUp, + }) + }) + expect(onFocusTwoMock).toHaveBeenCalledTimes(2) + expect(onFocusOneMock).toHaveBeenCalledTimes(2) + }) + + it('rotates focused elements', () => { + const Component = getComponent(true) + const { container } = render() + const firstChild = container.firstChild as HTMLElement + + Array.from(Array(9).keys()).forEach(() => { + fireEvent.keyDown(firstChild, { + key: KeyboardKeys.ArrowDown, + }) + }) + + expect(onFocusOneMock).toHaveBeenCalledTimes(3) + + Array.from(Array(9).keys()).forEach(() => { + fireEvent.keyDown(firstChild, { + key: KeyboardKeys.ArrowUp, + }) + }) + expect(onFocusOneMock).toHaveBeenCalledTimes(6) + }) +}) diff --git a/packages/react/src/common/useFocusWithArrows.tsx b/packages/react/src/common/useFocusWithArrows.tsx new file mode 100644 index 0000000000..4e619313a8 --- /dev/null +++ b/packages/react/src/common/useFocusWithArrows.tsx @@ -0,0 +1,114 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import type { KeyboardEvent, RefObject } from 'react' + +export const KeyboardKeys = { + ArrowUp: 'ArrowUp', + ArrowDown: 'ArrowDown', + ArrowRight: 'ArrowRight', + ArrowLeft: 'ArrowLeft', + Home: 'Home', + End: 'End', +} + +const FOCUSABLE_ELEMENTS = [ + 'a[href]:not([disabled])', + 'button:not([disabled])', + 'textarea:not([disabled])', + 'input[type="text"]:not([disabled])', + 'input[type="radio"]:not([disabled])', + 'input[type="checkbox"]:not([disabled])', + 'select:not([disabled])', +] + +type FocusWithArrowsOptions = { + ref: RefObject + rotating?: boolean + directChildrenOnly?: boolean + horizontally?: boolean +} + +const useFocusWithArrows = ({ + ref, + rotating = false, + directChildrenOnly = false, + horizontally = false, +}: FocusWithArrowsOptions) => { + const next = horizontally ? KeyboardKeys.ArrowRight : KeyboardKeys.ArrowDown + const previous = horizontally ? KeyboardKeys.ArrowLeft : KeyboardKeys.ArrowUp + const keyDown = (e: KeyboardEvent) => { + if (ref.current) { + const element = ref.current + + const { activeElement } = window.document + const directChildSelector = directChildrenOnly ? ':scope > ' : '' + const focusableEls: Array = Array.from( + element.querySelectorAll(`${directChildSelector}${FOCUSABLE_ELEMENTS.join(`, ${directChildSelector}`)}`), + ) + + const getIndex = (el: Element | null) => { + return el && focusableEls.includes(el) ? focusableEls.indexOf(el) : 0 + } + + let el + + switch (e.key) { + case next: { + if (getIndex(activeElement) !== focusableEls.length - 1) { + el = focusableEls[getIndex(activeElement) + 1] + // If there is nothing focused yet, set the focus on the first element + if (activeElement && !focusableEls.includes(activeElement)) { + el = focusableEls[0] + } + } else if (rotating) { + el = focusableEls[0] + } + + break + } + + case previous: { + if (getIndex(activeElement) !== 0) { + el = focusableEls[getIndex(activeElement) - 1] + } else if (rotating) { + el = focusableEls[focusableEls.length - 1] + } + + break + } + + case KeyboardKeys.Home: { + el = focusableEls[0] + break + } + + case KeyboardKeys.End: { + el = focusableEls[focusableEls.length - 1] + break + } + + default: + } + + if ( + (e.key === KeyboardKeys.ArrowDown || + e.key === KeyboardKeys.ArrowUp || + e.key === KeyboardKeys.ArrowLeft || + e.key === KeyboardKeys.ArrowRight || + e.key === KeyboardKeys.Home || + e.key === KeyboardKeys.End) && + el instanceof HTMLElement + ) { + el.focus() + e.preventDefault() + } + } + } + + return { keyDown } +} + +export default useFocusWithArrows diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 4842ff7820..c2c19ca6f6 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './Tabs' export * from './Column' export * from './Fieldset' export * from './LinkList' diff --git a/proprietary/tokens/src/components/amsterdam/tabs.tokens.json b/proprietary/tokens/src/components/amsterdam/tabs.tokens.json new file mode 100644 index 0000000000..8dc150bff9 --- /dev/null +++ b/proprietary/tokens/src/components/amsterdam/tabs.tokens.json @@ -0,0 +1,34 @@ +{ + "amsterdam": { + "tabs": { + "list": { + "border-bottom": { "value": ".125rem solid {amsterdam.color.primary-blue}" } + }, + "button": { + "background-color": { "value": "transparent" }, + "border": { "value": "none" }, + "color": { "value": "{amsterdam.color.primary-blue}" }, + "cursor": { "value": "{amsterdam.action.activate.cursor}" }, + "font-family": { "value": "{amsterdam.typography.font-family}" }, + "font-weight": { "value": "{amsterdam.typography.font-weight.normal}" }, + "font-size": { "value": "{amsterdam.typography.text-level.5.font-size}" }, + "line-height": { "value": "{amsterdam.typography.text-level.5.line-height}" }, + "outline-offset": { "value": "-0.25rem" }, + "padding-block": { "value": ".5rem" }, + "padding-inline": { "value": "1rem" }, + "hover": { + "color": { "value": "{amsterdam.color.dark-blue}" }, + "box-shadow": { "value": "inset 0 -0.125rem 0 0 {amsterdam.color.dark-blue}" } + }, + "selected": { + "background-color": { "value": "{amsterdam.color.primary-blue}" }, + "color": { "value": "{amsterdam.color.primary-white}" } + }, + "disabled": { + "color": { "value": "{amsterdam.color.neutral-grey2}" }, + "cursor": { "value": "{amsterdam.action.disabled.cursor}" } + } + } + } + } +} diff --git a/storybook/src/components/Tabs/Tabs.docs.mdx b/storybook/src/components/Tabs/Tabs.docs.mdx new file mode 100644 index 0000000000..e086fd4abc --- /dev/null +++ b/storybook/src/components/Tabs/Tabs.docs.mdx @@ -0,0 +1,13 @@ +import { Canvas, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as TabsStories from "./Tabs.stories.tsx"; +import README from "../../../../packages/css/src/components/tabs/README.md?raw"; + + + +{README} + + + +## Tab + + diff --git a/storybook/src/components/Tabs/Tabs.stories.tsx b/storybook/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 0000000000..28205d3f91 --- /dev/null +++ b/storybook/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,100 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { Heading, Paragraph, Tabs } from '@amsterdam/design-system-react' +import { Meta, StoryObj } from '@storybook/react' +import { PropsWithChildren } from 'react' +import { exampleParagraph } from '../shared/exampleContent' + +const SlowPanel = ({ children }: PropsWithChildren) => { + console.log('[ARTIFICIALLY SLOW] Adding a 1000ms delay') + + let startTime = performance.now() + while (performance.now() - startTime < 1000) { + // Do nothing for 1000 ms to emulate extremely slow code + } + + return children +} + +const meta = { + title: 'Components/Containers/Tabs', + component: Tabs, +} satisfies Meta + +export default meta + +const tabMeta = { + component: Tabs.Button, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta + +type Story = StoryObj +type TabStory = StoryObj + +export const Default: Story = { + args: { + children: [ + + Gegevens + Aanslagen + Documenten + Acties + , + +
+ Gegevens + {exampleParagraph()} +
+
, + +
+ Aanslagen + {exampleParagraph()} +
+
, + +
+ Documenten + (This tab panel simulates a load time of 500 milliseconds.) + +
+
, + +
+ Acties + {exampleParagraph()} +
+
, + ], + }, +} + +export const Tab: TabStory = { + args: { + children: 'Gegevens', + tab: 0, + disabled: false, + }, + argTypes: { + children: { + table: { disable: false }, + }, + tab: { + control: { + type: 'number', + min: 0, + max: 9, + }, + }, + }, + render: ({ children, ...args }) => {children}, +}