-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WB-1676.2: Add a11y features to announce combobox changes to screen r…
…eaders (#2225) ## Summary: This PR focuses on bringing a better accessibility experience by adding a live region to announce the different combobox statuses: - When the visual focus changes in the listbox. - When the visual focus changes in the selected options (multi-select mode). - When it is closed. - Describing a state for the focused option. e.g. `selected`, `disabled`. ### Combobox Implementation Plan: 1. #2216 2. #2221 3. #2223 4. **Improve accessibility support on Combobox (labels, aria-live).[CURRENT]** 5. Add autocomplete support to `Combobox` component. 6. Add async/dynamic support to `Combobox` component. Issue: https://khanacademy.atlassian.net/browse/WB-1676 ## Test plan: Turn on Voice Over and use Safari: ### Single Selection: 1. Navigate to: /iframe.html?args=&id=packages-dropdown-combobox--single-select-combobox&viewMode=story 2. Using the keyboard, open the listbox by focusing on the input then pressing `arrow down`. 3. Verify that the Screen Reader announces the selected item (`Pear selected, 3 of 9. 9 results available`). 4. Navigate on the listbox by using the arrow keys. 5. Verify that it announces the currently focused item (e.g. `Strawberry disabled, 2 of 9. 9 results available`). 6. Using the keyboard, Select one of the enabled options. 7. Verify that it announces the selection (e.g. `Apple selected`) and the listbox is closed. https://github.com/user-attachments/assets/dd7f6173-e00b-4a96-bed3-c5b0df6c2fbd ### Multiple Selection: 1. Navigate to /iframe.html?args=&id=packages-dropdown-combobox--multiple-selection&viewMode=story 2. Using the keyboard, open the listbox by focusing on the input then pressing `arrow down`. 3. Verify that the Screen Reader announces the selected item (`Pear selected, 3 of 9. 9 results available`). 4. Navigate on the listbox by using the arrow keys. 5. Verify that it announces the currently focused item (e.g. `Strawberry disabled, 2 of 9. 9 results available`). 6. Using the keyboard, Select one of the enabled options (pressing `Enter` on the focused option). 7. Verify that it announces the selection (e.g. `Apple selected, 5 of 9. 9 results available.`) and the listbox remains open). 9. Press arrow left or arrow right to move visual focus to the selected pills. 10. Verify that the focus moves to one of the pills and SR announces that (e.g. `Grape focused, 3 of 3. 3 selected options`). https://github.com/user-attachments/assets/80a6eabc-ef82-4828-8c18-ed8e05bf8fee Author: jandrade Reviewers: beaesguerra, jandrade Required Reviewers: Approved By: beaesguerra Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test (ubuntu-latest, 20.x, 2/2), ✅ Test (ubuntu-latest, 20.x, 1/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Lint (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: #2225
- Loading branch information
Showing
10 changed files
with
671 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@khanacademy/wonder-blocks-dropdown": minor | ||
--- | ||
|
||
Add a11y features to announce combobox changes to screen readers |
86 changes: 86 additions & 0 deletions
86
__docs__/wonder-blocks-dropdown/combobox-accessibility.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import {Meta} from "@storybook/blocks"; | ||
|
||
<Meta title="Packages / Dropdown / Combobox / Accessibility" /> | ||
|
||
# Combobox Accessibility | ||
|
||
The Combobox component is built with accessibility in mind. It includes the | ||
following features (as advised by the <a | ||
href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/">WAI-ARIA Authoring | ||
Practices</a>): | ||
|
||
## Keyboard interaction | ||
|
||
#### Combobox (input): | ||
|
||
- `Tab`: The combobox is focused. | ||
- `ArrowDown`: Opens the listbox if it is not already displayed and moves | ||
visual focus to the first option. DOM focus remains on the combobox (input). | ||
- `ArrowUp`: Opens the listbox if it is not already displayed and moves visual | ||
focus to the last option. DOM focus remains on the combobox (input). | ||
- `Escape`: Closes the listbox if displayed. | ||
|
||
#### Listbox (Dropdown menu): | ||
|
||
- `ArrowDown`: Moves visual focus to the next option. If the visual focus is | ||
on the last option, the visual focus moves to the first option (cyclic | ||
navigation). | ||
- `ArrowUp`: Moves visual focus to the previous option. If the visual focus in | ||
on the first option, the visual focus moves to the last option (cyclic | ||
navigation). | ||
- `Home`: Moves visual focus to the first option. | ||
- `End`: Moves visual focus to the last option. | ||
- `Enter`: Sets the Textbox/combobox value to the content of the focused | ||
option in the listbox, then closes the listbox if only one selection can be made. | ||
- `Escape`: Closes the listbox. | ||
|
||
#### Button (Arrow down icon): | ||
|
||
- It is removed from the tab sequence of the page, but still useful to open | ||
the listbox via touch events (e.g. click, press). | ||
|
||
## Screen Reader support | ||
|
||
The Combobox component is designed to be screen reader friendly. | ||
|
||
We provide a custom live region that will announce changes to the combobox due | ||
to some limitations in browsers (e.g. Safari). This live region will announce | ||
the following: | ||
|
||
- When the listbox is opened, the focused option in the listbox, including the | ||
index of the option and the total number of options (e.g. `Pear selected, 3 | ||
of 9. 9 results available`). | ||
- When the user navigates through the listbox, the focused option in the listbox, | ||
including the index of the option and the total number of options (e.g. | ||
`Strawberry disabled, 2 of 9. 9 results available`). | ||
- When an option is selected (e.g. `Pear selected`). | ||
- When the listbox is closed. | ||
|
||
## ARIA attributes | ||
|
||
#### Combobox (input): | ||
|
||
- The input[type=text] has a `combobox` role. | ||
- `aria-controls` points to the `listbox` id. | ||
- `aria-expanded` will change based on the listbox state (true = visible, false | ||
= hidden). | ||
- `aria-activedescendant` To announce which option item is visually focused. | ||
(see <a | ||
href="https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant">Developing | ||
a Keyboard Interface</a>). | ||
- `autocomplete` is set `off`, so we don't present the default autocomplete | ||
list provided by browsers, and instead rely on the listbox controlled by the | ||
input. | ||
|
||
#### Listbox (Dropdown menu): | ||
|
||
- The listbox has a `listbox` role. | ||
- `aria-disabled`: Whether the listbox container is disabled. | ||
- Labelling: | ||
- `aria-label`: Provides a label for the listbox. | ||
- `aria-labelledby`: Alternatively, this can be provided by pointing to a | ||
DOM reference (ID) that contains the description of the listbox. | ||
- `aria-multiselectable`: Whether the user can select more than one option. | ||
- `aria-readonly`: The user cannot change which options are selected or | ||
unselected, but the listbox is otherwise operable. | ||
- `aria-required`: Whether an option item must be selected. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
packages/wonder-blocks-dropdown/src/components/__tests__/combobox-live-region.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import * as React from "react"; | ||
import {render, screen} from "@testing-library/react"; | ||
import {ComboboxLiveRegion} from "../combobox-live-region"; | ||
import OptionItem from "../option-item"; | ||
|
||
const options = [ | ||
<OptionItem key="1" label="Option 1" value="option1" />, | ||
<OptionItem key="2" label="Option 2" value="option2" />, | ||
]; | ||
|
||
describe("ComboboxLiveRegion", () => { | ||
it("doesn't announce anything by default", () => { | ||
// Arrange | ||
|
||
// Act | ||
render( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selectedLabels={[]} | ||
selected={null} | ||
opened={false} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent(""); | ||
}); | ||
|
||
describe("multi-select", () => { | ||
it("announces when a pill is focused", () => { | ||
// Arrange | ||
|
||
// Act | ||
render( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={0} | ||
options={options} | ||
selected={["option2"]} | ||
selectedLabels={["Option 2"]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent( | ||
"Option 2 focused, 1 of 1. 1 selected options.", | ||
); | ||
}); | ||
|
||
it("does not announce anything when there are no selected pills", () => { | ||
// Arrange | ||
const {rerender} = render( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selected={["option2"]} | ||
selectedLabels={["Option 2"]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Act | ||
rerender( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selected={null} | ||
selectedLabels={[]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent(""); | ||
}); | ||
|
||
it("announces when an item is selected", () => { | ||
// Arrange | ||
const options = [ | ||
<OptionItem | ||
key="1" | ||
label="Option 1" | ||
value="option1" | ||
selected={true} | ||
/>, | ||
<OptionItem key="2" label="Option 2" value="option2" />, | ||
]; | ||
|
||
// Act | ||
// select the first option | ||
render( | ||
<ComboboxLiveRegion | ||
focusedIndex={0} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selected={["option1"]} | ||
selectedLabels={["Option 1"]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent( | ||
"Option 1 selected, 1 of 2. 2 results available.", | ||
); | ||
}); | ||
|
||
it("announces when an item is unselected", () => { | ||
// Arrange | ||
// Focus on the first pill | ||
const {rerender} = render( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={1} | ||
options={options} | ||
selected={["option1", "option2"]} | ||
selectedLabels={["Option 1", "Option 2"]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Act | ||
rerender( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={0} | ||
options={options} | ||
selected={["option1"]} | ||
// Option 2 is removed | ||
selectedLabels={["Option 1"]} | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent( | ||
"Option 1 focused, 1 of 1. 1 selected options.", | ||
); | ||
}); | ||
|
||
it("announces when it is closed", () => { | ||
// Arrange | ||
const {rerender} = render( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selectedLabels={[]} | ||
selected={null} | ||
selectionType="multiple" | ||
opened={true} | ||
/>, | ||
); | ||
|
||
// Act | ||
rerender( | ||
<ComboboxLiveRegion | ||
focusedIndex={-1} | ||
focusedMultiSelectIndex={-1} | ||
options={options} | ||
selectedLabels={[]} | ||
selected={null} | ||
selectionType="multiple" | ||
opened={false} | ||
/>, | ||
); | ||
|
||
// Assert | ||
expect(screen.getByRole("log")).toHaveTextContent( | ||
"Combobox is closed", | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.