Skip to content

Commit ace38af

Browse files
Adrián Boloniohectahertzcamchenry
authored
SelectPanel: Accessibility remediation (#2138)
* Change deprecated/ActionList semantics * Remove item selection via Enter key * Handle focus on deprecated/ActionList * Adapt deprecated/ActionMenu prop * Update deprecated/ActionList tests and stories * Update deprecated/ActionList docs * Remove focusZone from the FilteredActionList component * Add sr-only description for input in the FilteredActionList component * Adapt SelectPanel: semantics, visual layout, add title, close button, save/cancel button and functionality. Update tests, docs, and stories. * Update SelectPanel test snap * Update deprecated/ActionList axe test * Update deprecated/ActionList axe test * Delete commented code * Update stories, Add ESC reset functionality * Add changeset (breaking change) * Preselect items on the SelectPanel docs that aren't the first ones * Use Heading instead of h1 on SelectPanel * Don't use selected items as the SelectPanel anchor label * Replace custom SrOnly with VisuallyHidden * Make the SelectPanel example dimensions larger * Support setting the selected items asyncronously * Fix new select panel issues (#2147) * Fix onClose being called with event instead of gesture Co-authored-by: Hector Garcia <[email protected]> * Remove unsupported PageUpDown bindkeys from useFocusZone * Remove console.debug * Make `title` and `inputLabel` optional with defaults * Remove unused prop * Add a placeholder prop for the search input * Make footer buttons smaller * Use `ButtonClose` * Better default title for multiselect `SelectPanel` * Fix lint warnings * Fix the AutocompleteMenu tests * Fix lint error * Fix MarkdownEditor tests, remove one for old behavior Co-authored-by: Hector Garcia <[email protected]> Co-authored-by: Cameron McHenry <[email protected]>
1 parent 7b8bc81 commit ace38af

File tree

20 files changed

+2710
-2548
lines changed

20 files changed

+2710
-2548
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Accessibility fixes for SelectPanel.

docs/content/SelectPanel.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,28 @@ const items = [
3737
]
3838

3939
function DemoComponent() {
40-
const [selected, setSelected] = React.useState([items[0], items[1]])
40+
const [selected, setSelected] = React.useState([items[2], items[4]])
4141
const [filter, setFilter] = React.useState('')
4242
const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
4343
const [open, setOpen] = React.useState(false)
4444

4545
return (
4646
<SelectPanel
47-
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
48-
<Button trailingIcon={TriangleDownIcon} aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps}>
49-
{children || 'Select Labels'}
47+
renderAnchor={({...anchorProps}) => (
48+
<Button trailingIcon={TriangleDownIcon} {...anchorProps}>
49+
Select Labels
5050
</Button>
5151
)}
52-
placeholderText="Filter Labels"
52+
title="Select Items"
53+
inputLabel="Select Items"
5354
open={open}
5455
onOpenChange={setOpen}
5556
items={filteredItems}
5657
selected={selected}
5758
onSelectedChange={setSelected}
5859
onFilterChange={setFilter}
5960
showItemDividers={true}
60-
overlayProps={{width: 'small', height: 'xsmall'}}
61+
overlayProps={{width: 'medium', height: 'medium'}}
6162
/>
6263
)
6364
}

docs/content/deprecated/ActionList.mdx

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,26 @@ Use [new version of ActionList](/ActionList) with composable API, design updates
1414

1515
```jsx
1616
<ActionList
17+
role="listbox"
1718
items={[
18-
{text: 'New file'},
19-
{text: 'Copy link'},
20-
{text: 'Edit file'},
19+
{text: 'New file', role: 'option'},
20+
{text: 'Copy link', role: 'option'},
21+
{text: 'Edit file', role: 'option'},
2122
ActionList.Divider,
22-
{text: 'Delete file', variant: 'danger'}
23+
{text: 'Delete file', variant: 'danger', role: 'option'}
2324
]}
2425
/>
2526
```
2627

2728
**After**
2829

2930
```jsx
30-
<ActionList>
31-
<ActionList.Item>New file</ActionList.Item>
32-
<ActionList.Item>Copy link</ActionList.Item>
33-
<ActionList.Item>Edit file</ActionList.Item>
31+
<ActionList role="listbox">
32+
<ActionList.Item role="option">New file</ActionList.Item>
33+
<ActionList.Item role="option">Copy link</ActionList.Item>
34+
<ActionList.Item role="option">Edit file</ActionList.Item>
3435
<ActionList.Divider />
35-
<ActionList.Item variant="danger">Delete file</ActionList.Item>
36+
<ActionList.Item variant="danger" role="option">Delete file</ActionList.Item>
3637
</ActionList>
3738
```
3839

@@ -46,12 +47,13 @@ import {ActionList} from '@primer/react/deprecated'
4647

4748
```jsx live deprecated
4849
<ActionList
50+
role="listbox"
4951
items={[
50-
{text: 'New file'},
52+
{text: 'New file', role: 'option'},
5153
ActionList.Divider,
52-
{text: 'Copy link'},
53-
{text: 'Edit file'},
54-
{text: 'Delete file', variant: 'danger'}
54+
{text: 'Copy link', role: 'option'},
55+
{text: 'Edit file', role: 'option'},
56+
{text: 'Delete file', variant: 'danger', role: 'option'}
5557
]}
5658
/>
5759
```
@@ -60,6 +62,7 @@ import {ActionList} from '@primer/react/deprecated'
6062

6163
```jsx live deprecated
6264
<ActionList
65+
role="listbox"
6366
groupMetadata={[
6467
{groupId: '0'},
6568
{groupId: '1', header: {title: 'Live query', variant: 'subtle'}},
@@ -68,34 +71,37 @@ import {ActionList} from '@primer/react/deprecated'
6871
{groupId: '4'}
6972
]}
7073
items={[
71-
{key: '1', leadingVisual: TypographyIcon, text: 'Rename', groupId: '0', trailingVisual: '⌘R'},
72-
{key: '2', leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'},
73-
{key: '3', leadingVisual: SearchIcon, text: 'repo:github/github', groupId: '1'},
74+
{key: '1', leadingVisual: TypographyIcon, text: 'Rename', groupId: '0', trailingVisual: '⌘R', role: 'option'},
75+
{key: '2', leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0', role: 'option'},
76+
{key: '3', leadingVisual: SearchIcon, text: 'repo:github/github', groupId: '1', role: 'option'},
7477
{
7578
key: '4',
7679
leadingVisual: NoteIcon,
7780
text: 'Table',
7881
description: 'Information-dense table optimized for operations across teams',
7982
descriptionVariant: 'block',
80-
groupId: '2'
83+
groupId: '2',
84+
role: 'option'
8185
},
8286
{
8387
key: '5',
8488
leadingVisual: ProjectIcon,
8589
text: 'Board',
8690
description: 'Kanban-style board focused on visual states',
8791
descriptionVariant: 'block',
88-
groupId: '2'
92+
groupId: '2',
93+
role: 'option'
8994
},
9095
{
9196
key: '6',
9297
leadingVisual: FilterIcon,
9398
text: 'Save sort and filters to current view',
9499
disabled: true,
95-
groupId: '3'
100+
groupId: '3',
101+
role: 'option'
96102
},
97-
{key: '7', leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'},
98-
{key: '8', leadingVisual: GearIcon, text: 'View settings', groupId: '4', trailingVisual: ArrowRightIcon}
103+
{key: '7', leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3', role: 'option'},
104+
{key: '8', leadingVisual: GearIcon, text: 'View settings', groupId: '4', trailingVisual: ArrowRightIcon, role: 'option'}
99105
]}
100106
/>
101107
```
@@ -104,17 +110,21 @@ import {ActionList} from '@primer/react/deprecated'
104110

105111
```jsx deprecated
106112
<ActionList
113+
role="listbox"
107114
items={[
108115
{
109116
text: 'Vanilla link',
117+
role: 'option',
110118
renderItem: props => <ActionList.Item as="a" href="/about" {...props} />
111119
},
112120
{
113121
text: 'React Router link',
122+
role: 'option',
114123
renderItem: props => <ActionList.Item as={ReactRouterLikeLink} to="/about" {...props} />
115124
},
116125
{
117126
text: 'NextJS style',
127+
role: 'option',
118128
renderItem: props => (
119129
<NextJSLikeLink href="/about">
120130
<ActionList.Item as="a" {...props} />

src/Autocomplete/AutocompleteMenu.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
151151
items.map(selectableItem => {
152152
return {
153153
...selectableItem,
154+
_legacyEnterSupport: true, //TODO: Change behaviour, the enter key should not be used here.
154155
role: 'option',
155156
id: selectableItem.id,
156157
selected: selectionVariant === 'multiple' ? selectedItemIds.includes(selectableItem.id) : undefined,
@@ -217,6 +218,7 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
217218
? [
218219
{
219220
...addNewItem,
221+
_legacyEnterSupport: true, //TODO: Change behaviour, the enter key should not be used here.
220222
leadingVisual: () => <PlusIcon />,
221223
onAction: (item: T) => {
222224
// TODO: make it possible to pass a leadingVisual when using `addNewItem`

src/FilteredActionList/FilteredActionList.tsx

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import TextInput, {TextInputProps} from '../TextInput'
55
import Box from '../Box'
66
import {ActionList} from '../deprecated/ActionList'
77
import Spinner from '../Spinner'
8-
import {useFocusZone} from '../hooks/useFocusZone'
98
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
109
import styled from 'styled-components'
1110
import {get} from '../constants'
@@ -14,6 +13,7 @@ import useScrollFlash from '../hooks/useScrollFlash'
1413
import {scrollIntoView} from '@primer/behaviors'
1514
import type {ScrollIntoViewOptions} from '@primer/behaviors'
1615
import {SxProp} from '../sx'
16+
import VisuallyHidden from '../_VisuallyHidden'
1717

1818
const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8}
1919

@@ -22,7 +22,7 @@ export interface FilteredActionListProps
2222
ListPropsBase,
2323
SxProp {
2424
loading?: boolean
25-
placeholderText: string
25+
placeholderText?: string
2626
filterValue?: string
2727
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void
2828
textInputProps?: Partial<Omit<TextInputProps, 'onChange'>>
@@ -56,10 +56,10 @@ export function FilteredActionList({
5656
)
5757

5858
const scrollContainerRef = useRef<HTMLDivElement>(null)
59-
const listContainerRef = useRef<HTMLDivElement>(null)
6059
const inputRef = useProvidedRefOrCreate<HTMLInputElement>(providedInputRef)
6160
const activeDescendantRef = useRef<HTMLElement>()
6261
const listId = useSSRSafeId()
62+
const inputDescriptionTextId = useSSRSafeId()
6363
const onInputKeyPress: KeyboardEventHandler = useCallback(
6464
event => {
6565
if (event.key === 'Enter' && activeDescendantRef.current) {
@@ -74,28 +74,6 @@ export function FilteredActionList({
7474
[activeDescendantRef]
7575
)
7676

77-
useFocusZone(
78-
{
79-
containerRef: listContainerRef,
80-
focusOutBehavior: 'wrap',
81-
focusableElementFilter: element => {
82-
return !(element instanceof HTMLInputElement)
83-
},
84-
activeDescendantFocus: inputRef,
85-
onActiveDescendantChanged: (current, previous, directlyActivated) => {
86-
activeDescendantRef.current = current
87-
88-
if (current && scrollContainerRef.current && directlyActivated) {
89-
scrollIntoView(current, scrollContainerRef.current, menuScrollMargins)
90-
}
91-
}
92-
},
93-
[
94-
// List ref isn't set while loading. Need to re-bind focus zone when it changes
95-
loading
96-
]
97-
)
98-
9977
useEffect(() => {
10078
// if items changed, we want to instantly move active descendant into view
10179
if (activeDescendantRef.current && scrollContainerRef.current) {
@@ -119,16 +97,18 @@ export function FilteredActionList({
11997
placeholder={placeholderText}
12098
aria-label={placeholderText}
12199
aria-controls={listId}
100+
aria-describedby={inputDescriptionTextId}
122101
{...textInputProps}
123102
/>
103+
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
124104
</StyledHeader>
125105
<Box ref={scrollContainerRef} overflow="auto">
126106
{loading ? (
127107
<Box width="100%" display="flex" flexDirection="row" justifyContent="center" pt={6} pb={7}>
128108
<Spinner />
129109
</Box>
130110
) : (
131-
<ActionList ref={listContainerRef} items={items} {...listProps} role="listbox" id={listId} />
111+
<ActionList items={items} {...listProps} role="listbox" id={listId} />
132112
)}
133113
</Box>
134114
</Box>

0 commit comments

Comments
 (0)