Skip to content

Commit

Permalink
Improve programmatic state management of UnderlinePanels (#5527)
Browse files Browse the repository at this point in the history
* 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 <40550942+francinelucca@users.noreply.github.com>
  • Loading branch information
ddoyle2017 and francinelucca authored Jan 23, 2025
1 parent 16c572e commit ccc3c99
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/tasty-experts-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Add an onSelect callback for UnderlinePanels.Tab
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react'
import type {Meta} from '@storybook/react'
import UnderlinePanels from './UnderlinePanels'
import type {ComponentProps} from '../../utils/types'
import type {Meta, StoryFn} from '@storybook/react'
import UnderlinePanels from './UnderlinePanels'

export default {
title: 'Experimental/Components/UnderlinePanels/Dev',
component: UnderlinePanels,
subcomponents: {Tab: UnderlinePanels.Tab, Panel: UnderlinePanels.Panel},
} as Meta<ComponentProps<typeof UnderlinePanels>>

export const Default = () => (
Expand All @@ -18,3 +19,30 @@ export const Default = () => (
<UnderlinePanels.Panel>Panel 3</UnderlinePanels.Panel>
</UnderlinePanels>
)

export const SingleTabPlayground: StoryFn<ComponentProps<typeof UnderlinePanels.Tab>> = args => {
return (
<UnderlinePanels aria-label="Select a tab">
<UnderlinePanels.Tab {...args}>Users</UnderlinePanels.Tab>
<UnderlinePanels.Panel>Users Panel</UnderlinePanels.Panel>
</UnderlinePanels>
)
}

SingleTabPlayground.args = {
'aria-selected': true,
counter: '14K',
}

SingleTabPlayground.argTypes = {
'aria-selected': {
control: {
type: 'boolean',
},
},
counter: {
control: {
type: 'text',
},
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@
"name": "aria-selected",
"type": "| boolean | 'true' | 'false'",
"defaultValue": "false",
"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)."
"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)."
},
{
"name": "onSelect",
"type": "(event) => void",
"defaultValue": "",
"description": "The handler that gets called when the tab is selected"
},
{
"name": "counter",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,79 @@
import React from 'react'
import type {Meta} from '@storybook/react'
import type {Meta, StoryFn} from '@storybook/react'
import UnderlinePanels from './UnderlinePanels'
import type {ComponentProps} from '../../utils/types'

export default {
const meta: Meta<typeof UnderlinePanels> = {
title: 'Experimental/Components/UnderlinePanels',
component: UnderlinePanels,
} as Meta<ComponentProps<typeof UnderlinePanels>>
parameters: {
controls: {
expanded: true,
},
},
argTypes: {
'aria-label': {
type: {
name: 'string',
},
},
'aria-labelledby': {
type: {
name: 'string',
},
},
id: {
type: {
name: 'string',
},
},
loadingCounters: {
control: {
type: 'boolean',
},
},
},
args: {
'aria-label': 'Select a tab',
'aria-labelledby': 'tab',
id: 'test',
loadingCounters: false,
},
}

export const Default = () => (
<UnderlinePanels aria-label="Select a tab">
<UnderlinePanels.Tab>Tab 1</UnderlinePanels.Tab>
<UnderlinePanels.Tab>Tab 2</UnderlinePanels.Tab>
<UnderlinePanels.Tab>Tab 3</UnderlinePanels.Tab>
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
<UnderlinePanels.Panel>Panel 3</UnderlinePanels.Panel>
</UnderlinePanels>
)
export default meta

export const Default: StoryFn<typeof UnderlinePanels> = () => {
const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
const panels = ['Panel 1', 'Panel 2', 'Panel 3']

return (
<UnderlinePanels aria-label="Select a tab">
{tabs.map((tab: string, index: number) => (
<UnderlinePanels.Tab key={index} aria-selected={index === 0 ? true : undefined}>
{tab}
</UnderlinePanels.Tab>
))}
{panels.map((panel: string, index: number) => (
<UnderlinePanels.Panel key={index}>{panel}</UnderlinePanels.Panel>
))}
</UnderlinePanels>
)
}

export const Playgound: StoryFn<typeof UnderlinePanels> = args => {
const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
const panels = ['Panel 1', 'Panel 2', 'Panel 3']

return (
<UnderlinePanels {...args}>
{tabs.map((tab: string, index: number) => (
<UnderlinePanels.Tab key={index} aria-selected={index === 0 ? true : undefined}>
{tab}
</UnderlinePanels.Tab>
))}
{panels.map((panel: string, index: number) => (
<UnderlinePanels.Panel key={index}>{panel}</UnderlinePanels.Panel>
))}
</UnderlinePanels>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ describe('UnderlinePanels', () => {
const tabList = screen.getByRole('tablist')
expect(tabList).toHaveAccessibleName('Select a tab')
})
it('updates the selected tab when aria-selected changes', () => {
const {rerender} = render(
<UnderlinePanels aria-label="Select a tab">
<UnderlinePanels.Tab aria-selected={true}>Tab 1</UnderlinePanels.Tab>
<UnderlinePanels.Tab aria-selected={false}>Tab 2</UnderlinePanels.Tab>
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
</UnderlinePanels>,
)

// Verify that the first tab is selected and second tab is not
let firstTab = screen.getByRole('tab', {name: 'Tab 1'})
let secondTab = screen.getByRole('tab', {name: 'Tab 2'})

expect(firstTab).toHaveAttribute('aria-selected', 'true')
expect(secondTab).toHaveAttribute('aria-selected', 'false')

// Programmatically select the second tab by updating the aria-selected prop
rerender(
<UnderlinePanels aria-label="Select a tab">
<UnderlinePanels.Tab aria-selected={false}>Tab 1</UnderlinePanels.Tab>
<UnderlinePanels.Tab aria-selected={true}>Tab 2</UnderlinePanels.Tab>
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
<UnderlinePanels.Panel>Panel 2</UnderlinePanels.Panel>
</UnderlinePanels>,
)

// Verify the updated aria-selected prop changes which tab is selected
firstTab = screen.getByRole('tab', {name: 'Tab 1'})
secondTab = screen.getByRole('tab', {name: 'Tab 2'})

expect(firstTab).toHaveAttribute('aria-selected', 'false')
expect(secondTab).toHaveAttribute('aria-selected', 'true')
})
it('calls onSelect when a tab is clicked', () => {
const onSelect = jest.fn()
render(
<UnderlinePanels aria-label="Select a tab">
<UnderlinePanels.Tab onSelect={onSelect}>Tab 1</UnderlinePanels.Tab>
<UnderlinePanels.Panel>Panel 1</UnderlinePanels.Panel>
</UnderlinePanels>,
)

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

expect(onSelect).toHaveBeenCalled()
})
it('throws an error when the neither aria-label nor aria-labelledby are passed', () => {
render(<UnderlinePanelsMockComponent />)
})
Expand Down
Loading

0 comments on commit ccc3c99

Please sign in to comment.