Skip to content

fix(DropdownMenu, ContextMenu): Tab from any sub-menu level closes the whole menu - #8870

Open
cixzhang wants to merge 6 commits into
mainfrom
lane4/submenu-tab-closes-all
Open

cixzhang wants to merge 6 commits into
mainfrom
lane4/submenu-tab-closes-all

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8840

User impact

A keyboard user in a DropdownMenu or ContextMenu sub-menu flyout who presses Tab expects to leave the menu, as Tab does from a top-level row. Instead the menu stays open and focus stays on the flyout row, so the person has to press Escape once per level before they can move on. After this change Tab or Shift+Tab from a row at any level closes every level and the menu, and focus moves on from the trigger in page order.

Problem and solution fit

  • Why does the menu stay open? The root menus ignore key events that come from a flyout (ownsEvent is false), and DropdownMenuSubMenu's handler only closes itself on Tab when a header or footer sits above it.
  • Why does it harm the task? Tab is how keyboard users leave a menu (WAI-ARIA Menu Button), and it works one level up. The deeper level traps them silently.
  • Why does it matter? Menus with a header or footer already close each flyout as Tab bubbles past; a plain menu, the common case, had no path at all.

The fix completes the existing bubbling design:

  • each flyout without its own header or footer closes itself as Tab passes;
  • the root DropdownMenu or ContextMenu, seeing a Tab from a flyout, closes as it does for its own rows;
  • the key is never prevented, so the browser's Tab moves on from the restored trigger.

Expected behavior and authority

  • WAI-ARIA Menu Button: Tab closes the menu and moves focus on, from any level.
  • The specs owner decided the rule for both menus today. It's recorded here in the draft records:
    • DropdownMenu FR22;
    • ContextMenu FR11;
    • DropdownMenuSubMenu FR10.
  • A level with a header or footer keeps FR15's and FR20's Tab cycle within it.

Smallest restoration

  • DropdownMenuSubMenu: a Tab on a level without its own regions closes that level (close({focusTrigger: false})), whatever sits above it.
  • DropdownMenu and ContextMenu roots: a Tab from a flyout closes the menu, or walks the regions when the root has them, as before.
  • With that, MenuRegionsContext had no reader left. It only told a flyout whether a level above had regions, so the second commit removes it.

Evidence

Real Chrome 156 (headless), Storybook from main and from this branch, same keyboard probe. Tab to the trigger, Enter, ArrowDown to the sub-menu row, ArrowRight into the flyout, then Tab:

Story main after Tab in the flyout This PR after Tab in the flyout
DropdownMenu Submenu focus stays on "Folder A"; 2 menus open focus on the next control after the trigger; 0 menus open
DropdownMenu Nested submenu focus stays on "Email"; 2 menus open focus on the next control after the trigger; 0 menus open
  • Regression tests (jsdom), all failing on main:
    • DropdownMenuSubMenu.test.tsx "Tab from a flyout leaves the whole menu": one level, Shift+Tab, two levels. Each checks every level closed, onOpenChange(false) once, focus on the trigger, and the Tab not prevented.
    • ContextMenu.test.tsx matching case.
  • Representative unchanged path: the header and footer suites (FR15, FR20 cycling, Tab from a deeper flyout under a regions level), top-level Tab, and MoreMenu. DropdownMenu, ContextMenu and MoreMenu: 412 tests pass.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm vitest run for DropdownMenu, ContextMenu and MoreMenu: 412 pass.
  • tsc --noEmit for core, eslint, prettier, pnpm check:knowledge, pnpm check:sync.
  • The real-Chrome probe above.

Writing rubric results

  • R1 narration: none. The three new rows state the system in the present tense.
  • R2 research: none.
  • R3 evidence: basis cells cite the WAI-ARIA pattern and tests.
  • R4 rejected alternatives: none.
  • R5 change references: none.
  • R6 open questions: none added.

Carry onto main (conflict-only)

Rebased onto main, keeping both commits. The only conflicts: main moved the ContextMenu, DropdownMenu and DropdownMenuSubMenu specs to docs/specs/components/ and reshaped their requirement tables to ID | Invariant | Basis. The same rows are re-applied there:

  • DropdownMenu FR22 and its verification row;
  • ContextMenu FR11 and its verification row;
  • DropdownMenuSubMenu FR10 and its verification row.

The old draft-review-state column ("Decided; verified in jsdom") has no place in the new tables, so it is dropped. Code, tests and changeset are unchanged. DropdownMenu and ContextMenu tests: 427/427.

git range-diff (old 153d2d9 → new 7d5c40d)
1:  4914deed662 ! 1:  9c1a20f15d3 fix(DropdownMenu, ContextMenu): Tab from any sub-menu level closes the whole menu
    @@ .changeset/submenu-tab-closes-menu.md (new)
     +
     +@cixzhang
     
    - ## packages/core/src/ContextMenu/ContextMenu.spec.md ##
    -@@ packages/core/src/ContextMenu/ContextMenu.spec.md: Consumer props, content modes, and presentation policy remain documented in
    - | FR8  | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone.                                                                                                                                                                                                                                                                                                                                                                                                  | Current source and tests; shared `useListFocus` and `useTypeahead` suites    | Proposed; verified in jsdom; owner decision pending |
    - | FR9  | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface.                                                                                                                                                                                                                                                                                                                                                                                             | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | Current family projection; verified in jsdom        |
    - | FR10 | ContextMenu is built on the public headless context menu (`spec:AST-107` FR8): its invocation, open state, and cleanup are the factory's, with no behavior of its own beside the factory's.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Owner decision, 2026-10-26; `spec:AST-107` FR8                               | Decided                                             |
    -+| FR11 | Tab or Shift+Tab on a row of the pointer menu, its own or an open sub-menu flyout's at any depth, closes every open level and the menu and calls `onOpenChange(false)` once, as DropdownMenu FR22 states for its menu.                                                                                                                                                                                                                                                                                                                                                                                                                                        | WAI-ARIA Menu Button pattern; current tests                                  | Decided; verified in jsdom                          |
    + ## docs/specs/components/ContextMenu/ContextMenu.spec.md ##
    +@@ docs/specs/components/ContextMenu/ContextMenu.spec.md: Consumer props, content modes, and presentation policy remain documented in
    + | FR8  | In the pointer menu ArrowDown on the last row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. A disabled row is reached by these keys and by typeahead and stays inert, as `component:DropdownMenu` FR17 states. Implementation gap: the arrow keys and typeahead skip disabled rows today (Current-state impact).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Current source and tests                                                     |
    + | FR9  | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case |
    + | FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `spec:AST-107` FR8                                                           |
    ++| FR11 | Tab or Shift+Tab on a row of the pointer menu, its own or an open sub-menu flyout's at any depth, closes every open level and the menu and calls `onOpenChange(false)` once, as `component:DropdownMenu` FR22 states for its menu.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | WAI-ARIA Menu Button pattern; current tests                                  |
      
      ### Allowed variation
      
    -@@ packages/core/src/ContextMenu/ContextMenu.spec.md: anchor is positioning infrastructure rather than consumer anatomy.
    +@@ docs/specs/components/ContextMenu/ContextMenu.spec.md: anchor is positioning infrastructure rather than consumer anatomy.
      
      ## Verification map
      
    --| Contract            | Verification                                                                                      | Representative states                                                   | Mutation or failure expectation                                                                                              | Audit section                |
    --| ------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
    --| FR1, FR4            | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound                           | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests.                           | `audit:ContextMenu/anatomy`  |
    --| FR2                 | ContextMenu target tests, source inspection, and theming target inventories                       | Pointer and touch menu surfaces                                         | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence.                                    | `audit:ContextMenu/theming`  |
    --| FR3                 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests                                      | Pointer rows, touch frame, touch list/rows                              | A composed part loses its owner target or is documented as a new ContextMenu target.                                         | `audit:ContextMenu/theming`  |
    --| Layer relationships | `ContextMenu.test.tsx` nested-layer case and current layer/dismissal architecture records         | Outside click, Escape, nested context layers, sheet                     | A Pointer menu bypassing the shared stack or dismissing with its nested child fails.                                         | `audit:ContextMenu/behavior` |
    --| FR5                 | `ContextMenu.test.tsx` press model case; `module:DropdownMenu/useMenuPress` map                   | Finger slide across rows, stray click                                   | A row acting on the press row or the stray click fails.                                                                      | `audit:ContextMenu/behavior` |
    --| FR6                 | `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites             | wrap at both ends                                                       | Clamping arrows fail.                                                                                                        | `audit:ContextMenu/behavior` |
    --| FR10                | ContextMenu tests over `createContextMenu` and a source check                                     | a right click, a long press, Shift+F10; open, close, unmount while open | A context request that bypasses the factory, open state ContextMenu keeps on its own, or a listener left after unmount fails | `audit:ContextMenu/behavior` |
    --| Theming anatomy map | `scripts/check-knowledge.mjs`                                                                     | Canonical anatomy and current target                                    | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation.                                        | `audit:ContextMenu/theming`  |
    --| FR6, AR1            | `ContextMenu.test.tsx` "inline trigger (triggerAs)" suite                                         | `span` in a paragraph, default `div`                                    | A block trigger inside prose, or a lost invocation path, fails.                                                              | `audit:ContextMenu/behavior` |
    -+| Contract            | Verification                                                                                      | Representative states                                                   | Mutation or failure expectation                                                                                              | Audit section                     |
    -+| ------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
    -+| FR1, FR4            | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound                           | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests.                           | `audit:ContextMenu/anatomy`       |
    -+| FR2                 | ContextMenu target tests, source inspection, and theming target inventories                       | Pointer and touch menu surfaces                                         | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence.                                    | `audit:ContextMenu/theming`       |
    -+| FR3                 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests                                      | Pointer rows, touch frame, touch list/rows                              | A composed part loses its owner target or is documented as a new ContextMenu target.                                         | `audit:ContextMenu/theming`       |
    -+| Layer relationships | `ContextMenu.test.tsx` nested-layer case and current layer/dismissal architecture records         | Outside click, Escape, nested context layers, sheet                     | A Pointer menu bypassing the shared stack or dismissing with its nested child fails.                                         | `audit:ContextMenu/behavior`      |
    -+| FR5                 | `ContextMenu.test.tsx` press model case; `module:DropdownMenu/useMenuPress` map                   | Finger slide across rows, stray click                                   | A row acting on the press row or the stray click fails.                                                                      | `audit:ContextMenu/behavior`      |
    -+| FR6                 | `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites             | wrap at both ends                                                       | Clamping arrows fail.                                                                                                        | `audit:ContextMenu/behavior`      |
    -+| FR10                | ContextMenu tests over `createContextMenu` and a source check                                     | a right click, a long press, Shift+F10; open, close, unmount while open | A context request that bypasses the factory, open state ContextMenu keeps on its own, or a listener left after unmount fails | `audit:ContextMenu/behavior`      |
    -+| FR11                | `ContextMenu.test.tsx` "Tab from a flyout leaves the whole menu" case                             | a sub-menu row                                                          | A Tab that leaves the menu or a flyout open, or a second `onOpenChange(false)`, fails.                                       | `audit:ContextMenu/accessibility` |
    -+| Theming anatomy map | `scripts/check-knowledge.mjs`                                                                     | Canonical anatomy and current target                                    | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation.                                        | `audit:ContextMenu/theming`       |
    -+| FR6, AR1            | `ContextMenu.test.tsx` "inline trigger (triggerAs)" suite                                         | `span` in a paragraph, default `div`                                    | A block trigger inside prose, or a lost invocation path, fails.                                                              | `audit:ContextMenu/behavior`      |
    +-| Contract             | Verification                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Representative states                                                                                              | Mutation or failure expectation                                                                                                                                                                                                                                                                                | Audit section                |
    +-| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
    +-| FR1, FR4             | `ContextMenu.test.tsx` rendering, invocation, bottom-sheet, adaptive, compound, and disabled cases ("renders menu with role=\"menu\"", "opens menu on right-click", "opens from a keyboard-invoked contextmenu (Shift+F10 / Menu key)", "opens on touch long-press", "opens the real menu content in a BottomSheet when requested", "uses the BottomSheet for adaptive presentation on compact touch", "renders menuContent as menu items", "does not open when isDisabled is true") | Right-click, keyboard, long press; pointer and sheet; data and compound; disabled                                  | A missing invocation path, menu role, sheet, or compound interior, or a disabled trigger that opens, fails. Evidence gap: no test asserts that a disabled trigger leaves the native context menu alone.                                                                                                        | `audit:ContextMenu/anatomy`  |
    +-| FR2                  | `themingTargets.test.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Rendered targets                                                                                                   | A rendered class absent from the documented targets fails. Evidence gap: no test asserts `context-menu` on either painted surface or its absence from the trigger.                                                                                                                                             | `audit:ContextMenu/theming`  |
    +-| FR3                  | `ContextMenu.test.tsx` destructive-variant, section, divider, selectable, and alias cases                                                                                                                                                                                                                                                                                                                                                                                            | Shared rows through ContextMenu                                                                                    | A row that loses DropdownMenu's semantics or variant fails. Evidence gap: no test asserts the BottomSheet and List delegation beyond rendering.                                                                                                                                                                | `audit:ContextMenu/theming`  |
    +-| FR5                  | `ContextMenu.test.tsx` "acts on the row under a finger release, once, and swallows the stray click" and outside-dismissal cases; `ContextMenuLongPress.a11y.chromium.spec.ts`                                                                                                                                                                                                                                                                                                        | Finger release; long press lift; tap, mouse, pen, and scroll outside                                               | A row acting on the press row, a stray click that acts, a long press that closes its own menu, or a tap or mouse press outside that leaves it open fails. Evidence gap: no test asserts the FR31 grow, the opening-finger handoff, or the FR33 lift; the scroll-outside and pen cases assert the current gaps. | `audit:ContextMenu/behavior` |
    +-| FR5, DD1 touch scope | Browser tests on the touch story, the menu opened by a finger under a coarse pointer and by a mouse and by Shift+F10 under a fine pointer                                                                                                                                                                                                                                                                                                                                            | Touch-opened, mouse-opened and keyboard-opened menus on a block trigger                                            | A mouse- or keyboard-opened menu that lifts its trigger, tints the page or grows, or a touch-opened menu that blurs the page, grows a second surface, or animates anything but transform and opacity, fails. Evidence gap: the lift and the grow are unbuilt, so no test asserts either.                       | `audit:ContextMenu/behavior` |
    +-| FR5 Reduce Motion    | Browser tests on the touch story with reduced motion                                                                                                                                                                                                                                                                                                                                                                                                                                 | A touch-opened menu opening and closing under Reduce Motion                                                        | A lift, grow or shrink that moves rather than playing an opacity-only cross-fade of 200 ms or less (`spec:AST-094` FR8) fails. Evidence gap: no test exists.                                                                                                                                                   | `audit:ContextMenu/behavior` |
    +-| FR5 motion roles     | Role registry tests over `spec:AST-094` FR4                                                                                                                                                                                                                                                                                                                                                                                                                                          | `springs.contextMenuLift`, `springs.contextMenuDismiss` and `springs.contextMenuMorph` at tempo 1 and `'standard'` | A lift, return or grow that plays a curve or a literal instead of its spring role, or a role off its FR4 table value, fails.                                                                                                                                                                                   | `audit:ContextMenu/behavior` |
    +-| DEC-2                | Element-count browser test on the touch story (`spec:AST-017` FR60)                                                                                                                                                                                                                                                                                                                                                                                                                  | At rest and lifted, fine and coarse pointer                                                                        | A lift element at rest, a copy of the trigger, or an open count beyond the layer, the shadow, the anchor and the placeholder fails. Evidence gap: the lift is unbuilt, so no test exists.                                                                                                                      | `audit:ContextMenu/anatomy`  |
    +-| FR6, AR1, ORD1       | `ContextMenu.test.tsx` "inline trigger (triggerAs)" and cursor-anchor cases; "does not put aria-haspopup on the role-less trigger wrapper (menus-15)"; `ContextMenu.triggerProps.test.tsx`; `ContextMenuCursorAnchor.a11y.browser.spec.ts`                                                                                                                                                                                                                                           | `span` in a paragraph; default `div`; pointer and keyboard anchors                                                 | A block trigger in prose, a trigger with a role or `aria-haspopup`, or an anchor outside the trigger fails.                                                                                                                                                                                                    | `audit:ContextMenu/behavior` |
    +-| FR7                  | `ContextMenu.test.tsx` "drills into nested data items inside the BottomSheet", "renders a submenu from a nested items array and keyboard-reaches an item after it", and "shows a sub-menu with a header or footer as its rows alone, and says why in development"                                                                                                                                                                                                                    | Sheet drill-in; pointer sub-menu; regions under ContextMenu                                                        | A missing sub-menu, an unreachable row after it, or a sub-menu dialog under ContextMenu fails. Evidence gap: no test here asserts the pointer drill-in on a phone.                                                                                                                                             | `audit:ContextMenu/behavior` |
    +-| FR8                  | `ContextMenu.test.tsx` "wraps arrow navigation at the ends", typeahead cases, and disabled-row keyboard cases (`component:DropdownMenu` FR17)                                                                                                                                                                                                                                                                                                                                        | Wrap; typeahead; disabled rows                                                                                     | A clamped arrow, a missing typeahead match, a disabled row the keys or typeahead skip, or a disabled row that acts or closes the menu fails. Evidence gap: no test asserts Page keys or auto-repeat.                                                                                                           | `audit:ContextMenu/behavior` |
    +-| FR9                  | `ContextMenu.test.tsx` "lets a nested layer consume Escape before the context menu", "closes on Escape even when opened without auto-focus", and "ignores Escape during IME composition"                                                                                                                                                                                                                                                                                             | Nested layer; Escape; composition                                                                                  | A context menu that closes with its nested child, or on a composition Escape, fails.                                                                                                                                                                                                                           | `audit:ContextMenu/behavior` |
    +-| FR10, PR1            | `ContextMenu.test.tsx` invocation and handler-composition cases ("composes the consumer context-menu handler with menu opening", "keeps component-owned opening when a consumer prevents default", "composes consumer touch handlers with long-press detection")                                                                                                                                                                                                                     | Right click, long press, keyboard                                                                                  | A context request that does not open the menu, or a dropped consumer handler, fails. Evidence gap: no test asserts listener cleanup on unmount; nothing tests a headless factory, which does not ship (the FR10 gap).                                                                                          | `audit:ContextMenu/behavior` |
    +-| Theming anatomy map  | `scripts/check-knowledge.mjs`                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Canonical anatomy and target                                                                                       | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation.                                                                                                                                                                                                                          | `audit:ContextMenu/theming`  |
    +-| RTL relation         | RTL audit curated D2 target (`apps/storybook/rtl-audit/targets.json`) on the `Presentation / BottomSheet submenu` story                                                                                                                                                                                                                                                                                                                                                              | BottomSheet opened into a submenu, LTR and RTL                                                                     | The sheet header's back button not at the inline start of its heading in both directions fails D2.                                                                                                                                                                                                             | `audit:ContextMenu/i18n-rtl` |
    ++| Contract             | Verification                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Representative states                                                                                              | Mutation or failure expectation                                                                                                                                                                                                                                                                                | Audit section                     |
    ++| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
    ++| FR1, FR4             | `ContextMenu.test.tsx` rendering, invocation, bottom-sheet, adaptive, compound, and disabled cases ("renders menu with role=\"menu\"", "opens menu on right-click", "opens from a keyboard-invoked contextmenu (Shift+F10 / Menu key)", "opens on touch long-press", "opens the real menu content in a BottomSheet when requested", "uses the BottomSheet for adaptive presentation on compact touch", "renders menuContent as menu items", "does not open when isDisabled is true") | Right-click, keyboard, long press; pointer and sheet; data and compound; disabled                                  | A missing invocation path, menu role, sheet, or compound interior, or a disabled trigger that opens, fails. Evidence gap: no test asserts that a disabled trigger leaves the native context menu alone.                                                                                                        | `audit:ContextMenu/anatomy`       |
    ++| FR2                  | `themingTargets.test.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Rendered targets                                                                                                   | A rendered class absent from the documented targets fails. Evidence gap: no test asserts `context-menu` on either painted surface or its absence from the trigger.                                                                                                                                             | `audit:ContextMenu/theming`       |
    ++| FR3                  | `ContextMenu.test.tsx` destructive-variant, section, divider, selectable, and alias cases                                                                                                                                                                                                                                                                                                                                                                                            | Shared rows through ContextMenu                                                                                    | A row that loses DropdownMenu's semantics or variant fails. Evidence gap: no test asserts the BottomSheet and List delegation beyond rendering.                                                                                                                                                                | `audit:ContextMenu/theming`       |
    ++| FR5                  | `ContextMenu.test.tsx` "acts on the row under a finger release, once, and swallows the stray click" and outside-dismissal cases; `ContextMenuLongPress.a11y.chromium.spec.ts`                                                                                                                                                                                                                                                                                                        | Finger release; long press lift; tap, mouse, pen, and scroll outside                                               | A row acting on the press row, a stray click that acts, a long press that closes its own menu, or a tap or mouse press outside that leaves it open fails. Evidence gap: no test asserts the FR31 grow, the opening-finger handoff, or the FR33 lift; the scroll-outside and pen cases assert the current gaps. | `audit:ContextMenu/behavior`      |
    ++| FR5, DD1 touch scope | Browser tests on the touch story, the menu opened by a finger under a coarse pointer and by a mouse and by Shift+F10 under a fine pointer                                                                                                                                                                                                                                                                                                                                            | Touch-opened, mouse-opened and keyboard-opened menus on a block trigger                                            | A mouse- or keyboard-opened menu that lifts its trigger, tints the page or grows, or a touch-opened menu that blurs the page, grows a second surface, or animates anything but transform and opacity, fails. Evidence gap: the lift and the grow are unbuilt, so no test asserts either.                       | `audit:ContextMenu/behavior`      |
    ++| FR5 Reduce Motion    | Browser tests on the touch story with reduced motion                                                                                                                                                                                                                                                                                                                                                                                                                                 | A touch-opened menu opening and closing under Reduce Motion                                                        | A lift, grow or shrink that moves rather than playing an opacity-only cross-fade of 200 ms or less (`spec:AST-094` FR8) fails. Evidence gap: no test exists.                                                                                                                                                   | `audit:ContextMenu/behavior`      |
    ++| FR5 motion roles     | Role registry tests over `spec:AST-094` FR4                                                                                                                                                                                                                                                                                                                                                                                                                                          | `springs.contextMenuLift`, `springs.contextMenuDismiss` and `springs.contextMenuMorph` at tempo 1 and `'standard'` | A lift, return or grow that plays a curve or a literal instead of its spring role, or a role off its FR4 table value, fails.                                                                                                                                                                                   | `audit:ContextMenu/behavior`      |
    ++| DEC-2                | Element-count browser test on the touch story (`spec:AST-017` FR60)                                                                                                                                                                                                                                                                                                                                                                                                                  | At rest and lifted, fine and coarse pointer                                                                        | A lift element at rest, a copy of the trigger, or an open count beyond the layer, the shadow, the anchor and the placeholder fails. Evidence gap: the lift is unbuilt, so no test exists.                                                                                                                      | `audit:ContextMenu/anatomy`       |
    ++| FR6, AR1, ORD1       | `ContextMenu.test.tsx` "inline trigger (triggerAs)" and cursor-anchor cases; "does not put aria-haspopup on the role-less trigger wrapper (menus-15)"; `ContextMenu.triggerProps.test.tsx`; `ContextMenuCursorAnchor.a11y.browser.spec.ts`                                                                                                                                                                                                                                           | `span` in a paragraph; default `div`; pointer and keyboard anchors                                                 | A block trigger in prose, a trigger with a role or `aria-haspopup`, or an anchor outside the trigger fails.                                                                                                                                                                                                    | `audit:ContextMenu/behavior`      |
    ++| FR7                  | `ContextMenu.test.tsx` "drills into nested data items inside the BottomSheet", "renders a submenu from a nested items array and keyboard-reaches an item after it", and "shows a sub-menu with a header or footer as its rows alone, and says why in development"                                                                                                                                                                                                                    | Sheet drill-in; pointer sub-menu; regions under ContextMenu                                                        | A missing sub-menu, an unreachable row after it, or a sub-menu dialog under ContextMenu fails. Evidence gap: no test here asserts the pointer drill-in on a phone.                                                                                                                                             | `audit:ContextMenu/behavior`      |
    ++| FR8                  | `ContextMenu.test.tsx` "wraps arrow navigation at the ends", typeahead cases, and disabled-row keyboard cases (`component:DropdownMenu` FR17)                                                                                                                                                                                                                                                                                                                                        | Wrap; typeahead; disabled rows                                                                                     | A clamped arrow, a missing typeahead match, a disabled row the keys or typeahead skip, or a disabled row that acts or closes the menu fails. Evidence gap: no test asserts Page keys or auto-repeat.                                                                                                           | `audit:ContextMenu/behavior`      |
    ++| FR9                  | `ContextMenu.test.tsx` "lets a nested layer consume Escape before the context menu", "closes on Escape even when opened without auto-focus", and "ignores Escape during IME composition"                                                                                                                                                                                                                                                                                             | Nested layer; Escape; composition                                                                                  | A context menu that closes with its nested child, or on a composition Escape, fails.                                                                                                                                                                                                                           | `audit:ContextMenu/behavior`      |
    ++| FR10, PR1            | `ContextMenu.test.tsx` invocation and handler-composition cases ("composes the consumer context-menu handler with menu opening", "keeps component-owned opening when a consumer prevents default", "composes consumer touch handlers with long-press detection")                                                                                                                                                                                                                     | Right click, long press, keyboard                                                                                  | A context request that does not open the menu, or a dropped consumer handler, fails. Evidence gap: no test asserts listener cleanup on unmount; nothing tests a headless factory, which does not ship (the FR10 gap).                                                                                          | `audit:ContextMenu/behavior`      |
    ++| FR11                 | `ContextMenu.test.tsx` "Tab from a flyout leaves the whole menu" case                                                                                                                                                                                                                                                                                                                                                                                                                | a sub-menu row                                                                                                     | A Tab that leaves the menu or a flyout open, or a second `onOpenChange(false)`, fails.                                                                                                                                                                                                                         | `audit:ContextMenu/accessibility` |
    ++| Theming anatomy map  | `scripts/check-knowledge.mjs`                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Canonical anatomy and target                                                                                       | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation.                                                                                                                                                                                                                          | `audit:ContextMenu/theming`       |
    ++| RTL relation         | RTL audit curated D2 target (`apps/storybook/rtl-audit/targets.json`) on the `Presentation / BottomSheet submenu` story                                                                                                                                                                                                                                                                                                                                                              | BottomSheet opened into a submenu, LTR and RTL                                                                     | The sheet header's back button not at the inline start of its heading in both directions fails D2.                                                                                                                                                                                                             | `audit:ContextMenu/i18n-rtl`      |
      
    - ## Decision log
    + ## Current-state impact
      
     
    + ## docs/specs/components/DropdownMenu/DropdownMenu.spec.md ##
    +@@ docs/specs/components/DropdownMenu/DropdownMenu.spec.md: component-local concepts; each keeps its released default.
    + | FR17 | A disabled menu row MUST be reached by the keyboard and stay inert (DEC-11): in DropdownMenu, its sub-menus, MoreMenu, ContextMenu and NavHeadingMenu, ArrowDown, ArrowUp, Home, End and type-ahead visit a disabled row as they visit any other row, the row takes focus and exposes `aria-disabled="true"`, and Enter, Space or a click on it does nothing and leaves the menu open. Opening a menu from the keyboard still lands on an enabled row: the first, or the last after ArrowUp on the trigger (FR12, AR4). Implementation gap: the arrow keys and type-ahead skip disabled rows today.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | DEC-11; WAI-ARIA Menu pattern                                                  |
    + | FR20 | With `header` or `footer` on a sub-menu (`DropdownMenuSubMenu`), its surface is a modal dialog named by its row (AR7), holding the header, the sub-menu's `role="menu"` (rows only), then the footer. It sits in a host `DropdownMenu` renders beside its menu, never inside a menu; under a root with no such host (`ContextMenu`) the sub-menu shows its rows alone. On a fine pointer the dialog is the flyout; on a compact touch display, the drilled-in view, which replaces the root's menu and regions until popped. A region keeps its own keys and presses. The rows are one Tab stop: Tab and Shift+Tab walk the header's controls, the rows, then the footer's, and wrap; from a deeper flyout, that flyout closes and the walk goes on from the rows. An open lands as for any sub-menu (FR11, FR12), not on a region. Escape in the dialog, outside an IME composition and unless a control there handled it, returns focus to the row and leaves the root open. A pointer leaving the flyout keeps it open until focus leaves, and it closes with the flyout holding its row. Without either prop nothing changes.                                                                                                    | Current behavior; FR15; AR5                                                    |
    + | FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button on the theme's entrance duration and easing tokens and collapse back into it on the theme's exit duration and easing (`spec:AST-094` FR7, FR31, FR45), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6                      |
    ++| FR22 | Tab or Shift+Tab on a row at any level of a menu without `header` or `footer`, its own rows or an open sub-menu flyout's at any depth, closes every open level and the menu, calls `onOpenChange(false)` once, and leaves focus to move on from the trigger in page order (WAI-ARIA Menu Button). A level with a header or footer keeps FR15's and FR20's Tab cycle within it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | WAI-ARIA Menu Button pattern; current tests                                    |
    + 
    + ### Allowed variation
    + 
    +@@ docs/specs/components/DropdownMenu/DropdownMenu.spec.md: than adding a DropdownMenu-owned heading target.
    + | FR17, AR6           | DropdownMenu, MoreMenu, ContextMenu and NavHeadingMenu keyboard tests, plus a real-browser keyboard check of focus order and blocked activation and a real-browser accessibility-tree check that a focused disabled row is announced as disabled (`spec:AST-085` Platform support); disabled-row cases                                                                                                                                                        | Disabled rows                                                                          | A disabled row that runs a caller handler, closes the menu, is skipped by the arrow keys or type-ahead, or is not announced as disabled fails.                                                                                                                                                   | `audit:DropdownMenu/accessibility` |
    + | FR20, AR7           | `DropdownMenuSubMenu.test.tsx` header and footer cases, including "on a phone"; `SubMenuHeaderFooter.a11y.chromium.spec.ts`; `ContextMenu.test.tsx` "shows a sub-menu with a header or footer as its rows alone, and says why in development"                                                                                                                                                                                                                 | Fine pointer and phone; Escape paths; nested flyouts; pointer leaving                  | A region inside a menu, an unnamed dialog, a Tab walk that does not wrap, an Escape that closes the root, or a flyout that closes while focus is inside fails.                                                                                                                                   | `audit:DropdownMenu/accessibility` |
    + | FR21                | Evidence gap: no test, matching the FR21 gap                                                                                                                                                                                                                                                                                                                                                                                                                  | —                                                                                      | —                                                                                                                                                                                                                                                                                                | `audit:DropdownMenu/behavior`      |
    ++| FR22                | `DropdownMenu.test.tsx` Tab cases; `DropdownMenuSubMenu.test.tsx` "Tab from a flyout leaves the whole menu" suite                                                                                                                                                                                                                                                                                                                                             | top level, one and two levels down, Shift+Tab                                          | A Tab that leaves any level open, a second `onOpenChange(false)`, or a prevented Tab fails.                                                                                                                                                                                                      | `audit:DropdownMenu/accessibility` |
    + | PR1                 | Evidence gap: no test asserts the listener and observer bounds                                                                                                                                                                                                                                                                                                                                                                                                | —                                                                                      | —                                                                                                                                                                                                                                                                                                | `audit:DropdownMenu/behavior`      |
    + | Theming anatomy map | `scripts/check-knowledge.mjs`                                                                                                                                                                                                                                                                                                                                                                                                                                 | Canonical anatomy and targets                                                          | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation.                                                                                                                                                                                                            | `audit:DropdownMenu/theming`       |
    + 
    +
    + ## docs/specs/components/DropdownMenu/DropdownMenuSubMenu.spec.md ##
    +@@ docs/specs/components/DropdownMenu/DropdownMenuSubMenu.spec.md: Consumer migration instructions belong in consumer docs and release notes.
    + 
    + ## Behavioral and layout contract
    + 
    +-| ID  | Invariant                                                                                                                                                                                                                                                                                | Basis                                                              |
    +-| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
    +-| FR1 | The row exposes `role="menuitem"`, `aria-haspopup="menu"` (`"dialog"` with a header or footer), `aria-expanded` matching its surface, and `aria-controls` naming the surface while open. The surface is labelled by the row.                                                             | `spec:AST-020`; current source and tests                           |
    +-| FR2 | A click, Enter, Space, or the inline-end arrow (inline-start under RTL) opens the surface; a keyboard open focuses its first enabled row. Escape or the close arrow closes only this level and returns focus to the row. A hover opens a flyout after an intent delay.                   | `spec:AST-020`; `architecture:interaction-modality`; current tests |
    +-| FR3 | A flyout sits beside the row on the inline-end side, flips with direction, and stays within the viewport; its block size caps at the smaller of its own cap and the viewport.                                                                                                            | `architecture:layer-runtime`; current source                       |
    +-| FR4 | A drilled-in view replaces the root's rows with a Back row named after the parent menu and then this row's children; Back, Escape, and the close arrow return to the row.                                                                                                                | Current source and focused tests                                   |
    +-| FR5 | Selecting a leaf row inside the surface closes every level and the root menu.                                                                                                                                                                                                            | Current source and tests                                           |
    +-| FR6 | `onOpenChange` MUST report `true` once when the surface opens and `false` once when it closes, from the action that caused the change. Implementation gap: a drilled-in view reset by the root menu closing reports no `false` (OQ1).                                                    | Documented promise; `architecture:react-component-runtime` INV2    |
    +-| FR7 | A disabled row exposes `aria-disabled="true"`, carries `tabindex="-1"` so the parent's keyboard model reaches it (`component:DropdownMenu` FR17), takes no hover focus, opens on no input, and runs no caller `onClick`. Implementation gap: a disabled row renders no `tabindex` today. | Current source and tests                                           |
    +-| FR8 | With `hasSpinner` the row shows a decorative spinner in place of the caret and announces the busy start once the root menu shows.                                                                                                                                                        | `spec:AST-086` FR8; current source and tests                       |
    +-| FR9 | Ref, supported host attributes and handlers, `className`, `style`, and `xstyle` reach or compose on the row root after the row's own behavior; the open keys and a disabled click stop before the caller's handler.                                                                      | `architecture:public-component-api` INV5–INV8; current tests       |
    ++| ID   | Invariant                                                                                                                                                                                                                                                                                | Basis                                                              |
    ++| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
    ++| FR1  | The row exposes `role="menuitem"`, `aria-haspopup="menu"` (`"dialog"` with a header or footer), `aria-expanded` matching its surface, and `aria-controls` naming the surface while open. The surface is labelled by the row.                                                             | `spec:AST-020`; current source and tests                           |
    ++| FR2  | A click, Enter, Space, or the inline-end arrow (inline-start under RTL) opens the surface; a keyboard open focuses its first enabled row. Escape or the close arrow closes only this level and returns focus to the row. A hover opens a flyout after an intent delay.                   | `spec:AST-020`; `architecture:interaction-modality`; current tests |
    ++| FR3  | A flyout sits beside the row on the inline-end side, flips with direction, and stays within the viewport; its block size caps at the smaller of its own cap and the viewport.                                                                                                            | `architecture:layer-runtime`; current source                       |
    ++| FR4  | A drilled-in view replaces the root's rows with a Back row named after the parent menu and then this row's children; Back, Escape, and the close arrow return to the row.                                                                                                                | Current source and focused tests                                   |
    ++| FR5  | Selecting a leaf row inside the surface closes every level and the root menu.                                                                                                                                                                                                            | Current source and tests                                           |
    ++| FR6  | `onOpenChange` MUST report `true` once when the surface opens and `false` once when it closes, from the action that caused the change. Implementation gap: a drilled-in view reset by the root menu closing reports no `false` (OQ1).                                                    | Documented promise; `architecture:react-component-runtime` INV2    |
    ++| FR7  | A disabled row exposes `aria-disabled="true"`, carries `tabindex="-1"` so the parent's keyboard model reaches it (`component:DropdownMenu` FR17), takes no hover focus, opens on no input, and runs no caller `onClick`. Implementation gap: a disabled row renders no `tabindex` today. | Current source and tests                                           |
    ++| FR8  | With `hasSpinner` the row shows a decorative spinner in place of the caret and announces the busy start once the root menu shows.                                                                                                                                                        | `spec:AST-086` FR8; current source and tests                       |
    ++| FR9  | Ref, supported host attributes and handlers, `className`, `style`, and `xstyle` reach or compose on the row root after the row's own behavior; the open keys and a disabled click stop before the caller's handler.                                                                      | `architecture:public-component-api` INV5–INV8; current tests       |
    ++| FR10 | Without its own `header` or `footer` and with none on a level above it, Tab or Shift+Tab on a row in the flyout closes this level as the key bubbles to the menu, which closes the whole menu (`component:DropdownMenu` FR22, `component:ContextMenu` FR11).                             | WAI-ARIA Menu Button pattern; current tests                        |
    + 
    + ### Allowed variation
    + 
    +@@ docs/specs/components/DropdownMenu/DropdownMenuSubMenu.spec.md: No component-local visual decision is recorded.
    + | FR4      | `DropdownMenuSubMenu.test.tsx` drill-in cases; `DropdownMenuDrillIn.a11y.chromium.spec.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                   | Drilled view; Back                         | A missing Back row or a Back that does not return fails.                                                                                                                                                                                                                 | `audit:DropdownMenuSubMenu/behavior`      |
    + | FR5      | `DropdownMenuSubMenu.test.tsx` "closes the whole menu after selecting a nested item" and "activates a nested item with the Enter key and closes the menu"                                                                                                                                                                                                                                                                                                                                                                    | Pointer and Enter                          | A level left open after a leaf pick fails.                                                                                                                                                                                                                               | `audit:DropdownMenuSubMenu/behavior`      |
    + | FR6      | `DropdownMenuSubMenu.test.tsx` "reports each flyout open and close to onOpenChange once"                                                                                                                                                                                                                                                                                                                                                                                                                                     | Flyout open and close                      | A missing or repeated report fails. Evidence gap: the drill-in reset gap is not asserted.                                                                                                                                                                                | `audit:DropdownMenuSubMenu/behavior`      |
    ++| FR10     | `DropdownMenuSubMenu.test.tsx` "Tab from a flyout leaves the whole menu" suite; `ContextMenu.test.tsx` matching case                                                                                                                                                                                                                                                                                                                                                                                                         | one and two levels down, Shift+Tab         | A flyout left open after Tab fails.                                                                                                                                                                                                                                      | `audit:DropdownMenuSubMenu/accessibility` |
    + | FR7, FR9 | `DropdownMenuSubMenu.test.tsx` "does not open a disabled submenu", "a disabled row does not pass a click on to the caller's onClick", host-attribute and ref cases ("aria-\*, data-\* and id reach the trigger row, and the flyout stays labelled by it", "a ref reaches the trigger row", "a caller's handlers run after the row's own, and the row still opens and takes the hover focus")                                                                                                                                 | Disabled; caller handlers and attributes   | A disabled open or caller click, or a dropped attribute, ref, or handler, fails. Evidence gap: no test asserts `className`, `style`, or `xstyle` on the row, a disabled row's `tabindex="-1"`, or that the parent's keyboard reaches it (`component:DropdownMenu` FR17). | `audit:DropdownMenuSubMenu/api`           |
    + | FR8      | `BusyOwnerAnnouncement.test.tsx` "DropdownMenuSubMenu loading state" cases; `DropdownMenuSubMenu.test.tsx` loading-flyout cases                                                                                                                                                                                                                                                                                                                                                                                              | Loading closed and open                    | A busy start announced while closed, twice, or not at all, or an announced Spinner, fails.                                                                                                                                                                               | `audit:DropdownMenuSubMenu/accessibility` |
    + | AR4, PR1 | `DropdownMenuForcedColors.test.tsx` (the row's `:focus-visible` ring and token inset); `DropdownMenuAudit.a11y.chromium.spec.ts` forced-colors frames. Evidence gap: no test asserts the timer bounds                                                                                                                                                                                                                                                                                                                        | —                                          | —                                                                                                                                                                                                                                                                        | `audit:DropdownMenuSubMenu/visual`        |
    +
      ## packages/core/src/ContextMenu/ContextMenu.test.tsx ##
     @@ packages/core/src/ContextMenu/ContextMenu.test.tsx: describe('ContextMenu outside dismissal', () => {
          expect(onOpenChange).not.toHaveBeenCalledWith(false);
    @@ packages/core/src/ContextMenu/ContextMenu.tsx: export function ContextMenu({
            }
            if (e.key === 'Enter' || e.key === ' ') {
     
    - ## packages/core/src/DropdownMenu/DropdownMenu.spec.md ##
    -@@ packages/core/src/DropdownMenu/DropdownMenu.spec.md: keeps its released default.
    - | FR17                  | Disabled menu rows SHOULD remain keyboard-discoverable while activation stays blocked. Current shared `useListFocus` deliberately skips every `aria-disabled` row, including checkbox items.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | WAI-ARIA Menu pattern; current shared navigation source/tests                                                                                                                 | Objective mismatch; shared mechanism remains undecided in OQ1                     |
    - | FR20                  | With `header` or `footer` on a sub-menu (`DropdownMenuSubMenu`), its surface is a modal dialog named by its row (AR7), holding the header, the sub-menu's `role="menu"` (rows only), then the footer. It sits in a host `DropdownMenu` renders beside its menu, never inside a menu; under a root with no such host (`ContextMenu`) the sub-menu shows its rows alone. On a fine pointer the dialog is the flyout; on a compact touch display, the drilled-in view, which replaces the root's menu and regions until popped. A region keeps its own keys and presses. The rows are one Tab stop: Tab and Shift+Tab walk the header's controls, the rows, then the footer's, and wrap; from a deeper flyout, that flyout closes and the walk goes on from the rows. An open lands as for any sub-menu (FR11, FR12), not on a region. Escape in the dialog, outside an IME composition and unless a control there handled it, returns focus to the row and leaves the root open. A pointer leaving the flyout keeps it open until focus leaves, and it closes with the flyout holding its row. Without either prop nothing changes.                                                                                                    | Proposed DEC-13; FR15; AR5                                                                                                                                                    | Proposed; verified in jsdom and Chromium, pending owner review                    |
    - | FR21                  | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu grows out of the Trigger button and collapses back into it (`spec:AST-094` FR31), and closes on a pan that starts outside it (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Owner decision, 2026-10-26; `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6                                                                                         | Decided; pending implementation                                                   |
    -+| FR22                  | Tab or Shift+Tab on a row at any level of a menu without `header` or `footer`, its own rows or an open sub-menu flyout's at any depth, closes every open level and the menu, calls `onOpenChange(false)` once, and leaves focus to move on from the trigger in page order (WAI-ARIA Menu Button). A level with a header or footer keeps FR15's and FR20's Tab cycle within it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | WAI-ARIA Menu Button pattern; current tests                                                                                                                                   | Decided; verified in jsdom                                                        |
    - 
    - ### Allowed variation
    - 
    -@@ packages/core/src/DropdownMenu/DropdownMenu.spec.md: than adding a DropdownMenu-owned heading target.
    - | FR15                | `DropdownMenu.test.tsx` header and footer suite; `__tests__/MenuHeaderFooter.a11y.chromium.spec.ts` in real Chromium                                                                                                                                                                                                                                                                                                                                                                                                                                                   | dialog role, modality, name and trigger wiring; regions outside the menu element; typing in a header field; Tab walk header → menu → footer with wrap at both ends; Tab and Shift+Tab from an open sub-menu; hidden and hidden-wrapper controls; Escape from the header; a finger tap on a footer button; bottom sheet order; no regions | A region rendered inside the menu element, a popover with regions that is not a named modal dialog, a trigger still saying `aria-haspopup="menu"` with regions, a header key taken by typeahead, a Tab that leaves the dialog or closes it, focus stranded outside the dialog, or a footer tap swallowed by the press model fails. |
    - | FR16                | `DropdownMenuSelectable.test.tsx`, source inspection, scoped `pr-a11y` over the `a11y/Checkbox pattern` stories, and owned Storybook states                                                                                                                                                                                                                                                                                                                                                                                                                            | checked, unchecked, disabled, caller click/pointer handlers, marker size and ownership                                                                                                                                                                                                                                                   | A dropped consumer handler, wrong checked state, nested semantic checkbox, disabled activation, or menu that closes by default fails.                                                                                                                                                                                              | `audit:DropdownMenuCheckboxItem/behavior`      |
    - | FR21                | `DropdownMenu.test.tsx` touch motion and pan-outside cases; `menuSurfaceMotion.test.ts`; `__tests__/DropdownMenuTouchMotion.interaction.browser.spec.ts`                                                                                                                                                                                                                                                                                                                                                                                                               | open, close, an interrupted open and close, Reduce Motion, a mouse; a tap, a hold and a pan outside; focus in the menu at a pan dismissal                                                                                                                                                                                                | A pinned `'popover'` or a mouse open that grows from the trigger, a pan outside that leaves the menu open, or focus left in a hidden menu fails                                                                                                                                                                                    |
    -+| FR22                | `DropdownMenu.test.tsx` Tab cases; `DropdownMenuSubMenu.test.tsx` "Tab from a flyout leaves the whole menu" suite                                                                                                                                                                                                                                                                                                                                                                                                                                                      | top level, one and two levels down, Shift+Tab                                                                                                                                                                                                                                                                                            | A Tab that leaves any level open, a second `onOpenChange(false)`, or a prevented Tab fails.                                                                                                                                                                                                                                        | `audit:DropdownMenu/accessibility`             |
    - | FR17, AR6           | Shared navigation source and DropdownMenu keyboard tests                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | disabled checkbox row first, middle, and last                                                                                                                                                                                                                                                                                            | Current tests prove the shared hook skips the row; no keyboard-discoverability proof exists until OQ1 is settled.                                                                                                                                                                                                                  | `audit:DropdownMenuCheckboxItem/accessibility` |
    - | FR20, AR7           | `DropdownMenuSubMenu.test.tsx` "header and footer" suite; `ContextMenu.test.tsx` rows-alone case; `__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts` in real Chromium (fine pointer, coarse pointer)                                                                                                                                                                                                                                                                                                                                                                | flyout and phone view; dialog role, name, row wiring; regions outside menus; Tab walk and wrap from a field, row, deeper flyout; field keys, presses; Escape, IME and handled Escapes; keyboard open; pointer leaving, focus in then out; parent closing a nested flyout; view tracking its regions; ContextMenu rows alone              | A region inside a menu element, a surface with regions that is not a named modal dialog, a row still saying `aria-haspopup="menu"`, a field key taken by the menu, a Tab that leaves or closes the dialog, a flyout closed under focus by a pointer leaving, a flyout outliving its parent, or the root menu beside a view, fails. | `audit:DropdownMenu/behavior`                  |
    - 
    -
      ## packages/core/src/DropdownMenu/DropdownMenu.tsx ##
     @@ packages/core/src/DropdownMenu/DropdownMenu.tsx: function DropdownMenuPopover({
            // A submenu flyout renders inline inside this menu; its key events bubble
    @@ packages/core/src/DropdownMenu/DropdownMenu.tsx: function DropdownMenuPopover({
              return;
            }
     
    - ## packages/core/src/DropdownMenu/DropdownMenuSubMenu.spec.md ##
    -@@ packages/core/src/DropdownMenu/DropdownMenuSubMenu.spec.md: Consumer migration instructions belong in consumer docs and release notes.
    - Draft requirements identify their basis so observed code is not mistaken for an
    - intentional decision.
    - 
    --| ID  | Candidate invariant                                                                                                                                                                                                                                                          | Basis                                                             | Draft review state                                                                                       |
    --| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
    --| FR1 | The row MUST expose `role="menuitem"`, `aria-haspopup="menu"` (`"dialog"` with a header or footer), `aria-expanded` matching its surface, and `aria-controls` naming the surface while open. The surface MUST be labelled by the row.                                        | WAI-ARIA menu pattern; current source and focused tests           | verify                                                                                                   |
    --| FR2 | A click, Enter, Space, or the inline-end arrow (inline-start under RTL) MUST open the surface; a keyboard open MUST focus its first row. Escape or the close arrow MUST close only this level and return focus to the row. A hover MUST open a flyout after an intent delay. | WAI-ARIA menu pattern; `architecture:interaction-modality`; tests | verify                                                                                                   |
    --| FR3 | A flyout MUST sit beside the row on the inline-end side, flip with direction, and stay within the viewport; its block size MUST cap at the smaller of its own cap and the viewport.                                                                                          | `architecture:layer-runtime`; current source and browser evidence | verify                                                                                                   |
    --| FR4 | A drilled-in view MUST replace the root's rows with a Back row named after the parent menu and then this row's children; Back, Escape, and the close arrow MUST return to the row.                                                                                           | Current source and focused tests                                  | verify                                                                                                   |
    --| FR5 | Selecting a leaf row inside the surface MUST close every level and the root menu.                                                                                                                                                                                            | Current source, focused tests, and browser evidence               | verify                                                                                                   |
    --| FR6 | `onOpenChange` MUST report `true` once when the surface opens and `false` once when it closes, from the action that caused the change.                                                                                                                                       | Documented promise; `architecture:react-component-runtime/INV2`   | objective gap: a drilled-in view reset by the root menu closing reports no `false` (owner: DropdownMenu) |
    --| FR7 | A disabled row MUST expose `aria-disabled="true"`, render no `tabindex`, take no hover focus, open on no input, and run no caller `onClick`.                                                                                                                                 | Current source, focused tests, and browser evidence               | verify                                                                                                   |
    --| FR8 | With `hasSpinner` the row MUST show a decorative spinner in place of the caret and announce the busy start once the root menu shows.                                                                                                                                         | `spec:AST-086/FR8`; current source                                | verify                                                                                                   |
    --| FR9 | Ref, supported host attributes and handlers, `className`, `style`, and `xstyle` MUST reach or compose on the row root after the row's own behavior; the open keys and a disabled click stop before the caller's handler.                                                     | `architecture:public-component-api/INV5–INV8`; focused tests      | verify                                                                                                   |
    -+| ID   | Candidate invariant                                                                                                                                                                                                                                                          | Basis                                                             | Draft review state                                                                                       |
    -+| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
    -+| FR1  | The row MUST expose `role="menuitem"`, `aria-haspopup="menu"` (`"dialog"` with a header or footer), `aria-expanded` matching its surface, and `aria-controls` naming the surface while open. The surface MUST be labelled by the row.                                        | WAI-ARIA menu pattern; current source and focused tests           | verify                                                                                                   |
    -+| FR2  | A click, Enter, Space, or the inline-end arrow (inline-start under RTL) MUST open the surface; a keyboard open MUST focus its first row. Escape or the close arrow MUST close only this level and return focus to the row. A hover MUST open a flyout after an intent delay. | WAI-ARIA menu pattern; `architecture:interaction-modality`; tests | verify                                                                                                   |
    -+| FR3  | A flyout MUST sit beside the row on the inline-end side, flip with direction, and stay within the viewport; its block size MUST cap at the smaller of its own cap and the viewport.                                                                                          | `architecture:layer-runtime`; current source and browser evidence | verify                                                                                                   |
    -+| FR4  | A drilled-in view MUST replace the root's rows with a Back row named after the parent menu and then this row's children; Back, Escape, and the close arrow MUST return to the row.                                                                                           | Current source and focused tests                                  | verify                                                                                                   |

@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 5, 2026 9:14pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 28, 2026
@astracat-bot

astracat-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 9:29 PM UTC

@astracat-bot astracat-bot Bot 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 of this fix waits on an engineering owner's decision on the three draft requirements it adds (DropdownMenu FR22, ContextMenu FR11, DropdownMenuSubMenu FR10), because no current record states this Tab rule yet. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. The DropdownMenu and ContextMenu change works as described and its new tests fail without the fix, but one change is needed either way:

  • BreadcrumbItem menus render the same sub-menu, but their root still ignores keys from a flyout (BreadcrumbItem.tsx:582), so Tab on a breadcrumb sub-menu row now closes only the flyout and leaves focus on the page body with the menu open; giving that root the same Tab branch, with a test, would match BreadcrumbItem FR5 and FR6.

[Automated review]

// In a menu with a header or footer above this level, that level
// moves focus on; in a plain menu the root closes the whole menu,
// and focus moves on from its trigger (APG menu button).
close({focusTrigger: false});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This close now runs under every root that hosts DropdownMenuSubMenu, not only DropdownMenu and ContextMenu. BreadcrumbItem renders the same sub-menu (a nested items entry, or BreadcrumbMenuSubMenu), but its root still returns early for keys from a flyout (BreadcrumbItem.tsx:582). So Tab on a breadcrumb sub-menu row closes only the flyout and leaves focus on the page body with the breadcrumb menu open. Adding the same if (e.key === 'Tab') closeMenu(); branch to that root's non-owned path (BreadcrumbItem FR5 already says Tab closes its menu), with a test, would keep the three roots consistent.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 153d2d9

@astracat-bot

astracat-bot Bot commented Nov 5, 2026

Copy link
Copy Markdown

In reply to @cixzhang

I'll run a full review to re-check this with your approval noted.

@cixzhang
cixzhang force-pushed the lane4/submenu-tab-closes-all branch from a424847 to 7d5c40d Compare November 5, 2026 16:16
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 5, 2026
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 7d5c40d

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 831d715

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
Breadcrumbs 16.1 kB (0) · 3.4 kB CSS (0) — ✓ · ✓ · ✓
ContextMenuItem 26.4 kB (0) · 6.1 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 21.1 kB (0) · 6.4 kB CSS (0) — ✓ · ✓ · ✓
16 more components
Component Size LoC Visual · A11y · RTL
DropdownMenuDivider 5.2 kB (0) · 548 B CSS (0) — ✓ · ✓ · —
DropdownMenuGroup 6.3 kB (0) · 3.4 kB CSS (0) — ✓ · ✓ · —
DropdownMenuItem 20.4 kB (0) · 6.0 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioGroup 6.3 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioItem 21.2 kB (0) · 6.4 kB CSS (0) — ✓ · ✓ · —
lab/Comments 279.2 kB (−20 B) · 2.5 kB CSS (0) — — · — · —
lab/CommentThread 273.4 kB (−45 B) · 2.4 kB CSS (0) — — · — · —
ContextMenu · Storybook 88.0 kB (−51 B) · 16.4 kB CSS (0) 702 (+3) ✓ · ✓ · ✓
TableCell 89.2 kB (−51 B) · 16.8 kB CSS (0) — — · — · —
TableHeaderCell 88.8 kB (−60 B) · 16.5 kB CSS (0) — — · — · —
Table 97.3 kB (−68 B) · 17.0 kB CSS (0) — — · — · —
BreadcrumbItem 75.2 kB (−70 B) · 11.4 kB CSS (0) — ✓ · ✓ · ✓
MoreMenu 85.2 kB (−76 B) · 16.5 kB CSS (0) — — · — · —
DropdownMenu 84.9 kB (−81 B) · 16.5 kB CSS (0) — ✓ · ✓ · ✓
DropdownMenuSubMenu 63.5 kB (−81 B) · 9.4 kB CSS (0) — ✓ · ✓ · ✓
Markdown 159.0 kB (−86 B) · 19.3 kB CSS (0) — — · — · —

Package size:

  • @astryxdesign/cli 3.38 MB packed (−11 B); no new dependencies
  • @astryxdesign/core 6.62 MB packed (−255 B); no new dependencies

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 23 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@cixzhang
cixzhang marked this pull request as ready for review November 5, 2026 16:18

@astracat-bot astracat-bot Bot 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.

Thanks for closing this keyboard gap. The engineering-owner approval at this head settles the new Tab rows, and in DropdownMenu and ContextMenu the change works as described: the new tests fail without the fix and pass with it. One change is needed before approval:

  • BreadcrumbItem menus render the same sub-menu, but their root still ignores keys from a flyout (BreadcrumbItem.tsx:595), so Tab on a breadcrumb sub-menu row now closes only the flyout and leaves focus on the page body with the breadcrumb menu still open; giving that root the same Tab branch as the other two roots, with a Breadcrumbs test, would match BreadcrumbItem FR5 and FR6.

The fix makes a new commit, so an engineering owner needs to comment /approve-spec <full head SHA> again on the new head; the author can do this as an engineering owner. Three comment rewrites are suggested inline and are optional.

[Automated review]

// In a menu with a header or footer above this level, that level
// moves focus on; in a plain menu the root closes the whole menu,
// and focus moves on from its trigger (APG menu button).
close({focusTrigger: false});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This close now runs under every root that hosts DropdownMenuSubMenu, not only DropdownMenu and ContextMenu. BreadcrumbItem renders the same sub-menu (a nested items entry, or BreadcrumbMenuSubMenu), but its root still returns early for keys from a flyout (BreadcrumbItem.tsx:595). So Tab on a breadcrumb sub-menu row closes only the flyout and leaves focus on the page body with the breadcrumb menu open. Adding the same branch to that root's non-owned path keeps the three roots consistent and matches BreadcrumbItem FR5 and FR6:

if (!ownsEvent(e)) {
  if (e.key === 'Tab') {
    closeMenu();
  }
  return;
}

A Breadcrumbs test for Tab on a flyout row would guard it.

Comment on lines +926 to +930
// Otherwise Tab leaves the flyout stack: every open level closes as
// the key bubbles past it, this one's own rows or a deeper level's.
// In a menu with a header or footer above this level, that level
// moves focus on; in a plain menu the root closes the whole menu,
// and focus moves on from its trigger (APG menu button).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional (comment style): the second sentence carries three topics in 33 words, and "the root closes the whole menu" is not yet true under BreadcrumbItem. A shorter version:

// Tab leaves the flyout stack. Each open level closes as the key passes it.
// The root, or a level above with a header or footer, then moves focus on.

Comment on lines +1228 to +1231
// Except Tab: each open flyout has closed itself as the key bubbled
// up, and the root moves on as Tab from one of its own rows would.
// With a header or footer that walks the regions; otherwise the menu
// closes and focus moves on from the trigger (APG menu button).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional (comment style): one topic per sentence reads more easily here:

// Tab is the exception. Each open flyout has already closed itself.
// With a header or footer, the walk goes on through the regions.
// Otherwise the menu closes, and focus moves on from the trigger.

Comment on lines +551 to +552
// Except Tab: each open flyout has closed itself as the key bubbled up,
// and the menu closes as Tab from one of its own rows would (APG).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional (comment style), split into one fact per sentence:

// Tab is the exception. Each open flyout has already closed itself,
// so the menu closes as for Tab on its own row.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec e6face5

@astracat-bot astracat-bot Bot 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.

Thanks for the follow-up. BreadcrumbItem menus now close on Tab from a sub-menu row the same way DropdownMenu and ContextMenu do, and the engineering-owner approval at this head settles the new Tab rows. The new tests fail without the fix and pass with it, and in a real browser Tab from a flyout row closes every level and moves focus on in all three menus.

Approved pending CI: the required build, test and docsite-test suites are deferred on this push and run before merge; lint and typecheck pass.

One optional comment-style suggestion is inline.

[Automated review]

Comment on lines +595 to +596
// Except Tab: each open flyout has closed itself as the key bubbled up,
// and the menu closes as Tab from one of its own rows would (APG).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional (comment style): this is one 27-word sentence with two topics. A shorter version:

// Tab is the exception. Each open flyout has already closed itself,
// so the menu closes as for Tab on its own row.

@astracat-bot astracat-bot Bot 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.

Approved pending CI: the required build, test and docsite-test suites are deferred on this push and run before merge; lint, typecheck and the CLI smoke test pass.

The new commit removes the deleted menuRegionsContext module from the CLI's recorded module names, so the resolution test matches the source again; nothing else changed since the last review. Tab or Shift+Tab on a sub-menu row still closes every level of DropdownMenu, ContextMenu and BreadcrumbItem menus, and the engineering-owner approval of the new Tab rows holds on this head. The optional comment-style suggestions from earlier stay inline.

[Automated review]

…opdownMenu FR23 (main's FR22 is the entrance hold)
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 655a76d

@astracat-bot astracat-bot Bot 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.

Requesting one small change before approval.

  • Miss: the changeset (.changeset/submenu-tab-closes-menu.md, line 5) says Tab from a sub-menu flyout row now closes every DropdownMenu, but a DropdownMenu with its own header or footer still closes only the flyout and moves to its footer with the menu open, as before (the existing "Tab from a flyout row closes the flyout and moves to the footer" test asserts this), so please scope the note to menus without a header or footer and end it with "A menu or sub-menu with a header or footer keeps its Tab cycle."

The fix itself looks right: the new tests fail without the change and pass with it, and the new Tab rows (DropdownMenu FR23, ContextMenu FR11, DropdownMenuSubMenu FR10) carry an engineering owner's approval for this revision. The optional comment-style suggestions from earlier stay inline.

[Automated review]

Comment thread .changeset/submenu-tab-closes-menu.md Outdated
'@astryxdesign/core': patch
---

[fix] `DropdownMenu`, `ContextMenu` and `BreadcrumbItem` menus: Tab or Shift+Tab on a row inside an open sub-menu flyout now closes every open level and the menu, calls `onOpenChange(false)` once, and lets focus move on from the trigger, as Tab already did from a top-level row (WAI-ARIA Menu Button). Before, the menu stayed open. A sub-menu with a `header` or `footer` keeps its Tab cycle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This note reads as if every DropdownMenu changed, but a menu with its own header or footer still closes only the flyout on Tab and moves to its footer with the menu open, as before (the existing "Tab from a flyout row closes the flyout and moves to the footer" test in DropdownMenu.test.tsx). A scoped version:

[fix] `DropdownMenu`, `ContextMenu` and `BreadcrumbItem` menus without a `header` or `footer`: Tab or Shift+Tab on a row inside an open sub-menu flyout now closes every open level and the menu, calls `onOpenChange(false)` once, and lets focus move on from the trigger, as Tab already did from a top-level row (WAI-ARIA Menu Button). Before, the menu stayed open. A menu or sub-menu with a `header` or `footer` keeps its Tab cycle.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 831d715

@astracat-bot astracat-bot Bot 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.

Approved pending CI: the deferred build, typecheck and test suites have not run on this head yet; they run before merge.

Thanks for tightening the changeset. It now scopes the note to menus without a header or footer, which matches the new DropdownMenu FR23 row and the header-and-footer tests. The Tab rows (DropdownMenu FR23, ContextMenu FR11, DropdownMenuSubMenu FR10) carry an engineering owner's approval for this head, and the BreadcrumbItem branch follows its existing FR5 ("Escape, Tab, and selection close it"). The five new tests fail on main and pass here, and the header and footer suites still pass.

The earlier comment-style suggestions on the new source comments stay optional.

[Automated review]

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 5, 2026
@cixzhang
cixzhang marked this pull request as draft November 5, 2026 21:37
@cixzhang
cixzhang marked this pull request as ready for review November 5, 2026 21:37

This branch was successfully deployed

1 active deployment
Preview — 831d715d Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] DropdownMenu, ContextMenu: Tab in an open submenu flyout does not close the menu

1 participant