feat: rebuild `Select` as a select-only combobox
Proposed changes (including videos or screenshots)
Fuselage's Select no longer goes through React Aria's useSelect. Its trigger always carried an aria-labelledby, which overrode a label pointing at the select's id. So <FieldLabel htmlFor={id}> + <Select id={id}> never named the select (screen readers heard only the selected value), and React Aria logged "If you do not provide a visible label, you must specify an aria-label or aria-labelledby attribute" on every render.
The select is now a hand-built select-only combobox:
- The trigger is a
buttonwithrole="combobox". Alabelpointing at itsidnames it, like any other input, andaria-describedbyreaches it for hints and errors.errorsetsaria-invalid. - The focus stays on the trigger while the popup is open.
aria-activedescendantmarks the active option. - Keyboard: arrows, Home/End, PageUp/PageDown, Enter/Space to select, Tab selects and moves on, Escape closes, Alt+Up, and multi-character typeahead (a space typed mid-search extends the search).
- The popup is Fuselage's
PositionAnimated, placed below the select at its width, and the page doesn't scroll while it is open. The list is named after the select. - The props are unchanged, so no call site changes.
The PR's changes, by concern:
- fix: positioned popups no longer crash when two of them are removed in the same render. They share one portal container, and the second cleanup tried to remove it again.
- refactor: move
useActiveDescendantfrom the client to@rocket.chat/fuselage-hooks, so the client's comboboxes (composer popup, navbar search) and Fuselage share it. - feat: add
ListboxandListboxOption, the popup list for a combobox that keeps the focus. The other legacy selects (MultiSelect,PaginatedSelect, …) can move onto them later.OptiongainsfocusVisible. - feat: rebuild
Selecton them, and removeSelectAria,SelectTriggerand the React Aria listbox.useTypeaheadstays private toSelectfor now. - test: client tests wait for a select's options to appear (
findByRole), since the popup stays hidden until it is positioned, as the other positioned lists already do. Tests that found the select by label text now query the combobox, because its list carries the same name.
Visual comparison with develop
I compared Storybook pixel by pixel against develop in 13 states. All closed states, hover, keyboard focus, and the popup opened by keyboard are identical. Two differences, both intended:
- The option under the mouse gets the hover surface, not the focus ring. On
develop, React Aria's real focus made the ring follow the mouse. The ring now shows only while the keyboard drives the list, as it already does inMenu(focusVisible={isFocused && isFocusVisible}). - The
Statesstory now shows its states. It fakes hover, active and focus throughclassName, which React Aria dropped. No call site passesclassNametoSelect.
Issue(s)
https://rocketchat.atlassian.net/browse/ARCH-2589
Steps to test or reproduce
- Open any settings page that uses a select field, such as Administration → Settings → Retention Policy → Timer precision. The select is named by its label ("Timer precision"), and the console no longer shows the React Aria warning.
- With the keyboard, Tab to a select and press ArrowDown. The list opens on the selected option. Arrows move, Enter selects, Escape closes without changing. Type the first letters of an option to jump to it.
- With the mouse, open a select, pick an option, and click outside. The list closes, and the focus stays on the select (also in Safari).
- In Storybook, open Inputs/Select.
Further comments
- Checked in Chromium, Firefox and WebKit through Playwright: name from
<label for>, keyboard, pointer, outside click, Space without the button's own click toggling the list, popup position and width. A manual screen reader pass (VoiceOver + Safari) is still worth doing. - Some selects still have no label at all, such as the Threads, Files and Members filters. A follow-up adapts the call sites, mostly back to plain
htmlFor. react-ariaremains a dependency of Fuselage forMenu,PopoverandSlider.
🤖 Generated with Claude Code
Summary by CodeRabbit
- New Features
- Added reusable listbox and listbox option components, with support for active, selected, disabled, and focus-visible states.
- Select menus now support keyboard navigation, type-ahead search, controlled or default values, and form submission.
- Improvements
- Select menus are now select-only comboboxes with accessible labels. Use
disabled,value, andonChangeto control them; the previous React Aria selection props are no longer supported.
- Select menus are now select-only comboboxes with accessible labels. Use