Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WB-1671: Dropdown: use combobox role in all openers #2345

Merged
merged 23 commits into from
Jan 17, 2025

Conversation

jandrade
Copy link
Member

Summary:

Addreses an issue in WB dropdowns where we were using the button role in the
opener, which was causing issues with screen readers. This change updates the
role to combobox that is more appropriate for the dropdown opener.

Issue: https://khanacademy.atlassian.net/browse/WB-1671

Test plan:

Navigate to the dropdowns in the Storybook and verify that the role of the
opener is combobox.

More details TBD

@jandrade jandrade self-assigned this Oct 15, 2024
Copy link

changeset-bot bot commented Oct 15, 2024

🦋 Changeset detected

Latest commit: 65b00ff

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@khanacademy/wonder-blocks-dropdown Major
@khanacademy/wonder-blocks-birthday-picker Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khan-actions-bot khan-actions-bot requested a review from a team October 15, 2024 15:21
@khan-actions-bot
Copy link
Contributor

khan-actions-bot commented Oct 15, 2024

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .changeset/tasty-rockets-mix.md, __docs__/wonder-blocks-dropdown/multi-select.accessibility.mdx, __docs__/wonder-blocks-dropdown/multi-select.accessibility.stories.tsx, __docs__/wonder-blocks-dropdown/multi-select.stories.tsx, __docs__/wonder-blocks-dropdown/single-select.accessibility.mdx, __docs__/wonder-blocks-dropdown/single-select.accessibility.stories.tsx, __docs__/wonder-blocks-dropdown/single-select.stories.tsx, packages/wonder-blocks-dropdown/src/index.ts, packages/wonder-blocks-dropdown/src/components/action-menu.tsx, packages/wonder-blocks-dropdown/src/components/dropdown-core.tsx, packages/wonder-blocks-dropdown/src/components/dropdown-opener.tsx, packages/wonder-blocks-dropdown/src/components/multi-select.tsx, packages/wonder-blocks-dropdown/src/components/select-opener.tsx, packages/wonder-blocks-dropdown/src/components/single-select.tsx, packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx, packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx, packages/wonder-blocks-dropdown/src/components/__tests__/select-opener.test.tsx, packages/wonder-blocks-dropdown/src/components/__tests__/single-select.test.tsx

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

@jandrade jandrade removed the request for review from a team October 15, 2024 15:21
@jandrade
Copy link
Member Author

NOTE: unchecking the wonder-blocks team as I want to run some tests in webapp first.

Copy link
Contributor

github-actions bot commented Oct 15, 2024

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (1e9af86) and published all packages with changesets to npm.

You can install the packages in webapp by running:

./services/static/dev/tools/deploy_wonder_blocks.js --tag="PR2345"

Packages can also be installed manually by running:

yarn add @khanacademy/wonder-blocks-<package-name>@PR2345

Copy link
Contributor

github-actions bot commented Oct 15, 2024

Size Change: +122 B (+0.12%)

Total Size: 98.4 kB

Filename Size Change
packages/wonder-blocks-dropdown/dist/es/index.js 19.2 kB +122 B (+0.64%)
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 3.77 kB
packages/wonder-blocks-banner/dist/es/index.js 1.53 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.77 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 887 B
packages/wonder-blocks-button/dist/es/index.js 4.12 kB
packages/wonder-blocks-cell/dist/es/index.js 2.01 kB
packages/wonder-blocks-clickable/dist/es/index.js 3.06 kB
packages/wonder-blocks-core/dist/es/index.js 2.9 kB
packages/wonder-blocks-data/dist/es/index.js 6.24 kB
packages/wonder-blocks-form/dist/es/index.js 6.2 kB
packages/wonder-blocks-grid/dist/es/index.js 1.36 kB
packages/wonder-blocks-icon-button/dist/es/index.js 2.95 kB
packages/wonder-blocks-icon/dist/es/index.js 871 B
packages/wonder-blocks-labeled-field/dist/es/index.js 1.94 kB
packages/wonder-blocks-layout/dist/es/index.js 1.82 kB
packages/wonder-blocks-link/dist/es/index.js 2.28 kB
packages/wonder-blocks-modal/dist/es/index.js 5.42 kB
packages/wonder-blocks-pill/dist/es/index.js 1.65 kB
packages/wonder-blocks-popover/dist/es/index.js 4.85 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.52 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.36 kB
packages/wonder-blocks-switch/dist/es/index.js 1.92 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.74 kB
packages/wonder-blocks-testing/dist/es/index.js 1.07 kB
packages/wonder-blocks-theming/dist/es/index.js 693 B
packages/wonder-blocks-timing/dist/es/index.js 1.8 kB
packages/wonder-blocks-tokens/dist/es/index.js 2.36 kB
packages/wonder-blocks-toolbar/dist/es/index.js 905 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.99 kB
packages/wonder-blocks-typography/dist/es/index.js 1.23 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Oct 15, 2024

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-fixmheucmr.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 0
Tests with visual changes 1
Total stories 535
Inherited (not captured) snapshots [TurboSnap] 383
Tests on the build 383

