Skip to content

Commit ccc3c99

Browse files
Improve programmatic state management of UnderlinePanels (#5527)
* add onSelect prop to UnderlinePanels and UnderlinePanels.Tab * UnderlinePanels doc updates * unit test for programmatically selecting tab + updates to underline panels * rename unit test + code clean-up * add test for tab onSelect prop * comment explaining UnderlinePanels changes * pr feedback * add changeset * storybook updates * fixed UnderlinePanels.Tab story rendering issues * fix playwright vrt regressions * added UnderlinePanels.Tab story to .dev * remove no tabs selected case from dev story --------- Co-authored-by: Marie Lucca <[email protected]>
1 parent 16c572e commit ccc3c99

File tree

6 files changed

+246
-61
lines changed

6 files changed

+246
-61
lines changed

.changeset/tasty-experts-fix.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
Add an onSelect callback for UnderlinePanels.Tab

packages/react/src/experimental/UnderlinePanels/UnderlinePanels.dev.stories.tsx

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React from 'react'
2-
import type {Meta} from '@storybook/react'
3-
import UnderlinePanels from './UnderlinePanels'
42
import type {ComponentProps} from '../../utils/types'
3+
import type {Meta, StoryFn} from '@storybook/react'
4+
import UnderlinePanels from './UnderlinePanels'
55

66
export default {
77
title: 'Experimental/Components/UnderlinePanels/Dev',
88
component: UnderlinePanels,
9+
subcomponents: {Tab: UnderlinePanels.Tab, Panel: UnderlinePanels.Panel},
910
} as Meta<ComponentProps<typeof UnderlinePanels>>
1011

1112
export const Default = () => (
@@ -18,3 +19,30 @@ export const Default = () => (
1819
<UnderlinePanels.Panel>Panel 3</UnderlinePanels.Panel>
1920
</UnderlinePanels>
2021
)
22+
23+
export const SingleTabPlayground: StoryFn<ComponentProps<typeof UnderlinePanels.Tab>> = args => {
24+
return (
25+
<UnderlinePanels aria-label="Select a tab">
26+
<UnderlinePanels.Tab {...args}>Users</UnderlinePanels.Tab>
27+
<UnderlinePanels.Panel>Users Panel</UnderlinePanels.Panel>
28+
</UnderlinePanels>
29+
)
30+
}
31+
32+
SingleTabPlayground.args = {
33+
'aria-selected': true,
34+
counter: '14K',
35+
}
36+
37+
SingleTabPlayground.argTypes = {
38+
'aria-selected': {
39+
control: {
40+
type: 'boolean',
41+
},
42+
},
43+
counter: {
44+
control: {
45+
type: 'text',
46+
},
47+
},
48+
}

packages/react/src/experimental/UnderlinePanels/UnderlinePanels.docs.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@
7272
"name": "aria-selected",
7373
"type": "| boolean | 'true' | 'false'",
7474
"defaultValue": "false",
75-
"description": "Whether this is the selected tab. For more information about `aria-current`, see [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected)."
75+
"description": "Whether this is the selected tab. For more information about `aria-selected`, see [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected)."
76+
},
77+
{
78+
"name": "onSelect",
79+
"type": "(event) => void",
80+
"defaultValue": "",
81+
"description": "The handler that gets called when the tab is selected"
7682
},
7783
{
7884
"name": "counter",
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,79 @@
11
import React from 'react'
2-
import type {Meta} from '@storybook/react'
2+
import type {Meta, StoryFn} from '@storybook/react'
33
import UnderlinePanels from './UnderlinePanels'
4-
import type {ComponentProps} from '../../utils/types'
54

6-
export default {
5+
const meta: Meta<typeof UnderlinePanels> = {
76
title: 'Experimental/Components/UnderlinePanels',
87
component: UnderlinePanels,
9-
} as Meta<ComponentProps<typeof UnderlinePanels>>
8+
parameters: {
9+
controls: {
10+
expanded: true,
11+
},
12+
},
13+
argTypes: {
14+
'aria-label': {
15+
type: {
16+
name: 'string',
17+
},
18+
},
19+
'aria-labelledby': {
20+
type: {
21+
name: 'string',
22+
},
23+
},
24+
id: {
25+
type: {
26+
name: 'string',
27+
},
28+
},
29+
loadingCounters: {
30+
control: {
31+
type: 'boolean',
32+
},
33+
},
34+
},
35+
args: {
36+
'aria-label': 'Select a tab',
37+
'aria-labelledby': 'tab',
38+
id: 'test',
39+
loadingCounters: false,
40+
},
41+
}
1042

11-
export const Default = () => (
12-
<UnderlinePanels aria-label="Select a tab">
13-
<UnderlinePanels.Tab>Tab 1</UnderlinePanels.Tab>
14-
<UnderlinePanels.Tab>Tab 2</UnderlinePanels.Tab>
15-
<UnderlinePanels.Tab>Tab 3</UnderlinePanels.Tab>
16-
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
17-
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
18-
<UnderlinePanels.Panel>Panel 3</UnderlinePanels.Panel>
19-
</UnderlinePanels>
20-
)
43+
export default meta
44+
45+
export const Default: StoryFn<typeof UnderlinePanels> = () => {
46+
const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
47+
const panels = ['Panel 1', 'Panel 2', 'Panel 3']
48+
49+
return (
50+
<UnderlinePanels aria-label="Select a tab">
51+
{tabs.map((tab: string, index: number) => (
52+
<UnderlinePanels.Tab key={index} aria-selected={index === 0 ? true : undefined}>
53+
{tab}
54+
</UnderlinePanels.Tab>
55+
))}
56+
{panels.map((panel: string, index: number) => (
57+
<UnderlinePanels.Panel key={index}>{panel}</UnderlinePanels.Panel>
58+
))}
59+
</UnderlinePanels>
60+
)
61+
}
62+
63+
export const Playgound: StoryFn<typeof UnderlinePanels> = args => {
64+
const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
65+
const panels = ['Panel 1', 'Panel 2', 'Panel 3']
66+
67+
return (
68+
<UnderlinePanels {...args}>
69+
{tabs.map((tab: string, index: number) => (
70+
<UnderlinePanels.Tab key={index} aria-selected={index === 0 ? true : undefined}>
71+
{tab}
72+
</UnderlinePanels.Tab>
73+
))}
74+
{panels.map((panel: string, index: number) => (
75+
<UnderlinePanels.Panel key={index}>{panel}</UnderlinePanels.Panel>
76+
))}
77+
</UnderlinePanels>
78+
)
79+
}

packages/react/src/experimental/UnderlinePanels/UnderlinePanels.test.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,54 @@ describe('UnderlinePanels', () => {
5858
const tabList = screen.getByRole('tablist')
5959
expect(tabList).toHaveAccessibleName('Select a tab')
6060
})
61+
it('updates the selected tab when aria-selected changes', () => {
62+
const {rerender} = render(
63+
<UnderlinePanels aria-label="Select a tab">
64+
<UnderlinePanels.Tab aria-selected={true}>Tab 1</UnderlinePanels.Tab>
65+
<UnderlinePanels.Tab aria-selected={false}>Tab 2</UnderlinePanels.Tab>
66+
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
67+
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
68+
</UnderlinePanels>,
69+
)
70+
71+
// Verify that the first tab is selected and second tab is not
72+
let firstTab = screen.getByRole('tab', {name: 'Tab 1'})
73+
let secondTab = screen.getByRole('tab', {name: 'Tab 2'})
74+
75+
expect(firstTab).toHaveAttribute('aria-selected', 'true')
76+
expect(secondTab).toHaveAttribute('aria-selected', 'false')
77+
78+
// Programmatically select the second tab by updating the aria-selected prop
79+
rerender(
80+
<UnderlinePanels aria-label="Select a tab">
81+
<UnderlinePanels.Tab aria-selected={false}>Tab 1</UnderlinePanels.Tab>
82+
<UnderlinePanels.Tab aria-selected={true}>Tab 2</UnderlinePanels.Tab>
83+
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
84+
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
85+
</UnderlinePanels>,
86+
)
87+
88+
// Verify the updated aria-selected prop changes which tab is selected
89+
firstTab = screen.getByRole('tab', {name: 'Tab 1'})
90+
secondTab = screen.getByRole('tab', {name: 'Tab 2'})
91+
92+
expect(firstTab).toHaveAttribute('aria-selected', 'false')
93+
expect(secondTab).toHaveAttribute('aria-selected', 'true')
94+
})
95+
it('calls onSelect when a tab is clicked', () => {
96+
const onSelect = jest.fn()
97+
render(
98+
<UnderlinePanels aria-label="Select a tab">
99+
<UnderlinePanels.Tab onSelect={onSelect}>Tab 1</UnderlinePanels.Tab>
100+
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
101+
</UnderlinePanels>,
102+
)
103+
104+
const tab = screen.getByRole('tab', {name: 'Tab 1'})
105+
tab.click()
106+
107+
expect(onSelect).toHaveBeenCalled()
108+
})
61109
it('throws an error when the neither aria-label nor aria-labelledby are passed', () => {
62110
render(<UnderlinePanelsMockComponent />)
63111
})

0 commit comments

Comments
 (0)