Skip to content

[select] Release the forced popup mount and add keepMounted prop - #5343

Draft
atomiks wants to merge 6 commits into
mui:masterfrom
atomiks:claude/optimistic-payne-211dac
Draft

[select] Release the forced popup mount and add keepMounted prop#5343
atomiks wants to merge 6 commits into
mui:masterfrom
atomiks:claude/optimistic-payne-211dac

Conversation

@atomiks

@atomiks atomiks commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Closes #5184.

Problem

Select.Portal set forceMount on trigger focus and never released it: after the first interaction the whole portal subtree — positioner, popup, and every item — stayed in the DOM for the lifetime of the page. The mount can't simply be dropped on close, because closed-trigger typeahead and autofill matching read labels, values, and disabled state from mounted items.

Changes

  • forceMount becomes a transient, focus-scoped mount instead of a permanent latch. Closed-trigger typeahead can only receive keystrokes while the trigger is focused, so the hidden items are kept mounted exactly then: armed on trigger focus (as before), released on trigger blur and after a close completes with focus elsewhere. Autofill mounts the items for a single microtask to match the value, then releases. No snapshot of item data is taken, so the React 18 index-guessing hazards of the earlier mirror approach don't apply.
  • The close-complete release is deferred by one macrotask. With no exit animation it would otherwise run before focus returns to the trigger, unmounting and remounting the entire list on every close — a node-identity test guards against this.
  • Added keepMounted to Select.Portal, the only popup-family portal missing it.
  • The positioner clears selectedIndex and selectedItemTextRef on unmount. A stale index made SelectItemText re-adopt the ref on remount, misaligning alignItemWithTrigger after option replacement.
  • The registered item values (not just their count) live on the root rather than the positioner, so the dynamic-items reconciliation survives an unmount and prunes the value on any change to the registered values — including replacing the collection with one of equal size, which the previous size-only check missed even while permanently mounted.
  • Focus checks use shadow-safe containment rather than element identity, so focus resting inside a composite or shadow-backed trigger still counts as trigger focus — and such internal focus moves no longer commit field blur (touched state, validationMode="onBlur").

Behavior change

Removing the selected item from the list while the select is closed and unmounted reconciles the value the next time the items mount (any focus on the trigger remounts them), instead of immediately — there is no DOM to observe the removal against while unmounted.

The select-specific Motion handbook section is removed: Select now animates like every other popup, so the generic recipes (AnimatePresence unmounting, or keepMounted with actionsRef manual unmounting) apply.

Follow-ups

  • Combobox/Autocomplete's analogous sticky forceMounted latch is released the same way in a separate change.

  • Pre-existing and unchanged by this PR: useAnimationsFinished's close-complete scan can miss a JS-driven exit animation that registers a frame late (e.g. Motion animating off state.open without actionsRef), applying hidden mid-exit to a kept-mounted popup. The sticky mount on master has the same scan and timing. The documented actionsRef recipe is unaffected because it disables the automatic scan.

  • I have followed (at least) the PR section of the contributing guide.

@atomiks atomiks added component: select Changes related to the select component. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. labels Jul 26, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown

commit: 9378037

@code-infra-dashboard

code-infra-dashboard Bot commented Jul 26, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+491B(+0.11%) 🔺+155B(+0.11%)

Details of bundle changes

Performance

Total duration: 1,136.01 ms +105.65 ms(+10.3%) | Renders: 78 (+0) | Paint: 1,783.86 ms +175.85 ms(+10.9%)

Test Duration Renders
Scroll Area mount (300 instances) 73.90 ms 🔺+19.54 ms(+35.9%) 3 (+0)

14 tests within noise — details

Metric alarms

Test Metric Change
Scroll Area mount (300 instances) bench:paint 🔺 +21.03 ms

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

@netlify

netlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 9378037
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a752560cd943b0008d0970a
😎 Deploy Preview https://deploy-preview-5343--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.

@atomiks atomiks changed the title [select] Release the forced popup mount and add keepMounted [select] Release the forced popup mount and add keepMounted prop Jul 26, 2026
@atomiks
atomiks force-pushed the claude/optimistic-payne-211dac branch 3 times, most recently from f9f2a4f to 014692c Compare July 27, 2026 00:22
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 27, 2026
@atomiks
atomiks force-pushed the claude/optimistic-payne-211dac branch from 014692c to af66fec Compare July 27, 2026 05:23
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 27, 2026
@atomiks
atomiks force-pushed the claude/optimistic-payne-211dac branch from af66fec to 3d6ed91 Compare July 27, 2026 05:23
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 29, 2026
@atomiks
atomiks force-pushed the claude/optimistic-payne-211dac branch from 3d6ed91 to 611f3f6 Compare August 6, 2026 22:13
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 6, 2026
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 12, 2026
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. PR: out-of-date The pull request has merge conflicts and can't be merged. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[select] popup is not removed from the DOM after closing.

1 participant