Skip to content

feat(lab): RefreshControl pulls under a pen or a mouse as under a finger (spec:AST-094 FR43) - #8688

Draft
vjeux wants to merge 1999 commits into
meta:mainfrom
vjeux:uikit/c28-refresh-every-pointer
Draft

vjeux wants to merge 1999 commits into
meta:mainfrom
vjeux:uikit/c28-refresh-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8175 and #8665 (with #8666's pen driver). 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@01e856a...2758073; later heads only merge #8175, #8665 and #8666.

Why

With a mouse or a stylus, nothing pulls to refresh: the pull reads only a finger, so a drag at the top of a refreshable list does nothing.

What led to it

Part of the touch-presentations program (spec:AST-094). Its FR43, now on main, has gestures run for every pointer, keeping off only what it lists; the pull to refresh isn't on that list.

Options and tradeoffs

  • Keep the mouse off, as a row swipe is: FR43 doesn't list it.
  • Take the top edge under every pointer: under a fine pointer, overscroll-behavior: none would remove the page's own rubber band, with no pull to replace it for a wheel.
  • One pull for every pointer, leaving the top edge to the browser under a fine pointer.

Why this choice

The third follows FR43 with the least new behavior: every pointer shares one mapping, trigger, settle and drawing. Under a fine pointer the browser keeps its own overscroll at the page's top, and the control watches no scroll (FR24). A pen's or a mouse's pull clears the text selection its drag started, then holds selection off, as a finger's does. Under Reduce Motion a release cross-fades where a finger let go or the primary pointer is coarse (design:reduced-motion DR2) and lands at once elsewhere (DR3). A trackpad's or a wheel's scroll follows in #8697.

How to check it

In a desktop browser, on the RefreshControl story "On the page":

  1. Drag down from the top with the mouse: the content follows, the spinner turns past the trigger, and on release it settles under the spinner until the refresh ends.
  2. Start the drag on text: the selection it began is cleared, and nothing is selected while pulling.
  3. Turn on Reduce Motion and release a pull: the content lands at once.

Evidence

vjeux and others added 30 commits October 26, 2026 21:34
An attach reads the element's own will-change in a task of its own. A
shared animator (animatorFor) is never detached, so its node can leave the
document with that read still waiting, and the read then forces a style
pass on a node with no style: a test run that mounts and unmounts many
pagers piled up such reads until a later wait timed out. The read now
skips a node out of the document; a promotion that needs it reads it then,
as one on a node attached in the same task does. A detach and dispose
already cancel it.

The new test fails on the previous code (one read of a removed node); the
in-task read test now puts its element in the document, where the read
happens.
This pull request carried facebook#8561's earlier heads as a dependency. facebook#8561 is
on main now, so the lint rule's files (eslint.config.js, the plugin's
README, index.js, no-core-internal-import.js and its test) are main's,
byte for byte; this pull request's own change (the internal entries, their
tests, the export map, sync-exports and two changesets) is unchanged.
…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).
…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.
… base merge, d32c911) into the refresh pull
…sture system (spec:AST-094 FR24, FR43)

utils/pressFeedback.ts reaches gestures/coarsePointer.ts, so a page whose
only touch work is press feedback carries a gesture module, and a bundle
check that counts gestures/ code finds it in the root and hooks entries.
The coarse-pointer query belongs with the environment queries. This test
walks the controller's import graph and fails until it reaches no module
under gestures/.
…ries in utils/ (spec:AST-094 FR43)

gestures/coarsePointer.ts becomes utils/coarsePointer.ts, beside
utils/touchQueries.ts, with its test; the gesture host's selection lock,
the press controller and the pointer test helper import it from there. The
press controller's import graph now reaches no module under gestures/, so
a page whose only touch work is press feedback carries none of the gesture
system. No behavior changes.
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.
…e arena's reset

The arena resets a recognizer to possible after it ends, as UIKit does,
and the transition listener records that too, so the last transition is
possible: the test now asserts the ended-then-possible tail.
…sign/core/internal/motion

Lab no longer reaches Core's modules by path: Core's internal entries are
the route. The import-rewrite codemod moves refreshPull.ts's Bézier,
rubber-band and refresh-table imports onto @astryxdesign/core/internal/motion.
The entry names the refresh table among its re-exports from the uikit
barrel (a shared module may not import one component's table directly), and
the entry coverage test lists it.
…finger, meta#8589's watcher test, its lint fix; meta#8076 3bfd7c3; the coarse-pointer query in utils/) into meta#8293
…nter query in utils/) into meta#8356

# Conflicts:
#	packages/core/src/gestures/GestureHost.ts
#	packages/core/src/gestures/fingerScroll.ts
#	packages/core/src/gestures/pageHold.ts
#	packages/core/src/gestures/selectionLock.ts
#	packages/core/src/utils/pressFeedback.ts
… FR19, FR24)

Under a fine primary pointer: each element that uses a gesture keeps one
passive pointerdown listener and the page none while idle; a mouse or a
pen pans, its page listeners lasting only the press; a press consumer
takes a mouse press with no feedback-only mode (DEC-21); a pan writes its
touch-action for a touchscreen laptop's finger (FR18); perEventPointer is
taken as any attach, with no warning; a secondary button starts nothing.
All but the last fail until the host attaches for every pointer.
vjeux added 19 commits October 29, 2026 10:32
… attached, in this host's terms

meta#8360's action-group lifetime case pins that an element stays a group
while any attachment asks and stops with the last. Here every element
that uses a gesture is served as a group under a fine pointer, so the
same invariant reads as the element's one passive pointerdown: with two
gestures attached, detaching either keeps it, and detaching the last
removes it. It passes on this head.
…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.
…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.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
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.

This needs an engineering owner's decision before it can be approved: the branch adds a requirement to spec:AST-094 FR41 that main does not have (a mouse or pen drag that a recognizer claims takes no new selection; an unclaimed one keeps the browser's). RefreshControl's pull never claims, yet it still clears a mouse or pen drag's selection, which neither FR41 text allows. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, or by asking for a different FR41 text; the author may do so as an engineering owner. Separately from that decision:

  • Rebase onto main once the carried pull requests land, so the diff holds only the 7 RefreshControl files (537 authored lines instead of 37,012).
  • A mouse pull ends with the browser's click on the row it started on, because nothing claims the mouse or suppresses that click. A mouse case over a row that acts on click would pin the fix.
  • Under a fine pointer, a pull during a refresh the app started jumps the list 60 px down over an empty band and leaves it there until the refresh ends (refreshTouchController.ts:788-793).

[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 adds a normative FR41 requirement and, at line 663, narrows its verification row; main states neither. It needs an engineering owner's exact-head approval (/approve-spec <full head SHA>). As written it keeps an unclaimed mouse drag's released behavior, which RefreshControl's pull (a watcher that never claims) does not keep.

Comment on lines +781 to +783
if (pan.pointerType !== 'touch') {
clearDragSelection();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RefreshPullRecognizer never claims (claimsTouches returns false), so under FR41 on main, and under this branch's amendment, a mouse drag keeps its selection. Clearing it here, and refusing selection until the pull ends, waits on an engineering owner's FR41 decision.

Comment on lines +837 to +838
releaseSelection();
dispatch({type: pan.state === 'ended' ? 'pullEnd' : 'pullCancel'});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Under a mouse nothing claims this drag or takes it from the browser, so the arena resolves the lift as 'allow' and arms no click guard. The content travels with the pointer (10 px behind it), so the browser's click then lands on the row the drag began on, and a row that is a link or a button opens at the end of a pull to refresh. Arming the shared click guard (armClickGuard) when a mouse or pen pull ends, with a mouse case over a clickable row, would prevent it.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added 2 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.

This needs an engineering owner's decision before it can be approved: the branch adds a requirement to spec:AST-094 FR41 that main does not have (a mouse or pen drag that a recognizer claims takes no new selection; an unclaimed one keeps the browser's), and RefreshControl's pull, which never claims, still clears a mouse or pen drag's selection, which neither FR41 text allows. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, or by asking for a different FR41 text; the author may do so as an engineering owner. Separately from that decision:

  • Rebase onto main once the carried pull requests land, so the diff holds only the 7 RefreshControl files (537 authored lines instead of 37,052).
  • A mouse pull released while the content still tracks the pointer ends with the browser's click on the row it started on, because nothing claims the mouse or suppresses that click; a mouse case over a row that acts on click would pin the fix.
  • Under a fine pointer, a pull during a refresh the app started jumps the list 60 px down over an empty band and leaves it there until the refresh ends (refreshTouchController.ts:788-793).

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 079927a2 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