Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This pull request is on hold for an engineering owner's spec decision. ContextMenu's new touch timeline (growth from 150 ms, commit at 400 ms, menu at 735 ms), the trigger growing while its menu is up, the touch scroll lock and the no-haptic choice for iOS Safari rest only on the draft ContextMenu record and the unapproved spec:AST-094. An engineering owner can record the decision by commenting /approve-spec <full head SHA> on this pull request (the author can do so as an engineering owner), or say which decision should change. Whatever that decision is, these changes are still needed:
- Land the stacked predecessors first, or retarget onto the last of them, so that this diff is the 1,840-line ContextMenu commit rather than 24,862 authored lines.
- Regenerate the
search closeCLI golden, which the new ContextMenu guidance pushes from 44 to 45 matches, so that the requiredtestcheck passes. - Re-attach the press recognizer when the trigger node changes, because a
triggerAsswitch currently leaves it on the detached element (useGestureRecognizer.ts:71-79,ContextMenu.tsx:882). - Move the
setIsTouchOpen(false)andrelease()calls from the Effect atContextMenu.tsx:744-753into the close handlers, and restore the two consumer-handler regression cases from #7167. - Add the
## Writing rubric resultsfor the new AST-094 record to the description.
[Automated review]
7ef4051 to
bd95cd6
Compare
There was a problem hiding this comment.
Approval is on hold until an engineering owner decides the touch direction. The ContextMenu touch timeline, the growing trigger, the touch scroll lock and the no-haptic choice for iOS Safari rest only on the draft ContextMenu record and the unapproved spec:AST-094, so an owner needs to approve or reject them and the AST-094 requirements behind them. Any engineering owner can record that decision by commenting /approve-spec <full head SHA> on this pull request, and the author can do so as an engineering owner. Whatever the decision, these fixes are still needed:
- Land or retarget the predecessor pull requests so this one carries only the ContextMenu change (24,866 authored lines today) and lint can finish.
- Re-attach the press recognizer when
triggerAschanges, and clear the touch-opened state in the close handlers instead of an Effect. - Restore the two #7167 touch-handler regression cases on the new timeline, and keep a caller's
touch-actionfromtriggerXstyle. - Remove the gestures README section for the missing
touchPresentationmodule, and fix AST-094's citation of the nonexistent AST-090.
[Automated review]
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
bd95cd6 to
d01580f
Compare
There was a problem hiding this comment.
Approval is on hold until an engineering owner decides the ContextMenu touch direction, which rests only on the draft ContextMenu record and the unapproved spec:AST-094: the iOS timeline, the growing trigger, the touch scroll lock and no haptic in iOS Safari, the changes to the 500 ms press on other touch screens, and the AST-094 requirements behind them. Any engineering owner can record that decision by commenting /approve-spec <full head SHA> on this pull request, and the author can do so as an engineering owner. Whatever the decision, these fixes are still needed:
- Land or retarget the predecessor pull requests so this one carries only the ContextMenu change (26,052 authored lines today).
- Keep the released
(max-width: 768px) and (pointer: coarse)query forpresentation="adaptive", which the Selector record's DEC-1 says it keeps, or move the new compact query to its own pull request with a DEC-1 amendment. - Re-attach the press recognizer when
triggerAschanges, and clear the touch-opened state in the close handlers instead of an Effect. - Let a caller's
triggerXstyletouch-action win, and restore the two #7167 regression cases on the new timeline. - Fix AST-094's citation of the nonexistent AST-090, and add the
## Writing rubric resultssection the new record needs.
[Automated review]
|
Posted by Cindy's agent on her behalf. Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:
This is about where the behavior lives, not about the PR's intent. |
|
Agreed: one implementation.
|
d01580f to
7825a41
Compare
|
Converged in 7825a41:
|
|
Addressed in 7825a41:
|
|
I'll run a full review to re-check this with your note. |
There was a problem hiding this comment.
Approval waits on one owner's review.
ContextMenu's held-finger timeline matches the touch-presentations spec, the ContextMenu record edits and the second-document mouse dismissal carry an engineering owner's approval for this head, every CI check passes, and the description now reports the Android emulator run at this head in all three profiles.
- @humbertovirtudes's changes-requested review of 2026-11-08 is still open on GitHub, and only that owner can clear it: each of its three blocking items looks addressed at this head, so it needs a re-review or a dismissal from them.
Non-blocking, from earlier threads: the header of motion/uikit/contextMenu.ts still describes a press feedback and scales that ship in #9816, and .changeset/outside-press-pan-watch.md does not mention pressTypes or that ContextMenu's mouse dismissal now follows the menu's own document. This pull request is the bottom of its stack, so it can land first once approved.
[Automated review]
There was a problem hiding this comment.
Changes requested: one item.
ContextMenu's held-finger timeline matches the touch-presentation spec, the second-document mouse dismissal restores the overlay-dismissal rule, and the ContextMenu record edits carry the spec-owner approval at this head. What blocks is evidence for this head:
- The Android emulator takes (Chrome with Gboard at normal speed, at 4× CPU and with animations removed) are from the previous head, and this merge of
mainchanged six of this pull request's files (ContextMenu.tsx,Layer/outsidePress.tsandLayer/useLayer.tsxamong them), sospec:AST-094DEC-31 does not carry them and DEC-27 needs all three retaken at this head.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it. CI has not run on this draft head. A few non-blocking wording notes (the new outside-press changeset, two stale comments) are in the review.
[Automated review]
A long press that fails before its commit at 400 ms ends as a plain tap: the touch model suppresses the click only once a gesture takes the touch. The browser case holds a finger 250 ms, past core's press onset, checks that the lift reached the page before 400 ms, and reads one click and no menu. The outside-press changeset files the change as a fix: a menu in an iframe or a Picture-in-Picture window no longer closes on a press in the page.
|
/approve-spec a5a0e12 |
There was a problem hiding this comment.
Changes requested: one item. ContextMenu's held-finger timeline, the second-document mouse dismissal and the ContextMenu record edits are settled at this head, and an engineering owner approved the record; what blocks is head-bound evidence:
- The Android emulator takes (Chrome with Gboard at normal speed, at 4× CPU and with animations removed) are from the previous head, and
spec:AST-094DEC-31 carries earlier takes only to a head that only mergesmain, so DEC-27 needs all three taken again at this head.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking wording notes are in the review.
[Automated review]
A finger that rests past the commit and then drifts past 11.5 px before the menu's moment should get no menu and no click. WebKit cancels it. Chromium opens the menu at about 736 ms, so the case is a fixme there until a follow-up fixes it.
There was a problem hiding this comment.
Changes requested: two items. ContextMenu's held-finger timeline, the second-document mouse dismissal and the ContextMenu record edits are settled at this head; what blocks:
- Miss: in Chromium a finger that drifts more than 11.5 px after the 400 ms commit still gets the menu at 735 ms (the new case is
fixmethere and fails with thefixmelifted), which breaks the measured context-menu tolerancespec:AST-094FR12 and FR17 require and the changeset's "drifts more than 11.5 px on either axis before the menu opens, gets no menu", so please fix it in this pull request and drop thefixme. - The Android emulator takes (Chrome with Gboard at normal speed, at 4× CPU and with animations removed) are from an earlier head, and
spec:AST-094DEC-31 carries them only to a head that only mergesmain, so DEC-27 needs all three taken again at the head that carries the fix.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking wording notes are in the review.
[Automated review]
The case for a finger that drifts past 11.5 px after the commit, before the menu's moment, now runs in Chromium too.
Under load, a held finger's moves can reach the page after the menu's moment, though they came before it, and the gesture arena's clock has then passed that moment. A finger that drifted past 11.5 px after the commit still got the menu. The touch layer now reports each pointermove of the held finger with its own time. At its moment the menu waits while such moves still come, so a drift in them ends the hold.
There was a problem hiding this comment.
Changes requested: three items. The Chromium late-drift fix works, and the timeline, the second-document dismissal and the ContextMenu record edits stay settled at this head; what blocks:
- A finger still inside the 11.5 px tolerance at 735 ms gets no menu when its next move, stamped after 735 ms, crosses the tolerance, because
touchesMovedcancels on any drift while the new wait runs (contextMenuPress.ts:141-153), which breaks the measured rule thatspec:AST-094FR12 and FR16 require andcomponent:ContextMenuFR12 states (in Chromium phone emulation such a finger got the menu at 736 ms at the previous head and gets none now), so please cancel only for a move stamped before the moment and restamp the unit case whose "late" move comes 10 ms after it. ContextMenuTouchLayer.tsx:143-181gives every trigger its ownpointerdowncapture listener and windowpointermove,pointerupandpointercancelsubscriptions for its whole life, plus a copy of the gesture host'seventTime, where the gesture host already reads each move's time before the arena clamps it (architecture:react-component-runtimeINV16), so please carry that time through the gesture host totouchesMovedand drop the touch layer's listeners.- The Android emulator takes (Chrome with Gboard at normal speed, at 4× CPU and with animations removed) are from an earlier head, and
spec:AST-094DEC-31 carries them only to a head that only mergesmain, so DEC-27 needs all three taken again at the head that carries these fixes.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking notes are in the review.
[Automated review]
A finger still inside 11.5 px at 735 ms that crosses it just after keeps its menu: the tolerance holds until the menu shows.
The gesture arena holds a late input's time at its clock, so a move that reached the page after the menu's moment looked as if it came at that moment. The arena now also keeps the time the input came with, as the touch's inputTime, and no other recognizer reads it. The press reads it: while the menu waits at its moment, a drift in a move from before the moment ends the hold, and a move from the moment on shows the menu. ContextMenu's own pointer listeners and clock copy are gone.
There was a problem hiding this comment.
Changes requested: two items. The late-move fix works for separate moves, the touch layer's own listeners are gone, and the timeline, the second-document dismissal and the ContextMenu record edits stay settled at this head; what blocks:
- A late
pointermovethat coalesces several samples is judged by its last sample's time, so a finger that crossed 11.5 px at a sample stamped before 735 ms still gets the menu when the batch ends after 735 ms (contextMenuPress.ts:143-153), while the same samples delivered as separate moves end the hold, which breaks the measured rulespec:AST-094FR12 and FR16 require; please judge each coalesced sample by its own timestamp while the menu waits (the arena needs to keep those times, asinputTimedoes for the last sample) and add a unit case with a coalesced history. - The Android emulator takes (Chrome with Gboard at normal speed, with animations removed and in a slow-device profile) are from an earlier head, and
spec:AST-094DEC-31 carries them only to a head that only mergesmain, so DEC-27 needs all three taken again at the head that carries this fix.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking notes are in the review.
[Automated review]
A late move whose coalesced samples cross 11.5 px before the menu's moment ends the hold, as the same samples as separate moves do. A finger that shifts slightly just before 735 ms, with its move on time, gets the menu at 735 ms. A late timer after a recent move still waits. The test harness's finger moves can carry coalesced samples.
The gesture arena now keeps each coalesced sample of a move at the time it came with, as the touch's inputSamples, and no other recognizer reads them. The press follows the finger through each of them: a drift past 11.5 px before the menu's moment ends the hold, and a sample from the moment on shows a waiting menu. The menu waits at its moment only when the page is behind its input: a recent move from before the moment, or the moment's timer, ran more than two frames late. The wait runs on the arena's clock, like the hold's deadlines. An on-time finger gets the menu at 735 ms.
There was a problem hiding this comment.
Changes requested: two items. The coalesced-sample fix works while the menu waits, and the held-finger timeline, the second-document dismissal and the ContextMenu record edits stay settled at this head; what blocks:
- Miss: after a main-thread block of more than 50 ms over the menu's moment, the menu shows at 735 ms before a late coalesced move is judged, so a finger that crossed 11.5 px at 650 ms still gets the menu (
contextMenuPress.ts:253-263) while the same samples delivered as separate moves end the hold, which breaksspec:AST-094FR16 and FR12; please let the samples stamped before 735 ms decide first (wait for queued input when the moment's timer runs late, or have the arena deliver a batch's earlier samples before it fires a due timer) and add a unit case with a block and a coalesced history. - The Android emulator takes (Chrome with Gboard at normal speed, with animations removed and in a slow-device profile) are from an earlier head, and
spec:AST-094DEC-31 carries them only to a head that only mergesmain, so DEC-27 needs all three taken again at the head that carries the fix above.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking notes are in the review.
[Automated review]
A finger that crosses 11.5 px at 650 ms while the page is blocked past the menu's moment, until 760, 800 or 900 ms, ends the hold when its coalesced move arrives, as separate moves do. A still finger behind a block gets its menu once no move comes. Every case fakes the page's clock from 0, and the on-time browser case checks that the moment's own timer task shows the menu.
When the menu's timer runs more than a frame late, the page was blocked, and the finger's moves from before the moment may still be queued. The menu then waits up to 50 ms, counted from when the page runs again, so those moves are judged at their own times first. On an idle page the timer runs on time, and nothing waits. The record's FR16 gap line states the shift and its 50 ms bound.
|
/approve-spec 45bfb44 |
There was a problem hiding this comment.
Changes requested: three items.
ContextMenu's held-finger timeline matches the touch-presentation spec, and the owner approval for this head settles the record edits and the new FR16 gap; what blocks:
- Miss: under a finger, a ContextMenu rendered into a same-origin iframe (or a Picture-in-Picture window) opens its menu on any tap and loses the tap's click, because the gesture host times that document's touches on the page's
performance.now()(GestureHost.ts:913) and the recognizer readsperformance.now()too (contextMenuPress.ts:241,:270,:286); in Chromium a 120 ms tap in an iframe opened the menu 141 ms after the touch, so please put each document's gesture host on that document's clock, read time only through the arena, and add a touch case to the second-document tests. - Miss: the late-timer wait in
contextMenuPress.ts(lines 236–293) is a second copy of the gesture arena's own late-timer handling (GestureArena.ts:1153-1166), so other recognizers such as List's touch reorder hold keep the old behavior; please extend the arena's late-timer yield for every recognizer and keep only the tolerance rule in the recognizer, or say in the pull request why the arena cannot serve it. - The Android Chrome-with-Gboard emulator run that
spec:AST-094DEC-27 requires at the exact head (normal and removed animations, plus a slow-device profile) is reported for an earlier head, so rerun it at the head that carries the fixes above and update the description.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking notes are in the review.
The press's units drive the page's time with the gesture arena's own injected clock, the test harness's manual clock, instead of faking performance.now(). A move can reach the page after its own time, and a blocked page runs the menu's timer late on that clock. A blocked main thread until 900 ms now shows the still finger's menu at 950 ms.
The press measured how late a move or its timer ran with performance.now(). It now reads the gesture arena's injected clock, whose new pageTime() returns the clock's own reading. In a browser that clock is performance.now(), so nothing changes there; the units drive it with the test harness's manual clock.
|
/approve-spec cae3644 |
There was a problem hiding this comment.
Changes requested: two items.
ContextMenu's held-finger timeline matches the touch-presentation spec, and the /approve-spec for this revision covers the record edits; what blocks:
- Miss: under a finger, a ContextMenu rendered into a same-origin iframe (or a Picture-in-Picture window) opens its menu on any tap, because the gesture host times every document's touches on the page's clock (
GestureHost.ts:913) while the hold counts from the touch's own timestamp (contextMenuPress.ts:148-149), so in Chromium a 120 ms tap in an iframe whose clock ran 3 s behind the page opened the menu 143 ms after the touch; please give each document's gesture host that document's clock (or rebase another document's event times) and add a touch case to the second-document tests. - The Android Chrome-with-Gboard emulator run that
spec:AST-094DEC-27 requires at the exact revision (normal and removed animations, plus a slow-device profile) is not reported for this revision, so please run it at the revision that carries the fix above and update the description.
@humbertovirtudes's changes-requested review also stays in force until they re-review or dismiss it, and a few non-blocking notes are in the review.
| // The hold's deadlines count from the touch itself. This replaces the | ||
| // foundation's own press timer, which never fires. | ||
| this.holdFrom = {x: touch.x, y: touch.y}; | ||
| this.holdStart = event.time; |
There was a problem hiding this comment.
The hold counts from event.time, which a touch in another document (a same-origin iframe or a Picture-in-Picture window) takes on that document's clock, while the arena that runs these deadlines is on the page's performance.now() (GestureHost.ts:913), and pageTime() below reads that same page clock. In Chromium as a Pixel 7, with an iframe clock 3.16 s behind the page, a 120 ms tap opened the menu 110 ms after the touch and a 1 s hold at 111 ms; main opens nothing for the tap and opens the hold at 516 ms. Giving each document's host that document's clock in getGestureHost(doc) (or keeping the released long press for a trigger in another document) and adding a touch case to the second-document tests would close it.
Stack: #8125 → #9816 → #8133 → #8201
Merge group: #8125, #9816, #8133
Behavior module: ContextMenu's touch hold.
Supersedes #8065.
User need
A held finger should get UIKit's menu at about 0.74 s and no click at the lift. Main opens it at ~500 ms, then clicks.
Problem and solution fit
presentation="popover", a finger keeps today's 500 ms long press.spec:AST-094FR4, FR45); FR12 records them.Current specification
spec:AST-094FR13, FR25, FR32, FR33 anddesign:reduced-motiongovern; createContextMenu takes the hold later (spec:AST-107FR8).Public delta and caller burden
An omitted
presentationmeans the touch model. No API change. Eager import, main to #8133, min/gzip: ContextMenu +119.2/+38.3 kB, DropdownMenu +0.6/+0.2 kB, accepted by the spec owners.Design and alternatives
The watch judges a mouse or pen at its bubbling mousedown, so main's Effect goes (
architecture:react-component-runtimeINV16). Fine pointers (spec:AST-017FR59): unchanged. In an iframe or Picture-in-Picture window, a mouse press closes the menu in its own document only (family:overlay-dismissalFR8; Testing).The late-timer wait stays in the press, not the arena. The spec owners' condition for this change is 'no behavior change for other recognizers'. An arena-wide wait would change List's touch reorder hold and the other long presses.
Evidence
Android (
spec:AST-094DEC-27): the three emulated Chrome takes at this head follow. iPhone, iOS 26.2 Simulator: hold, menu, tap outside. Real-device iOS: the owners' check, not public.Scope
Writing rubric results
Over
component:ContextMenu: the pan gap goes; FR12 records the hold's moments; PR1 names the mount, listeners and queries; a focus gap is recorded.spec:AST-094's FR16 gap line states the 50 ms shift behind a block. Owner-gated.Testing
Merged main e09956b at a9188e6 ("Merge main").
git merge-treeagainst that main left one conflict,.changeset/outside-press-pan-watch.md. Main's v0.7.12 release (#11029) removed that changeset after shipping it, and this pull request had edited it. The merge keeps the removal and adds.changeset/contextmenu-outside-press-watch.mdwith this pull request's note. Otherwise this pull request's own diff is byte-identical to 20dc83f's, in the same 19 files. Main's merge changes six of this pull request's files, so the Android takes were taken again at a9188e6. Local checks on the merged tree pass: core typecheck, strict eslint on the changed files, check:changesets and check:knowledge. The ContextMenu, Layer, DropdownMenu and box-hold units pass (1,164), and so doesContextMenuLongPressin WebKit under the CI profile (3) and in Chromium (3). Its a11y spec passes too (3).At a5a0e12, a browser case pins a finger that lifts before 400 ms as a plain tap: one click and no menu. It fails by name when the lift reaches the page at 400 ms or later. The outside-press changeset now files the change as a fix. The product code is a9188e6's.
ContextMenuLongPresspasses in Chromium (4, three runs) and in WebKit (3). The tap case skips in WebKit, which makes a click only from its own tap gesture.At cae3644, a drift past 11.5 px between the commit and the menu's moment gets no menu and no click, in every engine (
spec:AST-094FR21). Red first at 3c39a05, which only lifts the case's Chromiumfixme: Chromium showed the menu at 1592.8 ms. On real phones the earlier head already cancels this drift, since moves arrive in time. The fix keeps the cancel when moves reach the page late. The gesture arena holds a late input's time at its clock, so such a move looked as if it came at the menu's moment. The arena now also keeps each input's own time, as the touch'sinputTime, and each coalesced sample's, asinputSamples.The press follows the finger through each sample at its own time. A drift past 11.5 px in a sample from before the moment ends the hold, as separate moves do. At the moment, the menu waits only when the page is behind its input. A recent move from before the moment, or the moment's own timer, ran over two frames late. The wait runs on the arena's clock, and a sample from the moment on ends it with the menu. A menu timer that runs more than a frame late ran behind a blocked page. The menu then waits up to 50 ms from when the page runs again, so moves queued behind the block decide first. The press reads the page's time from the arena's injected clock, which is
performance.now()in a browser. So this head's timing equals 45bfb44's, and the owners' real-phone still-finger check carries.In a same-origin iframe or a Picture-in-Picture window, the gesture host times touches on the page's clock, so a short tap opens the menu. That bug is on main too, for every timed gesture. #11155 fixes it on its own, and this pull request will add its ContextMenu iframe case once main has #11155.
Red first at 3b76a8c: a finger inside 11.5 px at 735 ms that crosses it just after got no menu at a4e0404. That was 3 of 3 in Chromium, and a6253f3 showed the menu 3 of 3. Red first at 46bc0da: at 88cc0d8, a late move whose coalesced samples cross 11.5 px before the moment still showed the menu. There, a 3 px move at 700 ms that reached the page on time got the menu at 786–788 ms. That was 3 of 3 in Chromium and in WebKit. Red first at d743922: under the test harness's clock, eight press units failed at 45bfb44, which read
performance.now(). Red first at 889aded: the page is blocked past the moment, until 760, 800 or 900 ms. At cad79ec, a finger that crossed 11.5 px at 650 ms still got the menu.No other code changes:
inputTimeandinputSamplesare read only incontextMenuPress.ts, set inGestureArena.ts, and pinned by two arena unit cases. The arena'spageTime()is read only by the press.GestureTouchandGestureArenaare not exported.pnpm api:report --checkflags two reports stale from main's own changes. Regenerated, they hold noGestureTouch,inputTimeorinputSamplesline. ContextMenu adds no listener, window timer or clock of its own: the touch layer equals a6253f3's. The other recognizers' tests pass unchanged: the gesture units (880 in 38 files) and the core unit suite (21,504). The gesture browser specs pass in Chromium (76 passed, 4 skipped). In WebKit under the CI profile, 68 passed and 12 skipped. Presented hit testing passes 6 in each.Measured in Chromium's phone profile, one worker, three runs each: a still finger gets the menu at 744 ms, as before. A finger creeping 6 px inside the tolerance gets it at 744–745 ms with 20 or 30 Hz moves. Those reach the page in about 25 ms. With the test driver's 60 Hz moves, which it delivers late, it gets it at 1,399–1,415 ms. Real phones' still-finger timing is the spec owners' check.
Local checks at this head, one worker:
ContextMenuLongPress.interaction: Chromium 7 passed; WebKit under the CI profile 6 passed, 1 skipped;ContextMenuLongPress.a11y(3 passed),ContextMenuAudit.a11y(18 passed) andContextMenuCursorAnchor.a11y.browser(3 passed) in Chromium;ContextMenuCursorAnchor.a11y.browserin WebKit (3 passed);ContextMenu/andgestures/, check:changesets and check:knowledge.Main's ContextMenu opens through the same drags in Chromium. Its menu shows at 512 ms for 14–20 px drifts that cross 11.5 px before its 500 ms long press.
Replayed onto main cd748d0 after #9814's squash: 2bcc49a is one commit that carries this pull request's own diff at 6d7fceb (same patch-id; its message lists the folded commits). Five commits follow. Red at 812d3a4: the 735 ms proof wants the show inside the timer task that decides it (a WebKit CI run had shown the menu at 758 ms, past its 20 ms). Fix at 0776b40: the open commits with
flushSync, under a reasoned@eslint-react/dom-no-flush-syncdisable. Red at e8f5ca1: the menu mounts, closed, at the commit. Fix at 0fbd309: the layer hook's internalprepare()andreleasePrepared()(the publicuseLayerdoes not change), andcomponent:ContextMenuPR1 names the commit-time mount. Then 32e75c7: the second-document test. ba77ca8 merges main c8b4cba, for the harness'sFrameStarvationError. df6fd04: PR1 names the outside-press watch's pointer and mousedown listeners and the coarse-pointer query. abb70e1: the 735 ms proof logs the page's frames (below). b8a9055 merges main 62c7802, which fixes NavigationStack's busy-thread proof. e66ff1d merges main 92784f6: main's #10160 dropped FR8's disabled-row gap sentence, and the FR table keeps main's FR8 text. 24c03c1: the proof's starvation rule takes WebKit's catch-up frame (below). 339bf7e and 3d78dae, from review: PR1 names each presentation's query subscriptions, and the record's current-state impact names the touch open's focus gap. Then 20dc83f, a maintainer's merge of main b87846c: its tree is the merge of the two, and this pull request's own diff is unchanged.Android exit timing (contextmenu-default): main none/64 ms, head none/72 ms (6 interleaved rounds; main max gap 101 ms)The menu at 735 ms, page-side, with the same code one pull request up (0c316d9): the show runs inside the deciding timer task in 96 of 96 takes (0 of 96 before), 1.7 ms after the decision in Chromium, 6.5 ms at 4× CPU and 3.0 ms in the WebKit CI container (8.8, 28.5 and 17 ms before). The commit-time mount's task takes 6.8 ms at normal speed, and the growth drops no compositor frame. At 3d78dae alone, the 735 ms and commit-mount proofs pass 48 of 48 in WebKit under the CI profile and 16 of 16 in Chromium. A WebKit CI run had shown the menu at 890 ms, inside its deciding task, so that task had started late. The proof now tells why: a deciding task that starts within a frame of touch + 735 keeps the 20 ms bound. A later start fails with
FrameStarvationErrorand its numbers when a frame gap over 50 ms held the due time and the task ran within two frames of the gap's end. After a long task, WebKit draws a frame and runs the delayed timer about 20 ms later; at e66ff1d an 80 ms block failed bare there for that reason (90 ms late). Any other late start fails bare. Arms, each in both engines: an 80 ms block across the due time (94a1b24) fails named, 8 of 8; a 40 ms read inside the deciding task (25f99e6) fails bare on the bound, 4 of 4; the menu committed 50 ms after its decision (c0bdd6f) fails bare, 4 of 4, the task having started late while frames ran.Fine pointers (
spec:AST-017FR59,family:overlay-dismissalFR8), before and after: a ContextMenu portaled into a same-origin iframe. Before, with main's ContextMenu.tsx under 3d78dae's test (c973d19): a mouse press in the page closes the menu, and the case fails. After, at 3d78dae: a press outside the menu in its own document closes it, and one in the page does not. In the page, the mouse and pen cases pass as before.Size, each import's eager closure (min / gzip):
DropdownMenu's 82 B gzip here come from the layer hook's two members.
Local checks at an earlier head, 3d78dae: check:knowledge and prettier on the record. At 24c03c1, before its two record-only commits: core typecheck; strict eslint on the spec;
ContextMenuLongPressin Chromium (3 passed) and in WebKit under the CI profile (3 passed). At e66ff1d, its parent: the ContextMenu, Layer and DropdownMenu units (1,099 passed);CI=true pnpm lint(0 errors); check:knowledge. At the stack's top on the same main: core typecheck andCI=true pnpm lintat all three heads (0 errors); the ui suite (22,774 passed;DropdownMenuLazySheet's keyboard-open case failed once under load and passes alone on main and here); test:node (17,446 passed; the one failure is the WebKit leg budget, see #8133). Earlier versions of this description saidCI=true pnpm lint, but those runs werepnpm lint, which reports@eslint-react/dom-no-flush-synconly as a warning; that claim is corrected here.The Android emulator run at an earlier head, 3d78dae (Chrome with Gboard; six takes: normal, 4× CPU and animations removed, twice each): the finger-still-down menu paints 752–837 ms after landing (735 ms plus paint; the 4× takes ran under host load), opens at the finger 6 of 6, an early lift and a pan open nothing 6 of 6, and an outside press closes it 6 of 6. Earlier, at abb70e1, the same six takes passed every event claim; since then this pull request merges main twice (62c7802, 92784f6) and changes no ContextMenu source, only the 735 ms proof's starvation rule (24c03c1) and two record sentences. Earlier, at b8b8d88 (Chrome with Gboard): the finger-down menu's first frame paints 745–868 ms after the landing (735 ms plus the emulator's frames); an early lift and a pan open nothing; an outside press closes it for touch, mouse and pen.
Earlier, at 543d05f, the same own diff ran in the Chrome-with-Gboard Android emulator: six takes at normal speed, a 4× CPU slowdown and animations removed. The menu opens at the finger. An early lift and a pan open nothing. An outside press closes it for touch, mouse and pen.
Earlier, restacked on main 4598223, which has #10275's guarded presentation read: that head merged #9814's head 57f4e92, ContextMenu's
usePresentationread goes through that guard, anduse-table-sortable-state-hook-usage.test.tsxpasses 3 of 3 here. The long-press spec's note keeps to 20 words. Local checks at 260feb3: core typecheck and that test. At the stack's top on the same base: the Layer, ContextMenu, Indicator, utils, motion, gestures, DropdownMenu and hooks units (4,997 passed);ContextMenuLongPressin Chromium (22 passed, 1 skipped) and in WebKit under the CI profile (19 passed, 4 skipped); the three ContextMenu a11y specs (27 passed).Earlier, restacked for the chain landing: that head merged #9814's head on main 79cc4ac and pins
boxHold.test.tsx's CheckboxList-row case topresentation="popover", since the touch model opens at 735 ms, past that case's 600 ms hold. Local checks at that head, 543d05f: core typecheck; the Layer, ContextMenu, Indicator, utils, motion and gestures units (3,550 passed); eslint on the changed files; check:knowledge; check:changesets.On #9814, at the merge worker's head, which merges main 3731ec3. FR5 merges row-wise with #9915's: the row is main's, minus the pan gap this pull request closes.
The review at 279fe10, addressed:
TouchLongPressPopover, which now ships here.watchOutsidePressjudges a mouse or a pen at its bubblingmousedown, as main's Effect did, so ContextMenu's own Effect goes. Red first: 5 of the 29outsidePress.test.tscases fail before the change; the two new guards pass either way. The shadow-root ContextMenu case now fires a mouse press'spointerdownbefore itsmousedown, as a browser does.Main's #9988 dismissal count: the one watch counts every outside press. Main's #9008: the record's FR8 rows and compatibility row merge row by row with this pull request's.
Local checks at c4f8964: core typecheck and the Layer, ContextMenu, DropdownMenu, utils and motion units (2,865 passed). At #9816's head on the same base, 4efcbc4:
CI=true pnpm lint(0 errors), core typecheck, the ContextMenu, Layer, DropdownMenu, motion, hooks, gestures, SegmentedControl and utils units (4,809 passed), check:knowledge, check:changesets, and the Chromium interaction specs for ContextMenu and Layer (34 passed, 1 skipped). On the previous base (main 9e559e9), the stack's top with this content (a4a1a46, apart from two comments) passed the full ui (21,276) and node (16,672) suites, the Chromium interaction specs for ContextMenu, Layer, DropdownMenu, Dialog and Drawer (60 passed, 1 skipped), the same specs in WebKit (51 passed, 10 skipped), and their a11y contracts (330 passed). Size (INV6), gzip, the import with its transitive modules: ContextMenu 85,043 → 110,004 B, DropdownMenu unchanged (79,793 B), CSS unchanged. ContextMenu's own touch code is about 2.4 KB of it for the whole stack (see #8133); the rest is core's shared gesture host, arena and long-press recognizer, which a page carries once for every touch component.Earlier local checks at 1af10a0, on #9814 at ed150e2 (main f10bc91): core typecheck; the ContextMenu, Layer, motion, hooks, gestures and DropdownMenu units with the press-coverage guard (3,408 passed); strict eslint and prettier on the changed files; check:knowledge; check:changesets; the comment-length check; and
pnpm review:pre(no findings). Browser:ContextMenuLongPress.interaction.browser.spec.ts(the 735 ms menu, and the outside pan that closes it while the page scrolls) in Chromium and WebKit. Red first: the 735 ms menu, the lift after the commit, an early lift, a second finger, a drift past the tolerance on one axis, the claimed finger's click and scroll, the outside pan and tap, the popover and fine-pointer long press, a touch open's focus,triggerAsswaps, consumer touch handlers, the commit haptic, and the recognizer's states each fail before the change. The timeline's other browser proofs come with #9816, held back for size. The Android emulator run at 653a5cd (Chrome; default and Remove animations), the unsplit touch model this stack rebuilds, passes: the commit lands at 399–460 ms, and the menu opens at 734–746 ms at the finger. A pan gives no menu, and the menu stays open after the lift. The full gates run before this leaves draft. It changes gesture feel, so it also waits for a human check on its preview.Test names against main: none dropped. Main's five "a menu that was never opened" tests stay exactly as main has them. Six main tests are renamed where the touch model changed what they check: "opens on touch long-press" is "opens for a finger still down at 735 ms (iOS 26.2), not a millisecond before"; "composes consumer touch handlers with long-press detection" is "passes touch events through to the consumer's own handlers" and "still opens at 735 ms beside a consumer onTouchStart, which runs (#7167)"; "clears a pending long-press when a consumer prevents default on touch end" is "opens nothing for an early lift whose consumer onTouchEnd prevents default (#7167)"; "cancels the long-press when the finger moves past the threshold" is "cancels when the finger drifts past the tolerance on one axis (11.5 px)" and the popover "a drift past 10 px" case; the compatibility-mousedown case is "outside the touch model, a tap outside closes the menu through its compatibility mousedown, and a scroll that starts outside leaves it open"; and "presses that send no mousedown leave the menu open, as before" is "a mouse press that sends no mousedown leaves the menu open, as before".
Review notes
Follow-ups after merge, from non-blocking review notes: the touch layer's header reads as if any second finger cancels, where only one before the commit does; and the menu's focus after a touch open, now a known gap in the record, needs an indicator under forced colors.