Skip to content

feat(core): one shared context request from a long press, a right click and Shift+F10 - #8670

Draft
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/f2-context-request
Draft

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/f2-context-request

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

User need

A context menu should open the same way from a held finger or pen, a right click, and Shift+F10 or the context-menu key. Each component wired its own handlers, so the rules differed: a held finger on Android could open the browser's own menu beside Astryx's.

Problem and solution fit

One internal attachContextRequest(element, {onRequest}) decides for every component. It reports one request per gesture, naming its input (hold, pointer or keyboard), its pointer and its point, so a keyboard-opened menu can focus its first row. No shipped component uses it yet; the headless context menu and row menus build on it next.

Current specification

spec:AST-094 FR43: one shared context request from a long press, a context click, or Shift+F10 and the context-menu key; FR24 for listening only while enabled. No record changes.

Public delta and caller burden

None: the module is internal and not exported. A component passes its element, an onRequest callback, and optionally isEnabled and onHold.

Design and alternatives

  • The hold is UIKit's long press, 500 ms and 10 pt, and an arena participant, so it loses to a claim another recognizer made first, such as a reorder handle's.
  • A mouse held still makes no request: it selects text and drags.
  • A control that keeps its press for a hold-repeat owns only its hold; its right click and Shift+F10 still reach the request.
  • The browser's own menu is refused once: a finger's while it is down inside the element, and a pen's during and right after any hold of it there.
  • Inside nested requests, a right click or a key reaches only the innermost.
  • It listens on its own element only, and only while enabled.
  • Rejected: per-component handlers, whose rules drift, and a public request; the headless context menu is the public layer.

Evidence

  • Unit cases for each rule above: a kept press, a mouse hold, the browser menu, a pen's menu after another participant's hold, nested requests, and a finger elsewhere during a right click.
  • In Chromium and WebKit, a held finger, a right click and Shift+F10 on one row each make exactly one request, naming their input.
  • One commit on main, without the earlier stack's merged work.

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.

Leaves draft with #8693, its first user. Feel check: not applicable here; #8693 carries the human check.

Testing

Local gates at 9e2d08a, one commit on main 273031c. It replays c9f05eb onto today's main: git range-diff changes only main's context lines around one GestureHost.ts hunk. There, main's hold?.keepLiftSelection() and this PR's pen-lift note both stay.

  • tsc --project packages/core/tsconfig.json --noEmit, strict ESLint, Prettier on the changed files and pnpm review:pre (no findings): clean.
  • Core's full unit suite: 682 files, 18,523 tests passed. At c9f05eb the four pen cases each failed before their fix; the replay carries those tests unchanged.
  • The gesture browser proofs: Chromium 67 passed and 4 skipped. In WebKit at CI's profile (4 CPUs, CI=1, LP_NUM_THREADS=4, one worker), three runs: 177 passed and 36 skipped, none failed.
  • The presented hit-testing proofs and ten WebKit runs run at the head that leaves draft with feat(ContextMenu): createContextMenu and useContextMenu, a headless context menu (spec:AST-107 FR8) #8693.

@vercel

vercel Bot commented Oct 27, 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 4, 2026 6:31pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 6:36 PM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 27, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
… selection off document-wide) into the meta#8228 helper
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
… for a recognizer inside a shadow root) into the meta#8228 helper

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

The context request needs an engineering owner's decision before it can be approved: attachContextRequest rests on spec:AST-094 FR43 and DEC-21, which exist only in open #8646, and its hold attaches a recognizer with a blocking touchmove under a fine pointer, which FR19, FR24 and architecture:react-component-runtime INV13 on main still rule out. An engineering owner records that #8646's text governs here by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or the change waits for #8646 to land; that decision should also settle whether a hold-repeat control keeps the row's right click and Shift+F10, which no record states yet. Independently of it:

  • Land #8665 and its ancestors first, or retarget, so this diff is the context request's 676 lines rather than 29,372.
  • Refuse a pen's own contextmenu after its hold and keep a pen barrel press from starting a press, since a pen now gets two requests for one gesture.
  • Schedule hold.requestMs on the arena's clock and report its deadline, as FR16 requires, with a test.

[Automated review]

Comment on lines +163 to +169
if (
pointerType === 'touch' ||
host.fingersDown() > 0 ||
time - lastKeyRequestAt < KEY_ECHO_MS
) {
return;
}

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 pen's own contextmenu is not refused here: fingersDown() counts no stylus (GestureHost.ts:2576) and the type is pen, not touch. A pen held still, or a pen barrel press held past 500 ms, then gets hold and pointer for one gesture. Could the handler refuse a menu event from the pointer the hold is tracking, and for a short window after the hold's request, as it does for the key?

