Skip to content

docs(spec): DropdownMenu records its shipped contract as current - #9113

Merged
cixzhang merged 9 commits into
mainfrom
spec-backfill/DropdownMenu
Oct 31, 2026
Merged

cixzhang merged 9 commits into
mainfrom
spec-backfill/DropdownMenu

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Exact decision needed

Make the DropdownMenu records current, recording shipped behavior: promote component:DropdownMenu, component:DropdownMenuItem, component:DropdownMenuDivider, component:DropdownMenuRadioGroup, component:DropdownMenuRadioItem, and component:DropdownMenuSubMenu from draft, and add component:DropdownMenuCheckboxItem and component:DropdownMenuGroup (each has its own .doc.mjs). No behavior, API, default, or DOM change.

Existing authority searched

Decision

  • Eight current records (approved_by: cixzhang, approved_at: 2026-10-31), template 7, written from what main ships today; existing content and attribution are kept.
  • Pending decisions were converted, with no owner decision taken. DEC-5 ("A row that goes somewhere is a link", owner confirmation pending) and DEC-13 ("A sub-menu's header and footer make its surface a dialog", proposed) both ship on main, so their behavior is now FR10 and FR20 with basis "current behavior"; their DEC entries and pending decider lines are removed and no cixzhang decider is added.
  • Requirements main does not yet meet are written as "MUST … Implementation gap": the finger press-open (FR8), the touch grow and pan dismissal (FR21), the checkbox row's Enter with no named exception (CheckboxItem FR2, its OQ1), and the sub-menu drill-in close report (SubMenu FR6, its OQ1). The mouse press-open, which module:DropdownMenu/useMenuPress DEC-4 still lists as proposed, is written as shipped behavior, not a requirement until that decision is ruled.
  • The draft's misplaced concept rows moved from the requirement table to Public concepts; the decision log is restored to one entry per decision: DEC-2's menuMaxHeight text and decider, which sat under a DEC-7 heading, are back under DEC-2, and the DEC-7 heading (safe triangle), which never had a body or decider of its own, is removed, so FR13 is written as current behavior with no owner decision taken.
  • component:DropdownMenu FR14 and FR16 now point at the new Group and CheckboxItem records.

Rejected alternatives

None.

Claim scope and non-goals

Shipped behavior only, traced from the DropdownMenu sources, docs, and tests. No code is fixed. Open questions stay open: component:DropdownMenu OQ1 (disabled rows and the keyboard), component:DropdownMenuItem OQ1–OQ2 (disabled rows; forced-colors focus), component:DropdownMenuRadioItem OQ1, component:DropdownMenuSubMenu OQ1.

Evidence and implementation relationship

  • DropdownMenu.test.tsx, DropdownMenuSubMenu.test.tsx, DropdownMenuSelectable.test.tsx, BusyOwnerAnnouncement.test.tsx, Menu.a11y.test.tsx, ContextMenu.test.tsx, Breadcrumbs.test.tsx, and the DropdownMenu Chromium suites; each verification row names the cases it relies on and lists its evidence gaps.

Current-state impact:

  1. Under a finger the menu opens only after a 500 ms hold, keeps the layer's own entrance and exit, and has no pan-outside dismissal, against spec:AST-094 FR29, FR31, FR32.
  2. Keyboard focus on menu rows shows only a ~1.1:1 overlay tint and no indicator in forced-colors mode (component:DropdownMenuItem OQ2); Enter on a checkbox row keeps the menu open with no named exception (component:DropdownMenuCheckboxItem OQ1).
  3. The delegated Trigger Button carries component:Button's reduced-motion and fixed-height gaps; a drilled-in sub-menu reset by the root closing reports no onOpenChange(false).

Writing rubric results

  • R1 narration: none; present tense; gap notes say what happens now.
  • R2 research in the record: none.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives over one line or outside a decision: decision rationale kept as written in the existing decisions.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none; every open question is unruled.

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract: none named.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; no ownership collision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge: aligned
  • Prettier: clean
  • git diff --check: clean

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Error Error Oct 31, 2026 9:55am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 31, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang
cixzhang marked this pull request as ready for review October 31, 2026 06:09
@astracat-bot

astracat-bot Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 10:11 AM UTC

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current change has no visual or preview-producing scope. Visual checks are not applicable; preview links are not shown.


Generated by PR Enrichment workflow | View current CI run

@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.

This spec backfill can't be approved yet: these eight records would make DropdownMenu requirements current, which needs an engineering owner's decision on this exact head. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Approving also settles the pending link-row and sub-menu header/footer decisions that become FR10 and FR20, and the mouse press-open that the useMenuPress module record still lists as proposed. Whatever the decision, please fix these record errors:

  • DropdownMenu.spec.md FR8 says touch opening follows AST-094 FR29, but the trigger still waits the released 500 ms hold, so it should be written as an implementation gap.
  • FR10, ORD1 and DropdownMenuItem FR3 leave out that a modified click on a link row still closes the menu.
  • DropdownMenuCheckboxItem keeps the menu open on Enter without naming the exception AST-020 FR15 asks for, and its ORD1 puts the close after onClick, though the code closes first.

[Automated review]

| FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7, FR10, and FR11: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. The grow, collapse, and pan dismissal under a finger are FR21. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR28 |
| FR6 | When the pointer menu closes, focus returns to the Trigger button with a visible ring only when the menu was driven by keyboard; a press outside that landed on a focusable control keeps focus there. | `architecture:interaction-modality` INV1; DEC-1 |
| FR7 | `menuMaxHeight` replaces the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | DEC-2 |
| FR8 | The Trigger button opens the pointer menu on a mouse press-down, with the module's settle rule on the opening release, and under a finger as `spec:AST-094` FR29 states; pressing the trigger of an open menu closes it without reopening in the same gesture; a mouse click and the keyboard open as before. | `module:DropdownMenu/useMenuPress` FR8, FR9 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The touch half of FR8 isn't shipped. The trigger still opens on the released 500 ms long press (MENU_PRESS_LONG_PRESS_MS = 500 in hooks/useMenuPress.ts, and DropdownMenu.tsx passes no longPressDelayMs), and the cited case "a held touch on the trigger opens with the finger down…" asserts exactly that delay. AST-094 FR29 asks for an open as the finger presses in a fixed bar. Could this become "MUST … Implementation gap" with a Current-state impact entry, as FR21 is written? The mouse half also cites useMenuPress FR8, which that module still lists as proposed.


### Transformation and precedence order

- **ORD2 — Link row activation.** modified click → the browser alone; else
- **ORD1 — Link row activation.** modified click → the browser alone; else

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"modified click → the browser alone" doesn't match the source. DropdownMenuItem.tsx skips onClick on a modified click but still calls closeMenu() ("The menu still closes: the row acted."), and the touch sheet does the same. Suggest: modified click → no onClick, close (hasCloseOnSelect), the browser opens it. FR10 and DropdownMenuItem FR3 need the same clause.

| ID | Invariant | Basis |
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| FR1 | The row renders one element with `role="menuitemcheckbox"` whose `aria-checked` equals `value`; the checkbox marker is decorative (`aria-hidden`), so the row is the only announced control. | `spec:AST-020` FR15; current source and tests |
| FR2 | An enabled activation, by pointer or by the parent's synthesized Enter/Space click, calls `onChange(!value)` and the caller's `onClick`; the menu stays open unless `hasCloseOnSelect` is set, in which case it closes after the toggle. | `architecture:public-component-api` INV7; current tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Enter on this row keeps the menu open by default. The adopted Menu pattern's Enter closes it, and __tests__/Menu.a11y.known-failures.ts records menu.keys.enter-activates-and-closes failing on this row. AST-020 FR15 asks the owning record to name a required part it does not adopt, with the reason. Could this record add that exception (a DEC), or list it as a gap with an open question?


### Transformation and precedence order

- **ORD1 — Activation.** Disabled guard → `onChange(!value)` and caller

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The source's order is onChange(!value), then closeMenu() when hasCloseOnSelect, then the caller's onClick (DropdownMenuCheckboxItem.tsx:176-188), the same order RadioItem FR3 states. Suggest: Disabled guard → onChange(!value) → close when hasCloseOnSelect → caller onClick.

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions (now FR10 and FR20) and the safe triangle (FR13). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Thanks for fixing the earlier record errors; whatever the decision, three fixes remain:

  • DropdownMenu.spec.md FR8 requires opening as the finger presses everywhere, but AST-094 FR29 asks for that only in a fixed bar, with a 150 ms wait inside a scroll view and the tap-open kept under presentation="popover".
  • DEC-7's decider line is DEC-2's, carried over when the safe-triangle heading was inserted above DEC-2's body, so FR13 reads as owner-decided.
  • Two verification rows quote aria-_, data-_ where the test names say aria-*, data-*.

[Automated review]

| FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7, FR10, and FR11: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. The grow, collapse, and pan dismissal under a finger are FR21. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR28 |
| FR6 | When the pointer menu closes, focus returns to the Trigger button with a visible ring only when the menu was driven by keyboard; a press outside that landed on a focusable control keeps focus there. | `architecture:interaction-modality` INV1; DEC-1 |
| FR7 | `menuMaxHeight` replaces the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | DEC-2 |
| FR8 | The Trigger button opens the pointer menu on a mouse press-down, with a settle rule on the opening release, and pressing the trigger of an open menu closes it without reopening in the same gesture; a mouse click and the keyboard open as before. The mouse press-open is shipped behavior, not a requirement until `module:DropdownMenu/useMenuPress` DEC-4 is ruled. Under a finger the trigger MUST open the menu as the finger presses (`spec:AST-094` FR29). Implementation gap: a held finger opens it after a 500 ms hold. | `module:DropdownMenu/useMenuPress` FR8, FR9; `spec:AST-094` FR29 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The touch half is wider than AST-094 FR29: FR29 opens on press only in a fixed bar, waits the 150 ms content-touch delay (or opens at a tap's lift) inside a scroll view, and keeps the tap-open under presentation="popover". As written, a change that opens on press inside a scroll view, or under a pinned popover, would conform to FR8 while failing FR29. Suggest: "Under a finger the trigger MUST open the menu as spec:AST-094 FR29 states: as the finger presses in a fixed bar, after the 150 ms content-touch delay or at a tap's lift inside a scroll view; presentation="popover" keeps the tap open." The End-user impact row (line 45, "opens only after a held press") also misses that a tap opens at its click today.

@@ -499,19 +499,7 @@ that and a case pins the behavior instead.
**Reference:** `component:DropdownMenu/DEC-7`
**Decider:** `cixzhang`, `2026-10-02`

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 decider line came with DEC-2: the safe-triangle heading was inserted between DEC-2's heading and its Reference/Decider/body, and the PR restores the same line under DEC-2 (line 390). main records FR13 as "Proposed … pending owner review", so keeping it here makes FR13 read as decided on a date nobody recorded for it. Could DEC-7 drop the decider line (FR13 basis "Current behavior", as FR10 and FR20 do), or name who actually ruled?

| FR3 | `DropdownMenu.test.tsx` "DropdownMenuItem href" and "link rows — the browser's own clicks" cases; "a middle click on a link row runs the caller's onAuxClick and still closes the menu" | Plain, modified, middle, right click; blocked destination | A non-anchor row, a modified click that runs the callback, a middle click that does not close, or a live blocked destination fails. | `audit:DropdownMenuItem/behavior` |
| FR4, AR3 | `DropdownMenu.test.tsx` keyboard-access cases ("moves focus to the item the mouse hovers, keeping a single highlight", "does not move focus on hover for a disabled item", "does not move focus for a non-mouse (touch) pointer") and "keeps the row's own role, tab stop and hover focus beside a caller's onPointerMove" | Mouse hover, touch, disabled | A second highlight, hover focus on a disabled row or under touch, or a lost tab stop fails. | `audit:DropdownMenuItem/accessibility` |
| FR5 | `DropdownMenu.test.tsx` destructive-variant cases; `themingTargets.test.ts` | Default and destructive; data and compound mode | A missing `data-variant` fails; a rendered class absent from the documented targets fails. Evidence gap: no test asserts the `dropdown-menu-item` class or `data-size`. | `audit:DropdownMenuItem/theming` |
| FR6 | `DropdownMenu.test.tsx` ref and host-attribute cases ("a ref reaches the menuitem element", "puts aria-_, data-_ and id on the row root, a link row included", "runs the DOM handlers the caller attaches, such as a drop target") | Ref; host attributes; caller handlers | A dropped ref, attribute, or handler fails. Evidence gap: no test asserts `className`, `style`, or `xstyle` on the row. | `audit:DropdownMenuItem/api` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The test is named "puts aria-, data- and id on the row root, a link row included" (DropdownMenu.test.tsx:1538); Prettier turned the asterisks into _. Escaping them (aria-\*, data-\*) keeps the quoted name searchable.

| 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` |
| 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. | `audit:DropdownMenuSubMenu/api` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here: the case is "aria-, data- and id reach the trigger row, and the flyout stays labelled by it" (DropdownMenuSubMenu.test.tsx:427).

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions (now FR10 and FR20), the safe triangle (FR13) and row focus visibility in forced colors. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Thanks for fixing the earlier FR8, DEC-7 and test-name issues; two record fixes remain whatever the decision:

  • DropdownMenu.spec.md still says "Released default preserved: yes" and "Migration decision: none", but FR8 and FR21 move the finger default that AST-094 FR25 lists as moved, with presentation="popover" as the way back.
  • The FR20 verification row calls the ContextMenu rows-alone case an evidence gap, but ContextMenu.test.tsx has "shows a sub-menu with a header or footer as its rows alone, and says why in development".

[Automated review]

Comment on lines +63 to +69
- Released default preserved: yes
- Compatibility class: records shipped behavior; no public prop, default,
export, or DOM change
- Controlled/uncontrolled behavior: `isMenuOpen` with `onOpenChange`, or
uncontrolled
- Migration decision: none; a caller that needs the released tap-open under a
finger passes `presentation="popover"` (`spec:AST-094/DEC-4`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 and FR21 make the omitted presentation open on press and grow from the trigger under a finger, which AST-094 FR25 lists as a moved default ("DropdownMenu's omitted presentation, released as 'popover', which presentation="popover" keeps"), and useMenuPress.spec.md records the same timing move as "Released default preserved: no". Suggest: "Released default preserved: no under a finger: the omitted presentation follows spec:AST-094 FR29, FR31 and FR32 (FR8, FR21); presentation="popover" keeps the released behavior (spec:AST-094 FR25)" and "Migration decision: spec:AST-094 FR25 and DEC-4".

| FR14 | `DropdownMenu.test.tsx` section cases | Titled and untitled sections | A missing group role or title fails. | `audit:DropdownMenu/accessibility` |
| FR15, AR5 | `DropdownMenu.test.tsx` header and footer cases, including the hidden-control, open-sub-menu, and header radio-group cases; `MenuHeaderFooter.a11y.chromium.spec.ts` | Regions present and absent; hidden controls; open flyout; custom trigger; bottom sheet | A region inside the menu, a missing or unnamed dialog, a wrong `aria-haspopup`, a Tab that leaves the popover, a hidden control taken as a stop, or a region whose keys the menu takes fails. | `audit:DropdownMenu/accessibility` |
| FR17, AR6 | `DropdownMenu.test.tsx` "skips aria-disabled menuitemradio and menuitemcheckbox items during arrow navigation" and "typeahead skips an aria-disabled item and matches the next enabled label" (the shipped behavior OQ1 leaves open); disabled-row cases | Disabled rows | A disabled row that runs a caller handler fails. | `audit:DropdownMenu/accessibility` |
| FR20, AR7 | `DropdownMenuSubMenu.test.tsx` header and footer cases, including "on a phone"; `SubMenuHeaderFooter.a11y.chromium.spec.ts` | 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. Evidence gap: no test here asserts the rows-alone case under ContextMenu. | `audit:DropdownMenu/accessibility` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ContextMenu.test.tsx:780 ("shows a sub-menu with a header or footer as its rows alone, and says why in development") asserts this case: the row keeps aria-haspopup="menu", no dialog or header field renders, and the dev warning fires. Suggest citing it here and dropping the evidence gap.

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions, the keyboard and safe-triangle rows, and the dropped requirement that row focus stay perceivable. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Two record fixes are needed either way:

  • The Item, RadioItem and SubMenu records list only the forced-colors focus gap; the ordinary-theme focus tint, about 1.1:1 against the menu surface, which the draft recorded, should be named too.
  • DropdownMenu PR1 leaves out the menu's MutationObserver and the media-query listener presentation="adaptive" holds, and says the compact-touch query has no listener.

[Automated review]

Comment on lines +164 to +166
- **AR4 — Focus visibility.** Keyboard focus paints only the menu overlay
tint with no outline, and nothing under forced colors. The shared row
treatment is `component:DropdownMenuItem` OQ2.

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 drops the measured ordinary-theme gap the draft recorded ("measures about 1.1:1 against the menu surface"). The row paints focus as --color-overlay-hover with outline: none (DropdownMenuRadioItem.tsx:49); with the default tokens that is 1.09:1 on the light menu surface and 1.15:1 on the dark one. Naming it here and under Current-state impact, and in the Item and SubMenu twins, keeps the gap visible whichever form the requirement takes.

Comment on lines +178 to +182
- **PR1 — Bounded resources.** While open, the pointer menu observes its own
size and listens for window and visual-viewport resizes and image loads to
decide whether it scrolls. A mouse press-open adds document pointer-up and
pointer-cancel listeners for the length of the press. The compact-touch query
is read once as the menu opens, with no listener.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useMenuOverflow.ts also watches the menu with a MutationObserver (childList, characterData, subtree), and with presentation="adaptive" useAdaptivePresentation subscribes to the same compact-touch query's change event while the menu is mounted, so "with no listener" holds only for the drill-in's one-time read. Suggest naming the content observer and: "With presentation=\"adaptive\" the root subscribes to the compact-touch query while mounted; the drill-in reads it once as the menu opens."

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions (now FR10 and FR20), the keyboard and safe-triangle rows (FR12, FR13), and the dropped requirement that row focus stay perceivable. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, PR1 in DropdownMenu.spec.md counts only the mouse press-open's document pointerup/pointercancel listeners, but a held finger that opens the menu adds the same pair, so that sentence needs to cover both. The focus-contrast and observer fixes from the last round check out.

[Automated review]

Comment on lines +180 to +181
and image loads to decide whether it scrolls. A mouse press-open adds document
pointer-up and pointer-cancel listeners for the length of the press. With

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A held finger that opens the menu adds the same pair: after the 500 ms long press, useMenuPress calls the trigger's press handler with the touch pointer type, and handleTriggerPress runs holdInvokerThroughPress, which adds capture pointerup and pointercancel listeners on the document until that finger lifts (DropdownMenu.tsx:1383, :508-527). FR8 already counts the held finger as a press-open. Suggest:

and image loads to decide whether it scrolls. A press-open, by a mouse press or
a held finger, adds document pointer-up and pointer-cancel listeners for the
length of the press. With

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions (now FR10 and FR20), the keyboard and safe-triangle rows (FR12, FR13), and the dropped requirement that row focus stay perceivable. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, FR10 in DropdownMenu.spec.md and FR3 in DropdownMenuItem.spec.md say a middle click on a link row always closes the menu, but only the pointer menu's row closes, and only when hasCloseOnSelect is on, while the touch sheet stays open, so both clauses need scoping. The PR1 listener fix from the last round checks out.

[Automated review]

| FR7 | `menuMaxHeight` replaces the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | DEC-2 |
| FR8 | The Trigger button opens the pointer menu on a mouse press-down, with a settle rule on the opening release, and pressing the trigger of an open menu closes it without reopening in the same gesture; a mouse click and the keyboard open as before. The mouse press-open is shipped behavior, not a requirement until `module:DropdownMenu/useMenuPress` DEC-4 is ruled. Under a finger the trigger MUST open the menu as `spec:AST-094` FR29 states: as the finger presses in a fixed bar, after the 150 ms content-touch delay or at a tap's lift inside a scroll view; `presentation="popover"` keeps the tap open. Implementation gap: a held finger opens it after a 500 ms hold, and a tap opens it at its click. | `module:DropdownMenu/useMenuPress` FR8, FR9; `spec:AST-094` FR29 |
| FR9 | With `renderTrigger`, the rendered control carries `aria-haspopup`, `aria-expanded`, `aria-controls` and the `id` the menu's `aria-labelledby` points at, and receives the same press-model, keyboard-open and toggle-click handlers as the Button path; the menu is named by that control. A disabled custom trigger never opens the menu or the sheet. `button` and `renderTrigger` are mutually exclusive: both given, `renderTrigger` wins and a dev warning fires, in both presentations. A drilled-in Back row (FR11) returns to the menu by that same name: the caller's `aria-label` when given, else the control's accessible name, read as the menu opens. | DEC-3 |
| FR10 | An action row with `href` renders as one anchor carrying `role="menuitem"` (the pointer menu) or the touch sheet's link row, routed through `LinkProvider`; a plain click runs `onClick`, then closes the menu (`hasCloseOnSelect`), then lets the browser navigate; a modified click does not run `onClick`, closes the menu, and is left to the browser; a middle click closes the menu and runs the caller's `onAuxClick`; a right click is left to the context menu; Enter and Space activate through a synthesized click that carries the key's modifiers. A disabled link row has no `href`; a blocked destination renders inertly. | Current behavior; `family:navigation-destinations` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The middle-click clause covers more than ships. Only the pointer row handles auxclick, and it closes only when hasCloseOnSelect is on, before the caller's onAuxClick (DropdownMenuItem.tsx:235-250, :288). The touch sheet's link row reacts to the click capture alone (MenuBottomSheetActionList.tsx:140-141), so a middle click there opens the tab and leaves the sheet open. Suggest: "in the pointer menu a middle click closes the menu (hasCloseOnSelect), then runs the caller's onAuxClick; the touch sheet leaves a middle click to the browser and stays open".

| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| FR1 | The pointer row exposes one `role="menuitem"` root. Label, description, icon, and end content are presentational content of that row rather than nested controls. | `spec:AST-020` FR15; current source and docs |
| FR2 | Enabled activation runs the row callback before optional close. Disabled rows expose `aria-disabled=true`, remove link destinations, and run neither callback nor close. | Current behavior and tests |
| FR3 | A destination row renders as the link root through LinkProvider. Plain activation runs the callback then closes and navigates; a modified click does not run the callback, closes the menu, and is left to the browser; a middle click runs the caller's `onAuxClick` and closes the menu; blocked schemes are inert. | `family:navigation-destinations` FR1–FR5; current tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here: the close follows the row's hasCloseOnSelect and comes before the caller's onAuxClick (DropdownMenuItem.tsx:235-250, :288). Suggest: "a middle click closes the menu (hasCloseOnSelect), then runs the caller's onAuxClick".

@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.

These eight DropdownMenu records can't be approved yet: making them current settles requirements no engineering owner has ruled on at this head, including the pending link-row and sub-menu header/footer decisions (now FR10 and FR20), the safe-triangle row (FR13), and the dropped requirement that row focus stay perceivable. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. The middle-click wording in FR10 and DropdownMenuItem FR3 now matches the source, and nothing else needs changing.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 90d9a7b

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@cixzhang
cixzhang merged commit 133fb80 into main Oct 31, 2026
56 of 57 checks passed

This branch had an error being deployed

1 failed deployment
Preview — 90d9a7be Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant