Skip to content

feat(lab): RefreshControl pulls under a trackpad's or a wheel's scroll at the top (spec:AST-094 FR43) - #8697

Draft
vjeux wants to merge 2052 commits into
meta:mainfrom
vjeux:uikit/c28-refresh-trackpad
Draft

vjeux wants to merge 2052 commits into
meta:mainfrom
vjeux:uikit/c28-refresh-trackpad

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8688 (the pen and mouse pull) and #8676 (wheel and trackpad input). Carries #8562 (Core's internal entries) as a dependency; if the entries land differently, this follows her decision. Review only this PR's change: vjeux/astryx@933330e...e6dbcd1; later heads only merge #8688 and #8676.

Why

On a laptop, a trackpad's two-finger scroll or a mouse wheel at the top of a refreshable list only rubber-bands the page: without a touch screen or pen, nothing pulls to refresh.

What led to it

Part of the touch-presentations program (spec:AST-094), which gives Astryx native-feeling touch behavior. Its FR43, now on main, has a pan take wheel and trackpad scrolling. #8688 made a pen's and a mouse's drag pull; this adds a trackpad's and a wheel's scroll.

Options and tradeoffs

  • Let the touch pull take scrolls too: simplest, but it would take scrolls mid-page.
  • A second pan that takes only scrolling, only at the top: one more recognizer, and every other scroll stays the page's.
  • Leave wheel notches out: FR43 has no such exception. Keeping them costs this: one 100 px notch (about 76 px of pull) starts a refresh on a list under about 369 px tall.

Why this choice

The second pan, with notches in, follows FR43 as written. It claims the scrolls it takes, so its blocking wheel listener is a claimer's (FR20); the touch pull stays a watcher. It drives the same pull as a finger and keeps the page's text selection. Whether a notch should pull less on short lists is asked on #8646.

How to check it

In Chromium or WebKit, on the RefreshControl story "On the page":

  1. At the top, scroll up with a trackpad: the content pulls down, refreshes past the trigger, and settles under the spinner.
  2. Scroll down the page, then back up short of the top: only the page scrolls.
  3. In a 714 px tall window, one mouse-wheel notch at the top shows the indicator and springs back; three notches in one scroll refresh.

Evidence

vjeux added 30 commits October 27, 2026 00:56
…8589's watcher test reads its end; the gestures-table gate retired) into meta#8360
…8589's watcher test reads its end; the gestures-table gate retired) into the every-pointer host
…xposes no ontouchstart is taken once

Chromium fixes ontouchstart when the page is created, so turning touch
emulation on later sends Touch Events without it: the host reads Pointer
Events for the finger and must not read its Touch Events too. A one-finger
long press fails here, the finger tracked twice.
…from their pointer events

install() reads a finger from its pointer events when the window has no
ontouchstart, but the four touch handlers still took the Touch Events a
browser sends anyway (Chromium after touch emulation turns on), tracking
the finger twice, so a one-finger long press never recognized. Each touch
handler now returns while usesTouchEvents is false, after noting the
fingers down, as install()'s comment promised.
…n before the click guard

internal/motion.ts (meta#8562) re-exports UIKIT_CLICK from the barrel, so a
tree carrying both fails typecheck and the Storybook build since this
pull request dropped it. The barrel keeps it, with the click guard's
window, and the table of every gesture group still leaves it out. Fails
until the barrel exports it.
…'s window

This pull request dropped UIKIT_CLICK when the click guard took the
stray-click window over, but importers written before it read the barrel's
UIKIT_CLICK (internal/motion.ts in meta#8562 re-exports it), so a tree carrying
both failed typecheck and the Storybook build. UIKIT_CLICK stays in the
barrel, deprecated, with the guard's 400 ms (CLICK_GUARD_WINDOW_MS), and
UIKIT_GESTURES still leaves it out.
…ter proofs

spec:AST-094 FR43 has gestures attach for every pointer, so each FR43 proof needs a pen and a wheel as real engine input, not only a finger. webInputDriver(page) adds wheel({x, y, dx, dy, ctrl}) and pen(points): in Chromium through CDP Input.dispatchMouseEvent, with a wheel step's deltas scaled back so the page receives exactly its CSS px (Chromium divides them by the device scale factor times the visual viewport's scale: a raw 120 px step reached a phone page as 40), and a pen stroke with pointerType pen, pressure, tilt and scripted timestamps; in WebKit through page.mouse.wheel in a desktop context. WebKit's phone emulation has no wheel and Playwright has no pen for WebKit: the driver's wheels and pens flags say so, with WHEEL_SKIP and PEN_SKIP. The input log also records wheel steps and pointer pressure.
…ter events) into the every-pointer host

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
… follow-ups

A helper for lanes on F2's line: the harness files (README, the harness self-test, index.ts) resolved by the harness owner, keeping both sides' instruments.
…k) into the FR43 harness helper

Keeps the helper's harness self-test green in WebKit too: meta#8535 fixes the backward case's flake under load.
…Core's internal entries

Main's internal/touch, its test, the touch-support bundle test, the
internal-entries test, sync-exports and the exports map are kept exactly;
this branch adds only what main lacks: internal/motion, the named
re-exports Lab builds on (the pan recognizer, the gesture types, the touch
presentation queries), their tests and the COVERED list.

# Conflicts:
#	packages/core/package.json
#	packages/core/src/internal/touch.test.ts
#	packages/core/src/internal/touch.ts
#	scripts/core-internal-entries.test.mjs
#	scripts/sync-exports.js
#	scripts/touch-support-bundle.test.mjs
On a page whose wheel listeners are all passive (every page with the input log), Chromium answers Input.dispatchMouseEvent before the page has the wheel step and delivers it afterwards, so a log read right after wheel() could miss it. wheel() now arms a one-shot passive capture listener before the dispatch and waits until the page has the step (2 s limit), in WebKit too. The ctrl-only note was wrong: a ctrl step reaches an all-passive page as well, and nothing zooms. The docs cite spec:AST-100 FR4, which is current.
Bundled with the size report's esbuild options, none of Core's public
entries carries a byte of gestures/ until a component imports it
(architecture:react-component-runtime INV13). The click guard in utils/
imports the haptic switch's attribute from gestures/, so the root and hooks
entries carry gestures/hapticSwitch.ts: both cases fail until it moves.
…blic entries carry no gesture code

The click guard (utils/clickGuard.ts) imported HAPTIC_SWITCH_ATTRIBUTE from
gestures/hapticSwitch.ts, which put gesture code in Core's root and hooks
entries. The attribute moves to utils/hapticSwitch.ts, as the coarse-pointer
query did; the click guard, the gesture host and haptics read it there, and
gestures/hapticSwitch.ts re-exports it, deprecated, for importers written
before the move. The module-graph case follows the new path.
vjeux added 22 commits October 29, 2026 11:32
This pull request carried meta#8666 only up to b246458; meta#8666's head adds
its main merge and its later harness text. The two conflicts are meta#8666's
harness files, resolved with meta#8666's author keeping both sides:
- internal/interaction-harness/README.md: meta#8360's module and
  engine-limits tables with meta#8666's webInput row and its wheel and pen
  rows, both engine columns (the same text as meta#8666 merged onto meta#8360);
- harness.interaction.browser.spec.ts: meta#8360's header with meta#8666's wheel
  and pen clause, and every test from both sides; this pull request's own
  changes to the backward-run timing stay.
…esture host's subscriptions

The gesture host subscribes no click: the one cancelling click subscriber
is the click guard's, added at a window's first arm and kept. The release
note, the stream's header and the gestures README listed click with the
host's touchend and contextmenu; they now name the guard's own.
meta#8360's input-stream notes no longer list click among the gesture host's
subscriptions. The merge is clean.
… lift and the browser's menu, keeps a kept press's finger, and goes when the page hides

From the review of this pull request, three cases fail on its head:
- an armed press installed the page listeners without the claiming
  ones, so a claimed finger's touchend stayed passive and the browser's
  menu opened during a claimed long press (spec:AST-094 FR21);
- a kept press got its blocking touchmove during an armed press, but its
  moves were cancelled only where the primary pointer is coarse, so a
  held stepper on a touchscreen laptop lost its finger (FR11, DEC-21);
- a page hidden mid-press cancelled the press but kept the page's
  listeners until the next lift anywhere (FR24).
…lift and the browser's menu, keeps a kept press's finger, and goes when the page hides

- A finger's or a pen's armed press now brings the claiming listeners
  (the cancelable touchend and the contextmenu canceller) with the page
  listeners, as wherever the host installs, so a claimed lift clicks
  nothing and a claimed hold opens no browser menu (spec:AST-094 FR21). A
  mouse's press needs neither and adds neither.
- A kept press keeps its finger under any pointer: its target blocks and
  its moves are cancelled once its press is delivered, not only where the
  primary pointer is coarse (FR11, DEC-21).
- A page hidden mid-press releases the armed press as a blur does, so the
  page's listeners go with the cancelled press (FR24).
…'s selection

An action group's press is a long press with no minimum duration, so it
claims at the press's pointerdown, before any movement. The drag
selection hold then cleared the page's selection, so a plain click on a
group's button left its Copy or Cut with nothing to act on. The case
fails on this head: the selection is gone after the pointerdown.
…age's selection

The drag selection hold cleared the page's selection when any mouse or
pen contact was claimed, but an action group's press claims at its
pointerdown, before it has moved or selected anything, so a plain click
on a group's button left its Copy or Cut with nothing to act on. A claim
before any movement now keeps the page's selection: the hold still
refuses a new one, and puts the page's back over a selection that
appears while it runs. A claim after movement clears what its drag
selected, as before (spec:AST-094 FR41).

The case before the new one now waits for its hold to end a task after
the lift, so no hold outlives that test.
…t every pointer does

- GestureHost's header, the gestures README and coarsePointer.ts no
  longer describe the coarse-only host: a finger or a pen takes the
  touch-down lock under any primary pointer, a kept press's finger is
  kept during a fine pointer's press too, and recognizers attach for
  every pointer while the page listeners last a press (spec:AST-094 FR19,
  FR24, FR41, FR43).
- The fine-pointer selection case asserted the replaced rule and passed
  only because it read iOS WebKit's inline lock without iOS WebKit: it
  now asserts the lock a fine pointer's finger and pen take, and that a
  mouse takes none.
- FR41's sentence and its verification row say that a press claimed at
  its own pointerdown, before it moves, keeps the page's selection, while
  a drag that moved before its claim loses what it selected.
…ion, in the present tense

The sentence said a drag that moved before its claim loses the selection
it started, but the hold clears the page's selection then, whatever made
it; and an unclaimed mouse drag kept "today's" behavior, a phrase that
ages. It now says a claim after movement clears the page's selection, a
press claimed at its own pointerdown keeps it, and an unclaimed mouse
drag keeps its released behavior.
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; meta#8360's
input-stream notes. The merge is clean.
…6c02fc, the harness README, spec and index.ts as in 801dfa6750 (meta#8666 on meta#8360)
…nto the pen and mouse pull

The harness README and browser spec take the helper's verified versions.
…he page presented

Two proofs failed on a loaded machine with no defect in the control:
- the busy-main-thread settle counted the screencast's delivered frames
  against its cadence. It now hands the compositor's own frame record to
  busyWindowProof, whose verdict alone judges the settle; a settle paused
  before the block fails it.
- the catch read the settle's position before the touch, which lands later
  under load. A capture listener now records where the settle stood as the
  finger landed; a catch that restarts from zero travel fails it.
The harness browser spec takes meta#8676's version (35445d0), which already
carries meta#8666 and meta#8665; the merge had dropped meta#8676's spec timing change.

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

Approval waits on an engineering owner's decision about new spec:AST-094 FR41 text this branch carries ("A mouse or pen drag that a recognizer claims takes no new text selection for that drag only…"), which is not on main. An engineering owner records that decision by commenting /approve-spec with this pull request's full head SHA, which the author may do as an engineering owner, or by landing the text on main through its own pull request. The trackpad and wheel pull itself matches FR43, which is current on main.

  • Please rebase once the carried pull requests land, so the diff holds only the RefreshControl change (639 of its 38,912 authored lines).
  • scripts/touch-support-bundle.test.mjs fails on this branch because useScrollLock.ts imports gestures/pageHold.ts into Core's root and hooks entries; it needs #8671 first, or that import kept out of those entries.

[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 FR41 text is not on main, and no engineering owner has approved it at this head. Approval of this pull request waits on that decision: an engineering owner comments /approve-spec with the full head SHA, or the text lands on main through its own pull request first.

vjeux added 3 commits October 29, 2026 18:31
…dy part

The proof failed on a loaded machine with one long task: it judged long
tasks over the whole trace, the pull's trigger and the release's render
included, while Layout and Paint were judged only in the steady part. Under a
4x CPU throttle the release's last commit frame also landed past the fixed
50 ms start. The steady part now starts once the page has rendered three
frames after the settle began, counted in frames, and long tasks are judged
there too. It passes 4 of 4 under the throttle; a paint on every frame of the
settle, or an 80 ms block inside it, still fails it.

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

Approval waits on an engineering owner's decision about the new spec:AST-094 FR41 text this branch carries ("A mouse or pen drag that a recognizer claims takes no new text selection for that drag only…"), which is not on main; the same decision settles whether RefreshControl's mouse pull, a watcher that never claims, may clear the drag's selection. An engineering owner records it by commenting /approve-spec with this pull request's full head SHA, which the author may do as an engineering owner, or by landing the text on main through its own pull request. The trackpad and wheel pull itself matches FR43, which is current on main, and the settle proof's new window looks sound.

  • Please rebase once the carried pull requests land, so the diff holds only the RefreshControl change (639 of its 38,952 authored lines).
  • scripts/touch-support-bundle.test.mjs fails on this branch because useScrollLock.ts imports gestures/pageHold.ts into Core's root and hooks entries; it needs #8671 first, or that import kept out of those entries.

[Automated review]

This branch had an error being deployed

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