Repository navigation
Conversation
A finger held on the trigger opens the menu on UIKit's timeline: the hold commits at 400 ms and the menu shows at 735 ms. One outside-press watch closes the menu for every pointer, and a touch closes it as it begins to pan. It carries this pull request's own diff from 6d7fceb, replayed on main after meta#9814's squash, and folds these 12 commits (the history of 6d7fceb has each one): - chore(motion): ContextMenu's touch timeline and press curve, measured on iOS 26.2 - test(ContextMenu): a held finger opens the menu on iOS 26's timeline, and a touch outside closes it as it pans - feat(ContextMenu): open from a held finger on iOS 26's timeline, with no page lock - docs(ContextMenu): FR11 cites AST-094 FR45 for the hold's measured moments - test(ContextMenu): the long-press a11y contract follows the touch model - docs(ContextMenu): the touch changeset and docs name their condition, not every touch device - test(Layer): the outside-press watch closes a layer at the press for a mouse or a pen - feat(Layer): the outside-press watch closes a layer at the press for a mouse or a pen - refactor(ContextMenu): one outside-press watch closes the menu for every pointer, and its mousedown Effect goes - docs(ContextMenu): split long file-header sentences - test(Indicator): the ContextMenu box-hold case pins the popover menu - test(ContextMenu): the long-press spec note keeps to 20 words
The hold's timer only sets ContextMenu's state, so React mounts and shows the menu in its next scheduled task. A WebKit CI run showed it at 758 ms, past the 735 ms proof's 20 ms. The proof now also wants the menu to show inside the timer task that decides it, and that task at the deadline counted from the touch. The page's stamps record the first show only.
The hold's timer opened the menu with a plain state update, which waits for React's next scheduled task: the menu showed 9 ms after the deadline in Chromium and 17 ms in the WebKit CI container. UIKit shows the menu at its moment. The open now commits with flushSync, in the timer task, as a lift's open commits in the lift's dispatch.
The menu's first show mounts its layer, a render of 6 ms in Chromium and 17 to 21 ms in the WebKit CI container inside the task that shows the menu at 735 ms. UIKit prepares a context menu as the interaction begins and only presents it at its moment. The proofs want the menu mounted, closed and out of the accessibility tree, at the commit; the same element shown at 735 ms or at the lift; and nothing left mounted by a move past the tolerance or a cancel.
A lazy context layer gains prepare(), which mounts its content without showing it, and releasePrepared(), which unmounts that content unless the layer has shown since. Both are internal to core's layer hook. At the hold's commit, ContextMenu prepares its layer, so the show at 735 ms or at the lift only reveals it. A press that ends with no menu releases it. component:ContextMenu PR1 names the commit-time mount.
On main, ContextMenu's mouse and pen dismissal listens on the page's document. A menu rendered into a same-origin iframe or a Document Picture-in-Picture window then closes on a press in the page and stays open on one in its own window. The shared outside-press watch listens in the trigger's own document, as `family:overlay-dismissal` FR8 has every member work in the document it renders in. With main's ContextMenu.tsx, this case fails.
PR1 listed one mousedown listener and the adaptive query. While the menu is open, the outside-press watch adds pointer listeners to the trigger's document and shadow roots, plus a mousedown listener where a mouse or a pen closes it at the press. With presentation omitted, ContextMenu subscribes to the coarse-pointer query while mounted.
A WebKit CI run showed the menu at 890 ms, inside the timer task that decided it, so that task itself started late. The proof now logs the page's frames. A deciding task that starts within a frame of its due time keeps the 20 ms bound. A later start that a frame gap over 50 ms covers fails with FrameStarvationError, its numbers named; any other late start fails bare. The bound does not move.
After a long task, WebKit draws a frame and runs the delayed timer about 20 ms later. The 735 ms proof counted a frame gap only when it ended within a frame of the timer's start, so an 80 ms block failed bare there. A gap over 50 ms that holds the due time now explains a start within two frames of its end, and fails by name. A task held longer while frames ran, and a slow read inside an on-time task, still fail bare. The 20 ms bound does not move.
Unless `presentation="popover"`, the menu subscribes to the coarse-pointer query while mounted, a bottom sheet included, and with `presentation="adaptive"` also to the compact query, as usePresentation reads them. PR1 named only the omitted and adaptive cases.
A touch open focuses the menu itself, and the menu draws no focus outline, forced colors included. The record's current-state impact now lists it: interaction-modality INV1 hides the indicator after pointer input, and no record names one for this menu under forced colors.
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.
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.
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.
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.
One raw page with the long-press story's trigger and menu, and an on-page log of the input events around a held finger's lift and of the popover calls, with the stack of each hide. It never merges.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
This preview is not ready to approve, and its own description says it should not merge.
Approval first waits on an engineering owner's decision on two record changes this head makes to component:ContextMenu. One is the new FR12: the hold commits at 400 ms and the menu shows at 735 ms. The other is the rewritten PR1: the outside-press watch's listeners, and the menu mounted closed at the hold's commit. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Even then, two changes are required:
- At 3,346 authored lines, this pull request bundles #8125's whole ContextMenu increment (3,045 lines) with a 301-line sandbox page, so the ContextMenu change should land through #8125 while this preview stays unmerged or carries only the page on top of #8125.
- The ContextMenu
presentationdefault change, from'popover'to the touch model, is outside this pull request'ssandboxscope, so either name it in the scope with an owner decision or leave it to #8125.
[Automated review]
| isDisabled = false, | ||
| onOpenChange, | ||
| presentation = 'popover', | ||
| presentation, |
There was a problem hiding this comment.
Tag-along (blocking): dropping the 'popover' default changes ContextMenu's public presentation default. That is outside this pull request's sandbox scope. Name it in the scope with an owner decision, or leave it to #8125.
| * finger opens on UIKit's timeline. They cover the menu's moments, the | ||
| * press feedback against UIKit's frames and a blocked main thread. They | ||
| * also cover the page under the finger, a pan outside and a storyboard of |
There was a problem hiding this comment.
Non-blocking: this header promises proofs of the press feedback against UIKit's frames and a storyboard of the press, but the file has no such test. Drop those claims or add the tests. The note at line 38 and "scaled by the press feedback" at line 278 are stale in the same way.
Preview only: #8125's head 88cc0d8 plus one sandbox page,
/sandbox/pages/context-menu-touch-log/, for a check on a real iPhone. Do not merge; it closes after the check.The page shows the ContextMenu long-press story's trigger and menu. Below them, a log lists the input events around a held finger's lift. Each pointer, touch, mouse and click event shows its time,
isTrusted,detail,cancelableanddefaultPrevented. A second line gives its outcome after the page's handlers. The log also lists eachshowPopoverandhidePopovercall, with the stack of each hide. A button clears the log.Testing
At 91c183e: eslint and the public-text check pass on the page.