@khan-actions-bot khan-actions-bot requested a review from a team October 18, 2024 15:58
@jandrade jandrade removed the request for review from a team October 18, 2024 16:01
@khan-actions-bot khan-actions-bot requested a review from a team November 4, 2024 15:59
@jandrade jandrade changed the title WB-1671: Dropdown: use combobox role in all openers [DRAFT - DO NOT REVIEW YET] WB-1671: Dropdown: use combobox role in all openers Nov 4, 2024
@jandrade jandrade removed the request for review from a team November 15, 2024 14:59
@khan-actions-bot khan-actions-bot requested a review from a team November 25, 2024 16:37
@jandrade jandrade removed the request for review from a team November 25, 2024 16:45
@khan-actions-bot khan-actions-bot requested a review from a team December 16, 2024 15:29
@jandrade jandrade removed the request for review from a team December 16, 2024 16:25
@khan-actions-bot khan-actions-bot requested a review from a team December 16, 2024 17:18
@jandrade jandrade removed the request for review from a team December 16, 2024 17:23
@khan-actions-bot khan-actions-bot requested a review from a team January 8, 2025 23:36
@marcysutton marcysutton changed the title [DRAFT - DO NOT REVIEW YET] WB-1671: Dropdown: use combobox role in all openers WB-1671: Dropdown: use combobox role in all openers Jan 8, 2025
@marcysutton
Copy link
Member

I rebased this one to bring it up to date and I'm proposing we merge it to deal with a number of screen reader issues for Dropdowns. Essentially, we need the combobox role to specify both a label and a value for these components. The one caveat is that VoiceOver and Safari are very buggy with comboboxes in React. I plan to add additional screen reader support in a later PR using the new Announcer in #2362.

@jandrade jandrade assigned marcysutton and unassigned jandrade Jan 9, 2025
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, looks great and improves the accessibility of our components :accessibility: 🎉
Had some comments and questions!

@@ -569,6 +569,7 @@ const MultiSelect = (props: Props) => {
disabled={isDisabled}
id={uniqueOpenerId}
aria-controls={dropdownId}
placeholder={noneSelected}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the placeholder be menuText so the aria-label changes when the selected values change? Otherwise, it'll always have an aria-label for no items selected. menuText isn't always a string though since it can be custom jsx! Or maybe the select opener wouldn't need an aria-label since the component should be used with a <label>? 🤔 What do you think!

Here's a screenshot that shows the aria-label with "0 items" while the component shows "3 items":
Screenshot 2025-01-09 at 4 05 35 PM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @beaesguerra! I don't think that aria-label is necessarily the same as a placeholder for a combobox. This text about the number of items is more of a value than a label. I'm guessing the intent was to use a placeholder as a label in the absence of a separate label element (or associated label, however it's marked up). But we have to be careful with aria-label to not override an associated label, if it's paired with <label for="$idMatchingThisCombobox">. I'm looking into it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jandrade or @beaesguerra do you know if the MultiSelect component is used with an explicit label (<label for="thisSelectComponentWithMatchingID">? I don't see a story for it like there is on SingleSelect.

I'll dig into this more -- but I'm wondering if we have any way to conditionally add an aria-label to the opener for placeholders and not clash with other labeling mechanisms. Any input you can add would be helpful!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be used with a label, specifically LabeledField 😄 Here's an example in Storybook: https://5e1bf4b385e3fb0020b7073c-fnsnwmxvwb.chromatic.com/?path=/story/packages-labeledfield--fields

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, we should use LabeledField (or label on very specific cases), but there might be places where we are not using labels and aria-label could be useful. For example, the course selector in the teacher dashboard.

You'll need to have "Teacher" access (available with your @khanacademy.org account), then go to /teacher/dashboard, click on any class and use the dropdown on the top left.

Screenshot 2025-01-14 at 2 59 58 PM

NOTE: This could be a case where we could benefit from having an actual label and this could be validated with the TX team.

Copy link
Member

@marcysutton marcysutton Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, LabeledField 🤦‍♀️! Thank you so much for this context. I think the challenge is at the Combobox level, where it won't know whether it's being used by labeled field. Fortunately I think we can leverage the Accessible Name Computation spec to prioritize the LabeledField label over an aria-label (in the event both are present). We can also instruct consumers when and when not to use aria-label in docs!

"@khanacademy/wonder-blocks-dropdown": patch
---

Update dropdown openers to use `role="combobox"` instead of `button`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(very) nit: The ActionMenu also uses DropdownOpener and it still uses role="button"!

Suggested change
Update dropdown openers to use `role="combobox"` instead of `button`.
Update dropdown openers for SingleSelect and MultiSelect to use `role="combobox"` instead of `button`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems very important to me! Good call!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added another note to the changelog about labeling!

@@ -77,7 +77,7 @@ describe("MultiSelect", () => {
const {userEvent} = doRender(uncontrolledMultiSelect);

// Act
await userEvent.click(await screen.findByRole("button"));
await userEvent.click(await screen.findByRole("combobox"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we make internal changes that break existing tests, do we consider it a major breaking change since it could break tests in consuming projects? In this case, I'm thinking it's not since the role isn't part of the external API of the component! I'm curious to hear what others think though!

(I might have asked this before but wanted to double check!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really good question. I do think it should be a major breaking change! Initially I thought it would be a minor change (and I think this PR already had a changeset with patch indicated?). But I asked Chat GPT about it to counter any of my own biases and got a really good response:

Changing an ARIA role from button to combobox in an open source design system component would generally be considered a major change according to Semantic Versioning (SemVer).

Here's why:

  • Breaking change: The ARIA role defines how assistive technologies and user agents interpret the element. Changing the role from button to combobox could fundamentally alter how the component behaves for users relying on assistive technologies like screen readers. For example:
    • A button role indicates a simple button for triggering an action, whereas a combobox role indicates an interactive control for selecting a value, often with a dropdown, which could change the expected behavior of the component significantly.
    • This change would affect accessibility, user experience, and potentially even the visual appearance or interaction model if other associated attributes (like aria-expanded, aria-controls, etc.) also need to be modified.
  • Backward compatibility: If users of the component are relying on the previous behavior (button), changing the ARIA role to combobox could break their existing implementations. This would be a backward-incompatible change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @jandrade in case you have thoughts on how this is normally handled in WB!

If this is a major breaking change, we'll have to make sure to coordinate the perseus + webapp updates since WB is a peer dependency in perseus and we have to be more careful with the order of releases for breaking changes!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point about changing how Screen Readers could change based on these changes is very compelling. I've tended not to mark as major these kind of changes as the public API does not change, but as it affects the way this component is announced by SRs, then that makes this a great candidate for a major change. For the consumer unit tests changing, I don't consider it a breaking change as the bundled code does not change (or the code that is being shipped and consumed by the end user).

@beaesguerra makes another great point about coordinating with the Perseus team as I know they use the dropdown package in their widgets.

@marcysutton
Copy link
Member

marcysutton commented Jan 16, 2025

I've updated this PR with a few things:

  • Using aria-label when applied, not falling back to placeholder content ("none selected", etc.)
  • Stories that show how to use <label> vs. aria-label
  • A couple of internal renames:
    • typing of Labels to LabelsValues since that object contains both things
    • a text variable in openers to content, since it also potentially contains JSX nodes. This one isn't strictly necessary and could be omitted. But it helped my understanding of the code when I was attempting to make automatic aria-label happen (it required pure text).

The one outstanding thing I'm not sure how to handle is labeling of a custom opener. My initial attempt didn't work, as I'm not totally understanding that API just yet! Seems like it could be done as a follow-up and not block this work.

Changing an ARIA role is considered a major breaking change in semver
This makes it more explicit that everything isn't a label in a Dropdown. You can also customize values using this object.
MultiSelect/SingleSelect no longer fall back to placeholder values for labels, so stories have been updated to reflect this.
VoiceOver was reading out "space" when it shouldn't have
"Text" in Dropdowns is confusing for content because it also potentially includes JSX nodes. Content seems more clear to me.
/>
</MultiSelect>
<LabeledField
label={<strong>Associated label element</strong>}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the label wrapped in the <strong> tag? I'm wondering if we should use the default styling in our examples so we can avoid one-offs!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need it! I was trying to make it match how it looked before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these examples even get used somehow? I don't think they are actually rendered. Maybe something broke along the way? I'm considering removing them since the examples come from the *.accessibility.stories files.

Copy link
Member

@beaesguerra beaesguerra Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also a bit unsure when we should create both .accessibility.mdx files and .accessibility.stories.ts files. I'm okay with combining them and keeping the docs in .accessibility.stories.ts files instead!

We could reference to it from the .mdx file though, here's an example:

<Canvas of={MultiSelectAccessibilityStories.UsingOpenerAriaLabel} />

cc: @jandrade in case you have thoughts on this!

@khan-actions-bot khan-actions-bot requested a review from a team January 17, 2025 21:56
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! 🎉 This will address many accessibility issues!

Comment on lines +971 to +1016
it("labels the custom opener with `aria-label` on MultiSelect", async () => {
// Arrange
doRender(
<MultiSelect
aria-label="Search"
onChange={jest.fn()}
opener={(eventState: any) => <button onClick={jest.fn()} />}
>
<OptionItem label="item 1" value="1" />
<OptionItem label="item 2" value="2" />
<OptionItem label="item 3" value="3" />
</MultiSelect>,
);

// Act
const opener = await screen.findByRole("combobox");

// Assert
expect(opener).toHaveAccessibleName("Search");
});

it("prioritizes `aria-label` on the custom opener", async () => {
// Arrange
doRender(
<MultiSelect
aria-label="Not winning the label race"
onChange={jest.fn()}
opener={(eventState: any) => (
<button
aria-label="Search button"
onClick={jest.fn()}
/>
)}
>
<OptionItem label="item 1" value="1" />
<OptionItem label="item 2" value="2" />
<OptionItem label="item 3" value="3" />
</MultiSelect>,
);

// Act
const opener = await screen.findByRole("combobox");

// Assert
expect(opener).toHaveAccessibleName("Search button");
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these test cases!

@@ -251,6 +253,7 @@ const MultiSelect = (props: Props) => {
shortcuts = false,
style,
className,
"aria-label": ariaLabel,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking) docs: One thing I realized with the aria attributes is that they don't automatically show up in the Storybook component docs 😢 : https://5e1bf4b385e3fb0020b7073c-oodutnedcb.chromatic.com/?path=/docs/packages-dropdown-multiselect--docs

We could explicitly add it to the story argTypes so they show up in the table! This can also be part of other work, since this is common to all components that extend the AriaProps type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great idea! I can open an issue for it, unless there already is one!

@khan-actions-bot khan-actions-bot requested a review from a team January 17, 2025 22:41
@marcysutton marcysutton merged commit 1a18e98 into feature/dropdown-combobox Jan 17, 2025
14 checks passed
@marcysutton marcysutton deleted the dropdown-combobox branch January 17, 2025 22:58
Copy link

codecov bot commented Jan 17, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (d8c91db) to head (b6a939b).
Report is 1 commits behind head on feature/dropdown-combobox.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                @@
##   feature/dropdown-combobox   #2345   +/-   ##
=================================================
=================================================

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d8c91db...b6a939b. Read the comment docs.

marcysutton added a commit that referenced this pull request Feb 3, 2025
…` 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants