Skip to content

feat(gestures): a recognizer created with claimsTouches: false is a watcher - #8589

Open
cixzhang wants to merge 1042 commits into
mainfrom
uikit/gesture-watcher
Open

cixzhang wants to merge 1042 commits into
mainfrom
uikit/gesture-watcher

Conversation

@cixzhang

Copy link
Copy Markdown
Contributor

Depends on #8315 (and its predecessors); review this pull request's own commit (feat(gestures): a recognizer created with claimsTouches: false is a watcher), about 210 lines over #8315's head.

Split out of #8336, which keeps the gesture host's ownership changes.

User need

Some touch behavior only follows the person's scroll and never takes the touch: a tab bar that minimizes while the page scrolls down, a header that collapses. It has to see the finger without delaying the scroll, without stopping another recognizer, and without cancelling a press or click under it.

Problem and solution fit

  • claimsTouches: false makes a recognizer a watcher: it never claims, so its element gets only passive listeners and a scroll never waits for it.
  • It prevents no other recognizer and none prevents it, as spec:AST-094 FR15 now states for watchers (spec(AST-094): FR15 exempts watchers from recognizer exclusivity (DEC-7) #8517).
  • It cancels no press or click under it when it begins (FR11, FR19).
  • Its direction writes no touch-action: it restricts no scroll (FR20, DEC-7).
  • It keeps tracking a touch the browser takes for a scroll, reporting through the native scroll and ending at the lift (FR20, DEC-7).

Current specification

spec:AST-094 FR11, FR15, FR19, FR20, DEC-7.

Public delta and caller burden

None: an internal recognizer option, not exported from the package entry point; the changeset is [chore].

Design and alternatives

  • One option on the base recognizer, so any recognizer kind can watch; a separate watcher class would duplicate each recognizer's tracking.

Evidence

  • Unit tests (watcherRecognizer.test.ts): runs alongside a claiming pan on the same touch and claims nothing; cancels no press under it; follows a touch the browser took for a scroll until the lift; gets only passive touchmove listeners; writes no touch-action for its direction.

Writing rubric results

No record text of its own: AST-094 FR15's watcher exemption is on main (#8517); R1–R6: none.

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.

vjeux and others added 30 commits October 26, 2026 03:27
…ated consumer

The gesture host has no delegated consumers and no scroll handoff, so its
header drops both, and one changeset covers the press tracking, the one
press writer and the hooks. Both texts are cixzhang's from c34bf73 on
#8073, taken verbatim so the next #8076 merges without a conflict.
…s pending

waitForAnimation and watch.started() left pending animations (no start time
yet) out of the running set, so an earlier animation that had finished
answered for the one the action started. A pending animation in scope now
keeps the wait going. Self-test: a short animation ends, then a longer one
starts; the wait returns the longer one (it returned the 40 ms one before).
The README and JSDoc say how a mid-run reverse or replay is misread.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ase is one handle, and REDUCED_TRANSITION keeps createAnimator's docs

- attach rebuilt only channels away from identity, so an identity the
  animator held inline over a stylesheet that differs (set({opacity: 1}) over
  opacity 0) was never written on the node that replaced the element; the
  new node showed 0 while get() read 1. The held identities now carry through
  a detach to the next node.
- decay under Reduce Motion with one axis past an edge and another coasting
  returned a handle that covered only the coasting axis; stop() left the
  cross-fade to land. The handle now covers both calls.
- REDUCED_TRANSITION moves above createAnimator's doc comment.
Storybook's highlight module scans the page on every insertion in WebKit as in Chromium, so the self-test now counts the insertion's getComputedStyle calls in both engines; only Chromium's trace counts the style recalculations they force. It no longer serves Storybook for an engine it skips.
…handle for a mixed release); decay's Reduce Motion docs

- attach(): #8055's held identities, plus a pseudo-element's: its identity
  hold (a static animation the attach cancels) is held again on the next
  node; a borrowed opacity is never held. Test: a ::backdrop's hold follows
  a node replacement.
- decay(): #8055's single handle for a release with one axis past an edge,
  with `started` covering both calls. Under this layer's plan a stop
  finishes the cross-fade and lands that axis at once, while invisible, so
  #8055's test checks that nothing moves after the stop.
- Docs: the header, decay()'s and MotionCurve's comments, the motion
  README and the changeset now say that a curve marked isTransition (a
  coast's spring back from past an edge) plays through the Reduce Motion
  policy: a cross-fade on a coarse pointer, at once elsewhere.
…lacement, one handle for a mixed-axis release) as a dependency
…e animations

Inside a trace, waitForRest waits on timers for each animation's scheduled
end. A scroll- or view-timeline animation (DateInput's wheel rows) has none:
its time is a percentage, the wait came out NaN, and the timer re-armed with
no delay until the deadline, where the untraced wait rests. It now skips
animations that are not on the document timeline, as holdAnimations does.
Self-test: a scroll-timeline row inside a trace; it rejected at the deadline
before (Chromium, 3x repeat pass).
…t moved frame, and the harness's waits run on the real timers

With a compositor record, the moved criterion counted every compositor
frame in the judged part, but only a gap between two moved frames can be
credited to the compositor, so the frames before the first and after the
last moved frame read as misses: at one screencast frame in six, a
composited animation failed in 5 of 7 runs, where the same frames passed
without the record. The criterion now counts the compositor's frames from
the first to the last moved frame; the gaps before and after are the stall
check's, which already measures the window's edges.

blockMainThread(..., {during}) and sampleRects waited on the page's own
requestAnimationFrame and setTimeout, which freezeTimers holds, so neither
settled under a frozen timer gate, though the harness's own waits are
documented as not held. They now run on the gate's real frame and timeout
(the gate exposes its real setTimeout too). A unit case at one frame in six
and a self-test under a frozen gate fail on the previous code.
…up a wait

waitForAnimation and watch.started() counted a scroll- or view-timeline
animation (DateInput's wheel rows) as running; it never progresses on its
own, so the wait never answered with the animation that finished and
rejected at its timeout. Only document-timeline animations count now, as for
holdAnimations. Self-test: a scroll-timeline row beside a short animation
that ends; it rejected before (Chromium, 3x repeat pass). The installCurveEnd
doc comment sits above installCurveEnd again.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…s this layer has, and coast says a transition lands at once on a fine pointer
… pointer); the changeset says the same

animator.test.ts keeps this layer's header (no compositor browser spec in
this layer). The changeset now says decay sends a spring back from past an
edge through the Reduce Motion policy: a cross-fade on a coarse pointer, at
once elsewhere.
…side a scroll view

The press profiles' README said a cell's highlight always waits the
content-touch delay. PressTracker waits it inside a scroll view and lights
at once outside one (spec:AST-094 FR11), as its own header says; the README
now says so too.
What one call becomes under Reduce Motion (an opacity-only cross-fade
whose travel lands while the element is transparent, or everything at
once; a dip on a borrowed stylesheet opacity) moves from createAnimator's
closure to reducedMotionPlan.ts, a pure planner that reads the animator
through a view and returns the plan; the animator turns the plan into its
segments and runs the landings. No behavior change: the animator's tests
pass unchanged, and the planner has its own.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ed pull requests

This pull request keeps the animator's Reduce Motion plan (now its own
planner module), the browser-set start of a call from rest, promotion,
atRest, rebase and animatorFor. Two features it carried move up the stack,
each with its own tests and changeset:
- play(curves), baking any closed-form curve like a spring;
- driving a pseudo-element (a dialog's ::backdrop).
The changeset is split the same way.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
play(curves) bakes any curve given as value and velocity functions of
time (a spring chasing a moving target from solveSpringTowardRamp, a
fitted UIKit curve) exactly like a spring: presentation values,
velocities, stop, finished and the shared clock work the same, a later
animate starts from the curve's value and velocity, and under Reduce
Motion a curve that moves is travel the policy replaces. Tests and a
changeset of its own.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
createAnimator(element, {pseudoElement: '::backdrop'}) animates a
pseudo-element: with no inline style, a static filling animation holds
its value after a curve ends and after set; a custom property on its host
promotes it; a Reduce Motion dip borrows and hands back the
pseudo-element's own opacity, and its holds follow a node replacement.
animatorFor keeps one animator per element and pseudo-element. Tests and
a changeset of its own.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ot the page's frames around it

"a harness read inside a trace is not the page's long task…" also asserted
that no long task started anywhere in the read's window. On CI's runner each
frame of the fixture's 1,200 running rows took 51-54 ms, the page's own long
tasks, and failed it; at 4x CPU locally they take 126-220 ms. The read
itself was set apart correctly.

The test now asserts the read's own classification, and removes the rows
before the timer, click and direct-call windows, so each holds only its own
work. It passes at 4x CPU, where the previous assertion failed.
…ter the browser takes the touch

A host that stopped feeding the finger at the takeover and still delivered
the lift would have passed. Each transition now records the list's
scrollTop, and the test expects a 'changed' while the list is scrolled
(spec:AST-094 FR20: it follows the scroll to the lift).
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…her browser test) into the arena fixes

The host-owner changeset keeps this PR's own listeners for the scroll lock
and finger-scroll watch, with meta#8336's lone-finger scroll-edge hold.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

The lint job's @typescript-eslint/array-type rule forbids Array<T>, and the watcher test now writes its record type as Array<{to: string; scrollTop: number}> three times (gestures.browser.spec.ts lines 1170, 1172 and 1195 on this head, b87b40b). That's the job's only error, and it fails lint here and on every head that merges this one (#8336, #8320). A fix that writes {to: string; scrollTop: number}[] instead, on this head: vjeux@42ada84 (branch uikit/f2-8589-array-type).

CI's lint rejected the three Array<T> types the previous commit added
(@typescript-eslint/array-type); the test now names its record once
(WatchTransition) and reuses it.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 27, 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. The watcher itself (claimsTouches: false) follows spec:AST-094 FR11, FR15, FR18, FR19, FR20 and DEC-7, and its real-touch browser case now checks that it keeps reporting moves after the browser takes the touch. Against main, though, this pull request carries 20,634 authored lines, all but the watcher's 525 from the open #8315 line, and it conflicts with main in packages/core/src/motion/animator.ts, so no CI test has run on this head. Please land #8315 and its predecessors and merge main, or base this pull request on a branch in this repository that holds #8315's head, so the diff is the eight-file watcher.

  • For the stack's own pull requests: the screen-edge pan decides at 10 px inside its band on a wide view (ScreenEdgePanGestureRecognizer.ts:117-125) against FR18's 15 px, and the fine-pointer action group cites a family:action-groups FR5 that has no record on main.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…finger, meta#8589's watcher test, its lint fix; meta#8076 3bfd7c3; the coarse-pointer query in utils/) into meta#8293
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

The arena resets a recognizer to possible after it ends, as UIKit does, and the transition listener records that reset too. So the watcher test's last transition is possible, and expect(states.at(-1)?.to).toBe('ended') fails in Chromium on this head (b57ac07, 2/2) and on every head that carries it. A fix that asserts the ended-then-possible tail, on this head: vjeux@1b6e19f (branch uikit/f2-8589-watcher-tail). With it, the test passes 3/3 and this head's Chromium gesture suite passes 32/32.

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
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
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
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…8589's watcher test reads its end; the gestures-table gate retired) into meta#8360
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
fZ retires the long-press check that kept DropdownMenu off the gestures table (meta#8358) and carries meta#8589's watcher-test fix and meta#8076 e6ab0b9.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
's base

fZ carries meta#8076 e6ab0b9 (DEC-22's 10 px rule and meta#8641's arena fix),
the meta#8589 watcher-test fix and the meta#8358 gate retirement; taken at this
stack's real push (LANES 01:13).
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026

This branch had an error being deployed

1 failed deployment
Preview — b57ac077 Deployed Oct 27, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants