Skip to content

feat: Add OptionList component - #5180

Open
amanabiy wants to merge 5 commits into
mainfrom
feat/add-option-list
Open

amanabiy wants to merge 5 commits into
mainfrom
feat/add-option-list

Conversation

@amanabiy

@amanabiy amanabiy commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

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 useListbox hook from #5239, the same engine as Select and Multiselect. Select and Multiselect behave the same.

How: src/option-list/use-option-list.ts builds on useListbox and adds only what an always visible list needs:

  • the list is a tab stop with its own focus and blur, like opening and closing Select or Multiselect with the keyboard: focus highlights the first selected option (in selection order) and scrolls it into view, or the first option when nothing is selected. An existing hover highlight is kept, so a click selects the hovered option. Blur clears the highlight.
  • type-ahead, and clearing a hover highlight when the pointer leaves the component (getRootProps())

OptionList renders the shared PlainList.

Shared listbox additions (only OptionList uses them; Select and Multiselect don't pass them):

  • onEscape is optional in useListbox and useMenuKeyboard. Without it, Escape is not handled and keeps propagating.
  • PlainList takes scrollStrategy. 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.
  • OptionsList statusType is optional, defaulting to 'finished'.
  • The shared item and SelectableItem take hideTopDivider.

Commits:

  1. refactor: Add OptionList support to the shared listbox code
  2. feat: Add OptionList component
  3. chore: Add OptionList dev pages
  4. test: Add OptionList unit and integration tests

Follow-ups after this PR (they change behavior for all three components, so they are kept out of here):

  • which selected option gets the highlight on focus when several are selected: selection order (today, as in Multiselect) or list order (ARIA listbox pattern)
  • scrolling on every keyboard highlight, the highlight following its option when options change, and aria-activedescendant only while an option is highlighted
  • Home/End preventDefault, wrapping past empty groups, type-ahead and renderOption fixes

Related links, issue #, if available: Docs: CUJAl2wq1b6Z and 5evHnSwN7iHe

How has this been tested?

  • Unit tests for selection, keyboard, type-ahead, focus and blur, highlight and scrolling, groups, disabled options, renderOption, i18n, accessibility and test-utils.
  • Unit tests for the shared additions: scrollOptionIntoView, PlainList scrollStrategy, useMenuKeyboard without onEscape, the OptionsList statusType default and hideTopDivider.
  • Integration tests for keyboard scrolling across scroll containers and inside a Dropdown, run at CI's window size.
  • Existing Select and Multiselect tests pass unchanged. Documenter and test-utils snapshots change only for OptionList.
  • Manual checks on the option-list dev pages.
Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.35897% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.85%. Comparing base (04cb6a0) to head (3dcfa93).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/option-list/internal.tsx 98.66% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@amanabiy
amanabiy force-pushed the feat/add-option-list branch from 9f3d361 to 290cc68 Compare October 23, 2026 08:45
@amanabiy
amanabiy force-pushed the feat/add-option-list branch from 290cc68 to 5601acf Compare October 26, 2026 10:54
@amanabiy
amanabiy force-pushed the feat/add-option-list branch from 4c74889 to 5aaaf47 Compare October 26, 2026 14:30
@amanabiy
amanabiy requested a balanced review from Copilot October 27, 2026 10:10
@amanabiy
amanabiy force-pushed the feat/add-option-list branch from 1dfd1e0 to 10601c8 Compare October 27, 2026 10:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved keyboard navigation, focus, scrolling, accessibility, and renderer-contract defects affect the new component’s behavior.

9 open issues
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

Comment thread src/option-list/interfaces.ts Resolved
Comment thread eslint.config.mjs Outdated
// 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: [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this rule still needed?

Comment thread src/option-list/interfaces.ts Outdated

export interface OptionListProps extends BaseComponentProps {
/**
* Specifies an array of options that are displayed inline as a list.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The "displayed inline" part can be misunderstood. I'd drop the "inline" bit.

Comment thread src/option-list/interfaces.ts Outdated
* - `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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Why do we need to describe this behaviour here?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Value-less group headers can cause ARIA-live callbacks to throw and lose their highlight after controlled multi-selection rerenders, preventing further Enter or Space selection.

2 open issues

Review effort: Balanced

Comment on lines +67 to +68
const isSameOption = (a: DropdownOption, b: DropdownOption) =>
a.option.value !== undefined && a.option.value === b.option.value && (a.type === 'parent') === (b.type === 'parent');
Comment thread src/option-list/internal.tsx Outdated
// The consumer receives its own option and group objects, as with `renderOption`.
renderHighlightedAriaLive:
renderHighlightedAriaLive &&
((option, group) => renderHighlightedAriaLive(toPublicOption(option), group && toPublicGroup(group))),

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

// 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need previous options use React state? Can we use the usePrevious util instead?

goHome,
goEnd,
}),
selectOption: () => selectOption(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we create a new function here?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Single-selection keyboard navigation fails to wrap around empty boundary groups, and Home/End do not prevent native page scrolling.

3 open issues

Review effort: Balanced

embedded: true,
isMultiSelect: selectionType === 'multi' || undefined,
...getListMouseHandlers({ options, selectOption, setHighlightedIndexWithMouse }),
onKeyDown: listKeyDownHandler,
pan-kot
pan-kot previously approved these changes Nov 5, 2026
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.

This branch was successfully deployed

3 active deployments
dev-pages-react16 — 3dcfa937 Deployed Nov 6, 2026 by jperals via deploy (React 16) / deploy #3224
dev-pages-react18 — 3dcfa937 Deployed Nov 6, 2026 by jperals via deploy (React 18) / deploy #3224
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