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: Allow a specific Tab to be made active #1258

Merged
merged 23 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1b349df
feat(tabs): :sparkles: enable setting active tab
RubenSibon Jun 12, 2024
d6fc9fe
docs(tabs): :memo: add set initial active tab story
RubenSibon Jun 12, 2024
77e47f1
Merge branch 'develop' of github.com:Amsterdam/design-system into fea…
RubenSibon Jun 12, 2024
51c314d
Finetune documentation
VincentSmedinga Jun 12, 2024
e53d91a
refactor(tabs): :truck: rename state var
RubenSibon Jun 14, 2024
2890041
Merge branch 'feat/DES-676-initial-active-tab' of github.com:Amsterda…
RubenSibon Jun 14, 2024
a1bcada
refactor(tabs): :recycle: loop over tab comps & content
RubenSibon Jun 14, 2024
18bf3cf
refactor(react): :truck: sync set state fn with state var
RubenSibon Jun 14, 2024
d60c36a
Merge branch 'develop' of github.com:Amsterdam/design-system into fea…
RubenSibon Jun 14, 2024
b337ce3
fix(story): :adhesive_bandage: do not nest paragraphs
RubenSibon Jun 14, 2024
d31fb85
Merge branch 'develop' of github.com:Amsterdam/design-system into fea…
RubenSibon Jun 14, 2024
0e87a8d
fix(react): :bug: guard against invalid active tab numbers
RubenSibon Jun 14, 2024
812cc19
fix(react): :bug: check if active tab is number
RubenSibon Jun 14, 2024
7d45a66
refactor(story): :recycle: do not hard-code control max
RubenSibon Jun 14, 2024
79ff1d6
refactor(story): :recycle: do not rename imported fn
RubenSibon Jun 14, 2024
b472a5d
test(react): :white_check_mark: meaningfully name selected tabs
RubenSibon Jun 14, 2024
59e43b9
Use extracted variable when logging
VincentSmedinga Jun 15, 2024
30c1582
Improve variable name
VincentSmedinga Jun 15, 2024
353d613
Hide tab number in Button example
VincentSmedinga Jun 15, 2024
895d31d
Remove unnecessary and buggy Button story
VincentSmedinga Jun 15, 2024
4044057
Set children correctly in meta
VincentSmedinga Jun 15, 2024
c03b74e
Mention the `tab` prop in docs
VincentSmedinga Jun 15, 2024
1d0190c
Merge branch 'develop' into feat/DES-676-initial-active-tab
RubenSibon Jun 17, 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
28 changes: 26 additions & 2 deletions packages/react/src/Tabs/Tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,31 @@ describe('Tabs', () => {
// 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
it('should be able to set the active initial tab', () => {
render(
<Tabs activeTab={2}>
<Tabs.List>
<Tabs.Button tab={0}>Tab 1</Tabs.Button>
<Tabs.Button tab={1}>Tab 2</Tabs.Button>
<Tabs.Button tab={2}>Tab 3</Tabs.Button>
<Tabs.Button tab={3}>Tab 4</Tabs.Button>
</Tabs.List>
<Tabs.Panel tab={0}>Content 1</Tabs.Panel>
<Tabs.Panel tab={1}>Content 2</Tabs.Panel>
<Tabs.Panel tab={2}>Content 3</Tabs.Panel>
<Tabs.Panel tab={3}>Content 4</Tabs.Panel>
</Tabs>,
)

const tabOne = screen.getByRole('tab', { name: 'Tab 1' })
const tabThree = screen.getByRole('tab', { name: 'Tab 3' })

expect(tabOne).toHaveAttribute('aria-selected', 'false')
expect(tabOne).toHaveAttribute('tabindex', '-1')

expect(tabThree).toHaveAttribute('aria-selected', 'true')
expect(tabThree).toHaveAttribute('tabindex', '0')

expect(screen.getByRole('tabpanel')).toHaveTextContent('Content 3')
})
})
59 changes: 32 additions & 27 deletions packages/react/src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,38 @@ import { TabsList } from './TabsList'
import { TabsPanel } from './TabsPanel'
import { useKeyboardFocus } from '../common/useKeyboardFocus'

export type TabsProps = PropsWithChildren<HTMLAttributes<HTMLDivElement>>

const TabsRoot = 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 } = useKeyboardFocus(innerRef, {
rotating: true,
horizontally: true,
})

return (
<TabsContext.Provider value={{ activeTab, updateTab, tabsId }}>
<div {...restProps} role="tabs" ref={innerRef} onKeyDown={keyDown} className={clsx('ams-tabs', className)}>
{children}
</div>
</TabsContext.Provider>
)
})
export type TabsProps = {
/** The initially active tab. */
activeTab?: number
} & PropsWithChildren<HTMLAttributes<HTMLDivElement>>

const TabsRoot = forwardRef(
({ activeTab, children, className, ...restProps }: TabsProps, ref: ForwardedRef<HTMLDivElement>) => {
const tabsId = useId()
const [_activeTab, setActiveTab] = useState(activeTab ?? 0)
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
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 } = useKeyboardFocus(innerRef, {
rotating: true,
horizontally: true,
})

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

TabsRoot.displayName = 'Tabs'

Expand Down
12 changes: 6 additions & 6 deletions packages/react/src/Tabs/TabsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ export type TabsButtonProps = {

export const TabsButton = forwardRef(
({ children, className, tab = 0, ...restProps }: TabsButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
const { activeTab, updateTab, tabsId } = useContext(TabsContext)
const { activeTab, tabsId, updateTab } = 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}
className={clsx('ams-tabs__button', className)}
id={`${tabsId}-tab-${tab}`}
onClick={() => {
startTransition(() => {
updateTab(tab)
})
}}
className={clsx('ams-tabs__button', className)}
ref={ref}
role="tab"
tabIndex={activeTab === tab ? 0 : -1}
>
{children}
</button>
Expand Down
11 changes: 9 additions & 2 deletions storybook/src/components/Tabs/Tabs.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import README from "../../../../packages/css/src/components/tabs/README.md?raw";

<Primary />

## Tab
## With an initial tab

<Canvas of={TabsStories.Tab} />
The first tab is active by default.
Another tab’s panel can be displayed initially as well.

<Canvas of={TabsStories.WithInitialTab} />

## Tab Button

<Canvas of={TabsStories.TabButton} />
99 changes: 61 additions & 38 deletions storybook/src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { Meta, StoryObj } from '@storybook/react'
import { PropsWithChildren } from 'react'
import { exampleParagraph } from '../shared/exampleContent'

const slowPanelDelay = 1000
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

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
while (performance.now() - startTime < slowPanelDelay) {
/* Emulate slow code. */
}

return children
Expand All @@ -22,6 +24,15 @@ const SlowPanel = ({ children }: PropsWithChildren) => {
const meta = {
title: 'Components/Containers/Tabs',
component: Tabs,
argTypes: {
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
activeTab: {
control: {
type: 'number',
min: 0,
max: 3,
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
} satisfies Meta<typeof Tabs>

export default meta
Expand All @@ -40,59 +51,71 @@ const tabMeta = {
type Story = StoryObj<typeof meta>
type TabStory = StoryObj<typeof tabMeta>

const defaultTabs = [
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
<Tabs.List key={0}>
<Tabs.Button tab={0}>Gegevens</Tabs.Button>
<Tabs.Button tab={1}>Aanslagen</Tabs.Button>
<Tabs.Button tab={2}>Documenten</Tabs.Button>
<Tabs.Button tab={3}>Acties</Tabs.Button>
</Tabs.List>,
<Tabs.Panel tab={0} key={1}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Gegevens</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
<Tabs.Panel tab={1} key={2}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Aanslagen</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
<Tabs.Panel tab={2} key={3}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Documenten</Heading>
<Paragraph>(This tab panel simulates a load time of {slowPanelDelay} milliseconds.)</Paragraph>
<SlowPanel />
</div>
</Tabs.Panel>,
<Tabs.Panel tab={3} key={4}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Acties</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
]

export const Default: Story = {
args: {
children: [
<Tabs.List key={0}>
<Tabs.Button tab={0}>Gegevens</Tabs.Button>
<Tabs.Button tab={1}>Aanslagen</Tabs.Button>
<Tabs.Button tab={2}>Documenten</Tabs.Button>
<Tabs.Button tab={3}>Acties</Tabs.Button>
</Tabs.List>,
<Tabs.Panel tab={0} key={1}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Gegevens</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
<Tabs.Panel tab={1} key={2}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Aanslagen</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
<Tabs.Panel tab={2} key={3}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Documenten</Heading>
<Paragraph>(This tab panel simulates a load time of 500 milliseconds.)</Paragraph>
<SlowPanel />
</div>
</Tabs.Panel>,
<Tabs.Panel tab={3} key={4}>
<div style={{ paddingTop: '2rem' }}>
<Heading level={3}>Acties</Heading>
<Paragraph>{exampleParagraph()}</Paragraph>
</div>
</Tabs.Panel>,
],
children: defaultTabs,
},
}

export const WithInitialTab: Story = {
args: {
activeTab: 3,
children: defaultTabs,
},
}

export const Tab: TabStory = {
export const TabButton: TabStory = {
args: {
children: 'Gegevens',
tab: 0,
disabled: false,
tab: 0,
},
argTypes: {
children: {
table: { disable: false },
},
disabled: {
table: { disable: true },
},
tab: {
control: {
type: 'number',
min: 0,
max: 9,
max: 3,
},
},
},
Expand Down
Loading