Skip to content

Commit 776e05e

Browse files
authored
ActionList: Add role="option" if role="listbox" is present (#6049)
1 parent d9058fd commit 776e05e

File tree

5 files changed

+57
-8
lines changed

5 files changed

+57
-8
lines changed

.changeset/soft-webs-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
ActionList: Ensure `role="option"` is added when `role="listbox"` is used; allow disabled items to remain focusable

packages/react/src/ActionList/ActionList.features.stories.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,35 @@ export const MultiSelect = () => {
329329
)
330330
}
331331

332-
export const ListBoxMultiSelect = () => {
332+
export const ListboxSingleSelect = () => {
333+
const [selectedIndice, setSelectedIndice] = React.useState<number>(0)
334+
const handleSelect = (index: number) => {
335+
setSelectedIndice(index)
336+
}
337+
338+
return (
339+
<ActionList selectionVariant="single" role="listbox" aria-label="Projects">
340+
{projects.map((project, index) => (
341+
<ActionList.Item
342+
key={index}
343+
selected={selectedIndice === index}
344+
aria-checked={selectedIndice === index}
345+
onSelect={() => handleSelect(index)}
346+
disabled={index === 3 ? true : undefined}
347+
role="option"
348+
>
349+
<ActionList.LeadingVisual>
350+
<TableIcon />
351+
</ActionList.LeadingVisual>
352+
{project.name}
353+
<ActionList.Description variant="block">{project.scope}</ActionList.Description>
354+
</ActionList.Item>
355+
))}
356+
</ActionList>
357+
)
358+
}
359+
360+
export const ListboxMultiSelect = () => {
333361
const [selectedIndices, setSelectedIndices] = React.useState<number[]>([0])
334362
const handleSelect = (index: number) => {
335363
if (selectedIndices.includes(index)) {
@@ -339,7 +367,7 @@ export const ListBoxMultiSelect = () => {
339367
}
340368
}
341369
return (
342-
<ActionList role="menu" selectionVariant="multiple" aria-label="Project">
370+
<ActionList role="menu" selectionVariant="multiple" aria-label="Projects">
343371
{projects.map((project, index) => (
344372
<ActionList.Item
345373
key={index}

packages/react/src/ActionList/ActionList.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ describe('ActionList', () => {
9494
expect(document.activeElement).toHaveTextContent('Option 2')
9595

9696
await userEvent.keyboard('{ArrowDown}')
97-
expect(document.activeElement).not.toHaveTextContent('Option 3') // option 3 is disabled
97+
expect(document.activeElement).toHaveTextContent('Option 3')
98+
99+
await userEvent.keyboard('{ArrowDown}')
98100
expect(document.activeElement).toHaveTextContent('Option 4')
99101

100102
await userEvent.keyboard('{ArrowDown}')

packages/react/src/ActionList/Item.test.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ function SingleSelectListStory(): JSX.Element {
4747
{projects.map((project, index) => (
4848
<ActionList.Item
4949
key={index}
50-
role="option"
5150
selected={index === selectedIndex}
5251
onSelect={() => setSelectedIndex(index)}
5352
disabled={project.disabled}
@@ -193,6 +192,7 @@ describe('ActionList.Item', () => {
193192
await userEvent.tab() // get focus on first element
194193
await userEvent.keyboard('{ArrowDown}')
195194
await userEvent.keyboard('{ArrowDown}')
195+
await userEvent.keyboard('{ArrowDown}')
196196
expect(inactiveOption).toHaveFocus()
197197
expect(document.activeElement).toHaveAccessibleDescription(projects[3].inactiveText as string)
198198
})
@@ -220,6 +220,7 @@ describe('ActionList.Item', () => {
220220
await userEvent.keyboard('{ArrowDown}')
221221
await userEvent.keyboard('{ArrowDown}')
222222
await userEvent.keyboard('{ArrowDown}')
223+
await userEvent.keyboard('{ArrowDown}')
223224
expect(inactiveOption).toHaveFocus()
224225
expect(document.activeElement).toHaveAccessibleDescription(projects[5].inactiveText as string)
225226
})
@@ -420,4 +421,18 @@ describe('ActionList.Item', () => {
420421
expect(button.parentElement?.tagName).toBe('LI')
421422
expect(button.textContent).toBe('Item 5')
422423
})
424+
425+
it('should add `role="option"` if `role="listbox"` and `selectionVariant` is present', async () => {
426+
const {getAllByRole} = HTMLRender(
427+
<ActionList role="listbox" selectionVariant="single">
428+
<ActionList.Item>Item 1</ActionList.Item>
429+
<ActionList.Item>Item 2</ActionList.Item>
430+
<ActionList.Item>Item 3</ActionList.Item>
431+
<ActionList.Item>Item 4</ActionList.Item>
432+
</ActionList>,
433+
)
434+
const options = getAllByRole('option')
435+
expect(options[0]).toBeInTheDocument()
436+
expect(options).toHaveLength(4)
437+
})
423438
})

packages/react/src/ActionList/Item.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
141141
if (selectionVariant === 'single') inferredItemRole = 'menuitemradio'
142142
else if (selectionVariant === 'multiple') inferredItemRole = 'menuitemcheckbox'
143143
else inferredItemRole = 'menuitem'
144-
} else if (container === 'SelectPanel' && listRole === 'listbox') {
145-
if (selectionVariant !== undefined) inferredItemRole = 'option'
144+
} else if (listRole === 'listbox') {
145+
if (selectionVariant !== undefined && !role) inferredItemRole = 'option'
146146
}
147147

148148
const itemRole = role || inferredItemRole
@@ -325,8 +325,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
325325

326326
let focusable
327327

328-
// if item is disabled and is of type (menuitem*, option) it should remain focusable, if inactive, apply the same rules
329-
if ((disabled && !inferredItemRole) || showInactiveIndicator) {
328+
if (showInactiveIndicator) {
330329
focusable = true
331330
}
332331

0 commit comments

Comments
 (0)