Skip to content

feat(core): gesture touches carry their modifier keys - #8763

Draft
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/press-tracker-factory
Draft

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/press-tracker-factory

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

One commit on main. It absorbs the closed #8797, whose change this is. Leaves draft with its first reader: the group that leaves a modified click on a link to the browser.

Maintainer impact

A group of links (a tab bar, a menu of links) that takes presses through the gesture host cannot tell a Ctrl-, Cmd- or Shift-click from a plain one, so it cannot leave the browser its own open-in-a-new-tab.

Intended invariant

A gesture touch reports the modifier keys its press began with. Nothing a control does changes: no component reads the keys yet. No behavior change.

Change

  • types.ts: GestureTouch carries ctrlKey, metaKey, shiftKey and altKey; a new contact's input may give them, none when left out.
  • GestureArena.ts: a touch takes the keys its input gives.
  • GestureHost.ts: it reads them from the pointer or touch event that begins a contact, a finger that lands where nothing takes part included.

Evidence

  • Failure before the change: with main's host, the two cases that read a press's modifier keys fail: a mouse or pen press with the keys held, and a finger that lands where nothing takes part beside a tracked one.
  • Success after the change: both pass, and a touch reports the keys held at its pointerdown or touchstart.
  • Product/runtime behavior verified unchanged: no component reads the keys yet.
  • CI impact: none.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split: the press tracker's import and the README moved to their own pull request.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at b29b3c5, one commit on main 3615956:

  • GestureHost.test.ts: the two modifier-key cases fail with main's host and pass here. The units of gestures, hooks, utils and Core's own tests: 134 files, 2,552 tests passed. The touch-support bundle test: 9 passed.
  • tsc --project packages/core/tsconfig.json --noEmit, strict ESLint and Prettier on the changed files, check:* and pnpm review:pre: clean.

@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 3, 2026 1:26pm 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 3, 2026, 1:29 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.

Requesting changes: against main, this pull request's diff includes all of #8665 (29,050 of its 29,164 authored lines), so it can't be reviewed or merged as the 642-line createPressTracker change it describes. Please land #8665 first and rebase this branch onto main, so the diff is the factory change alone. The factory itself follows architecture:react-component-runtime INV13 and keeps the tracker's behavior; while rebasing, please also fix the changeset and gestures README sentence that calls every touch module outside the recognizer family a factory, since VelocityTracker is still a class (INV13 lists it as a known gap).

[Automated review]

Comment thread .changeset/press-tracker-factory.md Outdated
'@astryxdesign/core': patch
---

[chore] Internal: press tracking is built by `createPressTracker(options)`, and `PressTracker` is the interface of what it returns, as every touch module outside the recognizer family is built from a factory. `usePressTracker` and its callers do not change. Not exported from the package entry point.

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.

VelocityTracker is still a class outside the recognizer family (packages/core/src/gestures/VelocityTracker.ts:59), the gap architecture:react-component-runtime INV13 records, so this release note overstates the rule. Suggest "…as every touch module outside the recognizer family is, apart from VelocityTracker", or dropping the clause; the same sentence is in packages/core/src/gestures/README.md:180-181.

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

Incremental review of d714d09..3e4d7c0; prior findings carried: spec_authority, authored_loc, context, user_impact, api, evidence_by_spec_claim, concrete_defects, disposition

Requesting changes: the changeset and gestures README now describe the factory rule accurately, naming VelocityTracker as the one class left outside the recognizer family, so that earlier note is resolved. Against main, the diff still includes all of #8665 (29,050 of its 29,165 authored lines), so it can't be reviewed or merged as the 643-line createPressTracker change it describes. Please land #8665 first and rebase this branch onto main, so the diff is the factory change alone; nothing else blocks the factory change.

[Automated review]

@github-actions

github-actions Bot commented Oct 28, 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 needs an engineering owner's decision before it can be approved, and the diff also needs to be split. Against main, it adds a sentence to spec:AST-094 FR41 that the current record does not state (“A mouse or pen drag that a recognizer claims suppresses text selection for that drag only; an unclaimed mouse drag keeps today's behavior.”); an engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, 28,082 of the diff's 28,197 authored lines are #8665's open stack, so please land #8665 first and rebase this branch onto main, leaving the 647-line createPressTracker change, which follows architecture:react-component-runtime INV13 and shows no defect.

[Automated review]

Comment on lines +471 to +473
(`spec:AST-017` FR59; DEC-9). A mouse or pen drag that a recognizer claims
suppresses text selection for that drag only; an unclaimed mouse drag keeps
today's behavior.

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 sentence is new normative text for FR41 that main does not state: today FR41 says a mouse “otherwise keeps its released behavior”. It arrives here from #8665's stack, and an engineering owner needs to approve or reject it; approval is recorded by commenting /approve-spec <full head SHA> on the pull request.

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Rebased in effect: this now sits on #8665's head, so the diff is the factory change alone; one test from #8665 now builds its tracker with createPressTracker (7147a67).

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
meta#8665's review fixes: an armed press under a fine pointer brings the
claiming listeners, keeps a kept press's finger and goes when the page
hides; a press claimed at its own pointerdown keeps the page's selection;
FR41 says what a claimed drag does with the selection. One conflict, in
finePointerListeners.test.ts's imports: this pull request's
createPressTracker with meta#8665's new recognizer imports, and meta#8665's new
kept-press case builds its tracker with createPressTracker, since the
class is gone here.

@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 needs an engineering owner's decision before it can be approved, and the diff also needs to be split. Against main it adds text to spec:AST-094 FR41 that the current record does not state (“A mouse or pen drag that a recognizer claims takes no new text selection for that drag only. A claim made after the drag has moved clears the page's selection; a press claimed at its own pointerdown, before it moves, keeps it.”), with a matching verification row; an engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, 28,320 of the diff's 28,445 authored lines are #8665's open stack, so please land #8665 first and rebase this branch onto main, leaving the 667-line createPressTracker change, which follows architecture:react-component-runtime INV13 and shows no defect.

[Automated review]

Comment on lines +471 to +475
(`spec:AST-017` FR59; DEC-9). A mouse or pen drag that a recognizer claims
takes no new text selection for that drag only. A claim made after the
drag has moved clears the page's selection; a press claimed at its own
pointerdown, before it moves, keeps it. An unclaimed mouse drag keeps its
released behavior.

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 is new normative FR41 text that main does not state: today FR41 says a mouse “otherwise keeps its released behavior”. It arrives from #8665's stack, rewritten there since the earlier review, with the matching verification row at line 663; an engineering owner needs to approve or reject it, recorded by commenting /approve-spec <full head SHA> on the pull request.

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Also in this push: PressTracker imports only the timing table's press clocks, so a control that uses it no longer bundles the table's curves (architecture:component-style-authoring INV6): e8f02fe → 16f35d1.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…king ships only the press clocks

A gesture touch carries the modifier keys its press began with, so a
group can leave a Ctrl-, Cmd- or Shift-click on a link to the browser.
Press tracking reads only the timing table's press clocks, with a bundle
guard that keeps the host's module graph small
(architecture:component-style-authoring INV6), and the README documents
the recognizers' kind and the press tracker factory. meta#8763's and
meta#8797's changes, rebased onto meta#8309 as one commit; its press tracker
factory and recognizer kind now come with meta#8076 and meta#8071.
@vjeux
vjeux force-pushed the uikit/press-tracker-factory branch from 3cdb556 to dbe81cd Compare October 31, 2026 17:10
@vjeux

vjeux commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #8309's new head, on #8098 and #8360 on current main. This PR's own change is unchanged (the same patch). Local gates at dbe81cd are in the description.

