Skip to content

feat(core): gesture touches carry their modifier keys, and press tracking ships only the press clocks - #8763

Draft
vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:uikit/press-tracker-factory
Draft

vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:uikit/press-tracker-factory

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Section 3 of the merge plan: #8360 → #8098 → #8309 → #8763 → #8342. One commit on #8309 holding only this change. It absorbs #8797 (the modifier keys), which closes with a link here; the commit credits its authors. The press tracker factory and the recognizers' kind it used to carry now come with #8076 and #8071.

Maintainer impact

  • A group of links (a tab bar, a menu of links) that takes presses through the gesture host could not tell a Ctrl-, Cmd- or Shift-click from a plain one, so it could not leave the browser its own open-in-a-new-tab.
  • Press tracking imported the whole UIKit timing table to read its press clocks, so every pressable control shipped the table's curves (architecture:component-style-authoring INV6).
  • The README did not document the recognizers' kind or the press tracker factory.

Intended invariant

A gesture touch reports the modifier keys its press began with; press tracking reads only the press clocks, and the host's module graph stays inside its bundle guard. Nothing a control does changes.

Change

  • types.ts, GestureArena.ts, GestureHost.ts: a gesture touch carries ctrlKey, metaKey, shiftKey and altKey as they were when its press began, read from the pointer or touch event; so does a finger that lands where nothing takes part, as recognizers tracking another finger hear it.
  • PressTracker.ts: imports the press clocks from the timing table, not the whole table; GestureHost.bundle.test.ts pins it.
  • README.md: each recognizer reports its kind, which code reads in place of instanceof (architecture:react-component-runtime INV13), and press tracking is built by createPressTracker, with VelocityTracker the one class INV13 lists as a gap.

Evidence

  • Failure before the change: the bundle case found the timing table's curves in the press tracker's graph before the import narrowed, and the host cases that read a press's modifier keys fail without them, a finger landing where nothing takes part included.
  • 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, and the press clocks keep their values.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at dbe81cd, one commit on #8309 holding only this change (8 files, +162/−11):

