Skip to content

feat(Slider): the thumbs track as a pan for every pointer - #8669

Closed
vjeux wants to merge 1928 commits into
meta:mainfrom
vjeux:uikit/c03-slider-every-pointer
Closed

vjeux wants to merge 1928 commits into
meta:mainfrom
vjeux:uikit/c03-slider-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Slider · finger tracking

Superseded by #8190, which carries this change on main's current base. This pull request closes when #8190 merges.

Depends on #8190 (which carries #8671) and #8676 (which carries #8665 and #8666). Review only this PR's change: vjeux/astryx@93e4ce4...ce50d06 (it also shows the parents' newer commits: #8676's last two, #8671 and #8360's animator fix)

User need

Part of Astryx's UIKit-parity work (spec:AST-094): #8190 made a Slider under a finger track like iOS's UISlider, but on a desktop a mouse drag still jumped the value to the pointer. FR43 asks gestures to work for every pointer.

Problem and solution fit

  • Mouse drag: a mouse gets a pan of its own: it waits 10 px in any direction (FR19), then follows the pointer's travel along the track.
  • Primary click only: a right click or a macOS ctrl-click on the track no longer moves the value.
  • No wheel: a scroll over the slider stays the page's, in any direction.
  • Touch targets stay touch-only (FR43); a fine pointer adds one passive listener per part (FR24); a finger behaves as before.

Current specification

spec:AST-094 FR43, FR19, FR24 and DEC-21; #8905 adds FR43's exception for a control's thumb. component:Slider FR6 (from #8190) now covers every pointer.

Public delta and caller burden

No API change. Size (component-size.js, base to head): the Slider import's JavaScript 85.1 → 85.2 KB gzip, CSS unchanged.

Design and alternatives

  • Keep a mouse on the old handlers: the value jumps to the pointer, as in a range input, but a mouse and a finger would track differently, against FR43.
  • A mouse through a pan (chosen): one tracking model. The cost: a mouse drag waits 10 px. A finger's pan stays limited to the track's direction, so a swipe across scrolls the page; a mouse has no page scroll to yield to, so its pan claims in any direction.
  • Wheel: pans take scroll input by default, but a slider is a control: like a range input, it opts out rather than take the page's scroll (spec(AST-094): a thumb that sets a value takes no wheel or trackpad scroll (FR43) #8905).

Left to the owner (component:Slider, per FR13; today's behavior stays): a lift on a mouse press, a coast after its release, a track press that sets the value on release.

Evidence

The test plan below, run in Chromium and WebKit.

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.

Testing

In Storybook (Slider › Default), with a mouse:

  1. Drag a thumb: it holds for 10 px, then follows the pointer without jumping, even when the drag starts straight down.
  2. Click the track: the value moves there, as before.
  3. Right-click the track (ctrl-click on a Mac): the value stays.
  4. Scroll a wheel or trackpad over a thumb: the value stays; the scroll goes to the page.

Run on this head, locally (fork pull requests start no CI): steps 1, 3 and 4 are browser tests in Chromium and WebKit (the ctrl-click a unit test), each failing before its change and passing now; the full core suite, typecheck, lint, the builds and pnpm check:knowledge pass. #8671 per-component bundle check, carried through #8190, passes here.

Writing rubric results

  • R1 narration: none; FR6's added sentence and the Compatibility line state behavior, as those sections do.
  • R2 research: none.
  • R3 evidence: FR6's verification row names the fine-pointer browser test.
  • R4 rejected alternatives: none added.
  • R5 change references: none.
  • R6 open questions: none touched.

vjeux added 30 commits October 26, 2026 23:31
… (spec:AST-094 FR43)

A drag from the edge band begins a screen-edge pan for a finger only,
unless the caller allows another pointer. The mouse and pen case fails
until the recognizer defaults to touches.
… FR19, FR24, DEC-21)

Recognizers and press consumers attach for a mouse, a pen and a finger
alike. Where the primary pointer is fine, each element with an enabled
recognizer or consumer keeps one passive pointerdown listener and the page
has none: a press on it (a mouse's primary button, a pen, or a finger)
installs the host's page listeners until that press's contacts end. The
action group's own fine-pointer path folds into this one, so a group is
served like every attachment.

- touch-action is written for every pointer, a kept press's manipulation
  too (FR18); the element's blocking touchmove stays a coarse pointer's,
  so under a fine one the element keeps its one listener (FR24).
- A press consumer takes a mouse press as a press (DEC-21): no
  feedback-only mode; PressTracker brakes only a finger's press (FR11).
- The screen-edge pan allows touches only unless its caller allows another
  pointer (FR43).
- perEventPointer is accepted and does nothing; the fine-pointer warning
  goes.

Tests that encoded the coarse-only design now state FR43's; a finger's
pointerdown comes before its touchstart in them, as in a browser.
…th real mouse drags (spec:AST-094 FR43, FR24)

Under a desktop's mouse the host still writes touch-action, leaves the
page no listener while idle and warns for nothing; a real mouse drag pans
both the bare page's pan and the fixture's horizontal pan, and the page's
touch listeners are gone again after the release.
…(spec:AST-094 FR18, DEC-13)

A screen-edge pan attached to a stack container, as wide as the screen,
with a touch 4 px from its edge: inside the 40.2 px band the pan decides at
UIKit's 15 px whatever the element's width, and then tracks the travel past
it. From several edges too. Both fail while a wider element decides at
FR19's 10 px.
…'s width (spec:AST-094 FR18, DEC-13)

FR18: inside the 40.2 px band a screen-edge pan decides at UIKit's 15 px
screen-edge hysteresis; outside it the author's scrolling is untouched and
FR19's 10 px governs every other claim. The pan decided at 10 px on any
element wider than the band, so NavigationStack's edge pan, attached to the
stack container, began 5 px early. The edge zone still decides only the
touch-action; a touch outside every band still fails at once. The two
cases that pinned the 10 px decision go.
…px whatever the element's width) into the experimental touch exports
… move keeps that finger until it lifts) into the experimental touch exports
…than the band (spec:AST-094 FR18, DEC-22)

FR18 as amended in meta#8631 (DEC-22): the band's pan-x claim and 15 px start
hold only where the screen-edge pan is attached to an element within the
band; on an element that reaches beyond it, whose touch-action cannot
claim per touch, a touch in the band decides at FR19's 10 px like every
other claim. This restores the 10 px decision 3d85c30 removed and keeps
15 px on an element within the band. The cases pin DEC-22 both ways (a wide
element with a touch at x=4 begins at 10, an in-band strip at 15, and from
several edges at 10 on a wide element); the 10 px cases fail on 3d85c30.
…he edge band decides at 10 px again, DEC-22) into the experimental touch exports
…ouch decides at 15 px on an element within the band and at 10 px beyond it, DEC-22; meta#8641 bec2283) into meta#8270
…8641's moving-touch claim kept on this line's internals delivery) into meta#8315

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
The case walked ContextMenu, DropdownMenu, useLongPress and useMenuPress's
imports and failed on the UIKit gestures table: it kept that table out of
the menus' bundles, the lazy design the no-lazy-load ruling and direct
imports retire. The long-press numbers keep one source, still pinned by the
table, recognizer and hooks case and by the hooks reaching longPress.ts.
…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
# Conflicts:
#	.github/workflows/ci.yml
…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.
vjeux added 10 commits October 29, 2026 13:25
… thumb

A mouse's first 12 px straight across a horizontal slider, then 60 px
along it: the thumb should move by the 60 px, since a mouse has no page
scroll to yield to. Today the thumb's pan, limited to the travel's
direction, fails on the first movement, and the thumb never moves.
…thumb

The thumb's pan was limited to the travel's direction for every pointer,
so a mouse drag whose first 10 px ran across the travel failed it and the
thumb never moved. A mouse now has a pan of its own that claims in any
direction after the same 10 px, and the value follows the drag's travel
along the axis, counted from the threshold's projection on it. A finger
and a pen keep the pan limited to the travel, so a swipe across the track
still scrolls the page.
…se drag

Slider.doc.mjs said a mouse keeps the pointer behavior, the changeset
counted listeners, and the primary-click comment named a pen rule the
gesture host does not apply. They now describe the mouse drag, past the
same 10 px in any direction, and the primary click. component:Slider FR6
covers every pointer (spec:AST-094 FR43): a mouse drag tracks past the
same threshold in any direction, a press on the track sets the value where
a primary click lands, and a scroll over the slider stays the page's.
Inside a scrolling sheet a wheel or trackpad scroll over a slider goes to
the sheet, not the page, so the record, the docs, the changeset and the
controller's comments say it goes to the scroll under the slider, as
spec:AST-094 FR43's exception for a control's thumb does.
…ame window

Both 'a dip held before its first frame lands once the hold is …' proofs
waited for no running animation, then one frame and a timeout, assuming
the next step starts within a frame. The dip lands on a timer due on the
call's own clock, though, and the proofs passed only because the evaluate
round trips covered the gap: in WebKit on a loaded box the window closed
first and the proof read the element mid-dip (opacity 0, translate none;
reproduced 1 in 4 with the round trips removed). They now wait on the
call's own finished, then on the browser's own animations still running
or waiting for their first frame, and read nothing from the animator.
…ment at once and settles then

A Reduce Motion dip whose landing runs more than its whole fade back in
late (a hold released long after the dip was due) played a 1 ms opacity
animation, which WebKit keeps pending until its next frame, and settled
the call on a 1 ms timer: the call's finished could resolve while the
element still showed opacity 0. The animator's notes say the element
appears at once there. Fails now: the landing plays one opacity animation
and the call has not settled.
…element at once and settles

A Reduce Motion landing that runs at least the whole fade back in late
had nothing left of the cross-fade, but still played a 1 ms opacity
animation and settled the call 1 ms later. WebKit keeps that animation
pending until its next frame, so the call's finished could resolve with
the element still at opacity 0. Such a landing now writes the end opacity
at once (handing a borrowed one back to the stylesheet), plays nothing,
and settles the call then, as the animator's notes say.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:38
@vjeux

vjeux commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ce50d06, answering the Oct 28 review point by point:

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

Approval waits on an engineering owner: the slider's thumb pans take no wheel or trackpad scroll, which AST-094 FR43 on main still requires of a pan until the amendment in #8905 lands, and the new every-pointer clause in component:Slider FR6 needs owner approval. An engineering owner records the decision by landing that FR43 amendment and commenting /approve-spec ce50d067e2324902a7f6c0b5c3db9669531a3ded here, or by rejecting the exception so the thumb's pan takes wheel input; the author may do so if they are an engineering owner. Whatever that decision, these changes are needed:

  • Rebase once #8190, #8665, #8676, #8666, #8671 and #8360 land, so the diff against main holds only this change (36,403 authored lines today; its own ten commits touch about 715).
  • A mouse drag that starts diagonally counts up to 2.9 px of the 10 px threshold when the pan begins (sliderTouchController.ts:531-534); subtract the travel the recognizer swallowed and add a diagonal test.
  • A pen under a fine pointer now lifts, coasts and sets the value on a track tap's lift, which FR6, the docs and the description do not describe.
  • Add ## Writing rubric results for the two edited records, as AGENTS.md asks of a record pull request.

[Automated review]

Comment on lines +531 to +534
interaction.beganOffset = interaction.isMouse
? distance > 0
? (pan.hysteresis * along) / distance
: 0

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 mouse pan begins once either axis reaches 10 px (a square in PanGestureRecognizer.tryBegin), but this subtracts the projection of a 10 px circle, so a 45° start counts about 2.9 px of threshold travel at once, against AST-094 FR17 (“its translation excludes that travel”). Subtracting what the recognizer swallowed, Math.sign(along) * Math.min(Math.abs(along), pan.hysteresis), keeps straight starts as they are; a diagonal test would pin it.

| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled (under a finger, once the release's coast settles; FR6), or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved |
| FR6 | Under a coarse primary pointer, after hydration (`spec:AST-094/FR13`), each thumb takes a finger or a pen through an invisible touch target of UISlider's measured 48 pt; on the side facing a range's other thumb it grows only to half the gap between them (`spec:AST-094/FR42`). A drag on a thumb moves the value by the finger's travel past `spec:AST-094/FR19`'s 10 px threshold over the track's length, writing each move to the thumb with no component render (`spec:AST-094/FR9`). The release coasts on UISlider's measured spring toward 0.4 of the momentum projection. A tap on the track sets the value when the finger lifts; a swipe across the track scrolls the page. Under reduced motion the thumb does not grow, a tap's value applies at once, and the coast continues (`design:reduced-motion/DR2`). Under every other pointer (`spec:AST-094/FR43`) a mouse drag on a thumb moves the value the same way, past the same threshold in any direction; a press on the track sets the value where a primary click lands; and a wheel or trackpad scroll over the slider goes to the scroll under it. | UIKit measurements (`motion/uikit/slider`) and maintainer decision (DEC-4) | Approved |

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 clause covers a mouse, but under a fine primary pointer a pen now takes the finger path: Slider.tsx leaves every pen press to the gesture host, so a pen on a thumb lifts and coasts and a pen press on the track sets the value only when its tap ends. AST-017 FR59 asks for the owner decision on a new fine-pointer feel in this record, so the pen needs a sentence here (or the mouse path).

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…R43, DEC-23)

FR43 gives every pan wheel and trackpad scrolling, with no exception, so a
slider's thumb, which tracks a drag through a pan, would read a page scroll
passing over it as a change of value. FR43 now excepts a control's thumb:
a scroll over it stays the page's. DEC-23 records why, and FR43's
verification row names the case. meta#8669 is the implementation.
cixzhang pushed a commit that referenced this pull request Oct 31, 2026
…croll (FR43) (#8905)

<!-- template: specification.md -->

## Exact decision needed

This comes from Astryx's UIKit-parity work (`spec:AST-094`), which brings iOS's touch behavior to the web components. FR43 said a pan takes wheel and trackpad scrolling alike. Read literally, a few quick mouse-wheel notches would move or close a sheet, and a page scroll that passes over a slider would change its value. Two answers are needed: which scroll input drives a pan, given that the web reports no trackpad contact or phase, and whether a thumb that sets a value takes any. Slider's every-pointer change, #8669, needs the second.

## Existing authority searched

FR3, FR19, FR36, FR37, FR41, FR43, DEC-1 and DEC-21 in `spec:AST-094`; `spec:AST-017` FR59; `spec:AST-057` DEC-7; `component:Slider`; `component:BottomSheet`; and the gesture host's per-recognizer scroll types. Touch work keeps released mouse behavior (AST-017 FR59, AST-094 FR41), and native parity (DEC-1) matches a range input, which ignores the wheel. No record told a trackpad from a mouse wheel or said where a trackpad gesture starts and ends. All 51 other open pull requests that edit AST-094 were checked at this head, and none changes FR43, DEC-23 or DEC-24.

## Decision

- **FR43:** a pan takes a trackpad's two-finger scroll where its record allows it, and never a mouse wheel's notches. A scroll counts as a trackpad's only when it carries the trackpad signature Chromium and WebKit report. Any other scroll, Shift+wheel and a tilt wheel included, is a mouse wheel's, and scrolls what is under it.
- **DEC-24** (decided by the record's owner): one splitter reads every trackpad stream, since the web reports no trackpad contact, phase or momentum. It ends a gesture on a pause and reads the fingers' lift from a shrinking run of deltas. Momentum never moves a surface. A reversal, a rising delta or a gap starts a new gesture. While the splitter has not yet decided between a lift and fingers still moving, the surface keeps moving at the fingers' last speed, never farther than that speed carries it over the held deltas, so a lift continues without a stop. A pan released at the lift keeps the fingers' velocity (FR36).
- **Surfaces that follow a trackpad:**
  - A BottomSheet follows a trackpad as it follows a finger. A `form` or `required` sheet stretches on FR3's rubber band and springs back; an `info` sheet closes on a flick or past its close distance; a mouse wheel never moves a sheet.
  - A pager moves at most one page per swipe.
  - A swipe row follows the fingers, settles to the nearest detent, and only closes within one gesture.
- **DEC-23** keeps its exception: a thumb that sets a value, a slider's or a switch's where it pans, takes neither, so a scroll over it goes to the scroll under it.
- **Engines:** a trackpad drives a pan only in a browser that reports `wheelDelta` (Chromium and WebKit). Elsewhere every scroll counts as a mouse wheel's.
- **Verification:** FR43 gains a trackpad verification row.

## Rejected alternatives

- DEC-24: a mouse wheel driving a pan; a discrete step per trackpad swipe for rows and paging; a longer end timeout instead of the splitter.
- DEC-23: the thumb taking scroll input like other pans.

## Claim scope and non-goals

In scope: which scroll input a pan takes and where a trackpad gesture starts and ends; the surfaces that follow it (BottomSheet, the shared pager, swipe rows); and a thumb that sets a value. Touch input, a pinch's ctrl-wheel, and keyboard and focus behavior stay outside this decision.

## Evidence and implementation relationship

- Slider's every-pointer change (#8669) implements the thumb rule: its thumb pan opts out of scroll input. Browser tests there scroll a wheel and a trackpad over a thumb.
- The trackpad rule's implementation, with its unit tests on timed wheel streams and WebKit trackpad tests, lands in the gesture and component pull requests that follow FR43 (#8676 and its dependents).

## Writing rubric results

- R1 narration: none. The rules are stated in the present tense, and the `component:BottomSheet` line under affected records names the finger's one-for-one drag below rest as an implementation gap.
- R2 research in the record: none. DEC-24 keeps its reasons as two sentences inside the decision.
- R3 evidence naming a change: none. FR43's verification rows name the behaviors and engines tested.
- R4 rejected alternatives over one line or outside a decision: two, each one line inside its decision (DEC-24, DEC-23).
- R5 pull request or issue references remaining: none in the record.
- R6 ruled open questions still listing options: none touched.

## Scope

- [x] This PR contains specification records only.
- [x] It records observable behavior and preserves equivalent internal implementations.
- [x] Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
- [x] Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
- [x] It records one intentional durable decision, not a rescue for a separable tagalong.
- [x] The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
- [x] Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
- [x] Public text and artifacts contain no internal Meta context.

## Validation

Local gates at a0bf771: `pnpm check:knowledge` (records aligned), Prettier, `git diff --check`, and `pnpm check:repo`.
@vjeux vjeux mentioned this pull request Oct 30, 2026
4 tasks done
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #8190, merged as 723e045.

@cixzhang cixzhang closed this Nov 2, 2026
@vjeux
vjeux deleted the uikit/c03-slider-every-pointer branch November 2, 2026 22:18
@vjeux
vjeux restored the uikit/c03-slider-every-pointer branch November 2, 2026 22:20
@vjeux
vjeux deleted the uikit/c03-slider-every-pointer branch November 2, 2026 22:21
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…nder a mouse

One change for Slider's touch presentation, with what was meta#8189 (UISlider's
measured values in the UIKit constants table) and meta#8669 (Slider for every
pointer) folded in:

- Under a coarse primary pointer each thumb takes a finger or a pen through a
  48 pt touch target, clamped to half the gap facing a range's other thumb
  (spec:AST-094 FR42). A drag moves the value by the finger's travel past the
  10 px threshold, writing each move to the thumb with no render (FR9); the
  release coasts on UISlider's measured spring; a tap on the track sets the
  value on lift; a swipe across the track scrolls the page.
- A mouse drag on a thumb tracks the same way, in any direction (FR43); only a
  primary click presses; the thumb takes no wheel or trackpad scroll.
- Under Reduce Motion the thumb does not grow, a tap applies at once, and the
  coast keeps moving (design:reduced-motion DR2).
- component:Slider records it (FR6, FR5's coast end, DEC-4).

Left out, as no record decides it yet: the gesture host's text-selection lock
during a mouse drag.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…nder a mouse

One change for Slider's touch presentation, with what was meta#8189 (UISlider's
measured values in the UIKit constants table) and meta#8669 (Slider for every
pointer) folded in:

- Under a coarse primary pointer each thumb takes a finger or a pen through a
  48 pt touch target, clamped to half the gap facing a range's other thumb
  (spec:AST-094 FR42). A drag moves the value by the finger's travel past the
  10 px threshold, writing each move to the thumb with no render (FR9); the
  release coasts on UISlider's measured spring; a tap on the track sets the
  value on lift; a swipe across the track scrolls the page.
- A mouse drag on a thumb tracks the same way, in any direction (FR43); only a
  primary click presses.
- Under Reduce Motion the thumb does not grow, a tap applies at once, and the
  coast keeps moving (design:reduced-motion DR2).
- component:Slider records it (FR6, FR5's coast end, DEC-4), and Slider joins
  the bundle check's list of gesture users.

Left out until a spec decides them: the text-selection lock during a mouse
drag, and wheel and trackpad scrolling, which no pan takes here.

This branch was successfully deployed

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