@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 needs an engineering owner's decision before it can be approved, and the diff also needs to shrink to its own commit. Against main it adds requirements the current records do not state to spec:AST-094 FR11 and FR19, architecture:react-component-runtime INV13, component:BottomSheetSwitcher FR5, component:Drawer FR5 and component:MobileNav FR3, all from the stack beneath this branch; an engineering owner records approval by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. Separately, 19,430 of the diff's 19,597 authored lines belong to #8360, #8098 and #8309, so those should land first and this branch be rebased onto main, leaving its own 173-line commit, which reads correctly; in that stack, useFingerScroll should call the shared useAttachment instead of copying its node-following.

[Automated review]

`family:action-groups` member tracks activates at the lift (FR26), and a toggle
under `spec:AST-020` FR9's slide-off exception (Switch, Checkbox,
CheckboxListItem) may also complete on a lift after the finger slid off.
CheckboxListItem) may also complete on a lift after the finger slid off. Where the engine hit-tests an element a compositor animation moves at the motion's start (iOS WebKit), a touch on moving content goes to what was drawn under the finger at touch-down, and a tap there (no recognizer claimed it, the browser did not take it, and it stayed within 45 pt) MUST activate it: the browser's own click for that touch is cancelled or swallowed, and that element receives one click, an editable focused first.

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.

New normative text from #8360's stack: a MUST that a tap on compositor-moving content activates what was drawn under the finger with one click, the browser's own click cancelled or swallowed. Current FR11 says a control's activation "stays the browser's click" with two exceptions, so this adds a third. It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

element whose recognizers decide only at the lift keeps its `touchmove`
listener passive, and its `touchend` listener stays cancelable so the
gesture can cancel the click (FR21). Known limit: a native scroll that starts on an element with a
gesture can cancel the click (FR21). While an element that holds, or sits inside, an element with a recognizer that claims moving touches moves on the compositor (iOS WebKit), the document MAY carry a non-passive `touchmove` listener; it cancels only the moves of a touch a recognizer owns under this rule and comes off when the motion ends. While it is on, a native scroll that starts anywhere on the page waits for a free main thread. Known limit: a native scroll that starts on an element with a

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.

New MAY from #8360's stack: a document-level non-passive touchmove listener while such content moves, during which every native scroll on the page waits for the main thread. Current FR19 says "Only an element with a recognizer that must claim mid-gesture registers that blocking listener". It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query; under a fine pointer only a component that
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The modules are not public exports and are not
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The gesture host, press feedback, page hold, finger-scroll watch and click guard hear document-wide input through one capture-phase listener per event type on the window, present while a subscriber needs it. They run in subscription order, before any document or element listener; it is passive unless a subscriber cancels the event. The modules are not public exports and are not

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.

New INV13 text from #8360's stack: the touch modules hear document-wide input through one capture-phase window listener per event type, in subscription order. Current INV13 does not state it. It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 1, 2026
…king ships only the press clocks

A gesture touch carries the modifier keys its press began with, so a
group can leave a Ctrl-, Cmd- or Shift-click on a link to the browser.
Press tracking reads only the timing table's press clocks, with a bundle
guard that keeps the host's module graph small
(architecture:component-style-authoring INV6), and the README documents
the recognizers' kind and the press tracker factory. meta#8763's and
meta#8797's changes, rebased onto meta#8309 as one commit; its press tracker
factory and recognizer kind now come with meta#8076 and meta#8071.
@vjeux
vjeux force-pushed the uikit/press-tracker-factory branch from dbe81cd to 38070ea Compare November 1, 2026 10:27
@vjeux

vjeux commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #8309's new head fe02656, on #8098 and #8360's new heads. This PR's own change is unchanged (the same changed lines). Local gates at 38070ea are in the description.

