Skip to content

fix(ContextMenu): a pointer open highlights no row - #7303

Open
vjeux wants to merge 6 commits into
facebook:mainfrom
vjeux:context-menu-pointer-open
Open

vjeux wants to merge 6 commits into
facebook:mainfrom
vjeux:context-menu-pointer-open

Conversation

@vjeux

@vjeux vjeux commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

User impact

A right-click or a long press opens a ContextMenu with its first row lit, as if already chosen. Under a held finger the lit row sits wherever the menu opened, not under the finger. A DropdownMenu a pointer opens lights nothing.

Problem and solution fit

  • Why it occurs: openAtLocalPoint focuses the first row on every open, and in a menu the highlight is DOM focus.
  • Why it harms the task: the first row reads as selected before the person moves, and the highlight stops following the pointer as the press model promises.
  • Why it matters: every pointer-opened context menu, and every long press on a phone.

A right-click or a long press now focuses the menu itself; a keyboard-invoked contextmenu still focuses the first row.

Expected behavior and authority

module:DropdownMenu/useMenuPress (current) AR1: the highlighted row is the focused row; FR2: while a press is held, the highlighted row is the one under the pointer. component:DropdownMenu ships the same split for its trigger: a pointer open focuses the menu, a keyboard open the first row. component:ContextMenu is a draft; this PR proposes the rule as FR10, pending owner review, and FR5 defers to it; #7308 proposes FR11 and #7840 FR12 in the same record.

Smallest restoration

openAtLocalPoint takes the open's modality: keyboard when the last input was a key (browsers report Shift+F10 at the focused element) or the event is at (0, 0) with no detail; otherwise pointer. A pointer open focuses the menu (tabIndex={-1}, outline: none like DropdownMenu's) instead of the first row. With no focusable row, a keyboard open leaves focus, and its ring, on the invoking element, as main does. No API, default or visual change; the keyboard path and the bottom sheet are unchanged.

Evidence

  • Before, in real Chromium on the Default story: after a right-click, and after a CDP touch long press, a menuitem holds focus.
  • After: the menu holds focus in both cases, and ArrowDown from it reaches the first row.
  • Unchanged: a keyboard-invoked contextmenu still focuses the first row, including a real Shift+F10 in Chromium on the inline-trigger story; the ContextMenu and DropdownMenu suites pass.
  • Regression proof: three of six new unit cases fail on main (the every-row-disabled keyboard case guards main's behavior); the right-click and long-press Chromium cases fail against a main build.
  • Out of scope: the long-press menu closing on the lift (fix(ContextMenu): the long press that opens the menu no longer closes it #7308).

Writing rubric results

R1 narration: none in FR10.
R2 research: none.
R3 evidence: FR10 cites a module record and tests.
R4 rejected alternatives: none added.
R5 change references: none.
R6 open questions: none.

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

Rebased onto main d0f9afe7ce, no conflicts or changes. The previous head's pr-a11y and smoke-test reds came from CI: main's #7802 added a pr-a11y step whose spec file the older head lacked, and jsdelivr was still propagating core@0.7.2.
  vitest ContextMenu, docProp*   Tests 89 passed
  typecheck, check:knowledge   ✓

Review round 5: FR10's keyboard condition reads the shared input modality, which is keyboard before any input, matching the code; the changeset says the same, and a new case pins an open before any input landing on the first row (a guard: the code already did this).
  vitest ContextMenu   Tests 67 passed
  typecheck, check:knowledge, prettier, lint-staged pre-commit (check:repo)   ✓

Review round 4 (rebased onto main 2c3ed99335; the previous head's test and test-node reds were main's own, a PostCSS fixture that #7797 fixed): FR10 says an open that matches neither keyboard condition, as from an assistive technology, is a pointer open (record wording only; the cell keeps its length).
  vitest ContextMenu, DropdownMenu.test.tsx   Tests 258 passed
  typecheck, check:knowledge, prettier   ✓

Rebased onto main 577a62e590 (one conflict, in ContextMenu.test.tsx's imports: main's sheet warm-up beside this PR's modality reset; both kept). 
  vitest ContextMenu, DropdownMenu, docProp*   Tests 365 passed
  playwright ContextMenu/__tests__ --project=chromium   24 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Review round 3: FR10 reads "Proposed; pending owner review", as FR5 already did (record wording only).

Review fix: a keyboard open with every row disabled leaves focus, and its ring, on the invoking element (new unit case; it fails on the previous head):
  vitest ContextMenu, DropdownMenu, hooks, docProp*   Tests 854 passed
  playwright ContextMenu/__tests__ --project=chromium   24 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Rebased onto main 22c592a7a5 as one commit. Main's record now has FR9 (overlay dismissal), so this rule is proposed FR10:
  vitest ContextMenu, DropdownMenu, hooks, docProp*   Tests 853 passed
  playwright ContextMenu/__tests__ --project=chromium   24 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓
  red on main: `ContextMenuOpenFocus.a11y.chromium.spec.ts` against a `main` build: 2 failed (right-click, long press), 1 passed (the Shift+F10 guard)

pnpm exec vitest run packages/core/src/ContextMenu/ContextMenu.test.tsx -t "ContextMenu open focus"
  main's ContextMenu.tsx: 3 failed, 2 passed (the keyboard cases) · with the change: 5 passed

pnpm exec vitest run packages/core/src/ContextMenu packages/core/src/DropdownMenu packages/core/src/hooks/useListFocus.test.tsx \
  packages/core/src/hooks/useLongPress.test.tsx packages/core/src/utils/interactionModality.test.ts \
  packages/core/src/docPropLiterals.test.ts packages/core/src/docPropReferences.test.ts --maxWorkers=8
  Test Files  10 passed (10)   Tests  343 passed (343)

pnpm storybook:build   (this branch; main for the red arm)
ASTRYX_STORYBOOK_DIR=<main build> pnpm exec playwright test \
  packages/core/src/ContextMenu/__tests__/ContextMenuOpenFocus.a11y.chromium.spec.ts --project=chromium
  2 failed, 1 passed   (right-click and long press: Received "menuitem"; Shift+F10 passes on main)
pnpm exec playwright test packages/core/src/ContextMenu/__tests__/ContextMenuOpenFocus.a11y.chromium.spec.ts --project=chromium
  3 passed · --repeat-each=3: 9 passed

pnpm -F @astryxdesign/core typecheck, pnpm lint:strict, check:knowledge   ✓

@vercel

vercel Bot commented Oct 10, 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 Oct 21, 2026 6: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 10, 2026
@astracat-bot

astracat-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 21, 2026, 6:51 PM UTC

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

ContextMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 657 -
Complexity N/A Very High (83) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

46 of 486 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
RichTextEditor Markdown parity: side by side probe dark 94,866
RichTextEditor Markdown parity: side by side probe light 89,762
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566

and 26 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@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 — the implementation itself looks sound: a pointer open now focuses the menu with no row pre-highlighted, the keyboard path still lands on the first row, and the tests covering right-click, long press, and keyboard invocation pass.

Approval is waiting on one maintainer decision rather than on code changes. This pointer/keyboard open-focus split is recorded as proposed FR9, and the ContextMenu spec on main is still a draft with no rule for where focus goes when the menu opens; the established press-model rules cover the highlight following a held pointer, but they do not by themselves settle initial focus on open. A maintainer needs to either approve FR9 as the ContextMenu rule or point to the existing governing spec claim for pointer-open focus. Once that is settled in favour of this behavior, I found nothing else blocking: no public API change, no concrete defects, and the bottom-sheet path is unchanged.

[Automated review]

@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 — approval is waiting on a maintainer decision plus one fix. The ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer needs to approve the proposed FR9 (pointer opens focus the menu, keyboard opens focus the first row) or point to the existing spec that governs it. Separately, the keyboard path regresses in Chromium: Shift+F10 and the Menu key are reported at the focused element's position rather than (0, 0), so they take the pointer path, and keyboard users land on the menu with no highlighted row and no visible focus indicator where main lands them on the first row. Please detect keyboard invocation from a signal real browsers provide (for example the shared last-input modality), add a Shift+F10 case to the new Chromium spec, and update the changeset and FR9 wording to match.

[Automated review]

localX,
localY,
e.currentTarget as HTMLElement,
isKeyboardInvoked ? 'keyboard' : 'pointer',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In real Chromium a keyboard-invoked contextmenu (Shift+F10 or the Menu key while focus is on an element inside the trigger) is reported at that element's position with detail: 0 and button: -1, not at (0, 0). In the inline-trigger story it arrived at (615, 388). So isKeyboardInvoked is false here, the open is classified as 'pointer', and focus lands on the menu itself with no row highlighted and its ring suppressed by outline: 'none'. On main the same keypress lands on the first row.

Consider classifying keyboard invocation from a signal real browsers provide, for example the shared last-input modality in utils/interactionModality.ts. ContextMenu would need to mount useInteractionModalityTracking(); a Shift+F10 or Menu-key keydown sets keyboard. Keep (0, 0) with no detail as an extra keyboard signal. Also add a Shift+F10 case to ContextMenuOpenFocus.a11y.chromium.spec.ts: the jsdom keyboard case only covers the synthetic (0, 0) event.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c2a1151. ContextMenu now mounts useInteractionModalityTracking() and treats the open as a keyboard open when getInteractionModality() is keyboard, keeping (0, 0) with no detail as a second signal. A right-click's own pointerdown sets pointer. Two new cases: a jsdom Shift+F10 reported at the focused element, and a real-Chromium Shift+F10 on the inline-trigger story. The Chromium one failed against the previous head's build (the menu opened with no row focused) and passes now.

'@astryxdesign/core': patch
---

[fix] A ContextMenu opened by a right-click or a long press no longer highlights its first row: focus goes to the menu itself, so the highlight starts where the pointer goes, as a DropdownMenu opened from its trigger already does. A keyboard-invoked open (Shift+F10, the Menu key) still lands on the first row.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head the last sentence does not hold in Chromium. Shift+F10 and the Menu key are reported at the focused element's position, so they take the pointer path and land on the menu itself, not on the first row. Worth re-checking once the keyboard detection is fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in c2a1151. The changeset and FR9 now say the keyboard open is detected from the last input being a key. With the fix the sentence holds in Chromium, checked by the new Shift+F10 case.

@vjeux

vjeux commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Decision (ENGOWNER), settling the open question: FR9 is admitted for ContextMenu. A right-click or a long press opens with focus on the menu itself and no row highlighted, as a pointer-opened DropdownMenu does. A keyboard-invoked open (Shift+F10 or the Menu key, detected from the last input being a key, or an event at (0, 0) with no detail) focuses the first enabled row. FR9 in this PR records it.

@vjeux

vjeux commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the one red case on the previous head, SideNav focus management > returns focus to the trigger when the collapsed submenu closes, is a load-sensitive flake on main, not this change. SideNav doesn't import ContextMenu, and Vitest isolates each test file. Alone, the SideNav file passes 3 of 3 on both main and this branch. With 12 runs of it sharing 2 CPUs, main (4aec44c) fails that case 5 of 12 times and this branch 8 of 12. I can't re-run jobs here, so I pushed the same tree again as e863173 to re-run CI.

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, ArrowDown then reaches the first row, and Shift+F10 and the Menu key still land on the first row exactly as before. Approval is waiting on a maintainer decision: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, and the decision recorded on this PR comes from its author, so a maintainer needs to approve FR9 (pointer opens focus the menu, keyboard opens focus the first enabled row) or point to the existing spec that governs open focus. One fix is needed either way: when no row is enabled, a keyboard open now moves focus to the menu container, whose static outline: 'none' hides the focus indicator the interaction-modality rules require after keyboard input, so please limit that suppression to pointer opens or keep focus on the invoking element in that case.

[Automated review]

Comment thread packages/core/src/ContextMenu/ContextMenu.tsx
@vjeux
vjeux force-pushed the context-menu-pointer-open branch from e863173 to 2d9a920 Compare October 11, 2026 21:36
@vjeux

vjeux commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

metadata-only rewrite: commit author identity; tree unchanged

The new head 2d9a920 has the same tree as the previous head e863173 (f1e0323274).

@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 is waiting on a maintainer decision: the ContextMenu record on main is still a draft with no rule for where focus lands when the menu opens, and the decision comment on this PR comes from its author, so a maintainer needs to approve the proposed FR9 (a right-click or long press focuses the menu with no row highlighted; a keyboard open focuses the first enabled row, or the menu if none can) or point to the existing spec that governs open focus. In real Chromium the change does what it says: no row is lit after a right-click, and Shift+F10 still lands on the first row exactly as on main. One fix is needed either way: when no row is enabled, a keyboard open now moves focus to the menu container, whose static outline: 'none' hides the focus indicator the interaction-modality rules require after keyboard input, so please limit that suppression to pointer opens or keep focus on the invoking element in that case.

[Automated review]

@vjeux
vjeux force-pushed the context-menu-pointer-open branch from 2d9a920 to 2fdb50f Compare October 13, 2026 02:27
@vjeux

vjeux commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main 6227933 (after #7369). One adjacent conflict in openAtLocalPoint: kept #7369's clamped anchor body (keepAnchorInside) and this PR's modality argument. Everything else applied cleanly. ContextMenu, DropdownMenu, SideNav, Layer and utils suites pass (1206 tests), as do both ContextMenu browser specs in Chromium.

@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 — this does what it says: a right-click or long press now opens the menu with no row highlighted, and Shift+F10 and the Menu key still land on the first enabled row. Approval is waiting on a maintainer decision, because the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens and the decision recorded here is the author's own: another maintainer needs to approve the proposed open-focus rule (a pointer open focuses the menu with no row highlighted; a keyboard open focuses the first enabled row, or the menu if none can) or point to the current spec that governs it. Two changes are needed either way:

  • When no row is enabled, a keyboard open moves focus to the menu container, whose static outline: 'none' shows no focus indicator after keyboard input, so please limit that suppression to pointer opens (or paint the shared indicator there) or keep focus on the invoking element, and add a keyboard test with every row disabled.
  • main now uses FR9 in the ContextMenu spec for overlay dismissal, and this branch conflicts with main there and in ContextMenu.test.tsx, so please rebase and give the open-focus rule the next free id (FR10).

[Automated review]

@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 — in real Chromium this does what it says: a right-click or long press now opens the menu with no row highlighted, and Shift+F10 and the Menu key still land on the first enabled row. Approval is waiting on a maintainer decision: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, and the decision recorded on this PR is the author's own, so another maintainer needs to approve the proposed FR10 (a pointer open focuses the menu with no row highlighted; a keyboard open focuses the first enabled row, or the menu if none can) or point to the current spec that governs it. One fix is needed either way: when every row is disabled, a keyboard open moves focus to the menu container, whose static outline: 'none' shows no focus indicator after keyboard input while main keeps the ring on the invoking element, so please paint the shared indicator there or keep focus on the invoking element, and add a keyboard test with every row disabled.

[Automated review]

Comment on lines +570 to +573
if (modality === 'keyboard' && focusFirst()) {
return;
}
listRef.current?.focus();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With every row disabled, a keyboard open (Shift+F10 or the Menu key) reaches this fallback and focuses the menu container, whose static outline: 'none' (line 154) paints no indicator, while main leaves focus, and its ring, on the invoking element. Painting the shared focus indicator when the container takes focus after keyboard input, or keeping focus on the invoking element when no row can take it, would fix it; a keyboard case with every row disabled would guard it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0900c15: with no row that can take focus, a keyboard open now leaves focus, and its ring, on the invoking element, as main does. Pointer opens still focus the menu. FR10 says so, and the new keyboard case "a keyboard open with every row disabled leaves focus, and its ring, on the invoking element" fails on the previous head.

@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 — in real Chromium this does what it says: a right-click or long press now opens the menu with no row highlighted, Shift+F10 and the Menu key still land on the first enabled row, and with every row disabled a keyboard open now keeps focus and its ring on the invoking element, as main does. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, and the decision recorded on this PR is the author's own, so a maintainer other than the author needs to approve the proposed FR10 as written or point to the current spec that governs open focus. If FR10 is approved, FR5's sentence that right-click and long-press invocation "keep their existing behavior" should defer to it; nothing else blocks.

[Automated review]

| FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Current inventory; verified |
| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Current delegation; verified |
| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Current branches; verified |
| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: 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 stray click never acts, and `touch-action` follows overflow. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If FR10 is approved, this row's last sentence ("Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior") no longer holds for right-click and long press; worth having it defer to FR10 so the record does not say both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in d64728f: FR5's last sentence now reads "Invocation and keyboard navigation keep their behavior; focus on open follows proposed FR10." That way it defers to FR10 if FR10 is approved, and says nothing new if it isn't.

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, Shift+F10 and the Menu key still land on the first enabled row, and FR5 now defers to FR10. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, and the decision recorded on this PR is the author's own, so a maintainer other than the author needs to approve the proposed FR10 as written or point to the current spec that governs open focus. Once that is settled, FR10's "Accepted" state and FR5's "proposed FR10" should say the same thing.

[Automated review]

| FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
| 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 | A right-click or a long press MUST open the Pointer menu surface with focus on the menu itself (`tabIndex={-1}`, no focus outline) and no row highlighted, so the highlight starts where the pointer goes; a keyboard-invoked contextmenu (Shift+F10 or the Menu key: the last input was a key, or the event is at (0, 0) with no detail) MUST focus the first enabled row, and with none leaves focus on the invoking element. | DropdownMenu trigger opens; `module:DropdownMenu/useMenuPress` AR1; tests | Accepted; verified in jsdom and Chromium |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Whichever way a maintainer decides FR10, this row's "Accepted" and FR5's "focus on open follows proposed FR10" (line 90) should end up saying the same thing; today the record calls the rule both accepted and proposed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 340dbff. FR10's status reads "Proposed; pending owner review", the PR description says this PR proposes the rule, and FR5 defers to "proposed FR10". The record no longer calls the rule accepted anywhere.

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, Shift+F10 and the Menu key still land on the first enabled row, and the record now calls FR10 proposed in both places. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer other than the author needs to approve the proposed FR10 as written (a pointer open focuses the menu with no row lit; a keyboard open focuses the first enabled row, or leaves focus on the invoking element when none can take it) or point to the current spec that governs open focus. I found nothing else blocking.

[Automated review]

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, and Shift+F10 and the Menu key still land on the first enabled row exactly as before. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer other than the author needs to approve the proposed FR10 as written or point to the current spec that governs open focus. If FR10 is approved, it would help for it to say how an open that arrives with no key press after earlier pointer use, such as an assistive tool's own context-menu command, should be treated; today it takes the pointer path. I found nothing else blocking.

[Automated review]

| FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
| 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 | A right-click or a long press MUST open the Pointer menu surface with focus on the menu itself (`tabIndex={-1}`, no focus outline) and no row highlighted, so the highlight starts where the pointer goes; a keyboard-invoked contextmenu (Shift+F10 or the Menu key: the last input was a key, or the event is at (0, 0) with no detail) MUST focus the first enabled row, and with none leaves focus on the invoking element. | DropdownMenu trigger opens; `module:DropdownMenu/useMenuPress` AR1; tests | Proposed; pending owner review |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If the owner approves FR10, it may be worth saying which way an open goes when it arrives with no key press after earlier pointer use and away from (0, 0), such as an assistive tool's own context-menu command. With "the last input was a key, or the event is at (0, 0) with no detail", that open takes the pointer path today: focus lands on the menu, not its first row.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in cc07014, rebased onto main 2c3ed99. FR10 now ends: "Any other open, as from an assistive technology, is a pointer open." That is how it behaves today. An assistive technology's context-menu command after pointer use arrives with no key press and away from (0, 0), so focus lands on the menu, and its arrow keys reach the rows, as after a right-click. The page cannot tell that open from a pointer's. The owner decision on FR10 stays open.

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, ArrowDown then reaches the first row, and Shift+F10 and the Menu key still land on the first enabled row exactly as before. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer other than the author needs to approve the proposed FR10 as written, including its assistive-technology clause, or point to the current spec that governs it. One wording point to settle with that decision: FR10 calls an open that arrives before any input on the page a pointer open, but the code gives it the keyboard result (the first row), because the shared input modality starts as keyboard.

[Automated review]

| FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
| 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 | A right-click or a long press MUST open the Pointer menu surface with focus on the menu (`tabIndex={-1}`, no focus outline) and no row highlighted; a keyboard open (Shift+F10 or the Menu key: the last input was a key, or the event sits at (0, 0) with no detail) MUST focus the first enabled row, and with none leaves focus on the invoking element. Any other open, as from an assistive technology, is a pointer open. | DropdownMenu trigger opens; `module:DropdownMenu/useMenuPress` AR1; tests | Proposed; pending owner review |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If FR10 is approved, its keyboard test may want to match the code for one case: an open that arrives before any input on the page. The code reads the shared last-input modality, which starts as keyboard, so in Chromium a contextmenu with no prior key or click lands on the first row, while this row's "the last input was a key" plus "Any other open ... is a pointer open" would make it a pointer open. Wording the condition as "the shared input modality is keyboard (as it is before any input)" would close the gap; the changeset sentence could follow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in c3f043a. FR10's keyboard condition is now "the input modality is keyboard, as before any input, or an event at (0, 0) with no detail", matching the code. The changeset says the shared input modality is keyboard when "a key was the last input, or there has been none yet". A new case pins an open before any input, away from (0, 0), landing on the first row. The FR10 cell keeps its width.

vjeux added 6 commits October 21, 2026 10:58
openAtLocalPoint focused the first row on every open, so a right-click or
a long press opened the menu with that row lit as if chosen. A pointer
open now focuses the menu itself (tabIndex -1, no outline, as DropdownMenu
does for its trigger), so the highlight starts where the pointer goes; a
keyboard-invoked contextmenu (Shift+F10, the Menu key) still focuses the
first row. Proposed FR9 of component:ContextMenu records it.
…n the invoking element

With every row disabled, Shift+F10 or the Menu key no longer moves focus to the menu container, which draws no indicator; focus and its ring stay on the element that invoked the menu, as before this change. A right-click or a long press still focuses the menu.
… the pointer path

An assistive technology's context-menu command after pointer use arrives with no key press and away from (0, 0), and opens as a right-click does: focus on the menu, whose arrows reach the rows. FR10 now says that any open matching neither keyboard condition is a pointer open. Record wording only; the cell keeps its length so the table is not re-padded.
…odality, keyboard before any input

The code opens on the first row when the shared input modality is keyboard, which it is before any input on the page; FR10 said "the last input was a key" and called every other open a pointer open, so an open before any input was described wrongly. FR10 and the changeset now name the modality, and a case pins an open before any input landing on the first row. The FR10 cell keeps its width.

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, ArrowDown then reaches the first row, and Shift+F10 and the Menu key still land on the first enabled row exactly as before. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer needs to approve the proposed FR10 exactly as written at this head, including its reworded keyboard condition and its assistive-technology clause, or point to the current spec that governs open focus. FR10 now matches the code for an open before any input, and I found nothing else blocking.

[Automated review]

@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 — in real Chromium this does what it says: a right-click or a long press now opens the menu with no row highlighted, ArrowDown then reaches the first row, and Shift+F10 and the Menu key still land on the first enabled row exactly as before. Approval is waiting on a maintainer decision rather than on code changes: the ContextMenu spec on main is still a draft with no rule for where focus lands when the menu opens, so a maintainer needs to approve the proposed FR10 exactly as written, including its keyboard condition and its assistive-technology clause, or point to the current spec that governs open focus. I found nothing else blocking.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — b3d45800 Deployed Oct 21, 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