Comment on lines +137 to +141
if (requestMs === commitMs) {
requestHold();
} else {
requestTimer = setTimeout(requestHold, requestMs - commitMs);
}

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.

hold.requestMs waits on a wall-clock setTimeout and stamps host.arena.now() when it fires, so a busy main thread moves the reported time and the arena clock cannot drive it in tests; spec:AST-094 FR16 wants the deadline. Scheduling it on the arena's timer and reporting commit + (requestMs - commitMs) would match the default path, which reports its 500 ms deadline.

}
// A right click, or a ctrl-click on macOS (its context click), starts no
// press (FR43).
if (event.pointerType === 'mouse' && !isPrimaryClick(event)) {

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.

Only a mouse is filtered here, so a pen's barrel button (button: 2) still starts a press, while utils/primaryClick.ts says a pen's barrel button starts none. Applying isPrimaryClick to a pen (here and at line 1149) would also stop the barrel press from feeding the context request's hold.

* listener is installed. A recognizer that must not begin under a second
* finger reads it in `shouldBegin`.
*/
/**

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 method lands between fingersDown()'s JSDoc (lines 1540-1547) and fingersDown() itself, so that method loses its documentation and this one carries two blocks. Moving keepsPressBetween above the fingersDown comment keeps both.

@vjeux
vjeux force-pushed the uikit/f2-context-request branch from d642a33 to fbb40c1 Compare November 3, 2026 07:56
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. At fbb40c1 a pen that leaves the long press's tolerance before its duration is no hold, so a barrel right click right after such a drag makes its request, and a drag-then-right-click case pins it. The context request's comment now keeps only its reason.

@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: the context request follows spec:AST-094 FR43, FR41, FR12, FR15 and FR24 and the owner's rulings on the hold-repeat control and the hold options, and a barrel right click after a pen drag now makes its request, but one pen case still opens nothing.

  • GestureHost.isPenDownIn (packages/core/src/gestures/GestureHost.ts:1349-1356) also counts a lifted pen that a recognizer is still deciding on, such as a double tap waiting 350 ms after the lift, so after a pen tap on a double-tap target in the row, contextRequest.ts:142-146 cancels that pen's barrel right click with no request; skipping ended and cancelled touches there, as touchForCancelledPointer does, fixes it, and a pen-tap-then-right-click case on a double-tap target belongs beside the other pen cases.

Non-blocking: the notePenDrift JSDoc (GestureHost.ts:3824) restates the condition under it, and driftedPens' JSDoc already gives the reason.

[Automated review]

Comment on lines +1350 to +1353
return this.arena.touches.some(
touch =>
touch.pointerType === 'pen' &&
touch.target instanceof 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.

arena.touches also holds lifted pens awaiting a decision (GestureArena.ts:208-211). After a pen tap on a target with a double tap, which waits 350 ms after the lift, this stays true, so contextRequest.ts:142-146 cancels that pen's barrel right click in the row with no request (a unit probe and trusted pen input in Chromium both show it). Skipping touch.phase === 'ended' || touch.phase === 'cancelled' here, as touchForCancelledPointer does, keeps all 16 context-request cases passing and makes that click request; a hold's own lift stays covered by lastPenLiftIn.

);
}

/** Note a pen contact that leaves the long press's tolerance before its duration. */

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: this restates the condition below, and driftedPens' JSDoc already says why. Drop it, or keep only the reason: "Such a contact is no hold, so its lift arms no menu refusal."

@vjeux
vjeux force-pushed the uikit/f2-context-request branch from fbb40c1 to 39b1dcb Compare November 3, 2026 08:28
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. At 39b1dcb a lifted pen that a recognizer still decides on no longer counts as down, so a barrel right click right after a pen tap on a double-tap target makes its request, and a case pins it. The drift helper's comment now keeps only its reason.

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

Recommending approval. attachContextRequest follows spec:AST-094 FR43 and its neighbouring claims (FR41, FR12, FR15, FR24) and the engineering owner's rulings on the hold-repeat control and the cut hold options, and a barrel right click right after a pen tap on a double-tap target now makes its request, pinned by a new case.

Two optional follow-ups, neither blocking:

  • A held Shift+F10 or context-menu key makes one request per auto-repeat, because onKeyDown never reads key.repeat; cancelling repeats without requesting keeps the README's one request per gesture.
  • onHold reports committed before it reads isEnabled, so a request disabled during the hold reports committed with no request; reword its JSDoc or read isEnabled first.

[Automated review]

(key.key === 'F10' && key.shiftKey) || key.key === 'ContextMenu';
// A hold-repeat control inside owns only its hold: Shift+F10 on it
// reaches this request.
if (!isRequestKey || !isEnabled() || answered.has(key)) {

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: an auto-repeated keydown (key.repeat) passes this check too, so a held Shift+F10 or context-menu key makes one request per repeat. Cancelling a repeat without requesting would keep one request per key press.

recognizer.addListener(() => {
const {state} = recognizer;
if (state === 'began') {
options.onHold?.('committed');

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: committed is reported before isEnabled is read, so a request disabled during the hold reports committed (then ended) with no request, while the option's JSDoc says committed comes as the hold requests. Reword the JSDoc, or read isEnabled first.

@vjeux
vjeux force-pushed the uikit/f2-context-request branch from 39b1dcb to c9f05eb Compare November 3, 2026 16:52
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main at c9f05eb. The only conflict was one line in the gesture host's cancel path: main now also clears its pointer kinds there, and both lines stay. The change itself is identical to the approved head.

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

Recommending approval. attachContextRequest follows spec:AST-094 FR43 and its neighbouring claims (FR41, FR12, FR15, FR24), and the engineering owner's rulings on the hold-repeat control and the cut hold options. Two optional follow-ups, fine to leave for the context menu that adopts it:

  • A held Shift+F10 makes one request per auto-repeat (onKeyDown in contextRequest.ts); ignoring key.repeat after cancelling it keeps the README's one request per gesture.
  • onHold reports committed for a request disabled during the hold, while its JSDoc says committed comes as the hold requests; read isEnabled first or reword the JSDoc.

[Automated review]

(key.key === 'F10' && key.shiftKey) || key.key === 'ContextMenu';
// A hold-repeat control inside owns only its hold: Shift+F10 on it
// reaches this request.
if (!isRequestKey || !isEnabled() || answered.has(key)) {

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 held Shift+F10 or context-menu key requests again on every auto-repeat keydown, so the README's "Each gesture makes one request" does not hold for a held key. Cancelling a repeat (key.repeat) without requesting keeps one request per key press.

recognizer.addListener(() => {
const {state} = recognizer;
if (state === 'began') {
options.onHold?.('committed');

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: committed is reported before isEnabled is read, so a request disabled during the hold reports committed then ended with no request, while the onHold JSDoc says committed comes as the hold requests. Read isEnabled first, or reword the JSDoc.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
useLongPress takes her createLongPressDetector with stages, the structure
kept whole, and my side's two changes on it: the defaults come from the one
long-press source (motion/uikit/longPress.ts) and a long press's drift is a
distance, as UIKit measures allowableMovement ((7, 7) keeps the press,
(7.2, 7.2) ends it). Her measured context-menu tolerance is per axis ((11,
11) still opens the menu, M-CM3), so the detector gains isPerAxis and
contextMenuPress passes it. Her FR24 case 'leaves no timer of the lift
behind when it unmounts while the lift is still growing' found the click
guard's 400 ms window (utils/clickGuard.ts, from my side) still armed at
unmount: that timer is the page's, not the lift's, so the case disarms the
guard before it counts. The two READMEs take both sides' rows and sections.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
… selection off document-wide) into the meta#8228 helper
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
… for a recognizer inside a shadow root) into the meta#8228 helper
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
… when asked, cancels on a drift, and times its request on the gesture clock) into the meta#8228 helper
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s menu trigger tolerance at the press, meta#8358's useLongPress on each axis, meta#8336's flat-tree scroll checks) into the meta#8228 helper

# Conflicts:
#	packages/core/src/hooks/useLongPress.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…t after the commit requests) into the headless context menu
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…into the headless context menu

packages/core/src/utils/pressFeedback.ts: meta#8670's text, with this
stack's own press-paint subscription (c338e93: subscribePressPaint,
which ContextMenu's press feedback follows) re-applied on it; meta#8670's
fold kept only its own line's edits to this file.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…, with no stages (spec:AST-094 FR43)

Red: meta#8670 cuts the context request's two-stage hold (commitMs,
requestMs, requestOnLift) and its per-axis allowance, as asked there:
the hold is UIKit's long press and requests as it begins. ContextMenu
and the factory's menu still open a held finger at 735 ms, open at a
lift after 400 ms, and allow 11.5 pt on each axis. They should open at
500 ms under any primary pointer, open nothing for a lift before it, and
open nothing for a drift past 10 pt.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
meta#8665 now carries meta#8360's new head and the maintainer's host review: a
blur ends an armed press, a kept press's touchmove follows the press,
hover reaches every pointer, and a pen's barrel button starts no press.
The one conflict, utils/pressFeedback.ts, takes meta#8665's text: both merges
of main resolved it the same way, and this pull request does not change
that file.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…peat control owns only its hold

- A pen held still requests through the hold, and the browser then fires
  the pen's own context menu event, during the hold or at its lift: a
  second request for one gesture. One request only, the hold's.
- The engineering owner's ruling on meta#8670: a hold-repeat control inside a
  row owns only its hold; its right click and Shift+F10 still reach the
  row's context request. Today both are refused there.
Both fail until the context request refuses the pen's echo and leaves a
kept press only its hold.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
… with no options a record does not need

The engineering owner's ruling on meta#8670: cut the two-stage commitMs,
requestMs and requestOnLift, and the allowableMovement and isPerAxis
tolerance, until a record calls for them. The hold is UIKit's long press,
500 ms and 10 px as a distance, and requests as it begins; onHold reports
it committed, then ended or cancelled. LongPressGestureRecognizer loses
isPerAxis and geometry its axisTravel, with their tests.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…press, with no options) into the headless context menu

packages/core/src/utils/pressFeedback.ts and its test: meta#8670's text,
with this stack's press-paint subscription (subscribePressPaint, which
ContextMenu's press feedback follows) kept beside it.
packages/core/src/hooks/useLongPress.ts, its test, its page and
useMenuPress.ts: meta#8670's text. This stack's useLongPress `stages` and
press start times served ContextMenu's two-stage hold, which no longer
exists (the context request opens ContextMenu, and its hold has no
stages), so they go, with their changeset and the hooks entry's
LongPressStage type.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
… with none of the hold options meta#8670 cut (spec:AST-094 FR43)

meta#8670 cuts the context request's two-stage hold (commitMs, requestMs,
requestOnLift), allowableMovement and isPerAxis, as asked there: the hold
is UIKit's long press and requests as it begins. ContextMenu and the
factory passed them for UIContextMenuInteraction's 400 ms commit, 735 ms
menu, open at a lift and 11.5 pt on each axis. They pass none now: under
any primary pointer a held finger gets the menu at 500 ms, a lift before
it is a tap, and a drift past 10 pt (a distance) opens nothing. Under the
touch model the commit, now the long press, plays the haptic and the
press feedback plays UIKit's press curve until the menu shows.

The UIKit table's per-axis drift guess has no reader left and goes. The
tests, the interaction specs, the docs, the story and the changesets
that described the two-stage hold describe the long press; the tests of
a lift after the commit and of a drag between the commit and the menu
go with that window.
…ck and Shift+F10

A context menu should open the same way from a held finger or pen, a
right click, and Shift+F10 or the context-menu key. Each component wired
its own handlers, so the rules differed: a held finger on Android could
open the browser's own menu beside Astryx's.

attachContextRequest(element, {onRequest}) is the one internal request a
context menu answers. Each gesture makes one request, naming its input (a
hold, a pointer or the keyboard), so a keyboard-opened menu can focus its
first row. The hold is UIKit's long press and an arena participant, so it
loses to a claim another recognizer made first. A mouse held still makes
no request. A control that keeps its press for a hold-repeat owns only its
hold, while its right click and Shift+F10 still reach the request. The
browser's own menu is refused once, for a finger while it is down inside
the element and for a pen during and right after any hold of it there.
Inside nested requests, a right click or a key reaches only the innermost.
It listens on its own element only, and only while enabled.

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

Approved pending CI: CI has not run on this head yet. attachContextRequest follows spec:AST-094 FR43 and its neighbouring claims (FR41, FR12, FR15, FR24), and the engineering owner's rulings on the hold-repeat control and the cut hold options; nothing blocks.

Next: mark the pull request ready for review, and a maintainer adds the ci:run label so CI runs on this fork branch.

Two optional follow-ups, fine to leave for the context menu that adopts it:

  • A held Shift+F10 makes one request per auto-repeat (onKeyDown in contextRequest.ts); ignoring key.repeat after cancelling it keeps the README's one request per gesture.
  • onHold reports committed for a request disabled during the hold, while its JSDoc says committed comes as the hold requests; read isEnabled first or reword the JSDoc.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 9e2d08a1 Deployed Nov 4, 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.

2 participants