Skip to content

Commit 915a6a3

Browse files
authored
This is a feature-branch pull-request from feature/dropdown-combobox to main (#2450)
## Summary: This PR includes the following commits: - WB-1671: Dropdown: use `combobox` role in all openers (#2345) - FEI-5533: Re-enable select keyboard tests for Dropdown and Clickable (#2420) Issue: https://khanacademy.atlassian.net/browse/WB-1824 ## Test plan: 1. Review tests to ensure they pass Original approved PRs: - #2345 - #2420 Author: marcysutton Reviewers: jandrade Required Reviewers: Approved By: jandrade Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build and test on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ✅ gerald, ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build and test on regular PRs / chromatic (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), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ gerald, ⏭️ dependabot, ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build and test on regular PRs / chromatic (ubuntu-latest, 20.x), ⌛ undefined, ⌛ undefined Pull Request URL: #2450
2 parents 205b79e + 232fc10 commit 915a6a3

27 files changed

+1011
-466
lines changed

.changeset/mean-cherries-press.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@khanacademy/wonder-blocks-clickable": major
3+
"@khanacademy/wonder-blocks-dropdown": major
4+
"@khanacademy/wonder-blocks-core": major
5+
---
6+
7+
Fixes keyboard tests in Dropdown and Clickable with specific key events. We now check `event.key` instead of `event.which` or `event.keyCode` to remove deprecated event properties and match the keys returned from Testing Library/userEvent.

.changeset/tasty-rockets-mix.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@khanacademy/wonder-blocks-dropdown": major
3+
---
4+
5+
1. Updates dropdown openers for SingleSelect and MultiSelect to use `role="combobox"` instead of `button`.
6+
2. SingleSelect and MultiSelect should have a paired `<label>` element or `aria-label` attribute for accessibility. They no longer fall back to text content for labeling, as those contents are now used as combobox values.
7+
3. Changes the type names for custom label objects from `Labels` to `LabelsValues` and `SingleSelectLabels` to `SingleSelectLabelsValues`, respectively.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {Meta, Story, Canvas} from "@storybook/blocks";
2+
import * as MultiSelectAccessibilityStories from './multi-select.accessibility.stories';
3+
4+
import {OptionItem, MultiSelect} from "@khanacademy/wonder-blocks-dropdown";
5+
import {View} from "@khanacademy/wonder-blocks-core";
6+
import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field";
7+
8+
<Meta of={MultiSelectAccessibilityStories} />
9+
10+
# Accessibility
11+
12+
## Using `LabeledField` with `MultiSelect`
13+
14+
To associate a `MultiSelect` with another visible element (e.g. a `<label>`),
15+
wrap it in a `LabeledField` component. The label will apply to the `MultiSelect`
16+
opener. With `LabeledField`, you can supply label text (or a JSX node)
17+
using the `label` prop to generate a paired `<label>` element. It comes with
18+
field validation and other features baked in!
19+
20+
If for some reason you can't use `LabeledField` for a visible label, you can still
21+
make `MultiSelect` accessible in a screen reader by associating it with `<label for="">`.
22+
Pass the `id` of the `MultiSelect` to the `for` attribute.
23+
24+
Alternatively, you can create an accessible name for `MultiSelect` using `aria-labelledby`.
25+
Put `aria-labelledby` on `MultiSelect` pointing to the `id` of any other element.
26+
It won't give you the same enhanced click target as a paired `<label>`, but it still
27+
helps to create a more accessible experience.
28+
29+
<Canvas of={MultiSelectAccessibilityStories.UsingAriaAttributes} />
30+
31+
## Using `aria-label` for the opener and/or child options
32+
33+
A visible label with `<LabeledField>` is preferred. However, for specific cases
34+
where the `MultiSelect` is not paired with a `LabeledField` or other visible
35+
`<label>` element, you **must** supply an `aria-label` attribute for an
36+
accessible name on the opener.
37+
38+
This will ensure the `MultiSelect` as a whole has a name that describes its purpose.
39+
40+
Also, if you need screen readers to understand relevant information on
41+
option items, you can use `aria-label` on each item. e.g. You can use it to let
42+
screen readers know the current selected/unselected status of the item when it
43+
receives focus. This can be useful when the options contain icons or other information
44+
that would need to be omitted from the visible label.
45+
46+
<Canvas of={MultiSelectAccessibilityStories.UsingOpenerAriaLabel} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as React from "react";
2+
import magnifyingGlassIcon from "@phosphor-icons/core/regular/magnifying-glass.svg";
3+
import {OptionItem, MultiSelect} from "@khanacademy/wonder-blocks-dropdown";
4+
import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field";
5+
import {View} from "@khanacademy/wonder-blocks-core";
6+
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
7+
8+
export default {
9+
title: "Packages / Dropdown / MultiSelect / Accessibility",
10+
component: MultiSelect,
11+
12+
// Disables chromatic testing for these stories.
13+
parameters: {
14+
previewTabs: {
15+
canvas: {
16+
hidden: true,
17+
},
18+
},
19+
20+
viewMode: "docs",
21+
22+
chromatic: {
23+
disableSnapshot: true,
24+
},
25+
},
26+
};
27+
28+
const MultiSelectAccessibility = () => (
29+
<View>
30+
<LabeledField
31+
label="Associated label element"
32+
field={
33+
<MultiSelect selectedValues={["one"]} onChange={() => {}}>
34+
<OptionItem label="First element" value="one" />
35+
<OptionItem label="Second element" value="two" />
36+
</MultiSelect>
37+
}
38+
/>
39+
</View>
40+
);
41+
42+
export const UsingAriaAttributes = {
43+
render: MultiSelectAccessibility.bind({}),
44+
name: "Using LabeledField",
45+
};
46+
47+
const MultiSelectAriaLabel = () => (
48+
<View>
49+
<MultiSelect
50+
aria-label="Class options"
51+
id="unique-single-select"
52+
selectedValues={["one"]}
53+
onChange={() => {}}
54+
>
55+
<OptionItem
56+
label="First element"
57+
aria-label="First element, selected"
58+
value="one"
59+
/>
60+
<OptionItem
61+
label="Second element"
62+
aria-label="Second element, unselelected"
63+
value="two"
64+
/>
65+
</MultiSelect>
66+
</View>
67+
);
68+
69+
export const UsingOpenerAriaLabel = {
70+
render: MultiSelectAriaLabel.bind({}),
71+
name: "Using aria-label attributes",
72+
};
73+
74+
const MultiSelectCustomOpenerLabeledField = () => {
75+
return (
76+
<View>
77+
<LabeledField
78+
label="Search"
79+
field={
80+
<MultiSelect
81+
onChange={() => {}}
82+
opener={(eventState: any) => (
83+
<button onClick={() => {}}>
84+
<PhosphorIcon
85+
icon={magnifyingGlassIcon}
86+
size="medium"
87+
/>
88+
</button>
89+
)}
90+
>
91+
<OptionItem label="item 1" value="1" />
92+
<OptionItem label="item 2" value="2" />
93+
<OptionItem label="item 3" value="3" />
94+
</MultiSelect>
95+
}
96+
/>
97+
</View>
98+
);
99+
};
100+
101+
export const UsingCustomOpenerLabeledField = {
102+
render: MultiSelectCustomOpenerLabeledField.bind({}),
103+
name: "Using custom opener in a LabeledField",
104+
};
105+
106+
const MultiSelectCustomOpenerLabel = () => {
107+
return (
108+
<View>
109+
<MultiSelect
110+
onChange={() => {}}
111+
opener={(eventState: any) => (
112+
<button aria-label="Search button" onClick={() => {}}>
113+
<PhosphorIcon
114+
icon={magnifyingGlassIcon}
115+
size="medium"
116+
/>
117+
</button>
118+
)}
119+
>
120+
<OptionItem label="item 1" value="1" />
121+
<OptionItem label="item 2" value="2" />
122+
<OptionItem label="item 3" value="3" />
123+
</MultiSelect>
124+
</View>
125+
);
126+
};
127+
128+
export const UsingCustomOpenerAriaLabel = {
129+
render: MultiSelectCustomOpenerLabel.bind({}),
130+
name: "Using aria-label on custom opener",
131+
};

__docs__/wonder-blocks-dropdown/multi-select.stories.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
1212
import {HeadingLarge} from "@khanacademy/wonder-blocks-typography";
1313
import {MultiSelect, OptionItem} from "@khanacademy/wonder-blocks-dropdown";
1414
import Pill from "@khanacademy/wonder-blocks-pill";
15-
import type {Labels} from "@khanacademy/wonder-blocks-dropdown";
15+
import type {LabelsValues} from "@khanacademy/wonder-blocks-dropdown";
1616

1717
import ComponentInfo from "../components/component-info";
1818
import packageConfig from "../../packages/wonder-blocks-dropdown/package.json";
@@ -220,7 +220,7 @@ export const ControlledOpened: StoryComponentType = {
220220
};
221221

222222
// Custom MultiSelect labels
223-
const dropdownLabels: Labels = {
223+
const dropdownLabels: LabelsValues = {
224224
...defaultLabels,
225225
noneSelected: "Solar system",
226226
someSelected: (numSelectedValues) => `${numSelectedValues} planets`,
@@ -595,7 +595,7 @@ export const VirtualizedFilterable: StoryComponentType = {
595595
* a function with the following arguments:
596596
* - `eventState`: lets you customize the style for different states, such as
597597
* pressed, hovered and focused.
598-
* - `text`: Passes the menu label defined in the parent component. This value
598+
* - `text`: Passes the menu value defined in the parent component. This value
599599
* is passed using the placeholder prop set in the `MultiSelect` component.
600600
* - `opened`: Whether the dropdown is opened.
601601
*
@@ -604,11 +604,16 @@ export const VirtualizedFilterable: StoryComponentType = {
604604
*
605605
* **Accessibility:** When a custom opener is used, the following attributes are
606606
* added automatically: `aria-expanded`, `aria-haspopup`, and `aria-controls`.
607+
* With a custom opener, you are still responsible for labeling the `MultiSelect`
608+
* by wrapping it in a `<LabeledField>` or using `aria-label` on the parent component
609+
* to describe the purpose of the control. Because it is a combobox, the value
610+
* can't also be used for the label.
607611
*/
608612
export const CustomOpener: StoryComponentType = {
609613
render: Template,
610614
args: {
611615
selectedValues: [],
616+
"aria-label": "Custom opener",
612617
opener: ({focused, hovered, pressed, text, opened}: OpenerProps) => {
613618
action(JSON.stringify({focused, hovered, pressed, opened}))(
614619
"state changed!",
@@ -660,7 +665,7 @@ export const CustomLabels: StoryComponentType = {
660665
>([]);
661666
const [opened, setOpened] = React.useState(true);
662667

663-
const labels: Labels = {
668+
const labels: LabelsValues = {
664669
clearSearch: "Limpiar busqueda",
665670
filter: "Filtrar",
666671
noResults: "Sin resultados",

__docs__/wonder-blocks-dropdown/single-select.accessibility.mdx

+38-38
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,49 @@ import * as SingleSelectAccessibilityStories from './single-select.accessibility
33

44
import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
55
import {View} from "@khanacademy/wonder-blocks-core";
6-
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
6+
import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field";
77

88
<Meta of={SingleSelectAccessibilityStories} />
99

10-
export const SingleSelectAccessibility = () => (
11-
<View>
12-
<LabelLarge
13-
tag="label"
14-
id="label-for-single-select"
15-
htmlFor="unique-single-select"
16-
>
17-
Associated label element
18-
</LabelLarge>
19-
<SingleSelect
20-
aria-labelledby="label-for-single-select"
21-
id="unique-single-select"
22-
placeholder="Accessible SingleSelect"
23-
selectedValue="one"
24-
>
25-
<OptionItem
26-
label="First element"
27-
aria-label="First element, selected"
28-
value="one"
29-
/>
30-
<OptionItem
31-
label="Second element"
32-
aria-label="Second element, unselelected"
33-
value="two"
34-
/>
35-
</SingleSelect>
36-
</View>
37-
);
38-
3910
# Accessibility
4011

41-
If you need to associate this component with another element (e.g. `<label>`),
42-
make sure to pass the `aria-labelledby` and/or `id` props to the `SingleSelect` component.
43-
This way, the `opener` will receive this value and it will associate both
44-
elements.
12+
## Using `LabeledField` with `SingleSelect`
4513

46-
Also, if you need screen readers to understand any relevant information on every
47-
option item, you can use `aria-label` on each item. e.g. You can use it to let
48-
screen readers know the current selected/unselected status of the item when it
49-
receives focus.
14+
To associate a `SingleSelect` with another visible element (e.g. a `<label>`),
15+
wrap it in a `LabeledField` component. The label will apply to the `SingleSelect`
16+
opener. With `LabeledField`, you can supply label text (or a JSX node)
17+
using the `label` prop to generate a paired `<label>` element. It comes with
18+
field validation and other features baked in!
19+
20+
If for some reason you can't use `LabeledField` for a visible label, you can still
21+
make `SingleSelect` accessible in a screen reader by associating it with `<label for="">`.
22+
Pass the `id` of the `SingleSelect` to the `for` attribute.
23+
24+
Alternatively, you can create an accessible name for `SingleSelect` using `aria-labelledby`.
25+
Put `aria-labelledby` on `SingleSelect` pointing to the `id` of any other element.
26+
It won't give you the same enhanced click target as a paired `<label>`, but it still
27+
helps to create a more accessible experience.
5028

5129
<Canvas of={SingleSelectAccessibilityStories.UsingAriaAttributes} />
30+
31+
## Using `aria-label` for the opener and/or child options
32+
33+
A visible label with `<LabeledField>` is preferred. However, for specific cases
34+
where the `SingleSelect` is not paired with a `LabeledField` or other
35+
visible `<label>` element, you **must** supply an `aria-label` attribute
36+
for an accessible name on the opener.
37+
38+
This will ensure the `SingleSelect` has a name that describes its purpose.
39+
40+
For example, an `aria-label` for `SingleSelect` in a compact UI could be "Division"
41+
while its value would be one of the selected options, such as specific division names.
42+
It might also have a placeholder such as "e.g., Division I (D1)", which would go away
43+
when the user selected an option.
44+
45+
Also, if you need screen readers to understand relevant information on
46+
option items, you can use `aria-label` on each item. e.g. You can use it to let
47+
screen readers know the current selected/unselected status of the item when it
48+
receives focus. This can be useful when the options contain icons or other information
49+
that would need to be omitted from the visible label.
50+
51+
<Canvas of={SingleSelectAccessibilityStories.UsingOpenerAriaLabel} />

0 commit comments

Comments
 (0)