Skip to content

[popups] Prevent unwanted flip with capped scrollable content - #5120

Merged
atomiks merged 3 commits into
mui:masterfrom
atomiks:fix-popup-flip-capped-height
Jun 29, 2026
Merged

[popups] Prevent unwanted flip with capped scrollable content#5120
atomiks merged 3 commits into
mui:masterfrom
atomiks:fix-popup-flip-capped-height

Conversation

@atomiks

@atomiks atomiks commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5118.

When a popup's scrollable content is taller than the viewport and capped with the documented max-height: min(<x>, var(--available-height)) pattern, the popup could open on the wrong side: it flipped away from its preferred side even when the capped popup fit there comfortably (e.g. a combobox in the lower half of the viewport opening upward despite plenty of room below).

Root cause: flip() runs before size() sets --available-height. On the first positioning pass that variable is undefined, so min(<x>, var(--available-height)) resolves to an invalid value and the whole max-height declaration is dropped. The list is therefore measured at its full, uncapped content height while flip() decides the side, so it picks the side with more raw space rather than the side where the capped popup actually fits.

Fix

Seed --available-width: 100vw and --available-height: 100vh on the positioner before size() writes the real values. This keeps the consumer's min(<x>, var(--available-height)) valid on the first flip() pass, so the popup is measured at its capped height and the side is chosen correctly. size() overwrites the vars with the real px values immediately, and React's style reconciliation does not clobber those imperative values (the seeded value string is constant across renders, so React never rewrites the property after mount).

No changes to the docs demos are required — the existing patterns now behave as intended.

Testing

  • Added a Chromium regression test in ComboboxPositioner.test.tsx (fails without the fix: the popup flips to top; passes with it).
  • All positioner suites pass: Combobox, Select, Menu, Navigation Menu, Popover, Tooltip, Preview Card.

@atomiks atomiks added component: combobox Changes related to the combobox component. type: bug It doesn't behave as expected. labels Jun 25, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

commit: 05ef735

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 25, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+40B(+0.01%) 🔺+24B(+0.02%)

Details of bundle changes

Performance

Total duration: 1,069.71 ms -35.81 ms(-3.2%) | Renders: 78 (+0)

Test Duration Renders
Checkbox mount (500 instances) 56.51 ms ▼-21.31 ms(-27.4%) 1 (+0)

13 tests within noise — details


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

@netlify

netlify Bot commented Jun 25, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 05ef735
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a42253b8db29c000864b23b
😎 Deploy Preview https://deploy-preview-5120--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 force-pushed the fix-popup-flip-capped-height branch from 403917a to c633b60 Compare June 25, 2026 09:46
@atomiks
atomiks marked this pull request as ready for review June 25, 2026 09:48
When a popup's scrollable content is taller than the viewport and capped with
the documented max-height: min(<x>, var(--available-height)) pattern, the popup
could open on the wrong side: it flipped away from its preferred side even when
the capped popup fit there comfortably.

flip() runs before size() sets --available-height, so on the first positioning
pass that variable is undefined, the min() is invalid, and the whole max-height
declaration is dropped. The list is then measured at its full, uncapped content
height while flip() decides the side, so it picks the side with more raw space
rather than the side where the capped popup actually fits.

Seed --available-width: 100vw and --available-height: 100vh on the positioner
before size() writes the real values, so the consumer min() stays valid on the
first flip() pass and the side is chosen against the capped height.

Closes mui#5118
@atomiks
atomiks force-pushed the fix-popup-flip-capped-height branch from c633b60 to e269f2b Compare June 25, 2026 10:03

@flaviendelangle flaviendelangle left a comment

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.

PR review

This is a tight, well-reasoned fix for an unwanted-flip bug: it seeds --available-width: 100vw / --available-height: 100vh in the positioner's React style object so consumer min(<x>, var(--available-height)) rules resolve to a valid length on the first flip() pass, before size() writes the real px values. The mechanism is correct — React's per-property style diff never rewrites a key whose value is constant across renders, so size()'s imperative px values survive — and the change only ever makes the flip pass measure a smaller (capped) popup, which strictly reduces unwanted flips rather than introducing new ones. Nothing is merge-blocking.

Bugs (0)

No findings.

Tests (1)

1. ℹ️ Regression test covers only the min(...) consumer pattern, not the bare var(--available-height) pattern

Location: packages/react/src/combobox/positioner/ComboboxPositioner.test.tsx:90

style={{ maxHeight: 'min(80px, var(--available-height))', overflowY: 'auto' }}

The new test exercises the documented min(<x>, var(--available-height)) shape. But the default Select/Menu dropdowns (and several demos) use the bare form max-height: var(--available-height) — see docs/src/components/Select.css:11 and Menu.css:11. The first-pass behavior change differs between the two: for the bare form, the declaration previously dropped entirely (uncapped) and now resolves to 100vh. Since the fix lives in shared useAnchorPositioning, the Combobox test does cover the shared code path, so this is informational rather than a real gap — but a Select/Menu case asserting the bare pattern no longer over-flips would pin the behavior these components actually ship with.

Failure scenario: A future change to how the bare var(--available-height) first-pass value resolves could regress Select/Menu flip behavior without any test failing.

Fix: Optionally add an analogous it.skipIf(isJSDOM) flip test to SelectPositioner (or Menu) using max-height: var(--available-height).

Simplifications (0)

No findings.

Docs (0)

No findings.

Verdict

Approve — correct, minimal fix; the explanatory comment accurately documents the React-reconciliation invariant the fix depends on, and the only note is an optional test-breadth suggestion.


🤖 Review generated with Claude Code

@atomiks
atomiks merged commit 3fde289 into mui:master Jun 29, 2026
23 checks passed
@atomiks atomiks added scope: all components Widespread work has an impact on almost all components. and removed component: combobox Changes related to the combobox component. labels Jun 29, 2026
@atomiks
atomiks deleted the fix-popup-flip-capped-height branch June 29, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: all components Widespread work has an impact on almost all components. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[combobox] Popup positioning gets tricky with scrollable content

2 participants