Skip to content

[menu] Fix detached-trigger store sync and context-menu navigation#5026

Open
atomiks wants to merge 1 commit into
mui:masterfrom
atomiks:claude/menu-detached-trigger-context-menu-fixes
Open

[menu] Fix detached-trigger store sync and context-menu navigation#5026
atomiks wants to merge 1 commit into
mui:masterfrom
atomiks:claude/menu-detached-trigger-context-menu-fixes

Conversation

@atomiks

@atomiks atomiks commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Correctness fixes around the menu store lifecycle, detached triggers, and context menus, plus cleanup.

Bugs

  • MenuStore parent observer no longer detaches itself / leaks its subscription. ReactStore.observe fires its listener synchronously before returning, so the single unsubscriber field was clobbered at construction (leaking the per-parent store subscription) and a later parent change tore down the observer itself. The observer's own unsubscriber and the per-parent subscription now live in separate fields, and the root-level branch restores this menu's own allowMouseUpTriggerRef rather than keeping a borrowed parent ref.
  • Hover-open mouseup listener is now cleaned up. A hover-open that closed again without a mouseup left a { once: true } document listener armed to fire on an unrelated later interaction.
  • Touch-to-close guard runs before onOpenChange / dispatchOpenChange. Previously the guard bailed after already notifying controlled consumers (and floating-ui's own state), so a controlled menu closed anyway.
  • ArrowLeft no longer closes a root context menu. A root context menu was treated as nested, so the cross-orientation close key closed it; native context menus don't (there's no parent list to return to). Its submenus remain regular parent.type === 'menu' menus.

Cleanup

  • Remove the never-constructed nested-context-menu MenuParent variant.
  • Drop the no-op render-time delete rootTriggerProps.id.
  • Use the deduping warn util for the modal-on-child-menu warning (was console.warn on every render) with corrected wording; use REASONS.triggerHover instead of the string literal; fix the stickIfOpen comment.

The duplicated trigger drag-release bounds check is kept inline for now (a shared helper is a planned follow-up).

Typecheck, eslint, prettier, and the Menu / Menubar / ContextMenu suites pass. Based on current master.

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

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

commit: a98297b

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 12, 2026

Copy link
Copy Markdown

Bundle size

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

Details of bundle changes

Performance

Total duration: 1,459.77 ms +5.53 ms(+0.4%) | Renders: 50 (+0) | Paint: 2,200.60 ms +16.22 ms(+0.7%)

Test Duration Renders
Select open (500 options) 75.77 ms 🔺+13.47 ms(+21.6%) 14 (+0)
Checkbox mount (500 instances) 82.41 ms ▼-31.68 ms(-27.8%) 1 (+0)

10 tests within noise — details


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

@netlify

netlify Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit a98297b
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a2bab7eea612400082c7c6f
😎 Deploy Preview https://deploy-preview-5026--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 claude/menu-detached-trigger-context-menu-fixes branch from 75cc9f4 to 286862a Compare June 12, 2026 06:40
Bugs: MenuStore's parent observer no longer detaches itself on the first parent change (the synchronous observe-fire was clobbering the per-parent subscription); the hover-open mouseup listener is now cleaned up; the touch-to-close guard runs before onOpenChange/dispatchOpenChange so controlled consumers aren't force-closed; ArrowLeft no longer closes a root context menu (it's not treated as nested).

Cleanup: remove the never-constructed nested-context-menu parent variant; drop the no-op render-time 'delete rootTriggerProps.id'; use the deduping warn util for the modal-on-child-menu warning (was console.warn every render) with corrected wording; use REASONS.triggerHover instead of the string literal; comment fix for stickIfOpen.

Keep the duplicated trigger drag-release bounds check inline for now (shared helper is a follow-up).
@atomiks atomiks force-pushed the claude/menu-detached-trigger-context-menu-fixes branch from 286862a to a98297b Compare June 12, 2026 06:47
@atomiks atomiks requested a review from Copilot June 12, 2026 06:49
@atomiks atomiks marked this pull request as ready for review June 12, 2026 06:50

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.

Pull request overview

This PR addresses correctness issues in the Menu store lifecycle (especially around detached triggers), improves cleanup of hover-open event listeners, and adjusts keyboard navigation behavior for root context menus. It also removes an unused MenuParent variant and reduces dev-mode warning spam.

Changes:

  • Fix MenuStore parent-state propagation and ref borrowing so parent subscriptions aren’t clobbered and root menus restore their own allowMouseUpTriggerRef.
  • Ensure hover-open mouseup listeners are cleaned up, and move the touch-to-close guard to run before notifying controlled consumers / dispatching open changes.
  • Adjust list-navigation nesting logic so root context menus aren’t treated as nested (e.g. ArrowLeft no longer closes them), and remove the unused nested-context-menu variant.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/react/src/menu/trigger/MenuTrigger.tsx Uses addEventListener cleanup to prevent stale mouseup listeners; aligns hover reason constant usage.
packages/react/src/menu/submenu-trigger/MenuSubmenuTrigger.tsx Removes render-time mutation (delete rootTriggerProps.id).
packages/react/src/menu/store/MenuStore.ts Fixes parent observation/subscription clobbering; restores root mouse-up gate ref when parent is removed.
packages/react/src/menu/root/MenuRoot.tsx Deduped dev warning via warn; moves touch-close guard earlier; adjusts useListNavigation nesting for root context menus; removes unused parent variant.
packages/react/src/menu/positioner/MenuPositioner.tsx Removes now-nonexistent nested-context-menu branch.
docs/src/app/(docs)/react/components/menu/types.md Updates docs to remove the nested-context-menu MenuParent variant.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 186 to 196
public static useStore<Payload>(
externalStore: MenuStore<Payload> | undefined,
initialState: Partial<State<Payload>>,
) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const internalStore = useRefWithInit(() => {
return new MenuStore<Payload>(initialState);
}).current;

return externalStore ?? internalStore;
}
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