@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 Oct 31, 2026 5:17pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 5:35 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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

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

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

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

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

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

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 30, 2026
…or, and Lab's route through core's exports

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks; the animator, which starts a motion from rest at
its commit, holds the value on screen inline while a curve runs, and lets a
call wait for a presented frame; and Lab reaching Core only through its
exports, internal entries included. It absorbs meta#8073 (closed: the press
tracking this rests on), meta#8562 (the internal entries) and meta#8340 (the
animator's start and hold fixes), with the held dip's load fixes, and keeps
meta#8641's rule: a gesture that cancels itself while its finger moves keeps
that finger until it lifts. Press tracking is built by createPressTracker
(from meta#8763), and a gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 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 334fe0e to a20b6c8 Compare October 30, 2026 23:40
@vjeux vjeux changed the title refactor(core): press tracking is built by createPressTracker, not a class feat(core): gesture touches carry their modifier keys, and press tracking ships only the press clocks Oct 30, 2026
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Oct 30, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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 diff carries all of #8309's open stack (31,164 of its 31,279 authored lines, including scroll-lock, DropdownMenu-trigger and press-feedback behavior changes), so it can't be reviewed or merged as the 128-line modifier-keys and press-clocks change it describes. Please land #8309 and the pull requests beneath it first, then rebase this branch onto main so the diff is this commit alone. The change itself follows the internal touch-module rules, and its tests and Core's typecheck pass. While rebasing, please also pass modifierKeysOf(event) on the untracked-touch path in GestureHost.ts, so a finger that lands where nothing takes part keeps its keys too.

[Automated review]

Comment on lines +1800 to +1807
untracked.push({
id: this.nextId++,
x: touch.clientX,
y: touch.clientY,
time,
pointerType: 'touch',
target,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This untracked contact reaches arena.untrackedBegan (line 1861), which builds the touch it hands to recognizers tracking another finger with modifiersOf(input). With no keys here, that touch always reads Ctrl, Cmd, Shift and Alt as false, unlike the tracked paths at lines 1844 and 2516. Adding ...modifierKeysOf(event), keeps the changeset's promise that a gesture touch carries the keys held when its press began.

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 a20b6c8 to 3cdb556 Compare October 31, 2026 10:15
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 31, 2026
@vjeux

vjeux commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 3cdb556: the untracked-touch path passes modifierKeysOf(event) too, so a finger that lands where nothing takes part hands its keys to a recognizer tracking another finger, with a case for it.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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 rewrites component:BottomSheetSwitcher FR5 and component:Drawer FR5 (pull-to-refresh and the page's bounce stop behind a modal) and component:MobileNav FR3 (the clip moves to the page's scroller), which the current records do not state; 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, nearly all of the diff (19,148 of its per-commit lines) comes from #8360, #8098 and #8309, so please land those and rebase so this shows only the modifier-keys and press-clocks change, which looks correct and is covered by its new tests.

[Automated review]

| 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

Choose a reason for hiding this comment

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

This adds a requirement the current FR5 does not state: pull-to-refresh and the page's bounce stop behind a modal flow (overscroll-behavior-y: none). spec:AST-094 FR19, cited here, only lets a modal scroll lock cancel an unowned one-finger pan. An engineering owner approves or rejects it by commenting /approve-spec <full head SHA> on the pull request.

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

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: the overscroll stop under the lock is not in the current FR5, which says "body scroll locking". It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

| 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

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 stays visible), which the current FR3 does not state; it needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

vjeux and others added 6 commits October 31, 2026 08:53
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes once taken or after 500 ms, kept
across the host's uninstall, which can run in the microtask after the
pointerup, before the touchend that comes later in the same task (FR24).
An armed press's page listeners go once the arena has decided its last
touch, a tap that waits on a double tap included, and after its last
detach the host holds no listener, note, frame or timer (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…finger landed

On iOS WebKit the host resolves moving content at touch-down and asks the
engine what is under the finger. When a busy main thread hands it the touch
later, the content has moved on: the host now asks at the point where what
was under the finger at the touch's timestamp is drawn, so a tap on a moving
sheet's Done still clicks Done (spec:AST-094 FR11). A presented source
reports its offset at a given time for this.

The presented browser proof drives the sheet with Core's animator and a
critically damped spring, up into place and down off the screen over the
scrim: a tap on the drawn Done clicks it once, and a drag on the drawn sheet
catches it where it is drawn, in both directions.
…the finger (FR11, FR19); INV13 names the shared input listener

Where the engine hit-tests an element a compositor animation moves at the
motion's start (iOS WebKit), a touch on that content goes to what was drawn
under the finger at touch-down, and a tap there activates that element with
one click while the browser's own click is cancelled or swallowed (FR11).
While such content around a recognizer that claims moving touches is moving,
the document may carry a non-passive touchmove listener that cancels only
owned moves and comes off when the motion ends; while it is on, a native
scroll waits for a free main thread (FR19). 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
(architecture:react-component-runtime INV13).
The scroll lock no longer pins <body>, which reset the page's offset to 0
(the page jumped and Safari's toolbar came back). It holds `overflow:
hidden` on the page's scroller and, through the gesture host's page hold,
cancels only a one-finger pan that nothing inside the overlay takes: a
scroller, a range input, a pinch or a zoomed page's pan still goes through.
The root's scroll styles become claim stacks (rootScrollStyles), so a lock
and a pull to refresh compose, and MobileNav's clip moves onto them. The
top-layer check asks CSS.supports for `:modal` and `:popover-open` instead
of catching errors from `matches`. The touch module bundle test names
useScrollLock and the imports that reach it as carrying the page hold.

The records state the clip on the page's scroller (MobileNav FR3) and that
pull-to-refresh and the page's bounce stop behind a modal overlay
(BottomSheetSwitcher FR5, Drawer FR5).

AlertDialog's layout tests stub the page hold, whose pointer read belongs to
the lock, so their guard still sees only the layout's own media queries.
…h rule of the Touch Events path

Where a finger comes from its pointer events, the host does what its
Touch Events path does (spec:AST-094 FR10, FR19, FR28): the lift of a
pen or a pointer-read finger the window's blur cancelled is refused,
while a mouse's keeps its click, and a contact that ends in
pointercancel is forgotten; a second finger fails an unclaimed
one-finger pan wherever it lands; and a target gets a blocking listener
only for an enabled participant. The arena cancels a moving recognizer
the browser takes. A refused lift is noted as a claimed lift is, and the
pointers a blur cancelled go, as the host's other notes do, after its
last detach (FR24). The claimed lift's touchend cancel these build on
comes with meta#8360. meta#8309's and meta#8336's changes, rebased onto meta#8098 as one
commit.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…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

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

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

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

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

This branch was successfully deployed

1 active deployment
Preview — dbe81cdc Deployed Oct 31, 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. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant