Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5180 +/- ##
==========================================
+ Coverage 97.83% 97.85% +0.01%
==========================================
Files 1008 1012 +4
Lines 32582 32735 +153
Branches 12092 12128 +36
==========================================
+ Hits 31878 32034 +156
- Misses 663 695 +32
+ Partials 41 6 -35 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
9f3d361 to
290cc68
Compare
290cc68 to
5601acf
Compare
4c74889 to
5aaaf47
Compare
1dfd1e0 to
10601c8
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved keyboard navigation, focus, scrolling, accessibility, and renderer-contract defects affect the new component’s behavior.
9 open issues
Scroll on repeated keyboard navigation requests · New Preserve parent group state for child renderers · New Avoid dangling aria-describedby for custom disabled content · New Reconcile highlight index when options change · New Wrap navigation based on highlightable options · New Initialize keyboard focus despite stale hover highlights · New Exclude nonmatching group headers from type-ahead targets · New Ignore handled events before updating type-ahead search · New Define an OptionList-specific renderer contract · New
What changed in this PR
Adds OptionList as an inline single- or multi-selection component, building on #5179’s shared selection models. It is the first caller of the new listbox engine, while Select and Multiselect retain their existing behavior.
Changes:
- Adds the public component, grouped selection, custom rendering, and localized announcements.
- Introduces listbox keyboard, focus, scrolling, and item-rendering support.
- Adds test-utils, unit and integration tests, demo pages, and generated API snapshots.
| File | Description |
|---|---|
src/test-utils/dom/option-list/index.ts |
Adds option and group wrappers. |
src/option-list/test-classes/styles.scss |
Defines stable test selectors. |
src/option-list/styles.scss |
Adds component root styling. |
src/option-list/internal.tsx |
Connects public options to the listbox engine. |
src/option-list/interfaces.ts |
Defines the public component API. |
src/option-list/index.tsx |
Adds the public entry point. |
src/option-list/__tests__/option-list.test.tsx |
Tests component behavior and accessibility. |
src/option-list/__integ__/option-list.test.ts |
Tests browser selection and focus. |
src/option-list/__integ__/option-list-scroll.test.ts |
Tests keyboard scrolling across containers. |
src/internal/components/selectable-item/styles.scss |
Supports hiding the first divider. |
src/internal/components/selectable-item/interfaces.ts |
Adds the divider-control prop. |
src/internal/components/selectable-item/index.tsx |
Applies the divider-control class. |
src/internal/components/options-list/utils/use-keyboard.ts |
Makes Escape handling optional. |
src/internal/components/options-list/index.tsx |
Supports mouse-leave and default status. |
src/internal/components/listbox/use-type-ahead.ts |
Adds buffered keyboard matching. |
src/internal/components/listbox/use-listbox.ts |
Implements the listbox interaction controller. |
src/internal/components/listbox/parts/styles.scss |
Styles selection indicators and items. |
src/internal/components/listbox/parts/scroll-to-index.ts |
Adds ancestor-aware option scrolling. |
src/internal/components/listbox/parts/render-options.tsx |
Supports the new listbox item renderer. |
src/internal/components/listbox/parts/list.tsx |
Renders and measures inline lists. |
src/internal/components/listbox/parts/item.tsx |
Renders selection states and disabled reasons. |
src/internal/components/listbox/index.tsx |
Connects rendering and highlight scrolling. |
src/internal/components/listbox/__tests__/use-type-ahead.test.ts |
Tests type-ahead handling. |
src/i18n/messages/all.zh-TW.json |
Adds Traditional Chinese selection text. |
src/i18n/messages/all.zh-CN.json |
Adds Simplified Chinese selection text. |
src/i18n/messages/all.tr.json |
Adds Turkish selection text. |
src/i18n/messages/all.th.json |
Adds Thai selection text. |
src/i18n/messages/all.pt-BR.json |
Adds Brazilian Portuguese selection text. |
src/i18n/messages/all.ko.json |
Adds Korean selection text. |
src/i18n/messages/all.ja.json |
Adds Japanese selection text. |
src/i18n/messages/all.it.json |
Adds Italian selection text. |
src/i18n/messages/all.id.json |
Adds Indonesian selection text. |
src/i18n/messages/all.fr.json |
Adds French selection text. |
src/i18n/messages/all.es.json |
Adds Spanish selection text. |
src/i18n/messages/all.en.json |
Adds English selection text. |
src/i18n/messages/all.en-GB.json |
Adds British English selection text. |
src/i18n/messages/all.de.json |
Adds German selection text. |
src/i18n/messages/all.ar.json |
Adds Arabic selection text. |
src/i18n/messages-types.ts |
Registers the localized message type. |
src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap |
Records generated wrapper APIs. |
src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap |
Records component test selectors. |
src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap |
Records component and wrapper documentation. |
src/__tests__/required-props-for-components.ts |
Supplies shared-test defaults. |
pages/option-list/simple.page.tsx |
Demonstrates single, multi, and grouped selection. |
pages/option-list/scroll.scss |
Defines demo scroll containers. |
pages/option-list/scroll.page.tsx |
Demonstrates configurable scrolling and dropdown placement. |
pages/option-list/permutations.page.tsx |
Adds visual permutations. |
eslint.config.mjs |
Restricts direct runtime imports from Select and Multiselect. |
build-tools/utils/pluralize.js |
Registers OptionList’s generated plural name. |
Review effort: Balanced
| // OptionList and the shared listbox engine sit below Select and Multiselect, so they must not depend on them at | ||
| // runtime. Type-only imports (e.g. the `renderOption` types) are allowed. The remaining listbox files join this | ||
| // rule when Select and Multiselect move onto the engine. | ||
| files: [ |
|
|
||
| export interface OptionListProps extends BaseComponentProps { | ||
| /** | ||
| * Specifies an array of options that are displayed inline as a list. |
There was a problem hiding this comment.
The "displayed inline" part can be misunderstood. I'd drop the "inline" bit.
| * - `disabled` (boolean) - (Optional) Determines whether the option group is disabled. Disables every option in the group. | ||
| * - `options` (Option[]) - The options in the group. | ||
| * | ||
| * In multi selection, choosing a group header selects or deselects all its enabled options. |
There was a problem hiding this comment.
nit: Why do we need to describe this behaviour here?
4925c40 to
24524f9
Compare
| const isSameOption = (a: DropdownOption, b: DropdownOption) => | ||
| a.option.value !== undefined && a.option.value === b.option.value && (a.type === 'parent') === (b.type === 'parent'); |
| // The consumer receives its own option and group objects, as with `renderOption`. | ||
| renderHighlightedAriaLive: | ||
| renderHighlightedAriaLive && | ||
| ((option, group) => renderHighlightedAriaLive(toPublicOption(option), group && toPublicGroup(group))), |
| // stale index is never committed. It keeps the highlight type and doesn't request a scroll. | ||
| const [previousOptions, setPreviousOptions] = useState(options); | ||
| if (previousOptions !== options) { | ||
| setPreviousOptions(options); |
There was a problem hiding this comment.
Do we need previous options use React state? Can we use the usePrevious util instead?
| goHome, | ||
| goEnd, | ||
| }), | ||
| selectOption: () => selectOption(), |
There was a problem hiding this comment.
why do we create a new function here?
| embedded: true, | ||
| isMultiSelect: selectionType === 'multi' || undefined, | ||
| ...getListMouseHandlers({ options, selectOption, setHighlightedIndexWithMouse }), | ||
| onKeyDown: listKeyDownHandler, |
Optional additions that OptionList needs and Select and Multiselect don't pass, so their behavior does not change: onEscape is optional in useListbox (without it Escape keeps propagating); PlainList takes scrollStrategy ('menu' by default, 'ancestors' scrolls through the page or a scroll container with the new scrollOptionIntoView); OptionsList statusType is optional with 'finished' as the default; useMenuKeyboard onEscape is optional; the shared item and SelectableItem take hideTopDivider.
OptionList shows selectable options inline, without a dropdown, with single or multi selection, option groups, disabled options with a disabled reason, icons, label tags and tags, custom option rendering (renderOption) and a custom live announcement (renderHighlightedAriaLive). useOptionList (src/option-list/use-option-list.ts) calls the shared useListbox with embedded: true, which handles focus and blur, and adds the mouse-leave reset and type-ahead. The list renders through the shared PlainList with scrollStrategy 'ancestors'. Also adds the OptionList test-utils and the option-list i18n namespace.
Simple, scroll, and single and multi selection permutation pages. The integration tests use the simple and scroll pages.

Description
Stacked on #5239.
What: Adds OptionList, an always visible list with single or multi selection, plus its test-utils and dev pages. It runs on the shared
useListboxhook from #5239, the same engine as Select and Multiselect. Select and Multiselect behave the same.How:
src/option-list/use-option-list.tsbuilds onuseListboxand adds only what an always visible list needs:getRootProps())OptionList renders the shared
PlainList.Shared listbox additions (only OptionList uses them; Select and Multiselect don't pass them):
onEscapeis optional inuseListboxanduseMenuKeyboard. Without it, Escape is not handled and keeps propagating.PlainListtakesscrollStrategy. The default'menu'scrolls the list itself.'ancestors'scrolls the highlighted row into view through the page or a parent scroll container (scrollOptionIntoView), because OptionList is not its own scroll container.OptionsListstatusTypeis optional, defaulting to'finished'.SelectableItemtakehideTopDivider.Commits:
Follow-ups after this PR (they change behavior for all three components, so they are kept out of here):
aria-activedescendantonly while an option is highlightedpreventDefault, wrapping past empty groups, type-ahead andrenderOptionfixesRelated links, issue #, if available: Docs: CUJAl2wq1b6Z and 5evHnSwN7iHe
How has this been tested?
renderOption, i18n, accessibility and test-utils.scrollOptionIntoView,PlainListscrollStrategy,useMenuKeyboardwithoutonEscape, theOptionsListstatusTypedefault andhideTopDivider.option-listdev pages.Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.