Repository navigation
feat(Switch): the touch presentation, for every pointer - #9636
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| Switch · Storybook | 🔴 86.6 kB (+39.5 kB) · 10.5 kB CSS (+855 B) | 1,777 (+1,199) | ✓ · ✓ · ✓ |
| richtext/RichTextEditor | 277.6 kB (+54 B) · 2.6 kB CSS (0) | — | — · — · — |
| NumberInput | 55.0 kB (+48 B) · 11.2 kB CSS (0) | — | — · — · — |
103 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| Pagination | 85.6 kB (+48 B) · 16.0 kB CSS (0) | — | — · — · — |
| SideNav | 65.7 kB (+48 B) · 10.9 kB CSS (0) | — | — · — · — |
| Stepper | 46.9 kB (+48 B) · 9.9 kB CSS (0) | — | — · — · — |
| DateTimeInput | 114.4 kB (+47 B) · 15.8 kB CSS (0) | — | — · — · — |
| Typeahead | 62.5 kB (+47 B) · 11.8 kB CSS (0) | — | — · — · — |
| richtext/RichTextView | 258.2 kB (+46 B) · 2.3 kB CSS (0) | — | — · — · — |
| AvatarGroupOverflow | 38.9 kB (+45 B) · 6.5 kB CSS (0) | — | — · — · — |
| lab/Comments | 231.9 kB (+45 B) · 2.5 kB CSS (0) | — | — · — · — |
| Toast | 59.4 kB (+44 B) · 10.1 kB CSS (0) | — | — · — · — |
| ChatDictationButton | 46.0 kB (+43 B) · 9.8 kB CSS (0) | — | — · — · — |
| RadioListItem | 51.3 kB (+43 B) · 10.1 kB CSS (0) | — | — · — · — |
| lab/TransferList | 74.3 kB (+43 B) · 1.7 kB CSS (0) | — | — · — · — |
| TextInput | 52.8 kB (+41 B) · 10.9 kB CSS (0) | — | — · — · — |
| TreeList | 37.9 kB (+41 B) · 5.0 kB CSS (0) | — | — · — · — |
| ChatComposer | 60.8 kB (+40 B) · 11.2 kB CSS (0) | — | — · — · — |
| ContextMenu | 75.0 kB (+40 B) · 15.3 kB CSS (0) | — | — · — · — |
| SelectorOption | 16.5 kB (+40 B) · 5.4 kB CSS (0) | — | — · — · — |
| SideNavItem | 54.5 kB (+40 B) · 10.4 kB CSS (0) | — | — · — · — |
| ToggleButton | 43.2 kB (+40 B) · 9.6 kB CSS (0) | — | — · — · — |
| TopNavMegaMenu | 54.2 kB (+40 B) · 11.2 kB CSS (0) | — | — · — · — |
| richtext/RichTextEditorToolbar | 221.4 kB (+40 B) · 454 B CSS (0) | — | — · — · — |
| ChatComposerTokenElement | 55.9 kB (+39 B) · 10.4 kB CSS (0) | — | — · — · — |
| SelectableCard | 14.6 kB (+39 B) · 7.3 kB CSS (0) | — | — · — · — |
| lab/CommentThread | 226.5 kB (+39 B) · 2.4 kB CSS (0) | — | — · — · — |
| Item | 13.2 kB (+38 B) · 4.9 kB CSS (0) | — | — · — · — |
| ListItem | 52.7 kB (+38 B) · 11.0 kB CSS (0) | — | — · — · — |
| PowerSearch | 183.0 kB (+38 B) · 20.6 kB CSS (0) | — | — · — · — |
| TableSelectionToolbar | 52.2 kB (+38 B) · 11.5 kB CSS (0) | — | — · — · — |
| TopNavMegaMenuItem | 10.7 kB (+38 B) · 4.2 kB CSS (0) | — | — · — · — |
| TopNavMenu | 58.1 kB (+38 B) · 10.5 kB CSS (0) | — | — · — · — |
| lab/ChatEmojiPicker | 64.9 kB (+38 B) · 773 B CSS (0) | — | — · — · — |
| lab/Schedule | 61.3 kB (+38 B) · 3.9 kB CSS (0) | — | — · — · — |
| ChatComposerInput | 55.9 kB (+37 B) · 10.4 kB CSS (0) | — | — · — · — |
| ClickableCard | 14.5 kB (+37 B) · 7.1 kB CSS (0) | — | — · — · — |
| Popover | 49.8 kB (+37 B) · 12.4 kB CSS (0) | — | — · — · — |
| Step | 27.8 kB (+37 B) · 5.6 kB CSS (0) | — | — · — · — |
| Tab | 11.2 kB (+37 B) · 4.2 kB CSS (0) | — | — · — · — |
| TabMenu | 55.2 kB (+37 B) · 10.4 kB CSS (0) | — | — · — · — |
| Token | 25.1 kB (+37 B) · 4.8 kB CSS (0) | — | — · — · — |
| Button | 42.3 kB (+36 B) · 9.5 kB CSS (0) | — | — · — · — |
| ChatLayoutScrollButton | 45.8 kB (+36 B) · 9.9 kB CSS (0) | — | — · — · — |
| CommandPaletteItem | 10.0 kB (+36 B) · 3.6 kB CSS (0) | — | — · — · — |
| Lightbox | 50.4 kB (+36 B) · 10.1 kB CSS (0) | — | — · — · — |
| lab/MobilePowerSearch | 168.0 kB (+36 B) · 1.2 kB CSS (0) | — | — · — · — |
| lab/MobileTokenizer | 100.7 kB (+36 B) · 880 B CSS (0) | — | — · — · — |
| BaseTypeahead | 54.9 kB (+35 B) · 10.2 kB CSS (0) | — | — · — · — |
| ChatSendButton | 45.1 kB (+35 B) · 9.5 kB CSS (0) | — | — · — · — |
| MoreMenu | 73.1 kB (+35 B) · 15.4 kB CSS (0) | — | — · — · — |
| SegmentedControlItem | 10.2 kB (+35 B) · 4.0 kB CSS (0) | — | — · — · — |
| lab/DiffViewer | 70.7 kB (+35 B) · 2.1 kB CSS (0) | — | — · — · — |
| lab/ListInput | 81.5 kB (+35 B) · 1.6 kB CSS (0) | — | — · — · — |
| Banner | 47.3 kB (+34 B) · 10.3 kB CSS (0) | — | — · — · — |
| DropdownMenuRadioItem | 18.2 kB (+34 B) · 5.9 kB CSS (0) | — | — · — · — |
| IconButton | 42.3 kB (+34 B) · 9.5 kB CSS (0) | — | — · — · — |
| Link | 42.9 kB (+34 B) · 7.9 kB CSS (0) | — | — · — · — |
| TableCell | 76.2 kB (+34 B) · 15.8 kB CSS (0) | — | — · — · — |
| Carousel | 50.7 kB (+33 B) · 10.5 kB CSS (0) | — | — · — · — |
| DateRangeInput | 69.5 kB (+33 B) · 11.7 kB CSS (0) | — | — · — · — |
| DialogHeader | 48.4 kB (+33 B) · 10.2 kB CSS (0) | — | — · — · — |
| DrawerHeader | 48.5 kB (+33 B) · 11.6 kB CSS (0) | — | — · — · — |
| DropdownMenuSubMenu | 55.9 kB (+33 B) · 9.9 kB CSS (0) | — | — · — · — |
| FileInput | 53.6 kB (+33 B) · 10.7 kB CSS (0) | — | — · — · — |
| MobileNav | 50.2 kB (+33 B) · 10.4 kB CSS (0) | — | — · — · — |
| TopNav | 51.8 kB (+33 B) · 10.7 kB CSS (0) | — | — · — · — |
| TopNavItem | 10.6 kB (+33 B) · 4.0 kB CSS (0) | — | — · — · — |
| lab/EmojiPicker | 70.2 kB (+33 B) · 722 B CSS (0) | — | — · — · — |
| CommandPalette | 48.5 kB (+32 B) · 12.2 kB CSS (0) | — | — · — · — |
| DropdownMenu | 72.8 kB (+32 B) · 15.4 kB CSS (0) | — | — · — · — |
| DropdownMenuCheckboxItem | 18.2 kB (+32 B) · 5.9 kB CSS (0) | — | — · — · — |
| SideNavCollapseButton | 46.1 kB (+32 B) · 9.8 kB CSS (0) | — | — · — · — |
| TableHeaderCell | 75.8 kB (+32 B) · 15.5 kB CSS (0) | — | — · — · — |
| lab/TourStep | 55.2 kB (+32 B) · 590 B CSS (0) | — | — · — · — |
| AlertDialog | 56.1 kB (+31 B) · 12.7 kB CSS (0) | — | — · — · — |
| DropdownMenuItem | 17.4 kB (+31 B) · 5.5 kB CSS (0) | — | — · — · — |
| TopNavHeading | 58.0 kB (+31 B) · 10.4 kB CSS (0) | — | — · — · — |
| Thumbnail | 49.5 kB (+30 B) · 11.3 kB CSS (0) | — | — · — · — |
| Tokenizer | 69.9 kB (+30 B) · 17.1 kB CSS (0) | — | — · — · — |
| lab/CommentPin | 47.1 kB (+30 B) · 1.2 kB CSS (0) | — | — · — · — |
| BreadcrumbItem | 66.9 kB (+29 B) · 11.4 kB CSS (0) | — | — · — · — |
| ChatLayout | 48.4 kB (+29 B) · 10.3 kB CSS (0) | — | — · — · — |
| CheckboxListItem | 53.3 kB (+29 B) · 11.0 kB CSS (0) | — | — · — · — |
| CheckboxInput | 48.7 kB (+28 B) · 9.7 kB CSS (0) | — | — · — · — |
| DateInput | 102.5 kB (+27 B) · 15.3 kB CSS (0) | — | — · — · — |
| lab/CommentComposer | 102.1 kB (+27 B) · 1.9 kB CSS (0) | — | — · — · — |
| CodeBlock | 59.1 kB (+26 B) · 11.1 kB CSS (0) | — | — · — · — |
| MultiSelector | 72.5 kB (+26 B) · 15.2 kB CSS (0) | — | — · — · — |
| Slider | 88.4 kB (+26 B) · 9.4 kB CSS (0) | — | — · — · — |
| AppShell | 68.5 kB (+25 B) · 12.2 kB CSS (0) | — | — · — · — |
| SideNavHeading | 58.6 kB (+25 B) · 10.6 kB CSS (0) | — | — · — · — |
| TimeInput | 88.6 kB (+25 B) · 14.3 kB CSS (0) | — | — · — · — |
| lab/ChatReactionBar | 65.4 kB (+25 B) · 1.1 kB CSS (0) | — | — · — · — |
| Selector | 76.7 kB (+24 B) · 15.7 kB CSS (0) | — | — · — · — |
| LayerProvider | 62.7 kB (+23 B) · 10.5 kB CSS (0) | — | — · — · — |
| Markdown | 145.0 kB (+22 B) · 18.3 kB CSS (0) | — | — · — · — |
| MobileNavToggle | 45.7 kB (+22 B) · 9.7 kB CSS (0) | — | — · — · — |
| Table | 84.3 kB (+22 B) · 16.0 kB CSS (0) | — | — · — · — |
| lab/TransferListSelector | 80.6 kB (+22 B) · 1.9 kB CSS (0) | — | — · — · — |
| Calendar | 54.2 kB (+21 B) · 10.3 kB CSS (0) | — | — · — · — |
| ComplexSelector | 55.4 kB (+21 B) · 11.1 kB CSS (0) | — | — · — · — |
| Outline | 24.2 kB (+20 B) · 4.2 kB CSS (0) | — | — · — · — |
| Collapsible | 22.5 kB (+19 B) · 4.5 kB CSS (0) | — | — · — · — |
| ContextMenuItem | 52.6 kB (+14 B) · 9.9 kB CSS (0) | — | — · — · — |
| Timestamp | 21.3 kB (+1 B) · 9.9 kB CSS (0) | — | — · — · — |
Package size: @astryxdesign/core 5.99 MB packed (+42.7 kB); no new dependencies.
Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
|
/approve-spec ea58f3c |
There was a problem hiding this comment.
Changes requested: the Switch touch presentation and its record changes are consistent with the touch-presentation spec, but two fixes are needed before approval.
- The switch adds its own click guard and
touchendcancel (useSwitchTouch.ts,CLICK_GUARD_MSinswitchTouch.ts) beside the shared guard inutils/clickGuard.tsthat the gesture host already arms; route the switch's release through the shared guard, extending the host's consumer interface if needed, and remove the copy. .changeset/switch-touch-presentation.mdsays "Mouse, keyboard and assistive technology behave as before", but this pull request changes mouse drags (seeswitch-every-pointer.md), so drop "Mouse," from that sentence.
Three small comment fixes are noted inline.
[Automated review]
| const onTouchEnd = (event: TouchEvent) => { | ||
| if (this.cancelsTouchEnd && event.cancelable) { | ||
| event.preventDefault(); | ||
| } | ||
| this.cancelsTouchEnd = false; | ||
| }; | ||
| // Where a `touchend` cannot be cancelled, the browser's trusted click is | ||
| // swallowed instead. Keyboard and assistive-technology clicks (`detail` | ||
| // 0) and the switch's own (untrusted) click always pass. | ||
| const onClick = (event: MouseEvent) => { | ||
| if (event.isTrusted && this.browserClickTimer != null) { | ||
| // The lift's own click: it activates the input natively. | ||
| clearTimeout(this.browserClickTimer); | ||
| this.browserClickTimer = null; | ||
| this.scheduleReconcile(); | ||
| return; | ||
| } | ||
| if (event.isTrusted && event.detail > 0 && now() < this.clickGuardUntil) { | ||
| this.clickGuardUntil = 0; | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
This touchend cancel and click swallower duplicate what utils/clickGuard.ts and the gesture host already own (the host cancels the touchend of a lift whose click must not happen and arms the shared guard). The copy uses a 1000 ms window instead of 400, disarms only on this switch's next press, and skips the shared exemptions. Could the press consumer ask the host to cancel its lift's click, with the switch's own input.click() inside withoutClickGuard, so CLICK_GUARD_MS and these listeners can go?
There was a problem hiding this comment.
Done in c7108c2, per the spec owners' ruling (architecture:react-component-runtime INV13: one stray-click swallower). The 1000 ms guard, CLICK_GUARD_MS and the touchend cancel are gone. When the switch decides a lift itself, or clicks its input after the browser's click did not come, it arms the existing utils/clickGuard.ts guard as it is and clicks inside withoutClickGuard. No Core change: a press consumer asking the host to cancel its lift stays with #9623.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Under a finger, on every touch screen, Switch moves like the iOS switch. A press lifts the thumb a little, with a soft shadow, and a horizontal drag moves it with the finger and flips the switch once the finger has travelled most of the track; lifting the finger commits. A tap toggles through the browser's own click, and a hold or a short drag toggles at the lift. The thumb stays inside the track however far the finger drags past either end, in either writing direction. A press slid off the switch and lifted outside still toggles it, as on iOS. A vertical drag scrolls the page and the switch lets go. The motion runs on the compositor, so it stays smooth while scripts run. The thumb keeps its circle and its size on every device, and under a coarse pointer the switch's invisible hit area grows toward 44 × 44 px with nothing visible added, stopping halfway to any neighbouring control so a tap goes to the nearer one. The track keeps its theme's fill, and the colour change crossfades over it. With reduced motion on, a change cross-fades instead of moving on a touch screen and is immediate with a mouse. A press shows the pressed overlay, through a drag too, and a still hold selects no text. Mouse, keyboard and assistive technology behave as before. In Chromium, a Switch retargeted right after a tap, such as one tapped again mid-travel, turns around from where it is, with no step back to the start of its travel. Switch is the first component built on the shared press tracking and touch negotiation. |
There was a problem hiding this comment.
"Mouse, keyboard and assistive technology behave as before" conflicts with switch-every-pointer.md: a mouse drag past the flip distance now flips the switch, and a drag past the flip and back no longer toggles. Suggest: "Keyboard and assistive technology behave as before."
There was a problem hiding this comment.
Done in c7108c2: "Keyboard and assistive technology behave as before."
| // --------------------------------------------------------------------- | ||
| // Under a finger the track keeps its own fill, the theme's. A layer over it | ||
| // fades the outgoing fill out on the compositor. The thumb is a capsule of | ||
| // constant size, in a box that useSwitchTouch moves and lifts on the | ||
| // compositor, as UISwitch's (iOS 26.2). The thumb is the circle a mouse | ||
| // sees, at each state's size, resting at its inset from its own end. Where | ||
| // a touch value shares a property with the mouse, it is a nested condition | ||
| // of that property above. The parts below exist only under a finger. | ||
| // --------------------------------------------------------------------- | ||
| // The fade layer shows the fill the track is leaving, or mid-drag the one | ||
| // it heads to. Its colour is the theme's, read from the track | ||
| // (switchFill.ts) and set inline. The animator owns its opacity, so it has | ||
| // no transition of its own. |
There was a problem hiding this comment.
Non-blocking: this block calls the thumb "a capsule of constant size" and then "the circle a mouse sees, at each state's size", and "the parts below" are not below (labelWrapper follows). The comments on the track and thumb styles already give the reasons; suggest deleting this block.
| /** | ||
| * The track box of each size, px (width × height), for the touch | ||
| * presentation's thumb geometry. | ||
| * SYNC: wrapperSizeStyles, inputSizeStyles and trackSizeStyles below. |
There was a problem hiding this comment.
Non-blocking: inputSizeStyles no longer exists. Suggest SYNC: wrapperSizeStyles and trackSizeStyles below.
| readonly attributes: Readonly<Record<string, string>>; | ||
| } | ||
|
|
||
| /** The track state of rendered props (`className` and `data-*` props). */ |
There was a problem hiding this comment.
Non-blocking: this JSDoc describes trackStateOf but sits above sameTrackState's own JSDoc. Suggest moving it to trackStateOf.
There was a problem hiding this comment.
Done in c7108c2: moved onto trackStateOf.
ea58f3c to
c7108c2
Compare
|
/approve-spec c7108c2 |
There was a problem hiding this comment.
Changes requested. Thanks for routing the switch's clicks through the shared click guard and fixing the changeset; three fixes remain, plus the size note that depends on the first.
- Switch FR15 says the switch's own click for a finger hold and a flipping drag departs from
spec:AST-094FR11 ("activation stays the browser's click"), and this pull request leaves FR11 unchanged, so either amend FR11 here with an engineering owner's approval or drop the own click. useSwitchTouch.tsadds its owncontextmenulistener to cancel the menu during a press, beside the gesture host's context-menu cancelling inGestureHost.ts, so let a press consumer ask the host to cancel the menu and remove the switch's listener.touch-action: pan-y pinch-zoominSwitch.tsxalso applies to disabled and loading switches, which attach no gesture, so a horizontal swipe that starts on one no longer scrolls a horizontal scroller; apply it only while the switch is interactive.- At 6,804 authored lines the change relies on the owner-directed batch exemption, which applies once the FR11 point is resolved.
Three small comment cleanups are noted inline; they do not block.
[Automated review]
| | FR12 | `ref`, `onFocus`, and `onBlur` reach the input, and a caller `aria-labelledby` and `aria-describedby` name and describe it. Other base props (`id`, `data-*`, other ARIA, `onClick`, and other handlers), `className`, `style`, `xstyle`, and `width` reach the field root. Caller ARIA attributes name and describe the switch, so they MUST reach the input (`architecture:public-component-api` INV5). Implementation gap: ARIA other than `aria-labelledby` and `aria-describedby`, such as `aria-label`, lands on the field root. | Current tests; `architecture:public-component-api` INV5 | | ||
| | FR13 | The thumb is one circle under any pointer, at each state's size: a touch presentation moves and lifts it and never reshapes it, and its lift and rubber band derive from the box it renders. | Owner direction, 2026-10-26 | | ||
| | FR14 | Under a finger the thumb, lifted or banded past either end, paints inside the track, in either writing direction. | Owner direction, 2026-10-25 | | ||
| | FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed, wherever the pointer is released (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen slid off and released outside without a flip leaves it alone, as the native checkbox does. Under a finger the thumb follows the drag as UISwitch's does (iOS 26.2); under a fine primary pointer the switch keeps its own look, and the value moves its thumb (`spec:AST-094` FR13). The switch's own click, for a hold and for a flipping drag, is a stated departure from FR11's activation by the browser's click. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging finger still moves the thumb. | `spec:AST-094` FR11, FR13, FR43; `spec:AST-020` FR9/DEC-5; `design:reduced-motion` | |
There was a problem hiding this comment.
This row states the switch's own click for a hold and a flipping drag "is a stated departure from FR11's activation by the browser's click", but spec:AST-094 FR11 itself is not amended, so the two current records disagree. Could FR11 gain this Switch exception in this pull request (with an engineering owner's approval), or the own click go?
There was a problem hiding this comment.
Done in e6c1eda, with the spec owners' text: AST-094 FR11 now reads "…may also complete on a lift after the finger slid off; and a Switch completes its own toggle at the lift of a finger hold or a flipping drag, rather than through the browser's click (component:Switch FR15)". FR15's last sentence points at that exception.
| } | ||
| }; | ||
| // A held switch opens no menu (UISwitch shows none). | ||
| const onContextMenu = (event: Event) => { |
There was a problem hiding this comment.
GestureHost already owns cancelling the browser's context menu during a held gesture (onContextMenu, installed by syncClaimingListeners, which counts this press consumer as a claimer but cancels only claimed touches). Could a press consumer ask the host to cancel the menu while its press is held, so this listener can go?
There was a problem hiding this comment.
Done in e6c1eda: a press consumer can now hold the menu closed through an opt-in holdsContextMenu (touch consumer and press tracker), which the host's existing contextmenu cancel reads. No other consumer changes. Switch uses it, and its own listener is gone.
| // Horizontal touch moves are the switch's drag and vertical ones the | ||
| // page's scroll, under any primary pointer. A touchscreen laptop's finger | ||
| // counts too. It is set in CSS so it holds before hydration. | ||
| touchAction: 'pan-y pinch-zoom', |
There was a problem hiding this comment.
This applies to disabled and loading switches too, which attach no gesture, so a horizontal swipe that starts on one stops scrolling a horizontal container. Could the pan-y style apply only while the switch is interactive (still in CSS, keyed on isDisabled and busy)?
There was a problem hiding this comment.
Done in e6c1eda: pan-y pinch-zoom is its own style, applied only while the switch is neither disabled nor loading.
| * The browser sends no click for a finger's hold, so the switch clicks its | ||
| * input once the click window passes. A mouse or a pen gets a click when | ||
| * it is released on the input. A drag that flipped the switch gets no | ||
| * click, so the switch cancels its `touchend` and clicks its input. A |
There was a problem hiding this comment.
Non-blocking: the touchend cancel is gone. Suggest "so the switch clicks its input, and the shared click guard swallows the browser's late click."
| * tracking, flip rule and release decide the value, with no animator, and | ||
| * the thumb moves when the value does. Only its press listens at rest. The | ||
| * click guard and the context-menu block come with a press. | ||
| */ |
There was a problem hiding this comment.
Non-blocking: the switch no longer has a click guard of its own. Suggest "Its click and context-menu listeners come with a press."
There was a problem hiding this comment.
Done in e6c1eda: "Its click listener comes with a press." (the context menu is now the host's).
| this.detachers = this.attachPassive(host, elements); | ||
| return; | ||
| } | ||
| // The switch's springs play as its motion roles. |
There was a problem hiding this comment.
Non-blocking: this restates registerSwitchTable(); suggest deleting it (and the similar comment above const lift = switchControl.lift).
| disabledMessageTooltip.interactionRef(el); | ||
| }} | ||
| {...(isDisabled ? undefined : pressable)} | ||
| // The thumb's drag keeps the row's press painted. |
There was a problem hiding this comment.
Non-blocking: this restates pressHoldsDragProps; suggest deleting it.
c7108c2 to
e6c1eda
Compare
|
/approve-spec e6c1eda |
There was a problem hiding this comment.
Changes requested. Thanks for amending spec:AST-094 FR11, moving the context-menu cancel into the gesture host, and limiting touch-action to interactive switches; those three points are resolved, and an engineering owner has approved the record changes at this head. Two small fixes remain:
docs/specs/components/Switch/Switch.spec.mdPR1 still says an enabled switch adds "atouchend, aclickand acontextmenulistener", but it now adds only theclicklistener (andSwitch.test.tsxasserts no touch or context-menu listener), so trim that clause to theclicklistener; the record change then needs a fresh/approve-specfor the new head..changeset/switch-touch-presentation.mdends "Switch is the first component built on the shared press tracking and touch negotiation", but Slider's touch controller already creates a press tracker and attaches it to the gesture host, so drop that sentence or say Switch joins Slider.
The size is within the owner-directed batch allowance and does not block.
[Automated review]
| switch also adds one consumer to the shared gesture host, whose listeners | ||
| are installed once per page, and a `touchend`, a `click` and a `contextmenu` | ||
| listener to the element around its track: at once under a coarse primary |
There was a problem hiding this comment.
The switch now adds only its click listener here (useSwitchTouch.ts listen): the touchend cancel and the contextmenu listener are gone, and Switch.test.tsx asserts no touch or context-menu listener in any state. Suggest: "…and a click listener to the element around its track…".
There was a problem hiding this comment.
Done in 27c6a6e: "…and a click listener to the element around its track…".
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Under a finger, on every touch screen, Switch moves like the iOS switch. A press lifts the thumb a little, with a soft shadow, and a horizontal drag moves it with the finger and flips the switch once the finger has travelled most of the track; lifting the finger commits. A tap toggles through the browser's own click, and a hold or a short drag toggles at the lift. The thumb stays inside the track however far the finger drags past either end, in either writing direction. A press slid off the switch and lifted outside still toggles it, as on iOS. A vertical drag scrolls the page and the switch lets go. The motion runs on the compositor, so it stays smooth while scripts run. The thumb keeps its circle and its size on every device, and under a coarse pointer the switch's invisible hit area grows toward 44 × 44 px with nothing visible added, stopping halfway to any neighbouring control so a tap goes to the nearer one. The track keeps its theme's fill, and the colour change crossfades over it. With reduced motion on, a change cross-fades instead of moving on a touch screen and is immediate with a mouse. A press shows the pressed overlay, through a drag too, and a still hold selects no text. Keyboard and assistive technology behave as before. In Chromium, a Switch retargeted right after a tap, such as one tapped again mid-travel, turns around from where it is, with no step back to the start of its travel. Switch is the first component built on the shared press tracking and touch negotiation. |
There was a problem hiding this comment.
"Switch is the first component built on the shared press tracking and touch negotiation" is not accurate: Slider/sliderTouchController.ts already creates a press tracker (createPressTracker) and attaches it to the gesture host (host.attachConsumer). Suggest dropping the sentence, or "Switch joins Slider on the shared press tracking and touch negotiation."
There was a problem hiding this comment.
Done in 27c6a6e: "Switch joins Slider on the shared press tracking and touch negotiation."
|
/approve-spec 27c6a6e |
There was a problem hiding this comment.
Recommend approval. An engineering owner has approved the Switch record changes and the spec:AST-094 FR11 amendment at this head, and both points from the last round are fixed: PR1 now names only the click listener, and the changeset says Switch joins Slider on the shared press tracking.
Two optional comment cleanups are noted inline; they do not block.
[Automated review]
| /** | ||
| * The press and the pan both report every touch event; the first report | ||
| * takes the step and this skips the second (cleared after the event). | ||
| */ |
There was a problem hiding this comment.
Non-blocking: no pan recognizer is attached here; only the press tracker calls move(). Could this comment name the case the guard still covers, or could the guard go if none remains?
| * a coarse primary pointer the track's and thumb's layers fade the press. | ||
| * So the strength holds through the release, not animated on the main | ||
| * thread. With any other pointer the row keeps `pressedAlpha`'s release. |
There was a problem hiding this comment.
Non-blocking: under Reduce Motion with a fine pointer, line 238 sets animation-name: none, so the row does not keep pressedAlpha's release there (the shared style keeps a shortened one). Suggest: "Under a coarse pointer, or with Reduce Motion, the strength holds through the release."
The C-controls Switch batch, on current main, as one commit. It supersedes the three Switch drafts. - UISwitch's measured values join the UIKit constants table (motion/uikit/switch.ts), with the flip rule as a pure model (Switch/switchTouch.ts). - Under a finger, Switch moves like UISwitch: a press lifts the thumb, a horizontal drag moves it with a rubber band and flips at the measured distance, the lift commits, and a vertical drag scrolls. Motion runs on the compositor, through a busy main thread. - A mouse and a pen drag by the same flip rule, with the desktop look (FR15). A press slid off and released outside leaves the switch alone. - The switch's own clicks arm Core's shared click guard, which swallows the browser's late click, so a lift toggles once. It keeps no click guard of its own (architecture:react-component-runtime INV13). - Under a coarse pointer the hit area grows toward 44 x 44 px, stopping halfway to a neighbour. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…tch's menu closed, and touch-action follows interactivity From review, with the spec owners' text and rulings: - spec:AST-094 FR11 states the Switch exception: a Switch completes its own toggle at the lift of a finger hold or a flipping drag, rather than through the browser's click (component:Switch FR15). Switch FR15's last sentence now points at that exception. - A press consumer can hold the browser's context menu closed while its press is down: an opt-in `holdsContextMenu` on the touch consumer and on the press tracker, which the gesture host's existing `contextmenu` cancel reads. No other consumer changes. Switch uses it and drops its own `contextmenu` listener. - `touch-action: pan-y pinch-zoom` applies only while the switch takes a drag; a disabled or loading switch leaves every move to the page. - Four comments that restated code or named the removed guard are fixed. Red first: the host case (an opted-in consumer's held press cancels the menu) and the coarse-pointer listener case (no context-menu listener of the switch's own) fail on the old code; the disabled and loading touch-action case fails on the old build. A held switch still opens no menu, in Chromium and WebKit.
…ngeset says Switch joins Slider From review: an enabled switch now adds only its `click` listener to the element around its track; the `touchend` cancel and the `contextmenu` listener are gone. Slider already builds on the shared press tracking and touch negotiation, so Switch joins it rather than coming first.
27c6a6e to
e3a07de
Compare
|
/approve-spec e3a07de |
There was a problem hiding this comment.
Recommend approval. An engineering owner approved the Switch record changes and the spec:AST-094 FR11 amendment at this head, and the code, tests and CI agree with them: under a finger a hold, a drag past the flip distance and a press slid off now toggle the switch, a mouse or pen drag flips it by the same rule, and the coarse hit area reaches 44 × 44 px.
Two optional comment fixes, noted inline:
useSwitchTouch.ts:240-243mentions a pan recognizer that is not attached.Switch.tsx:229-231says a fine pointer keeps the shared press release, but Reduce Motion with a fine pointer turns it off.
[Automated review]
| /** The finger's last travel along x, for a lift that reports none. */ | ||
| private lastTravel = 0; | ||
| /** | ||
| * The press and the pan both report every touch event; the first report |
There was a problem hiding this comment.
Optional (INV29): no pan recognizer is attached here; only the press tracker calls move(). State the case this guard still covers, or drop the guard if none remains.
| * Composed after `interactionOverlayStyles.pressedAlpha` on the row. Under | ||
| * a coarse primary pointer the track's and thumb's layers fade the press. | ||
| * So the strength holds through the release, not animated on the main | ||
| * thread. With any other pointer the row keeps `pressedAlpha`'s release. |
There was a problem hiding this comment.
Optional (INV29): line 238 sets animation-name: none under Reduce Motion with a fine pointer, so the row does not keep pressedAlpha's release there. For example: "Under a coarse pointer, or with Reduce Motion, the strength holds through the release."
Batch:
C-controls · Switch and SegmentedControl under a finger.User need
On a phone, Switch acts like a desktop checkbox: a finger cannot drag its thumb, and it freezes while scripts run.
Problem and solution fit
Current specification
component:SwitchFR6; FR9 and FR10, changed here; FR13–FR15, added here.spec:AST-094FR11, changed here; FR4, FR6, FR13, FR18, FR24, FR43 and DEC-5;spec:AST-020FR9 and DEC-5.Switch.spec.md, AST-094spec.mdand its generatedmotion-roles.md. They need an engineering owner's/approve-spec <head sha>.Public delta and caller burden
No prop, type or theme change, and no new element: under a coarse pointer the track paints its press and fade on its pseudo-elements.
Design and alternatives
Chosen: passive press tracking, compositor animations, the gesture host's click guard and menu cancel. Rejected: CSS only (no drag); per-frame restyling (stutters); claiming the touch at the press (blocks scrolls).
Size
The Switch import grows from 44.8 to 80.8 KB gzip; four fifths is core's shared gesture host and animator, loaded once per page. The cold first change takes 14–16 ms, or 54–62 ms at 4× CPU throttling; later ones take 1–2 ms. Authored lines: 6,630 added and 230 removed, under the ~8,000 batch cap; 4,330 are tests.
Supersedes #8123, #8124 and #8681, with co-author credit.
Evidence
iOS 26.2 simulator and Android 15 emulator: main, this change, then iOS's switch; the red ring is the finger. P11 and M1–M3 use a mouse, M4 a pen. Recorded on the superseded heads.
Scope
Writing rubric results
Switch.spec.md: R1, R2, R4, R5 and R6: none. R3: FR13 and FR14 cite the owner's direction. AST-094spec.md: R1–R6: none (FR11's clause is the owners' wording).Testing
In Storybook's Switch › Default on a phone: tap, drag the thumb across, and slide off and lift. Each toggles once. In In scrolling list, a vertical swipe scrolls and toggles nothing.
No late
mousedown,mouseupor trusted click follows a lift the switch commits itself, in Chromium or WebKit (a local probe, not part of the diff), so no gap is recorded.--check--cpus=4, CI, 1 worker, ×10--cpus=3, 1 and 16 workers (report only)beginGrouptreats a call inside a curve's settle tail as continuing motion, so the new animation starts at the call and steps when a starved compositor commits late. The settle-tail cases go to a follow-up animator change (#9733); the mid-travel catch is an openspec:AST-094FR6 question for the spec owners