Repository navigation
fix(frontend): expose RSelect as a combobox with an active descendant - #5318
Conversation
The activator is now role=combobox with aria-controls to the listbox it opens and aria-activedescendant on the active option (on the search input when searchable), so screen readers announce the option under keyboard focus. Adds Home, End, PageUp, PageDown and type-to-select, and falls back to the placeholder for the accessible name since a combobox takes its name from a label, not its contents. useSpatialNav no longer treats a closed button-based combobox as owning the arrow keys, so pad and keyboard focus still leave a closed select. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq
The play test read the Size header button synchronously and raced the first render, failing intermittently. Same change as in #5315. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq
|
The Generated by Claude Code |
…y tests The synthetic All row could only be activated with the mouse: the active index reached -1 through hover alone. Arrow keys, Home and Page keys now include it, and Enter toggles it. Adds play tests for the searchable and multi-select keyboard paths and for RDateField, which had none. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq
A combobox takes its name from a label, not its contents, so selects that relied on their visible value now fail axe (button-name). An aria-label attribute passed to RSelect now names the activator instead of the wrapper div, and the five selects without any label get one from existing locale keys: the language selector, the permission group, the folder mapping type cell, and the manual and walkthrough pickers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq
|
The
No new locale keys. The full Generated by Claude Code |
[Medium impact] The PR appears safe to merge; no new actionable issue was found. Summary
Reviews (2) · Last reviewed commit: "fix(frontend): keep RSelect typeahead fr..." · Reviewed by Greptile |
There was a problem hiding this comment.
🟡 Changes recommended
Keyboard navigation still has paging, typeahead, and shortcut conflicts, and searchable selects can lose active-option announcements when focus stays on the activator.
4 open issues
What changed in this PR
This PR improves accessibility and keyboard navigation in RomM’s shared RSelect component by exposing combobox semantics and active-option references.
Changes:
- Adds active-descendant bindings, accessible labels, paging, typeahead, and keyboard access to the “All” row.
- Adjusts spatial navigation so closed button-based comboboxes do not trap arrow-key focus.
- Expands unit and Storybook interaction coverage and fixes a table-story rendering race.
| File | Description |
|---|---|
frontend/src/lib/forms/RTextField/RTextField.vue |
Supports active-descendant references through the popup prop. |
frontend/src/lib/forms/RSelect/RSelect.vue |
Adds combobox semantics, labels, and keyboard navigation. |
frontend/src/lib/forms/RSelect/RSelect.test.ts |
Tests accessibility attributes and keyboard behavior. |
frontend/src/lib/forms/RSelect/RSelect.stories.ts |
Adds keyboard, search, and multi-select interaction stories. |
frontend/src/lib/forms/RDateField/RDateField.stories.ts |
Adds keyboard date-selection coverage. |
frontend/src/lib/data/RTable/RTable.stories.ts |
Waits for the sort button before interacting. |
frontend/src/composables/useSpatialNav/index.ts |
Allows navigation away from closed button comboboxes. |
frontend/src/composables/useSpatialNav/index.test.ts |
Covers closed-combobox focus navigation. |
frontend/src/components/shared/PlatformSelect.stories.ts |
Queries the select by its new combobox role. |
frontend/src/components/shared/LanguageSelector.vue |
Adds an accessible language label. |
frontend/src/components/Settings/FolderMappingTypeCell.vue |
Labels the mapping-type select. |
frontend/src/components/Settings/CreateUserDialog.vue |
Labels the permission-group select. |
frontend/src/components/GameDetails/WalkthroughSubtab.vue |
Labels the walkthrough select. |
frontend/src/components/GameDetails/ManualSubtab.vue |
Labels the manual select. |
Review effort: Balanced
Paging backs off a disabled last or first row instead of stopping, the type-to-select buffer resets when the menu closes, and the activator keeps aria-activedescendant when a searchable select does not move focus to its search input (touch-primary devices). Interaction coverage now lives in the Storybook play stories, with a new story for disabled rows. The unit tests keep only the cases the stories do not cover. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq
A space inside a typing burst now continues the query instead of picking the active row, and useGlobalHotkeys ignores keys a widget has already claimed, so typing g then p in a select no longer navigates away. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq

Description
Explain the changes or enhancements you are proposing with this pull request.
RSelectis a custom listbox (a<button>activator plus a teleportedrole="listbox"panel). It had noaria-activedescendant, so a screen reader announced nothing while arrowing through options, and it lacked the keys a native select has. This brings it in line with the ARIA Authoring Practices select-only combobox pattern. It stays custom: chips, the "All" row, search and the clear/remove buttons can't be done with a native<select>.RSelect
role="combobox", witharia-controlspointing at the listbox only while it is mounted.aria-activedescendantfor the active option. Whensearchable, focus lives in the search input, so the input carries the combobox semantics andaria-activedescendantinstead, throughRTextField's existingpopupprop (extended with an optionalactivedescendant).aria-labelattribute passed toRSelectnow names the activator instead of the wrapper<div>, an unlabelled field falls back to itsplaceholder, and an inline label is referenced througharia-labelledby.e2eaxe check reportsbutton-nameon the login, logs and library-management pages.Side effect fixed:
useSpatialNavuseSpatialNavtreats everycomboboxas owning the arrow keys, which would have left a closedRSelecttrapping focus for pad and keyboard users. A button-based combobox now owns the arrows only while open (the existingaria-haspopup+aria-expandedcheck), with a test that fails without the change.Tests
aria-labelplacement.RSelectkeyboard navigation, searchable keyboard, and multi-select All row; plus a keyboard-selection story forRDateField, which had noplay().PlatformSelect.stories.tslooked the select up as abutton; it now looks for acombobox.Not changed: no visual change, so no screenshots.
aria-haspopup="listbox"is kept, which the e2e role spec anduseSpatialNavrely on.AI disclosure: written with Claude Code (AI-assisted), including the code, the tests and the checks below. A human has not yet reviewed the diff, and I did not test with an actual screen reader (NVDA, VoiceOver); verification is axe plus the DOM-level assertions above.
Checklist
Please check all that apply.
Verified locally on top of current
master(including #5315):vue-tsc --build, ESLint on the changed files, theappvitest project, and the full Storybook project including axe (62 files, 488 tests). CI, includinge2e, was green before the merge frommaster.Screenshots (if applicable)
None: no visual change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JKqnUCfoaS7bWkMLmGJ5qq