fix(fluid-select)!: make options operable by keyboard [sc-217433] - #191
Open
mnutt wants to merge 1 commit into
Open
fix(fluid-select)!: make options operable by keyboard [sc-217433]#191mnutt wants to merge 1 commit into
mnutt wants to merge 1 commit into
Conversation
FluidSelect is built on ember-basic-dropdown, whose trigger handles only Enter, Space and Escape. There was no arrow-key navigation into the popup and the options were divs with role="button", no tabindex and no key handler, so a keyboard user could open the dropdown and then select nothing. Multi-select escaped this by accident by rendering FluidCheckbox, a real button. Implements the APG listbox pattern for both modes: the list is a role=listbox that takes focus on open, options are role=option with aria-selected, and arrow keys, Home/End, Enter, Space and Escape drive an active option tracked with aria-activedescendant. Escape restores focus to whatever opened it. The active option is written straight to the DOM rather than held in component state, because re-rendering the popup makes ember-basic-dropdown recompute its position, which re-renders the popup. Also gives the search input an accessible name and replaces role="search", which is a landmark role and invalid on an input, with role="combobox". BREAKING CHANGE: multi-select options are no longer a FluidCheckbox. The checkbox is now presentational markup inside the role=option element, which owns the click handler, so no `[role="checkbox"]` element exists in the list. The bundled page object is updated to match: every option is clicked the same way and `hasCheckbox` matches a descendant. Consuming tests that reach for `[role="checkbox"]` inside a FluidSelect option need updating.
mnutt
force-pushed
the
mn/fluid-select-keyboard-a11y
branch
from
August 27, 2026 20:10
c1663e9 to
56da301
Compare
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #190 — base is
mn/accessibility, so review that one first. The diff here is just the fluid-select commit.Shortcut: [sc-217433]
Closes the two WCAG Level A failures in
FluidSelectthat were deliberately scoped out of #190 because the fix needed its own design.Options could not be reached by keyboard — WCAG 2.1.1 (A)
FluidSelectis built onember-basic-dropdown, notember-power-select. Its trigger handles only Enter, Space and Escape (basic-dropdown-trigger.ts:85-99) — no arrow-key navigation, no focus management into the popup. The options were<div role="button">with notabindexand no key handler, so a keyboard user could open the dropdown and then select nothing. Multi-select escaped this by accident, because it renderedFluidCheckbox— a real<button>.Now implements the APG listbox pattern for single and multi select:
role="listbox"and takes focus on openrole="option"witharia-selected↓/↑(wrapping),Home/End,Enter,SpaceandEscapedrive an active option tracked viaaria-activedescendantEscapecloses and restores focus to whatever opened itaria-haspopup="listbox"and drops a redundantrole="button"The search input had no accessible name — WCAG 4.1.2 (A)
It had no label, only an optional placeholder, and carried
role="search"— a landmark role that is invalid on an<input>. It is now a properly labelledrole="combobox"that forwards arrow keys and Enter to the listbox, while still typing spaces normally.Two things worth knowing while reviewing
The active option is written straight to the DOM, not held in component state. This looks unidiomatic and is deliberate. Routing it through the template re-renders the popup, which makes ember-basic-dropdown recompute its position, which re-renders the popup. That loop wedges the browser hard enough to kill every subsequent test in the suite. There's a comment on
activateOptionsaying so; please don't "clean it up" into tracked state.Options register themselves rather than the list enumerating them.
listrenders options through theawaithelper, so none of them exist yet when the list'sdid-insertfires — not even inafterRender.Breaking change
Multi-select options are no longer a
FluidCheckbox. The checkbox is presentational markup (FluidSelect::OptionCheckbox) inside therole="option"element, which owns the click handler — so there is no[role="checkbox"]in the list any more. A real checkbox nested inside an option would be a second focus stop with a conflicting role.The bundled page object is updated to match, and gets simpler for it:
click()no longer branches on option type, andhasCheckboxmatches a descendant. Consuming test suites that reach for[role="checkbox"]inside a FluidSelect option will need updating.Visually identical — same
fluid-checkboxclasses.Testing
yarn lint-hbsandyarn lint-jsclean.yarn test: 96 tests, 95 pass, 1 pre-existing skip, 0 fail. All 18 pre-existing fluid-select tests pass unchanged apart from the page-object updates; 8 new keyboard-navigation tests cover listbox semantics, focus on open, arrow/Home/End wrapping, Enter-to-select, Escape-restores-focus, selected-option-starts-active, multi-select Space, and the combobox search input.NODE_OPTIONS=--openssl-legacy-provider yarn storybook(SB 6.4 bundles webpack 4, whose MD4 hashing OpenSSL 3 rejects).