@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 needs an engineering owner's decision before it can be approved, and the diff also needs to shrink to its own commit. Against main it adds requirements the current records do not state to spec:AST-094 FR11 and FR19, architecture:react-component-runtime INV13, component:BottomSheetSwitcher FR5, component:Drawer FR5 and component:MobileNav FR3, all from the stack beneath this branch; an engineering owner records approval by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. Separately, 19,586 of the diff's 19,753 authored lines belong to #8360, #8098 and #8309, so those should land first and this branch be rebased onto main, leaving its own 173-line commit, which reads correctly and whose new tests fail before it and pass with it. In that stack, useFingerScroll should call the shared useAttachment instead of copying its node-following.

[Automated review]

Comment on lines +48 to +76
const attachedRef = useRef<{node: Node; detach: () => void} | null>(null);
// Follows the node on every commit: a ref that now points at another
// element moves the watch to it.
useIsomorphicLayoutEffect(() => {
const node = isDocument(scroller) ? scroller : scroller.current;
if (attachedRef.current?.node === node) {
return;
}
attachedRef.current?.detach();
attachedRef.current =
node == null
? null
: {
node,
detach: watchFingerScrolls(
getGestureHost(isDocument(node) ? node : node.ownerDocument),
node,
),
};
});
// Synchronizes with the gesture host's finger-scroll watch: unmounting
// releases the watch this hook attached.
useIsomorphicLayoutEffect(
() => () => {
attachedRef.current?.detach();
attachedRef.current = null;
},
[],
);

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 two layout Effects re-implement the node-following useAttachment already owns (gestures/useAttachment.ts:73-98: follow the node behind a ref after each commit, release a replaced node, release at unmount), which useGestureRecognizer and usePressTracker call. architecture:react-component-runtime INV16 asks for a shared concern to be extended in its owner and called once. Suggest letting useAttachment take a document as well as an element ref and calling it here once, so a fix to replaced-node release reaches the finger-scroll watch too.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 1, 2026
…king ships only the press clocks

A gesture touch carries the modifier keys its press began with, so a
group can leave a Ctrl-, Cmd- or Shift-click on a link to the browser.
Press tracking reads only the timing table's press clocks, with a bundle
guard that keeps the host's module graph small
(architecture:component-style-authoring INV6), and the README documents
the recognizers' kind and the press tracker factory. meta#8763's and
meta#8797's changes, rebased onto meta#8309 as one commit; its press tracker
factory and recognizer kind now come with meta#8076 and meta#8071.
@vjeux
vjeux force-pushed the uikit/press-tracker-factory branch from 38070ea to ffb66b1 Compare November 1, 2026 16:21
@vjeux

vjeux commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #8309's new head b35db3f, on #8098 and #8360's new heads. This PR's own change is unchanged (the same changed lines). Local gates at ffb66b1 are in the description.

@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 needs an engineering owner's decision before it can be approved, and the diff also needs to shrink to its own commit. Against main it adds requirements the current records do not state to spec:AST-094 FR11 and FR19, architecture:react-component-runtime INV13, component:BottomSheetSwitcher FR5, component:Drawer FR5 and component:MobileNav FR3; an engineering owner approves or rejects them by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Independently of that decision:

  • #8360, #8098 and #8309 need to land first, so this diff is its own 173-line commit instead of 19,834 lines.
  • hooks/scrollLockTouchGuard.ts (#8098) re-implements the edge checks and scroll-chain walk of gestures/nativeScroll.ts; extend the native-scroll probe and call it instead.
  • gestures/useFingerScroll.ts (#8360) re-implements the node-following of useAttachment; call it instead.

[Automated review]

Comment on lines +38 to +57
function canMoveY(element: Element, dy: number, range: number): boolean {
// A finger moving down drags the content down: scrollTop decreases.
return dy > 0
? element.scrollTop > EDGE_TOLERANCE - 0.5
: element.scrollTop < range - EDGE_TOLERANCE + 0.5;
}

function canMoveX(
element: Element,
dx: number,
range: number,
isRtl: boolean,
): boolean {
const min = isRtl ? -range : 0;
const max = isRtl ? 0 : range;
// A finger moving right drags the content right: scrollLeft decreases.
return dx > 0
? element.scrollLeft > min + EDGE_TOLERANCE - 0.5
: element.scrollLeft < max - EDGE_TOLERANCE + 0.5;
}

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 edge checks re-implement canMoveY and canMoveX from gestures/nativeScroll.ts:189-212 (the same ±0.5 px tolerance and RTL scrollLeft range), and isPanAbsorbedBeforePage below walks the same parentAcrossShadow chain as createNativeScrollProbe. nativeScroll.ts imports nothing of the gesture host, so the page hold could call it: export the edge helpers, or a chain walk that stops before the page and reports a contained edge, and delete these copies so a fix to one reaches both.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 2, 2026
…king ships only the press clocks

A gesture touch carries the modifier keys its press began with, so a
group can leave a Ctrl-, Cmd- or Shift-click on a link to the browser.
Press tracking reads only the timing table's press clocks, with a bundle
guard that keeps the host's module graph small
(architecture:component-style-authoring INV6), and the README documents
the recognizers' kind and the press tracker factory. meta#8763's and
meta#8797's changes, rebased onto meta#8309 as one commit; its press tracker
factory and recognizer kind now come with meta#8076 and meta#8071.
@vjeux
vjeux force-pushed the uikit/press-tracker-factory branch from ffb66b1 to 8e3889e Compare November 2, 2026 02:04
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #8309's new head e601777. This PR's own change is unchanged (the same changed lines). Local gates at 8e3889e are in the description.

@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 needs an engineering owner's decision before it can be approved, and the diff also needs to shrink to its own commit. Against main it changes component:BottomSheetSwitcher FR5 and component:Drawer FR5 (pull-to-refresh and the page's bounce stop behind a modal flow or drawer) and component:MobileNav FR3 (the clip moves to the page's scroller). The overscroll stop also reaches Dialog, AlertDialog, BottomSheet and Lightbox, whose records do not mention it. An engineering owner records approval or rejection of these record changes by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.

  • hooks/scrollLockTouchGuard.ts:38-57 re-implements canMoveY, canMoveX and the scroll-chain walk from gestures/nativeScroll.ts, and the two already differ; please share one implementation there.
  • The last sentence of .changeset/scroll-lock-keeps-the-page.md says closing MobileNav used to release another overlay's lock, but on main the lock pinned <body> while MobileNav wrote only the root, so it never did; please reword it.
  • 2,663 of the 2,836 changed lines are #8098 and #8309; once those land, a rebase leaves this pull request's own 173-line commit.

[Automated review]

Comment on lines +38 to +57
function canMoveY(element: Element, dy: number, range: number): boolean {
// A finger moving down drags the content down: scrollTop decreases.
return dy > 0
? element.scrollTop > EDGE_TOLERANCE - 0.5
: element.scrollTop < range - EDGE_TOLERANCE + 0.5;
}

function canMoveX(
element: Element,
dx: number,
range: number,
isRtl: boolean,
): boolean {
const min = isRtl ? -range : 0;
const max = isRtl ? 0 : range;
// A finger moving right drags the content right: scrollLeft decreases.
return dx > 0
? element.scrollLeft > min + EDGE_TOLERANCE - 0.5
: element.scrollLeft < max - EDGE_TOLERANCE + 0.5;
}

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 edge checks re-implement canMoveY and canMoveX from gestures/nativeScroll.ts:189-212: the same 1 px tolerance, the same ±0.5 px slack and the same RTL scrollLeft range. isPanAbsorbedBeforePage below walks the same flat-tree scroll chain as buildChain. The copies already differ: only the probe counts overflow: overlay and honours touch-action, and only this guard stops at top-layer and fixed elements. Exporting the edge checks, and a chain walk with a stop rule, from nativeScroll.ts keeps one implementation. It imports nothing of the gesture host, so the overlays' import guard still holds.

'@astryxdesign/core': patch
---

[fix] `useScrollLock`, and so Dialog, BottomSheet, Drawer and Lightbox, keeps the page where it is while an overlay is open. The lock used to pin `<body>` with `position: fixed` at minus the scroll offset and scroll back on release: the page's offset read 0 for the whole lock, and on iOS Safari the jump to the top brought the toolbar back, where it stayed after the overlay closed. Now the page gets `overflow: hidden` (on the root, or on `<body>` when the body's own overflow is the one the viewport uses), and on touch screens a one-finger pan that would scroll the page is cancelled, which iOS Safari before 26.4 needs because it scrolls an `overflow: hidden` page under a finger. Pans that a scroller inside the overlay can take, drags on a native range input, pinch-zoom, and one-finger drags on a pinch-zoomed page (they pan the zoomed view) still work. The lock no longer pins `<body>`, and `window.scrollY` keeps its value while the lock holds. The lock cancels those pans itself, leaving a touch a gesture recognizer owns to it, and judges each pan after a small slop so a swipe's first wobble does not fix the wrong axis. It follows the browser's scroll chain, so an overlay rendered inside a scrolling container still holds the page: a modal dialog's pans go to the page, not to that container. It also sets `overscroll-behavior-y: none` on the root and `<body>`, which turns off pull-to-refresh (Chrome on Android) while an overlay is open. MobileNav's own clip goes through the same page scroll-style claims, so closing or unmounting it no longer releases another overlay's lock.

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 last sentence says closing or unmounting MobileNav no longer releases another overlay's lock. On main it never did: the lock pinned <body> (position: fixed; overflow: hidden), MobileNav wrote only the root's overflow, and closing the drawer left the pin in place. The collision arises only once this change moves the lock onto the root. Suggested wording: "MobileNav's clip now shares the page's scroll-style claims with the lock, which also holds the root, so neither overwrites the other's overflow."

| FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. Under reduced motion the handoff follows `design:reduced-motion`: no transform motion runs, and the sheets cross-fade (DR2) or change at once (DR3) in the same order. | Current source and transition tests; `design:reduced-motion` DR2, DR3 |
| FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests |
| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |
| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented, and pull-to-refresh and the page's bounce stop behind it (`overscroll-behavior-y: none`), since the page is held under a modal presentation (`spec:AST-094` FR19), as behind a UIKit modal presentation; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |

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.

New requirement that the current FR5 does not state: pull-to-refresh and the page's bounce stop behind a modal flow (overscroll-behavior-y: none). The spec:AST-094 FR19 citation covers only cancelling one-finger pans under a modal scroll lock. This needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

| FR3 | The desktop inline-size budget MUST accept pixel numbers or CSS lengths and remain bounded by the viewport. At or below 640px, page-reveal mode MUST preserve 56px and use `width` as a cap; full-mobile mode MUST override `width` with `100dvw`. | Current docs, styles, and width tests |
| FR4 | Visibility through Drawer-owned behavior MUST follow `isOpen`, from the render after hydration on (FR17). Uncanceled Escape routed by the shared stack and uncanceled modal-backdrop activation request `false` when `purpose` allows (FR15), and a `DrawerHeader` close action requests `false` through the callback it is given; Drawer prevents its handled native `cancel` event from closing the dialog directly. During controlled exit, Drawer keeps its host and content region mounted while rendering the caller's current `children`. | Current prop contract and lifecycle tests |
| FR5 | At open time, `hasScrim={true}` MUST select native `showModal()` with `aria-modal`, a focus trap, body scroll locking, and a visible backdrop; `hasScrim={false}` MUST select a manual Popover API host that leaves the page behind interactive. Changing `hasScrim` while open is not a supported native-mode transition. | Current source, docs, and initial-mode tests |
| FR5 | At open time, `hasScrim={true}` MUST select native `showModal()` with `aria-modal`, a focus trap, page scroll locking, and a visible backdrop; under the lock, pull-to-refresh and the page's bounce stop (`overscroll-behavior-y: none`), since the page is held under a modal presentation (`spec:AST-094` FR19), as behind a UIKit modal presentation; `hasScrim={false}` MUST select a manual Popover API host that leaves the page behind interactive. Changing `hasScrim` while open is not a supported native-mode transition. | Current source, docs, and initial-mode tests |

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.

Same new requirement as BottomSheetSwitcher FR5, plus "page scroll locking" in place of "body scroll locking". It needs the same engineering-owner approval.

| FR1 | MobileNav renders one native `<dialog>` Navigation overlay containing a Drawer with a stable Header, Close button, and scrollable Content region. The overlay carries `mobile-nav` with the resolved `side`; Drawer, Header, and Content carry no MobileNav public target. | Current source, docs, and tests |
| FR2 | `isOpen` and `onOpenChange` win when given; otherwise the drawer reads and changes the enclosing AppShell's drawer state. The dialog uses AppShell's drawer id when there is one, else its own; a caller `id` replaces either (Current-state impact). | Current source and tests |
| FR3 | Opening calls `showModal()`, and records the focused element as the opener; where focus lands inside the open drawer is uncontracted here. While open, the page's root clips its overflow and keeps its scrollbar gutter; closing releases both. | Current source and tests |
| FR3 | Opening calls `showModal()`, and records the focused element as the opener; where focus lands inside the open drawer is uncontracted here. While open, the page's scroller clips its overflow and keeps its scrollbar gutter: the root, or `<body>` when `<body>` sets its own overflow and the root's stays `visible`; closing releases both. | Current source and tests |

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 moves the clip from the page's root to the page's scroller (<body> when <body> sets its own overflow and the root's stays visible), which the current FR3 does not state. It needs an engineering owner's approval.

A gesture touch carries the modifier keys its press began with, read
from its pointer or touch event, so a group can leave a Ctrl-, Cmd- or
Shift-click on a link to the browser. A finger that lands where nothing
takes part reports its keys to a recognizer tracking another finger too.
Nothing reads the keys yet: the group that leaves such a click to the
browser is their first user.
@vjeux
vjeux force-pushed the uikit/press-tracker-factory branch from 8e3889e to b29b3c5 Compare November 3, 2026 13:17
@vjeux vjeux changed the title feat(core): gesture touches carry their modifier keys, and press tracking ships only the press clocks feat(core): gesture touches carry their modifier keys Nov 3, 2026
@github-actions github-actions Bot removed needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval labels Nov 3, 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.

Recommending approval. Gesture touches now carry the modifier keys their press began with. The change stays inside Core's internal gesture modules, and nothing reads the keys yet, so no behavior changes. Both new tests fail against main's gesture sources and pass here.

Optional comment tidy-ups, none blocking:

  • The one-line JSDoc on modifierKeysOf (GestureHost.ts) and modifiersOf (GestureArena.ts) repeats each helper's name and return type, so both can go.
  • The TouchModifiers JSDoc (types.ts) joins two topics in its second sentence, and it says a group of links already leaves modified clicks to the browser, though nothing reads the keys yet.

[Automated review]

return event.composedPath()[0] ?? retargeted;
}

/** The modifier keys an input event reports, for the contact it begins. */

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 repeats the name and the TouchModifiers return type. It could go.

*/
export type ClickDisposition = 'allow' | 'suppress' | 'defer';

/** The modifier keys a contact's input reports, none when left out. */

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 name and the === true lines below already say this. It could go.

Comment on lines +72 to +73
* reports them. A Ctrl-, Cmd- or Shift-click on a link opens a new tab or
* window, and a group of links leaves that 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.

Non-blocking: this sentence joins two topics, and no group reads these keys yet. Suggested: "A Ctrl-, Cmd- or Shift-click on a link opens a new tab or window. A group of links needs these keys to leave that click to the browser."

This branch was successfully deployed

1 active deployment
Preview — b29b3c5e Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant