Skip to content

[Bug]: Escape in a Menu nested in a groupper/modalizer escapes the groupper #36315

@layershifter

Description

@layershifter

Component

Menu (@fluentui/react-menu)

Package version

@fluentui/react-menu@9.25.0 — regression vs 9.21.2.

React version

18.3.1 (also reproduces on React 17 / 19).

Environment

Any browser — this is a Tabster focus-management timing issue, not browser-specific.

Current Behavior

When a Menu is nested inside a Tabster groupper + modalizer (e.g. a focusMode Card, or a list item whose hover/actions popover shares a modalizer id with the item), pressing Escape to close the menu moves focus to the groupper root instead of restoring focus to the menu trigger.

Expected Behavior

Closing the menu with Escape restores focus to the menu trigger (e.g. the "More options" button), as it did in @fluentui/react-menu@9.21.2.

Reproduction

https://github.com/layershifter/fluent-v9-menu-groupper-esc-repro

npm install && npm run dev for the interactive demo (with surfaceMotion toggles), or npm test for the deterministic Playwright assertions.

Steps to reproduce

  1. Focus the "message" group (a groupper + modalizer) — this activates the modal scope.
  2. Move focus into the actions popover and onto the "More options" Menu trigger.
  3. Press to open the menu (focus moves to the first item).
  4. Press Esc.
  5. Actual: focus lands on the message group. Expected: focus returns to the "More options" trigger.

Root cause

react-menu@9.25.0 wraps the menu popover in a surfaceMotion presence component (presenceMotionSlot, unmountOnExit), which defers the popover unmount by a tick.

Pre-9.25.0 the popover unmounted synchronously on close; removing the focused menu item dropped focus to <body>, which fired Tabster's restorer source → a higher-priority Restorer async-focus intent that pre-empted the groupper's pending EscapeGroupper intent. With the deferred unmount, react-menu moves focus menu-item → trigger directly (never through <body>), so the restorer never arms and the groupper-escape fires and steals focus to the group.

Tabster itself is unchanged between 9.21.2 and 9.25.0 — only the menu's unmount/restore timing changed. surfaceMotion={null} on the <Menu> restores the synchronous unmount and fixes it; surfaceMotion={false} does not (only null is special-cased by presenceMotionSlot).

Note: this is the same class of bug already fixed for Combobox/Dropdown in #36275 (event.stopPropagation() can't stop Tabster because it listens in the capture phase).

Are you reporting an Accessibility issue?

Yes — focus is not restored to the trigger on dismiss (WCAG 2.4.3 Focus Order).

Related

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions