Skip to content

feat(ContextMenu): after a long press opens the menu at the finger, the same finger slides onto an item and its lift selects it - #8133

Draft
vjeux wants to merge 64 commits into
meta:mainfrom
vjeux:uikit/c20-contextmenu-scrub
Draft

vjeux wants to merge 64 commits into
meta:mainfrom
vjeux:uikit/c20-contextmenu-scrub

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #9814 → #8125 → #9816 → #8133
Merge group: #9814, #8125, #9816, #8133

User need

A long press opens the ContextMenu under the finger, but that finger can't pick anything: the user lifts and presses again. iOS context menus and DropdownMenu let it slide onto an item and lift.

Problem and solution fit

  • When the menu opens under a finger still down (735 ms), the touch layer hands it to DropdownMenu's press model: pointerIdOf names its pointer, and the internal continueGesture takes it as the opening press.
  • UIMenu's handoff applies (spec:AST-094 FR33): past 10 px from the first position the open menu sees, the item under the finger lights and the lift selects it; a still lift selects nothing.
  • A menu opened at the lift hands nothing over; mouse, keyboard, popover presentation and fine pointers are unchanged.

Current specification

module:DropdownMenu/useMenuPress FR11 asks for this continuation, and component:ContextMenu FR5 for the handoff. New FR13 states the slide: "After a long press opens the menu at the finger, the same finger may slide onto an item without lifting; lifting over an item selects it, and lifting over no item leaves the menu open", then FR33's handoff.

Public delta and caller burden

No API change; touch only. Own commits: 691 authored lines, mostly tests. It adds 202 B gzip to ContextMenu's import and 87 B to DropdownMenu's (Testing).

Design and alternatives

Rejected: ContextMenu tracking the finger itself, since the shared press model owns the highlight, pick and stray click. continueGesture is ported from #8507, which closes as absorbed.

Evidence

Human check: the owner's card 33c pass, on this head's product files. iPhone, iOS 26.2 Simulator at this head: hold, menu, tap outside and early lift. Android Chrome: Code Quality's Chrome-with-Gboard emulator run on this head's product files passes every event claim, the slide to an action included (spec:AST-094 DEC-27); the curves wait for a quiet host.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Writing rubric results

Over component:ContextMenu: FR13 adds the slide, then FR33's handoff; FR5 points to it in place of its gap, at the same cell width. Owner-gated.

  • R1 narration: none.
  • R2 research: none.
  • R3 evidence: FR13's basis is module:DropdownMenu/useMenuPress FR11 and spec:AST-094 FR33.
  • R4 rejected alternatives: none.
  • R5 change references: none.
  • R6 open questions: none.

Testing

On #9816 at 4efcbc4, over #8125 c4f8964 and the merge worker's #9814 head (main 3731ec3). This pull request carries only its own five commits, the same ones that passed the stack's gates on main 9e559e9 (6b4f1dc). Two lines met #9816's: the touch story's wording keeps the coarse-pointer condition, and FR5's row points to FR13 on main's text. FR12 keeps #9816's springs.

The stack, as the owner checked it on cards 33 and 33b at fec928b, differs in what #9816 and #8125 name: the growth and both returns play springs.contextMenuLift and springs.contextMenuDismiss (#9915), one outside-press watch serves every pointer (INV16), and the growth keeps its 150 ms onset under #9765's touch-down mark. This pull request's slide onto an item is unchanged.

Local checks at efe3f12. Later commits change changesets, docs, comments, the story text, test titles and FR13's scope (a long press that opens the menu at the finger), with #9816's never-read field and its anchor fix (only the trigger's own growth is cancelled). At this head, f913915: core typecheck and the ContextMenu units (171 passed). On its code before a one-helper move (8bb478c): CI=true pnpm lint (0 errors), the ContextMenu, Layer, DropdownMenu, hooks and utils units (3,010 passed), check:knowledge, check:changesets, the ContextMenu interaction specs in Chromium (22 passed, 1 skipped) and WebKit (19 passed, 4 skipped), and the ContextMenu a11y specs in Chromium (24 passed). At efe3f12:

  • CI=true pnpm lint (0 errors); core typecheck; the ContextMenu, Layer, DropdownMenu, motion, hooks, gestures and utils units (4,701 passed); check:knowledge; check:changesets; the Chromium interaction specs for ContextMenu, Layer and DropdownMenu (44 passed, 1 skipped); WebKit's ContextMenuLongPress, inputHold and DropdownMenuDragHighlight (34 passed, 8 skipped).
  • Real fingers: the finger the menu opened under slides onto Share, which lights under it, and its lift selects it. A 6 px lift selects nothing and leaves the menu open.
  • Red first: the slide-to-select unit case on the iOS and the Android stub, and seven continuation cases in useMenuPress.test.tsx, each fail before the change.
  • Desktop: no mouse, keyboard or presentation="popover" path changes.

Size (architecture:component-style-authoring INV6), gzip, import with its transitive modules:

Import main + #9814 (5aff040) #8125 #9816 #8133
ContextMenu JS 85,043 B 110,004 B 122,771 B 122,973 B
DropdownMenu JS 79,793 B 79,793 B 79,793 B 79,880 B
CSS (each) 16,667 / 16,766 B same same same

Of ContextMenu's growth, its own touch code is about 4.2 KB minified. The rest is core's shared gesture host, arena, long-press recognizer and motion engine, which a page that uses Switch, Slider, SegmentedControl or DateInput already loads. The spec owners accepted the cost (Code Quality's measurement), and splitting the gesture host is a separate core item.

@vercel

vercel Bot commented Oct 24, 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 6, 2026 11:38am UTC

Request Review

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

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Nov 6, 2026, 12:13 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision: sliding the finger that opened a ContextMenu onto an action is a clear win on touch, but a lift before the finger has moved 10 px acts on nothing even over a row, which departs from the current useMenuPress FR1 ("A row MUST act when a press is released over it"), and no spec record states that handoff yet. An engineering owner can settle it by amending the useMenuPress record with the handoff exception (or asking for FR1 behavior), approving or rejecting the new draft requirements (AST-094, the touchOpen press mode in FR9 and DEC-8), and recording the decision by commenting /approve-spec 30320a7995ce2444f8b9a8a3abfee712ce6c1655 on this pull request; the author may do so as an engineering owner. Independently of that decision, please narrow this pull request to the ContextMenu change once its dependencies land (it currently carries about 27,000 authored lines) and fix these:

  • useMenuPress.spec.md still lists a 500 ms long-press delay while useMenuPress.ts now uses 150 ms.
  • The isOpen Effect in ContextMenu.tsx sets state after the menu closes; do that work in the close handler instead.
  • MenuPressContinuation documents x and y, which continueGesture never reads.

[Automated review]

const wasOpenRef = useRef(isOpen);
useEffect(() => {
if (wasOpenRef.current && !isOpen) {
setIsTouchOpen(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This Effect clears isTouchOpen and releases the press feedback after isOpen turns false. Under architecture:react-component-runtime INV2 and INV3, do this where the menu closes (the close or toggle handler), or name the external system this Effect synchronizes.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
* UIKit's hold-open delay for a pull-down button (`menu.holdOpenMs`). A tap
* released sooner opens the menu through the trigger's click.
*/
export const MENU_PRESS_LONG_PRESS_MS: number = menu.holdOpenMs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This lowers the held-finger delay from 500 ms to 150 ms for every DropdownMenu, but useMenuPress.spec.md still lists the long-press delay as 500 ms. Update the record and FR9, or keep 500 ms.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
pointerId: number;
pointerType: MenuPressPointerType;
/** Where the pointer is now, in viewport px. */
x: number;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

continueGesture never reads x or y; the handoff counts from the first move after the open. Remove these fields or document that they are ignored.

// The finger never travelled the handoff distance: whatever row is
// under it now appeared under a press nobody moved, so nothing acts,
// the menu stays, and the trigger's click for this press is spent.
return idle({type: 'settle', stray: true, dismiss: false});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A finger lifted here before 10 px of travel acts on nothing even over a row, which departs from current module:DropdownMenu/useMenuPress FR1. This waits on an engineering owner's decision; if accepted, the module record should state the handoff.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision: letting the finger that opened a ContextMenu slide onto an action is a clear improvement on iPhone and iPad, but a lift before that finger has moved 10 px acts on nothing even over a row, which departs from the current useMenuPress FR1 ("A row MUST act when a press is released over it"), and the branch also adds draft requirements no owner has approved yet (AST-094, the rewritten useMenuPress FR9 and DEC-8, and DropdownMenu FR8). An engineering owner can settle it by amending the useMenuPress record with the handoff exception (or asking for FR1 behavior), approving or rejecting those draft requirements, and recording the decision by commenting /approve-spec 8507258b8d930890afc2e5c68bc86d6ca7bbe152 on this pull request; the author may do so as an engineering owner. Independently of that decision, please narrow this pull request to the ContextMenu change once its dependencies land (it currently carries about 28,000 authored lines) and fix these:

  • useMenuPress.spec.md still lists a 500 ms long-press delay while useMenuPress.ts now uses 150 ms.
  • The isOpen Effect in ContextMenu.tsx sets state after the menu closes; do that work in the close handler, or name the external system it synchronizes.
  • MenuPressContinuation documents x and y, which continueGesture never reads.

[Automated review]

@vjeux
vjeux force-pushed the uikit/c20-contextmenu-scrub branch from 8507258 to 53ad03a Compare October 25, 2026 01:37
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 53ad03a:

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision: letting the finger that opened a ContextMenu slide onto an action is a clear improvement on touch screens, but a lift before that finger has moved 10 px acts on nothing even over a row, which departs from the current useMenuPress FR1 ("A row MUST act when a press is released over it"), and the branch also adds draft requirements no owner has approved yet (AST-094, the rewritten useMenuPress FR9 and DEC-8, and DropdownMenu FR8). An engineering owner can settle it by amending the useMenuPress record with the handoff exception (or asking for FR1 behavior), approving or rejecting those draft requirements, and recording the decision by commenting /approve-spec 53ad03a281b02154cf8d36087e0436518cc2dfb8 on this pull request; the author may do so as an engineering owner. Independently of that decision, please narrow this pull request to the ContextMenu change once its dependencies land (it carries about 34,000 authored lines today) and fix these:

  • ContextMenu's import grows by about 28 KB gzip of touch-only code; load it lazily for coarse pointers or justify the cost, as component-style-authoring INV6 requires.
  • The new platform theme-adaptation condition is outside the closed vocabulary of AST-012 FR2; drop it or have FR2 amended first.
  • CI's pressableCoverage.test.ts fails because ContextMenu.tsx calls usePressFeedback() without a press arm the guard recognises.
  • useMenuPress.spec.md still lists a 500 ms delay and the settle rule for the opening finger; #8078's current head already fixes both.
  • The slide-to-pick changeset says Android keeps today's menu, and the touch-presentation changeset says a theme's md is followed; neither matches the code.
  • MenuPressContinuation documents x and y, which continueGesture never reads.

[Automated review]

'@astryxdesign/core': patch
---

[feat] On iPhone and iPad, a finger still down when ContextMenu's menu opens can slide onto an action and lift to pick it, as on iOS. Once the finger has moved 10 pt in the open menu, the highlight follows it, and the action under the lift runs. A lift before that picks nothing and leaves the menu open, and a slide off the rows leaves none lit, with no focus ring on the menu. Other touch screens, Android included, keep today's menu: it stays open for a tap.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This says Android keeps today's menu, but the continuation runs wherever the recognizer runs (isEnabled: !isDisabled && isTouch, a coarse primary pointer, ContextMenu.tsx:805), the slide tests run on iOS and Android stubs alike, and the docs say every touch device. Say every touch screen here.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-012 FR2 (current) closes the when vocabulary to width, height, pointer, contrast and motion: "Future environment fields require explicit additions to this closed vocabulary." Nothing in this branch amends it, so drop platform (motion roles can be set without it) or have an owner amend FR2 first.

// grows it while that machine believes the press, springs back when it
// ends with no menu, and stays grown while the menu it opened is up. Not
// shown on an inline trigger.
const pressable = usePressFeedback();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CI test-ui (2/4) fails pressableCoverage.test.ts:190 on this file: it calls usePressFeedback() but carries no ':active' or '[data-astryx-press=' arm, and the guard does not see the growth that contextMenuPressFeedback.ts reads through PRESSED_ATTRIBUTE. Teach the guard about that reader, or give the trigger an attribute arm.

import {useLongPress} from '../hooks/useLongPress';
import {useScrollLock} from '../hooks/useScrollLock';
import {useTouchPresentation} from '../utils/touchPresentation';
import {getGestureHost, playHaptic, useGestureRecognizer} from '../gestures';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These static imports put the gesture host, arena, recognizers, animator and UIKit tables in every ContextMenu import, desktop included: +28,282 bytes gzip measured base against head, against the 2 KB line in architecture:component-style-authoring INV6. Load the touch layer lazily on the first coarse-pointer press, or justify the cost.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision: letting the finger that opened a ContextMenu slide onto an action is a clear improvement on touch screens, but a lift before that finger has moved 10 px acts on nothing even over a row, which departs from the current useMenuPress FR1 ("A row MUST act when a press is released over it"), and the branch also adds draft requirements no owner has approved yet (AST-094, the rewritten useMenuPress FR9 and DEC-8, and DropdownMenu FR8). An engineering owner can settle it by amending the useMenuPress record with the handoff exception (or asking for FR1 behavior), approving or rejecting those draft requirements, and recording the decision by commenting /approve-spec 86076e91586049ffb2f1c493270f5b0998440577 on this pull request; the author may do so as an engineering owner. Independently of that decision, please narrow this pull request to the ContextMenu change once its dependencies land (it carries about 38,500 authored lines today) and fix these:

  • An explicit presentation="popover" no longer opens the menu from a held finger, so on iPhone and iPad that ContextMenu cannot be opened by touch at all; keep the previous long press there, as AST-017 FR3 requires of the restoring value.
  • ContextMenu's import grows by about 31 KB gzip of touch-only code, including spring rows it never reads; load it lazily for coarse pointers or justify the cost, as component-style-authoring INV6 requires.
  • useMenuPress.spec.md still lists a 500 ms delay and the settle rule for the opening finger, while the code uses 150 ms and the 10 px handoff.
  • The slide-to-pick changeset says Android keeps today's menu, and the touch-presentation changeset says a theme's md is followed; neither matches the code.
  • MenuPressContinuation documents x and y, which continueGesture never reads.
  • The new @astryxdesign/core/motion/reducedMotion.stylex subpath is stable and cites an AST-094 open question that does not exist; drop it or keep it experimental.

[Automated review]

break;
}
},
{isEnabled: !isDisabled && touchModes},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With presentation="popover" this turns the recognizer off, and nothing else listens for a held finger, so on iOS Safari (no contextmenu for a long press) that ContextMenu can no longer be opened by touch. On the base every ContextMenu opened from a 500 ms hold through useLongPress. spec:AST-017 FR3 requires the restoring presentation value to keep the previous input mode; keep the base long press when presentation is 'popover'.

'@astryxdesign/core': patch
---

[feat] Under a finger, a ContextMenu whose `presentation` is omitted now opens from a held finger on iOS's timeline (the touch model). Pass `presentation="popover"` to keep today's menu on every device: the menu at the pointer, with a held finger left to the browser.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Today's menu" opened from a held finger on every touch device, iOS included. With this change presentation="popover" leaves the finger to the browser, which on iOS opens nothing. Either restore the long press for 'popover' or describe the regression here.

contextMenu,
registerContextMenuTable,
registerSpringsTable,
springs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Importing the whole springs table (and registering it at line 112) ships every spring row with ContextMenu, including springs.sheetPresent and springs.sheetDismiss, which no ContextMenu path reads. architecture:component-style-authoring INV6: importing a component does not pull in rows only other components read.

Comment thread scripts/sync-exports.js Outdated
@@ -177,6 +190,13 @@ const STATIC_EXPORTS = {
types: './dist/utils/interactionOverlay.stylex.d.ts',
default: './dist/utils/interactionOverlay.stylex.js',
},
// Proposed; pending owner review (spec:AST-094 OQ4): the Reduce Motion

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-094 at this head has only OQ1 and OQ2, so this export cites nothing. As an ordinary subpath it becomes a stable import once released (spec:AST-017 FR21 puts experimental surfaces behind an experimental boundary). Drop it here or keep it experimental until an owner decides.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
@vjeux
vjeux force-pushed the uikit/c20-contextmenu-scrub branch from 86076e9 to a885d27 Compare October 25, 2026 09:07
…ords: this stack's FR11/FR12 renumbered to FR12/FR13 after meta#8870's FR11)
…ords: this stack's FR11/FR12 renumbered to FR12/FR13 after meta#8870's FR11)
…ords: this stack's FR11/FR12 renumbered to FR12/FR13 after meta#8870's FR11)
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec b9c1d9b

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

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested for this stack; a pull request in it lands only when it and every pull request below it are approved.

#9814: approve, pending CI. No blocking findings; CI has not run on this head yet.

#8125: changes requested.

  • Attach this head's Chrome-with-Gboard Android-emulator result (animations at normal scale and removed, plus a slow-device profile), which spec:AST-094 Platform support and DEC-27 require for the touch model; the description reports an event pass with no result and says the curve runs still wait.
  • Regression: main now carries Indicator/boxHold.test.tsx "leaves a CheckboxList row in a ContextMenu unchanged after a long press that opened the menu", which holds a finger 600 ms and expects the menu open, so it fails once this branch takes main because the touch model opens at 735 ms; move that case to the touch model's timeline or pin it to presentation="popover", since the behavior it guards still holds.

#9816: changes requested.

  • Miss: contextMenuPressFeedback.ts imports the whole springs table and calls registerSpringsTable(), which puts every spring role row, including rows only other components read, in every ContextMenu import, against architecture:component-style-authoring INV6; import CONTEXT_MENU_LIFT_SPRING and CONTEXT_MENU_DISMISS_SPRING and drop the registration, since each role row registers itself as it loads.
  • Attach this head's Android-emulator result for the growth, which DEC-27 requires.

#8133: changes requested.

  • component:ContextMenu FR5 ends its slide sentence with "(FR12)", but FR12 is the hold growth and the slide is FR13, so point it to FR13.
  • Attach this head's Android-emulator result for the slide, which DEC-27 requires; the description names a run but gives no result.

Whole stack: changes requested on the findings above; otherwise the pull requests fit together and merge with main without conflicts. The record changes carry an engineering owner's approval for this head; the FR5 fix makes a new head, so an engineering owner (the author included) comments /approve-spec <full head SHA> again.

[Automated review]

import {observeMutations} from '../utils/sharedMutationObserver';
import {createMotionEngine, type Animator} from '../motion/animator';
import {contextMenu} from '../motion/uikit/contextMenu';
import {registerSpringsTable, springs} from '../motion/uikit/springs';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Miss: this imports the whole springs table, and line 123 registers it, so every ContextMenu import now carries every spring row (sheet, alert, banner, slider, navigation) that only other components read. architecture:component-style-authoring INV6: a shared spring table "ships to a component only the rows that component uses". Import CONTEXT_MENU_LIFT_SPRING and CONTEXT_MENU_DISMISS_SPRING here and drop the registerSpringsTable() call: each role row registers itself as it loads.

| FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source |
| 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 |
| 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 |
| 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. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The held finger may then slide onto an item and lift to select it (FR12). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The long-press presentation is a gesture component, so the trigger's lift plays `springs.contextMenuLift`, the menu's grow `springs.contextMenuMorph`, and the trigger's return, part of the menu's exit, `springs.contextMenuDismiss` (`spec:AST-094` FR4, FR7, FR45). Implementation gap: it keeps the layer's own entrance and exit, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After the merge of main renumbered the rows, FR12 is the hold growth and the slide is FR13, so this sentence now points to the wrong row. Suggested: "The held finger may then slide onto an item and lift to select it (FR13)."

* @input A DOM node
* @output Exports parentAcrossShadow, containsAlongFlatTree, which walks it,
* and deepActiveElement
* deepActiveElement, shadowHostOf, a shadow root's host, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: "a shadow root's host" restates the export's name (architecture:knowledge-contracts INV29). Suggested: list shadowHostOf and shadowRootsOutward alone.

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 6, 2026
vjeux and others added 5 commits November 6, 2026 00:38
The headers of `ContextMenuLongPress.interaction.browser.spec.ts`,
`contextMenuPress.test.ts`, `outsidePress.test.ts` and `outsidePress.ts`
ran past INV29's sentence length, and one line past 100 columns. Each
now says one thing per sentence. Comments only.
The headers of `outsidePress.ts` (`@output`) and `flatTree.ts`
(`@position`) ran past INV29's sentence length. Each now says one
thing per sentence. Comments only.
…e-header sentences: the slide onto an item kept)
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 1a63672

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested for this stack; a pull request in it lands only when it and every pull request below it are approved. The record changes carry an engineering owner's approval at each head, so what remains are the fixes below.

#9814 — changes requested

  • Regression: packages/core/src/Layer/outsidePress.ts no longer merges with main, which now imports isNode, isElement and isShadowRoot from utils/nodeType.ts, so take that import, drop the local copies (lines 282-292) and have shadowHostOf in utils/flatTree.ts use isShadowRoot from the same module.

#8125 — changes requested

  • Regression: merged with main, Indicator/boxHold.test.tsx "leaves a CheckboxList row in a ContextMenu unchanged after a long press that opened the menu" fails because it holds the finger 600 ms under a coarse pointer and the touch model opens at 735 ms, so hold past 735 ms there or pin that case to presentation="popover".
  • The description reports no Android-emulator result under spec:AST-094 DEC-27's conditions (the exact head, animations at normal scale and removed, and a slow-device profile), so report those three runs.

#9816 — changes requested

  • Miss: contextMenuPressFeedback.ts imports the whole springs table and calls registerSpringsTable() (lines 33 and 123), so every ContextMenu import carries spring rows only other components read, against architecture:component-style-authoring INV6; import CONTEXT_MENU_LIFT_SPRING and CONTEXT_MENU_DISMISS_SPRING instead.
  • The description reports no Android-emulator result under DEC-27's conditions for the growth, so report those three runs.

#8133 — changes requested

  • docs/specs/components/ContextMenu/ContextMenu.spec.md FR5 (line 109) points the slide to FR12, which is the hold growth, where it should say FR13.
  • The description reports no Android-emulator result under DEC-27's conditions for the slide, so report those three runs.

Whole stack: changes requested. The slide itself follows spec:AST-094 FR33 and useMenuPress FR11, and mouse, keyboard and presentation="popover" keep their released behaviour.

[Automated review]

Comment on lines +19 to +21
import {UIKIT_PAN} from '../motion/uikit/gestures';
import {shadowHostOf, shadowRootsOutward} from '../utils/flatTree';
import {subscribeInput} from '../utils/inputStream';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Regression: main (#10224) now imports isElement, isNode and isShadowRoot from ../utils/nodeType here and deletes this file's local copies, so these lines and the local isNode/isElement further down conflict with main. When this branch takes main, keep main's import (isElement, isNode) beside these three and drop the local helpers.

Comment thread packages/core/src/utils/flatTree.ts Outdated
* any other node.
*/
export function shadowHostOf(node: Node): Element | null {
return node.nodeType === node.DOCUMENT_FRAGMENT_NODE && 'host' in node

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Regression: main now has isShadowRoot in utils/nodeType.ts, the same cross-window check. Once this branch takes main, use it here so there is one shadow-root check: return isShadowRoot(node) ? node.host : null;.

vjeux added 8 commits November 6, 2026 02:54
`utils/nodeType.ts` now holds the cross-window shadow-root check, so
`shadowHostOf` uses it and the flat tree has one shadow-root test.
Under a coarse pointer the menu now opens on the touch model's timeline
by default. This case covers the box-side rule for the popover menu's
long press, so it asks for `presentation="popover"` and keeps its
600 ms hold.
`contextMenuPressFeedback.ts` imported the whole `springs` table and
registered it, which put every spring role row in every ContextMenu
import. It now imports `CONTEXT_MENU_LIFT_SPRING` and
`CONTEXT_MENU_DISMISS_SPRING`, which register their own roles when the
module loads.
After the renumber, the slide onto an item is FR13 and FR12 is the
hold growth. FR5's slide sentence still named FR12.
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ebe2c47

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

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested for this stack; a pull request in it lands only when it and every pull request below it are approved.

#9814 — approved pending CI

No blocking findings; approved pending CI, which has not run on this head yet.

#8125 — changes requested

  • Report the Android-emulator result for the touch model's open under spec:AST-094 DEC-27's conditions: this head, animations at normal scale and with animations removed, plus a slow-device profile.

#9816 — changes requested

  • Report the Android-emulator result for the trigger's growth and both of its returns under DEC-27's conditions.

#8133 — changes requested

  • Miss: the slide changeset and the title promise the slide "after a long press opens the menu at the finger", but the docs and tests call the presentation="popover" and fine-pointer 500 ms path "the long press" and that path hands nothing over, so scope both to "where the primary pointer is coarse and presentation is unset, after the held finger opens the menu at the finger".
  • Report the Android-emulator result for the slide onto an item under DEC-27's conditions.

Whole stack: changes requested on the findings above; it merges cleanly with main, the slide follows spec:AST-094 FR33 and the useMenuPress FR11 handoff, and mouse, keyboard and presentation="popover" keep their released behaviour.

[Automated review]

| 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` FR23 states for its menu. | WAI-ARIA Menu Button pattern; current tests |
| FR12 | Under a finger, a block trigger whose menu opens at the finger grows during a hold toward 1.0735 times its size on `springs.contextMenuLift`, from the onset, 150 ms after the finger lands, and it returns after a lift before the commit on `springs.contextMenuDismiss`, as FR5 names them. The hold commits at 400 ms and the menu displays at 735 ms, as UIKit's do. They are measured input, not motion, and stay internal (`spec:AST-094` FR4, FR45). | Measured on iOS 26.2 (`motion/uikit/contextMenu.ts`): UIKit commits at 400 ms, shows the menu at 735 ms, and grows the view on a linear ramp chased by a spring. `spec:AST-094` FR4, FR45: the two moments are measured input, and the long-press presentation is a gesture component (FR5), so the growth and its return play their spring roles |
| FR13 | After a long press opens the menu at the finger, the same finger may slide onto an item without lifting; lifting over an item selects it, and lifting over no item leaves the menu open. A lift before the finger has moved UIMenu's handoff distance selects nothing (`spec:AST-094` FR33). | `module:DropdownMenu/useMenuPress` FR11; `spec:AST-094` FR33 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the docs call the presentation="popover" 500 ms path "the long press" (ContextMenu.doc.mjs:135), and that path hands nothing over. Suggested: "After the touch model's hold opens the menu at the finger, the same finger may slide onto an item without lifting; …" The slide changeset says the same and could follow.

This branch was successfully deployed

1 active deployment
Preview — ebe2c479 Deployed Nov 6, 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) ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) 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.

2 participants