Skip to content

feat(SegmentedControl): a mouse or a pen drags the selection as a finger does (spec:AST-094 FR43) - #8724

Closed
vjeux wants to merge 1992 commits into
meta:mainfrom
vjeux:uikit/c05-segmented-every-pointer
Closed

vjeux wants to merge 1992 commits into
meta:mainfrom
vjeux:uikit/c05-segmented-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8127, #8665, #8682, #8666. Review only this PR's change: vjeux/astryx@e2d6971...e7243b2

User need

Astryx's touch work now covers every pointer: spec:AST-094 FR43 (current since #8646) puts press tracking on a mouse and a pen too, and its exceptions don't include this drag. Today, with a mouse or a pen, SegmentedControl's selection can't be dragged, and once focus is in the control a press on another segment selects it as the button goes down.

Problem and solution fit

  • A drag from the selected segment carries the highlight and the release selects the nearest segment (onChange once); a drag that starts elsewhere selects where it lifts.
  • Under a mouse the highlight stays under the pointer and settles on the theme's spring; clicks and keys keep today's color fade.
  • Color transitions pause during the drag, so the selected look doesn't flicker.

Current specification

spec:AST-094 FR43 and its non-goals; the mouse press highlight comes with #8682. component:SegmentedControl FR7–FR10 (#8596, a draft) still say "under a finger"; widening them is the record owner's to do.

Public delta and caller burden

No API or theme change. With focus already in the control, a mouse click now selects at release rather than at pointerdown, which drag-to-select under FR43 requires. Keyboard and screen readers are unchanged.

Design and alternatives

  • Chosen: one tracker for every pointer, with theme timing under a mouse, so touch and mouse behave alike.
  • Rejected: keeping drag-to-select touch-only (FR43's exceptions don't name it); iOS springs under a mouse (FR43 leaves desktop timing to the theme); selecting at press (a drag that starts elsewhere would call onChange twice).

Evidence

Chromium with a mouse (red dot), Grid to Table: the highlight follows the pointer while nothing is selected, the release selects Table, and it settles there
Chromium with a mouse, List to Table after a click left focus in the control: nothing is selected at the press, the release selects Table, and no highlight moves
Chromium, a mouse click on List: today's color fade, no moving highlight

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.
  • Size: 589 lines, 9 files (316 tests); the SegmentedControl import 77.0 → 77.3 KB gzip (+0.3 KB; size report); CSS +0 B gzip.

Testing

  1. In the "Default" story with a mouse, press Grid and drag to Table: the highlight follows; the release selects Table.
  2. Click Grid, press List and release on Table: only the release selects (Table).
  3. Click List: it selects with the color fade.
  4. With a pen, a drag works like the mouse.

Automated, written to fail first and now passing (a mouse or pen drag selected nothing; a press after a click selected at pointerdown): pnpm test:interaction SegmentedControl.interaction drags with a real mouse (Chromium, WebKit) and pen (Chromium); unit tests cover the drag, the settle and Reduce Motion. Known red: touch-support-bundle "is absent from the root entry" (#8671). A pr-rtl red is main's newer RTL test, not this change.

cixzhang and others added 30 commits October 26, 2026 20:07
…inter reaches its recognizers) as a dependency
… under a fine pointer) as a dependency
…tion group's finger under a fine pointer) into the gesture watcher
Main's meta#8521 is its head 25b468c squashed, which this pull request already
carries: every file it touches is byte-identical to 25b468c. In the two
files that conflicted (the motion README and animator.ts), the resolution is
this side's version, main's merged meta#8521 with this pull request's later work
on top. Nothing of main is dropped.
…ta#8521) into meta#8270

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
…g) into meta#8293

# Conflicts:
#	packages/core/src/motion/animator.ts
The note's reword joined its next sentence onto one long line; it now
wraps like the rest of the file. No code change.
cixzhang's meta#8589 keeps the host's touchend passive, and its contextmenu
and click listeners off, until a claimer is attached
(syncClaimingListeners). Ported onto this pull request's input stream with
no change to what it does: install() subscribes the touchend passive, and
syncClaimingListeners swaps it for a cancelling subscription and adds the
contextmenu and click subscriptions while a claimer is attached. A group
press brings its own click subscription only while the host has none, and
the host's takes it over. Her watcher test reads those listeners on the
window, where the input stream registers them; her new group test (a
finger, then a mouse, under a fine pointer) sits before the detach test.
The test file and the bare fixture import both her
ScreenEdgePanGestureRecognizer and this layer's selection lock from utils.
Here the click guard (utils/clickGuard.ts) owns clicks, so cixzhang's
meta#8589 syncClaimingListeners, ported at meta#8356, switches only the touchend's
cancelability and the contextmenu subscription; the group press subscribes
no click of its own. Her meta#8072 5c59af1 (a finger on an action group
under a fine pointer reaches its recognizers) arms the click guard where it
armed the swallower. Her watcher test reads the touchend and the
contextmenu on the window, and no click listener: the guard's comes only
once a gesture acts.
… the lift, under real touch input

spec:AST-094 FR20's "follows the scroll to the lift" had only a jsdom
test with a synthetic takeover; the FR18-FR22 verification row asks for
real touch input. In Chromium with injected touches, a vertical drag on a
list a watcher pan is attached to scrolls the list natively, no touchmove
is cancelled, and the watcher goes from began to ended at the lift.
… at the edge stays the page's

spec:AST-094 FR10: two fingers before a claim are the page's pinch. The
scroll-edge hold (DEC-18) counted any finger, so with two fingers down it
cancelled the pinch's moves before any drag was claimed. It now holds only
while a single finger is down. The new two-finger test fails on the
previous head.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…s watcher browser test) into the arena fixes
vjeux added 14 commits October 27, 2026 13:38
… paint

Button.spec.md FR15: main's text (meta#8613: the 200 ms release at tempo 1,
scaled by the theme's tempo, spec:AST-108 FR4) with this pull request's
own wording kept; the measured 250 ms release is likewise stated at
tempo 1.
…busy first frame outran

pressFeedback.ts removes data-astryx-press="fading" on a timer of the
release's duration, started at the write. When the main thread is busy
right after the lift, the release animation starts only at the first frame
after the block, so the timer removes the release partway through it: on
a list row after a 150 ms block, 0.736 -> 0.558 -> gone about 52 ms in.
These fail on that code:

- pressFeedback.test.ts: with a release animation the stylesheet started
  late (or still waiting for its first frame) when the clock runs out,
  the controller's release and another painter's (paintPress) stay
  fading until the animation finishes. Guards: a cancelled animation ends
  it, no animation ends it on the clock (reduced motion's instant
  release, a surface without a press layer), a descendant's, an endless or
  an earlier animation is not waited for, and a new press keeps its paint
  when the old release ends later.
- ButtonPressRelease.interaction.browser.spec.ts, both engines: after
  150 ms of script at the release, the button's layer keeps the release
  until its strength reaches 0.
…y first frame started it

endAfterRelease replaces the clock that removed data-astryx-press="fading"
at the release's duration after the write, for the controller's release
and for every other painter's (paintPress). The clock now only looks: at
the release's duration, a release animation the write started on the
surface (one of its own CSS animations, on it or its ::before or ::after,
finite, started at or after the write or still waiting for its first
frame) is waited for until it finishes or is cancelled, so a busy main
thread that starts the animation late ends it late too, at strength 0.
With no such animation (reduced motion's instant release, a surface
without a press layer, an engine without Web Animations) the release ends
on the clock, as before. Any new write, or a new press, cancels the wait.
A slot's pending clock is now a cancel function, so the release's wait
and every timer clear the same way.
…join it), so a compare from it shows only this PR's change
…ompare from it shows only this PR's 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 pull request needs an engineering owner's spec decision before it can be approved, and two fixes are required either way. An engineering owner (the author is one) can approve the new segmented.selectionSpring row in spec:AST-094 FR4, the reworded component:Button FR15 release timing, and SegmentedControl's mouse and pen drag-to-select, whose component record is still draft, including its release-time selection and the spring its release plays, by commenting /approve-spec <full head SHA> on this pull request, or say which of them to drop or amend.

  • The comparison with main carries 46,752 authored lines from #8127, #8665, #8682 and #8666, so please land those first and rebase until this pull request shows only its own 9-file change.
  • The new layout effect at useSegmentedSelectionMotion.ts:1526-1528 needs a comment naming the system it synchronizes, as architecture:react-component-runtime INV3 requires.
  • The earlier request to retime or reword the fine-pointer settle is withdrawn: main now lets a theme's tempo and character reach springs.stateChange, so which motion that release plays is part of the decision above.

[Automated review]

@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 Nov 2, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 2, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…s with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from meta#8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines meta#8094, meta#8127 and meta#8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
cixzhang added a commit that referenced this pull request Nov 3, 2026
…s with any pointer (#9632)

feat(SegmentedControl): the selection slides on every change and drags with any pointer

Under a finger the selection slides between segments on
UISegmentedControl's measured spring (the internal role
`segmented.selectionSpring`), for taps, arrow keys and programmatic changes
alike; under Reduce Motion it cross-fades in place. A press shows the shared
pressed overlay and nothing dims. Dragging the selected segment carries the
selection with the pointer and commits once, at the lift, on the segment
under it; a lift outside the control selects nothing. Under Reduce Motion
a drag still follows the pointer, and only the release cross-fades. A mouse
or a pen drags the selection the same way, every change slides on the
theme's timing, a click included, and a press while focus is in the
control no longer selects before the click.
useMediaQuery, which SegmentedControl now reads, returns its server default
where the window has no matchMedia, instead of throwing.

The SegmentedControl record states the slide, the drag, the label color and
the commit at the lift (FR9-FR13, PR2), carried over from #8596's
owner-decided clauses as built. Both SegmentedControl records drop the
implementation gaps this closes, and the touch presentation spec names the
new motion role. The action-group tracker owns the tap path's click helpers,
which SegmentedControl now calls.

Combines #8094, #8127 and #8724.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Closing: superseded by #9632, which shipped this behavior.

@vjeux vjeux closed this Nov 3, 2026
@vjeux
vjeux deleted the uikit/c05-segmented-every-pointer branch November 3, 2026 05:20

This branch was successfully deployed

1 active deployment
Preview — e7243b2e 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants