Skip to content

[popups] Keep transition state consistent when reopening a retained popup - #5738

Open
LukasTy wants to merge 4 commits into
mui:masterfrom
LukasTy:fix/menu-reopen-transition-state
Open

LukasTy wants to merge 4 commits into
mui:masterfrom
LukasTy:fix/menu-reopen-transition-state

Conversation

@LukasTy

@LukasTy LukasTy commented Sep 17, 2026

Copy link
Copy Markdown
Member

Noticed while working on mui/material-ui#48823.

open reaches a popup store synchronously, while mounted and transitionStatus are synced in a layout effect. A popup that stays in the DOM while closed therefore renders one commit as open but not yet starting. A transition driven by open && transitionStatus !== 'starting' then enters twice, and onOpenChangeComplete can fire before the enter animation finishes.

Derive the starting status while the lifecycle is not mounted. This matches useTransitionStatus and keeps the existing mounting and completion logic.

The defect is in the shared ordering rather than in Menu, so the fix lives in popupStoreSelectors and covers Menu, Popover, Dialog, Tooltip and Preview Card together. Select and Combobox declare their own selectors and do not have the defect: they hold open in React state and synchronize open, mounted and transitionStatus in one store update.

Tests

  • A unit test for the selector in packages/react/src/utils/popups/store.test.ts. It covers the uncontrolled and controlled starting windows, pass-through once mounted, the reversed exit that must keep ending, and the closed states. This one runs in the JSDOM job.
  • A Menu browser regression for controlled and uncontrolled menus, both initial open states, both portal modes, two reopen cycles, exit reversal, and animation-completion timing.
  • A Popover browser regression for a retained popup across two close and reopen cycles.

The browser tests use a transition that outlasts every phase, then finish the animations to end each phase. The Menu exit-reversal case waits for the same popup to have data-open before finishing the animation. This prevents the helper from finishing the exit before the reopen handler runs. No assertion depends on catching a short animation window.

Validation

Latest validation at f17a8227a:

  • All 63 focused transition cases pass across Chromium, Firefox, and WebKit.
  • TypeScript, ESLint, stylelint, and Prettier pass.

Earlier validation runs:

  • Reverting the selector fails 2 of the 5 new unit tests, the 4 retained-popup Menu cases, and the Popover case. The remaining cases pass either way by design: without a retained popup the affected window never renders.

  • Chromium: 1,788 tests pass across Popover, Dialog, Alert Dialog, Tooltip, Preview Card and Drawer. 1,119 pass across Menu and Popover.

  • JSDOM: 1,897 tests pass across the same component trees, plus 134 store tests.

  • TypeScript, ESLint, stylelint, and Prettier pass.

  • I have followed the PR section of the contributing guide.

@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026

Copy link
Copy Markdown

commit: f17a822

@code-infra-dashboard

code-infra-dashboard Bot commented Sep 17, 2026

Copy link
Copy Markdown

Bundle size

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

Details of bundle changes

Performance

Total duration: 1,178.33 ms -66.36 ms(-5.3%) | Renders: 76 (+0) | Paint: 1,914.99 ms -119.90 ms(-5.9%)

Test Duration Renders
Select open (500 options) 47.82 ms ▼-15.72 ms(-24.7%) 14 (+0)

14 tests within noise — details


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

@netlify

netlify Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

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

@LukasTy LukasTy added component: menu Changes related to the menu component. type: bug It doesn't behave as expected. labels Sep 18, 2026
LukasTy and others added 2 commits September 18, 2026 12:10
`open` reaches the popup store synchronously, while `mounted` and
`transitionStatus` sync in a layout effect. A retained popup therefore
renders one commit as open but not yet starting, so a transition driven
by `open && transitionStatus !== 'starting'` enters twice, and completion
can fire before the enter animation finishes.

Derive the starting status in `popupStoreSelectors` instead of `MenuStore`.
The defect is in the shared ordering, so this fixes Menu, Popover, Dialog,
Tooltip and Preview Card together. Select and Combobox keep their own
selectors and do not have the defect.

Add unit coverage for the selector and a Popover browser regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`waitFor` on a running 200ms transition needed a poll to land inside that
window. Once it closed, `getAnimations()` stayed empty and the wait failed at
timeout instead of retrying. Use a transition that outlasts every phase, then
finish the animations to end the phase. This also removes the `pause()`
workaround for the reversed exit.

Stop asserting inside the `onOpenChangeComplete` mock. The library calls it
from `flushSync` inside a promise continuation, so a throw escaped as an
unhandled rejection rather than failing the test, and the check was vacuous on
close. The test body already asserts the same thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasTy LukasTy changed the title [menu] Keep transition state consistent when reopening a retained popup [popups] Keep transition state consistent when reopening a retained popup Sep 18, 2026
@LukasTy LukasTy self-assigned this Sep 18, 2026
@LukasTy
LukasTy requested a balanced review from Copilot September 18, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused selector correction matches the lifecycle behavior and is comprehensively covered by unit and browser regressions.

Pull request overview

Keeps popup transition state consistent when reopening retained popups.

Changes:

  • Derives starting while an open popup lifecycle is not mounted.
  • Adds selector unit tests and browser regressions for Menu and Popover.
File summaries
File Description
packages/react/src/utils/popups/store.ts Corrects the shared transition selector.
packages/react/src/utils/popups/store.test.ts Covers selector lifecycle states.
packages/react/src/popover/popup/PopoverPopupTransitionState.test.tsx Tests retained Popover reopen cycles.
packages/react/src/menu/popup/MenuPopupTransitionState.test.tsx Tests Menu lifecycle combinations and exit reversal.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@LukasTy
LukasTy marked this pull request as ready for review September 18, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants