Skip to content

[select] Fix autofill and selected state edge cases#4934

Open
atomiks wants to merge 2 commits into
mui:masterfrom
atomiks:codex/select-autofill-state-fixes
Open

[select] Fix autofill and selected state edge cases#4934
atomiks wants to merge 2 commits into
mui:masterfrom
atomiks:codex/select-autofill-state-fixes

Conversation

@atomiks
Copy link
Copy Markdown
Contributor

@atomiks atomiks commented May 28, 2026

Fixes several Select edge cases where displayed selection state, autofill matching, and locked interactions could drift from the current value.

Changes

  • Match Select autofill against rendered item text after value and label checks so primitive values like CA can accept browser-filled text like Canada.
  • Keep item data-selected tied to the current value when controlled value changes while the popup is open.
  • Keep Field filled state consistent with placeholder semantics for empty string values.
  • Ignore item commits when a controlled-open Select is disabled or read-only.
  • Keep scroll arrows mounted through transition status so ending styles can apply.
  • Add regression coverage for the fixed Select behaviors.

@atomiks atomiks added type: bug It doesn't behave as expected. component: select Changes related to the select component. labels May 28, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

commit: 6f30a2d

@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented May 28, 2026

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+48B(+0.01%) 🔺+19B(+0.01%)

Details of bundle changes

Performance

Total duration: 1,095.84 ms -53.61 ms(-4.7%) | Renders: 50 (+0) | Paint: 1,674.76 ms -67.03 ms(-3.8%)

No significant changes — details


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 28, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 6f30a2d
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a196daa2a4b740007f357df
😎 Deploy Preview https://deploy-preview-4934--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment on lines -100 to -104
// `selectedIndex` is only updated after the items mount for the first time,
// the value check avoids a re-render for the initially selected item.
if (state.selectedIndex === index && state.selectedIndex !== null) {
return true;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Select open renders are unchanged (14) according to the benchmark and running the benchmark showed ~no difference

What the old code was trying to do: use selectedIndex as a fast path after items mount, while falling back to the value comparison before selectedIndex is initialized so the initially selected item doesn’t need a second render. The problem is that selectedIndex is not kept in sync while open, so it can become stale and override the actual controlled value.

@atomiks atomiks marked this pull request as ready for review May 28, 2026 11:23
Copy link
Copy Markdown
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

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

PR #4934 Review Summary — [select] Fix autofill and selected state edge cases

Four review agents (code, comments, tests, silent failures) ran in parallel against branch pr-4934. Tests, lint, and types were verified clean by the code reviewer.

The fixes themselves are solid — every behavior change is correctly implemented and aligned with peer components. The main risk is test coverage: several behavioral changes ship under-tested, and one existing test may not actually exercise the new guard it appears to verify.


Critical (should fix before merge)

  1. commitSelection disabled/readOnly test may not isolate the new guardSelectRoot.test.tsx:872-904 uses user.click(...), but useButton({disabled}) already swallows the click upstream. Verify the test fails when you remove SelectItem.tsx:134-138; if not, add a path that reaches commitSelection (keyboard Enter on a highlighted item, or defaultOpen variant).
  2. No coverage for SelectScrollArrow visible → mounted change — the entire fix (and the new transitionStatusMapping) is untested. At minimum add a jsdom smoke test that the arrow remains in the DOM through the exit tick; ideally a chromium test asserting data-ending-style / data-transition-status="ending" during exit.
  3. Initial-mount regression scenario for the removed isSelected short-circuit is untested — add <Select.Root defaultOpen value="b"> (and an object-value + isItemEqualToValue variant) asserting option b has data-selected on first paint, without any user interaction. The deleted short-circuit existed precisely for that scenario.
  4. Multi-select stale-index not covered — the line 168 assertion is single-select. Mirror it for value={['a','b']} → ['b'] while open.

Important (should address)

  1. Autofill matcher precedence is untested — the rewrite changed the closure but the test only covers "rendered label matches". Add tests that pin (a) value wins over rendered text of a later item, (b) case-insensitive matching of the new rendered-text path, (c) tolerance of null rendered labels.
  2. Autofill success path with useValueChanged — only the cancel path is tested. Add a Field-wrapped test that asserts a successful autofill produces data-dirty="" and triggers a validate spy. Pins that setDirty + validation.change still run after the refactor.
  3. Rewrite comment at SelectRoot.tsx:584-586 — currently restates the three matching paths (the WHAT). Suggested condensed form:
    // Browsers autofill the rendered text (e.g. "United States"), which is neither the
    // serialized value nor the serialized label, so match it against `labelsRef` too.
    

Suggestions

  1. Delete redundant test comments at SelectRoot.test.tsx:167 and :806 — both restate what the it() title + assertion already convey. (If keeping :806, expand it to describe the cancel → useValueChanged-doesn't-fire chain.)
  2. hasSelectedValue: add a coverage case using a non-string value + custom itemToStringValue returning '' (the present test happens to also pass on the old code).
  3. UX follow-up (out of scope): items in a forced-open disabled/readOnly Select have no visible disabled state or aria-disabled, so the new no-op click is invisible to users. Worth a separate PR cascading the lock state to items.

Strengths

  • Each fix correctly delivers what its description claims (verified by code reviewer and silent-failure hunter independently).
  • isSelected change makes value the single source of truth and correctly fixes the simultaneous-data-selected bug when controlled value changes while the popup is open (syncSelectedIndex defers while open).
  • hasSelectedValue local definition correctly mirrors selectors.hasSelectedValue — Field's filled state now agrees with the trigger placeholder semantics.
  • Removing the explicit setDirty / validation.change and deferring to useValueChanged is the right refactor — it fixes the canceled-autofill-still-dirty bug on master.
  • SelectScrollArrow changes align it with peer components (SelectItemIndicator, SelectPopup, SelectArrow, SelectBackdrop) which all already merge transitionStatusMapping.
  • Comments at SelectItem.tsx:134, SelectRoot.tsx:197,600, and store.ts:100 are good examples of non-obvious WHY documentation.
  • All AGENTS.md rules observed: no as any, no React.useLayoutEffect, no window.setTimeout, Vitest-only matchers, etc.

Recommended action

  1. Investigate finding #1 first (test isolation) — if confirmed, expand to also cover keyboard activation.
  2. Add the missing tests for findings #2, #3, #4 — all are short additions.
  3. Apply finding #7 (comment rewrite) and #8 (comment deletions).
  4. Re-run pnpm test:jsdom SelectRoot --no-watch and pnpm test:chromium SelectRoot --no-watch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: select Changes related to the select component. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants