Skip to content

feat(Wheel): UIPickerView's motion under a finger, with flat rows - #8159

Merged
cixzhang merged 16 commits into
meta:mainfrom
vjeux:uikit/c21-wheel
Nov 3, 2026
Merged

cixzhang merged 16 commits into
meta:mainfrom
vjeux:uikit/c21-wheel

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8159 → #8178 → #8179 → #8209 → #8200 → #8206

Batch: B4 · Wheel / picker (DateInput, TimeInput, DateTimeInput)

The change: vjeux/astryx@9136e28...2c6622c

Supersedes #8156, #8526, #8162, #8361, #8527, #8480 and #8698.

#8156, #8162, #8361 and #8698 combine here; the shared animator replaces #8526's player. #8527's turns from script move to #8179. #8480's cylinder look (DEC-2) and #8366's looping stay out.

User need

Under a finger, the sheet wheels are scroll-snap columns; in WebKit a flick stops at a fifth of the iOS picker's travel. This moves them like UIPickerView; a mouse keeps the scrolling column. Keys step in time order: 12 PM is reachable.

Current specification

  • spec:AST-094 FR4 and its implementation gaps, FR5, FR6, FR23, FR43.
  • component:DateInput DEC-1, DEC-2, FR13 to FR15, FR17, FR18.
  • design:reduced-motion DR2, DR3; architecture:react-component-runtime INV6, INV13, INV16; architecture:interaction-modality INV1.

Owner decisions

DEC-2, FR14's tap-spin role, FR17, FR18 and FR4's snap-rate gap carry the owners' rulings verbatim; FR13 no longer repeats the gap.

Design and alternatives

  • Keep the scroll-snap column: it cannot decelerate like UIKit or be caught.
  • A player of its own with tipping rows: a second engine (INV13), and DEC-2 rejects the look.
  • Chosen: flat rows on one strip the shared animator moves, coasting with decay and turning with play, faded by a static column mask.

Turns play the role picker.tapSpin.durationMs; the snap rate is FR4's recorded gap. A trackpad takes the pan's continuous path; a mouse notch turns one row.

Public delta and caller burden

No API change; the mouse column's keys change as a defect fix (FR17). Gzip import, main 0f25a0f to this head: DateInput 103.0 → 144.0 KB, TimeInput 90.0 → 129.3, DateTimeInput 114.8 → 154.9, PowerSearch 183.5 → 223.5, MobilePowerSearch 168.5 → 208.5; CSS unchanged. The growth is the shared touch runtime, paid once per page.

Writing rubric results

Over DEC-2, FR13 to FR18, FR4's gap line, the Current-state lines and front matter:

  • R1 narration, R2 research, R3 pull requests as evidence, R4 rejected alternatives, R5 change references, R6 open questions: none.

Evidence

  • A 1 s main-thread block never stops the coast. A flick's coast follows UIKit's plan within 0.75 px at the measured velocity.
  • Red-first: ArrowDown from 5 PM at a 12:00–17:00 bound committed 12 PM; a key with nowhere to go mid-turn stopped the wheel 21.5 px off its row.
  • A focused wheel under a finger carries no mask, so its whole focus ring shows (Chromium).
  • Red-first at 8× CPU throttling: PM from 10 AM ended on 9:00 AM or 12:00 PM, not the 5 PM maximum, in 6 of 30 runs on main; here 30 of 30.

Testing

Range-diff from the head the feel check passed (5c8919c) to this one, on main 9136e28: its 11 commits, 6 of them differing only in test, record or context lines, plus one identifier-only commit (createAnimator → createMotionAnimator in wheelStrip.ts, main's rename) and four test-only commits. The PR's own product diff is byte-identical except those two lines. The last rebase met one conflict, in scripts/touch-support-bundle.test.mjs: main's import list is kept as it is, and the wheels' reason joins its comment. The rest is test changes only: the meridiem turn proof judges the drawn frames, a new 8× starvation case, the scrolling column's Reduce Motion unit cases follow the dip's own timeline, the Reduce Motion step proof keeps main's polled recorder, and the touch bundle lists PowerSearch and Table once. Gates at 2c6622c: build, typecheck, lint (0 errors), check:knowledge, check:changesets and the Storybook build pass; test:ui 19,683 of 19,683; the date, time and motion units 2,356; WheelRest ×10 100 of 100 at 1 worker and at 4 workers on two threads; Chromium a11y contracts 134 and interaction 22 passed; WebKit at CI's profile ×10 180 passed, 60 skipped. test:node: under full load, CLI tests time out on main too. Run beside main's build at the same time, five times (three at 58c5b8a, two at 9136e28 with each launched first), main failed 13, 4, 10, 4 and 1 files and this branch 16, 4, 11, 4 and 2, nearly all timeouts. The other errors were the same, and no file failed every time here and never on main. That load issue on main is tracked separately. The docsite's one CI failure (a copied heading link under the sticky header, right-to-left and dark) passed 66 of 66 runs on both builds here, with CI's settings and without; in one full run both builds failed the same other docsite case.

On a phone or the iOS simulator, open Storybook's TimeInput › Bottom Sheet With Min Max:

  1. Flick the minutes: the wheel coasts and commits where it rests.
  2. Touch a coasting wheel: it stops; lift, and it eases onto the nearest row.
  3. Tap a row below the band: the wheel turns onto it and commits it.
  4. At 10:00 AM, tap PM: the time clamps to 5:00 PM and the hour wheel turns onto 5.
  5. With a keyboard at 5 PM, press ArrowDown: nothing moves. At 12 PM it moves to 1 PM.

Automated: PickerWheel.test.tsx, Wheel.test.tsx, wheelStrip.test.ts, wheelPhysics.test.ts and TimeInput's Presentation.test.tsx in jsdom; the browser specs in Chromium and WebKit.

Merge order: this pull request, then #8178, #8179 and #8209.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 7:41pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 7:55 PM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 2c6622c

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
DateInput · Storybook 🔴 147.8 kB (+41.9 kB) · 15.4 kB CSS (+51 B) 5,007 (+1,219) ✓ · ✓ · ✓
PowerSearch 🔴 235.3 kB (+40.9 kB) · 21.7 kB CSS (+45 B) — — · — · —
TimeInput · Storybook 🔴 132.7 kB (+39.9 kB) · 14.4 kB CSS (+70 B) 1,251 (+1) ✓ · ✓ · —
8 more components
Component Size LoC Visual · A11y · RTL
lab/MobilePowerSearch 🔴 224.9 kB (+6.2 kB) · 1.2 kB CSS (0) — — · — · —
DateTimeInput · Storybook 🔴 168.5 kB (+5.9 kB) · 17.1 kB CSS (+50 B) 3,644 (+1) ✓ · ✓ · ✓
AlertDialog 84.4 kB (0) · 13.1 kB CSS (0) — — · — · —
SegmentedControl 82.5 kB (0) · 7.8 kB CSS (0) — — · — · —
Slider 89.4 kB (0) · 9.4 kB CSS (0) — — · — · —
Switch 87.6 kB (0) · 10.6 kB CSS (0) — — · — · —
lab/Comments 259.2 kB (0) · 2.5 kB CSS (0) — — · — · —
lab/CommentThread 254.1 kB (0) · 2.4 kB CSS (0) — — · — · —

Package size: @astryxdesign/core 6.27 MB packed (+36.8 kB); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: core 2 rows; run pnpm api:report.

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

@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's decision on draft spec:AST-094 (FR1–FR23 and the amendments it proposes to current records) and on the wheel's new look (32 px pitch, magnified band, plain selected row); an engineering owner records it by commenting /approve-spec ece20fbd147eca75500fb8542994e3257c42ef7c, and the author may do so as an engineering owner. Whatever that decision, please fix the issues below and split the stack so this pull request carries only the Wheel commit, since it currently changes 28,785 authored lines.

  • A wheel still coasting when another wheel commits stops between rows and never commits, because a new options array reruns motion.attach(), which stops the motion (Wheel.tsx:413-423, wheelMotion.ts:117-123).
  • With presentation="bottom-sheet" and a mouse, dragging a wheel down now dismisses the sheet; the base guarded against this and its tests were removed.
  • Home and End pressed while a wheel is moving land on the wrong row (Wheel.tsx:785-791).

[Automated review]

@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's decision on draft spec:AST-094 (FR1–FR23 with DEC-1 and DEC-2, including the amendments it proposes to current records) and on the wheel's new look (the cylinder with a magnified band, the 32 px row pitch and the selected row without accent or semibold); any engineering owner, the author included when they are one, records it by commenting /approve-spec ee9446ddeeb92e94419d9276cf620e19605718dc on this pull request. Whatever that decision, these still need changes:

  • A mouse drag on a wheel in the fine-pointer bottom sheet now dismisses the sheet instead of turning the wheel, and the tests that guarded it were deleted.
  • Land the stacked predecessors separately so this pull request carries only the wheel commit, then fix Home and End pressed while the wheel coasts, the active row repaired in a layout effect, and the changeset and documentation references the review lists.

[Automated review]

@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's decision on draft spec:AST-094 (FR1–FR23 with DEC-1 and DEC-2, including the amendments it proposes to current records) and on three Wheel choices no spec states yet: the new look (cylinder with a magnified band, 32 px pitch, plain selected row), turns that ignore Reduce Motion, and the nearest-enabled rest for bounded wheels. Any engineering owner records that decision by commenting /approve-spec 2def93ccb659fd3f9d6d484b1256c633124e790f on this pull request, and the author may do so as an engineering owner. Whatever the decision, these need changes:

  • A mouse drag on a wheel in the forced bottom sheet now dismisses the sheet instead of turning the wheel, and the tests that guarded it were deleted.
  • @astryxdesign/core/DateInput/tokens.stylex drops the released wheelItemSize and wheelEdgePadding keys in a patch; keep them and move wheelGeometry out of the public module.
  • Land the stacked predecessors separately so this pull request carries only the Wheel commit, then fix Home and End pressed during a coast, the active row repaired in a layout effect, and the changeset and documentation references noted inline.

[Automated review]

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated

// Keep the finger: inside a BottomSheet the sheet would otherwise read a
// downward drag here as swipe-to-dismiss (see useOwnScrollGesture).
useOwnScrollGesture(columnRef, 'all', {isEnabled: isActive});

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 covers touch only. The base also called usePointerDragScroll, which stopped a mouse pointerdown before BottomSheet's body drag could arm and capture the pointer; without it, a downward mouse drag on a wheel in a forced presentation="bottom-sheet" dismisses the sheet instead of turning the wheel. Please keep a press that starts on a wheel away from the sheet's drag again, and restore the deleted mouse-drag tests (DateInputTouch.test.tsx) against the gesture path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved at adb185f. A mouse, a fine pointer, gets today's scrolling column, which keeps usePointerDragScroll; the six mouse-drag tests in DateInputTouch.test.tsx run unchanged under a fine pointer with the sheet asked for. Under a finger the column stops pointerdown from reaching the sheet (jsdom case "keeps a finger that lands on it from an enclosing sheet"; Chromium case "a downward drag on a wheel moves its rows and leaves the sheet where it is", where the sheet moved 9.3 px before). PickerWheel.test.tsx also has mouse cases for a mouse on a touch device: a primary-button drag turns the wheel and commits where it rests, a secondary-button drag does nothing.

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +785 to +791
case 'Home':
event.preventDefault();
moveBy(-activeIndex);
moveBy(-restIndex);
break;
case 'End':
event.preventDefault();
moveBy(options.length - 1 - activeIndex);
moveBy(count - 1 - restIndex);

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.

moveBy steps from motion.target() (the row the wheel is heading to), but these deltas are measured from restIndex, the last committed row. While a coast, a tap spin or a scroll-wheel snap is running, Home or End lands between the motion's target and the end: Home pressed while a coast from minute 10 heads for 40 commits 30, not 0. Making Home and End absolute (the first and last enabled rows) fixes it; a test that presses them mid-coast would guard it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in adb185f: Home and End go to the first and last enabled row wherever the wheel is heading. New case "takes Home and End to the first and last enabled rows, even mid-coast" fails before the change (Home committed 30 from a coast heading past 40) and passes now.

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +430 to +439
useIsomorphicLayoutEffect(() => {
if (!isActive || dragRef.current != null || motion.isMoving()) {
return;
}
const position = nearestPosition(selectedIndex, motion.offset(), layout);
if (Math.abs(motion.offset() - position * ROW_HEIGHT) > 0.01) {
motion.setOffset(position * ROW_HEIGHT);
}
// eslint-disable-next-line @eslint-react/set-state-in-effect -- the active row follows the wheel's resting offset, which only exists in the motion engine
setRestIndex(selectedIndex);

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.

A value set from outside first renders the old restIndex into aria-activedescendant, and this layout effect then repairs it, which docs/architecture/react-component-runtime.md INV1 rules out. Adjusting restIndex during render when selectedIndex changes at rest avoids the follow-up update; the effect can keep syncing the motion engine's offset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in adb185f: the restIndex state is gone. The active descendant derives from the committed value during render, and the park effect only syncs the motion engine's offset. New case "points the active row at a value set from outside in the same commit" (React Profiler) saw two commits, the first with the old row, before the change; now one commit with the new row.

* inlines only literal constants, so the values are repeated here; a test
* holds them to the table.
*/
export const wheelGeometry = stylex.defineConsts({

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.

@astryxdesign/core/DateInput/tokens.stylex is a published subpath, so removing dateInputTouchSizes.wheelItemSize and dateInputTouchGeometry.wheelEdgePadding breaks code that reads them in what the changesets declare a patch, and wheelGeometry becomes public API. Consider keeping the two keys and moving wheelGeometry into a .stylex.ts module outside the exports map; only Wheel.tsx and WheelGroup.tsx read it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in adb185f: tokens.stylex.ts is unchanged from main (both released keys stay; today's column still uses them), and wheelGeometry moved to the unexported DateInput/wheelGeometry.stylex.ts. A new test keeps the published module to its released names (WEEKS_PER_PANE, dateInputTouchGeometry, dateInputTouchSizes).

* ```
* <WheelGroup>
* <Wheel label="Hour" options={hours} value={hour} onChange={setHour} />
* <Wheel label="Minute" options={minutes} value={minute} onChange={setMinute} looping />

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 prop is isLooping; looping is not a Wheel prop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the example uses isLooping.

Comment thread .changeset/wheel-uikit-picker.md Outdated
'@astryxdesign/core': patch
---

[fix] The picker wheels in the TimeInput, DateTimeInput and DateInput bottom sheets now look and move like iOS's own picker. The rows sit on a cylinder seen in perspective, with one capsule band across every column and the row inside it magnified. A flick coasts at UIKit's deceleration onto the row UIKit's projection picks. A finger landing on a moving wheel stops it. A tap on a row turns it onto the band. Past the first or last row, a drag rubber-bands and the wheel bounces back. As on iOS, the value commits when the wheel comes to rest, and the coast keeps running while the page's main thread is busy. Arrow keys and clicks on a row still commit at once.

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.

Nothing renders WheelGroup at this head, so each column draws its own band; "one capsule band across every column" describes the follow-up that adopts the group, not this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the changeset no longer mentions a band across every column (that is #8161's), and it is now tagged [feat] as a new visual direction under AST-017 FR3.

@vjeux vjeux changed the title feat(Wheel): UIPickerView's wheel on the gesture recognizer and compositor curves feat(Wheel): UIPickerView's wheel in the iOS idiom, on the gesture recognizer and compositor curves Oct 24, 2026

@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's decision on draft spec:AST-094 (FR1–FR23 with DEC-1 and DEC-2), on three Wheel choices no spec states yet (the new look, turns that ignore Reduce Motion, and the nearest-enabled rest), on the theme motion roles and platform adaptation condition (which also need spec:AST-012 FR2 amended), and on the motion/experimental and gestures/experimental subpaths, which sit outside the experimental boundary spec:AST-017 FR21 names. Any engineering owner, the author included, records that decision by commenting /approve-spec 1c1e2386fe3a81037daa6fa879c4c82bd6349161 on this pull request. Whatever the decision, these need changes:

  • The hard-coded 744 px compact width for presentation="adaptive" contradicts spec:AST-012 FR9, which defines compact as width below the theme's md point or height below 500 px.
  • Home and End pressed while a wheel is still moving commit the wrong row, and wheelGeometry becomes a public export of @astryxdesign/core/DateInput/tokens.stylex.
  • Land the stacked predecessors separately and bring the Wheel commit under 2,000 lines, then fix the layout-effect repair of the active row and the changesets noted inline.

[Automated review]

Comment on lines +735 to +742
case 'Home':
event.preventDefault();
moveBy(-restIndex);
break;
case 'End':
event.preventDefault();
moveBy(count - 1 - restIndex);
break;

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.

moveBy steps from motion.target() (the row the wheel is heading to, line 646), but these deltas are measured from restIndex, the last rested row. While a coast, a tap spin or a rest turn runs, the two differ: on the 60-row minute wheel, Home pressed 200 ms into a flick up from 10 commits 23, and End 200 ms into a flick down from 50 commits 36. Make Home and End absolute (the first and last enabled rows) and add a test that presses them mid-motion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed at adb185f: Home and End go to the first and last enabled rows wherever the wheel is heading. The case "takes Home and End to the first and last enabled rows, even mid-coast" fails before the change.

Comment on lines +362 to +372
useIsomorphicLayoutEffect(() => {
if (!isActive || dragRef.current != null || motion.isMoving()) {
return;
}
const position = nearestPosition(selectedIndex, motion.offset(), layout);
if (Math.abs(motion.offset() - position * ROW_HEIGHT) > 0.01) {
motion.setOffset(position * ROW_HEIGHT);
}
// eslint-disable-next-line @eslint-react/set-state-in-effect -- the active row follows the wheel's resting offset, which only exists in the motion engine
setRestIndex(selectedIndex);
}, [isActive, selectedIndex, layout, motion]);

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.

A value set from outside first renders the old restIndex into aria-activedescendant (line 757), then this layout effect repairs it, which docs/architecture/react-component-runtime.md INV1 rules out ("MUST NOT render a known-wrong initial state and repair it in a follow-up Effect"). The lint reason names the component's own motion engine rather than an external system. Adjust restIndex during render when selectedIndex changes at rest, and keep the effect for the engine's offset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed at adb185f: there is no restIndex state. The active descendant derives from the committed value during render, and the effect only syncs the motion engine's offset. The case "points the active row at a value set from outside in the same commit" uses a React Profiler to check for one commit.

export const TOUCH_QUERY = '(pointer: coarse)';

/** The viewport is compact: layout behaviours. */
export const COMPACT_QUERY = '(width < 744px), (height < 500px)';

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.

spec:AST-012 FR9 on main makes compact "viewport width … below the effective md point (FR1) or its viewport height … below 500", with no size numbers of a resolver's own, and architecture:public-component-api INV17 says the same. A hard-coded 744 px edge sends a coarse 744–767 px screen (a 744 px iPad mini in portrait) to the popover where FR9 makes it compact, and ignores a theme that moves md. Build the query from the theme's effective md point (768 by default) and the 500 px height, and have the changeset name presentation as the way to keep a surface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not in this pull request's commits: utils/touchPresentation.ts is #8076's. At #8076 1203525, which this branch now sits on, the resolver reads the theme's compact definition (DEFAULT_COMPACT_DEFINITION, AST-012 FR9) and keeps no numbers of its own.

* holds them to the table. Outside the iOS idiom the wheels keep the sizes
* above.
*/
export const wheelGeometry = stylex.defineConsts({

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.

@astryxdesign/core/DateInput/tokens.stylex is a published subpath, so this adds a stable public export (spec:AST-017 FR3) although the PR body says there is no API change, and only PickerWheel.tsx and WheelGroup.tsx read it. Moving it to a .stylex.ts module outside the exports map keeps UIKit's picker numbers free to change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed at adb185f: tokens.stylex.ts is unchanged from main, and the picker geometry lives in the unexported wheelGeometry.stylex.ts. A test keeps the published module to its released names.

Comment thread .changeset/presentation-resolver.md Outdated
'@astryxdesign/core': patch
---

[chore] An internal `usePresentation` / `readPresentation` resolves a component's `presentation` prop into the surface to render and whether its touch input modes apply, keeping an omitted value distinct from an explicit `'popover'` so a caller's choice always wins over a touch default. No component reads it yet; behavior is unchanged.

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.

At this head Wheel and WheelGroup read usePresentation, so "No component reads it yet; behavior is unchanged" is no longer true; the same goes for the "no component uses it yet" lines in the gesture, UIKit table and wheel-values changesets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved: this stack no longer contains #8298. The wheel reads useTouchPresentation(), so presentation-resolver.md is not in this pull request.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

This is about where the behavior lives, not about the PR's intent.

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

This looks ready to approve. Every behavior change is covered by the current component:DateInput and spec:AST-094 records, and the record changes carry an engineering owner's /approve-spec on this head. The wheel's jsdom suites and its Chromium interaction and accessibility proofs, the coast through a 1 s main-thread block included, pass at this head; CI's lanes, WebKit's included, still need to pass once the pull request leaves draft.

Optional tidy-ups, none blocking:

  • Scope FR18 to a coarse primary pointer: the scrolling column under a mouse still marks rows while it scrolls.
  • Update DEC-2's decider date, the PickerWheel.test.tsx header (it still lists a programmatic spin and a custom rest rule), and the changeset line that calls the tap spin an internal constant (it is the internal motion role picker.tapSpin.durationMs).

[Automated review]

| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. The turn plays the internal motion role `picker.tapSpin.durationMs`, UIKit's measured 300 ms, on `timing.curves.move`, as a tap on a row does. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). | `spec:AST-094` FR43; current source |
| FR17 | In a sheet wheel, Arrow, Page, Home and End move through the wheel's enabled values in time order, skipping disabled ones, and stop at the first or last enabled value; a key with no enabled value in its direction commits nothing, under every presentation. On a 12-hour hour wheel, time order follows the full time (12 PM comes before 1 PM), not the column's row order. | Owner decision (cixzhang, 2026-11-02); current source |
| FR18 | While a wheel coasts, snaps or turns, no row carries the selection band's accent mark; the mark returns on the row that comes to rest, as UIPickerView does (`spec:AST-094` FR3). | Owner decision (cixzhang, 2026-11-02); UIDatePicker on the iOS 26.2 simulator; current source |

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.

Non-blocking: FR18 reads as every sheet wheel, but under a mouse the scrolling column still sets the marked row on each scroll frame (Wheel.tsx:455-474) and marks a key's target before its smooth scroll. Scoping it to a coarse primary pointer ("Under a coarse primary pointer, while a wheel coasts…") matches the shipped behavior and its evidence row.

'@astryxdesign/core': patch
---

[chore] Internal: UIPickerView's values measured on iOS 26.2 join the touch motion tables as internal constants: the row pitch, the picker's height and its rubber band, the coast's stop speed and velocity floor, the snap at rest, the tap spin and the selection tick. The pure math a wheel moves with comes with them. It projects the row a release rests on as UIScrollView sums its deceleration, coasts with the rate re-solved to stop on that row, snaps, bounces at an end, rubber-bands, and spins to a tapped row. It is checked against UIKit's recorded frames. Nothing public changes: none of it is exported from the package entry point, and themes do not set these values.

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.

Non-blocking: the tap spin is no longer only an internal constant: registerPickerTable makes its duration the internal motion role picker.tapSpin.durationMs (FR14). Consider "…and the selection tick, with the tap spin's duration as an internal motion role".

* dispatched through the gesture system with a fake clock
* @output Proves what a wheel commits and when: a drag, a release onto a row,
* past the bounds, a fling, a tap, a finger catching a moving wheel, the
* keyboard, a click, a programmatic spin, a custom rest rule

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.

Non-blocking: this file no longer tests a programmatic spin or a custom rest rule (they moved with spinTo and resolveRest), so the @output can end at "the keyboard, a click".

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 98cd5be

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

Changes requested for one fix. The record changes carry an engineering owner's /approve-spec on this head, every other behavior change is covered by the current component:DateInput, spec:AST-094 and design:reduced-motion records, and the jsdom suites, typecheck, lint and record checks pass.

  • The rebase puts back the fixed 45-frame window in the Reduce Motion step proof of WheelRest.a11y.chromium.spec.ts (lines 213–248), which #9734 replaced on main with a recorder that reads frames until the column is at rest; keep main's polled recorder and change only what it samples (the selected row and the strip that fades).

Optional tidy-ups: scope FR18 to a coarse primary pointer (the mouse column still marks rows while it scrolls), and update DEC-2's decider date, the PickerWheel.test.tsx header and the changeset line that calls the tap spin an internal constant. CI's product lanes, WebKit's included, still need to pass once the pull request leaves draft.

[Automated review]

),
opacity: Number(getComputedStyle(faded).opacity),
});
if (seen.length < 45) {

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 brings back the fixed 45-frame window that #9734 replaced on main with a recorder polled until the column is at rest (a drawn frame with the rows moved at opacity 1). On a loaded runner the window can close before the dip ends, and expect(seen.at(-1)?.opacity).toBe(1) below then fails. Keep main's polled recorder and only change what it samples: the selected row and its parent, the strip that fades under the touch column.

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 745f99c

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

Changes requested for one fix. The record changes carry an engineering owner's /approve-spec on this head, every other behavior change is covered by the current component:DateInput, spec:AST-094 and design:reduced-motion records, and the Reduce Motion step proof again records frames until the column rests.

  • packages/core/src/DateInput/wheelStrip.ts:19 imports createAnimator from motion/animator, which main has since renamed createMotionAnimator (#8653), so the branch merged with main fails core's typecheck even though GitHub shows no conflict; rebase and take the rename at lines 19 and 135.

After the rebase, rerun the wheel suites, since main also changed when the animator starts a retarget on a settle tail (#9733), and drop the second PowerSearch and Table entries the merge leaves in scripts/touch-support-bundle.test.mjs. The other notes (FR18's scope, DEC-2's date, a test header, one changeset word, a few comments) are optional.

[Automated review]

*/

import {canVibrate, playHaptic} from '../gestures/haptics';
import {createAnimator, type MotionCurve} from '../motion/animator';

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.

main renamed this factory to createMotionAnimator (#8653), so once merged this import fails core's typecheck (TS2305: Module '"../motion/animator"' has no exported member 'createAnimator'), though GitHub shows no conflict. Rebase and import createMotionAnimator here and at line 135.

@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 one engineering-owner decision; no blocking code defect remains, and the earlier merge break with main (the animator rename) is fixed on this head.

This head changes requirement text in the current component:DateInput record (DEC-2's flat touch rows, FR14's tap-spin role sentence, the new FR17 and FR18) and adds the snap-rate implementation gap to spec:AST-094 FR4. The same lines carried an owner approval on the previous head, but the rebase commits end it. One behavior also has no governing record yet: under a finger, a tap on a row now commits when its 300 ms turn ends instead of at the tap, while a keyboard or assistive-technology click still commits at once. An engineering owner needs to approve both (or say which should change, for example a sentence in FR13 or FR14 naming when a tap commits) by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Optional notes, none blocking:

  • FR18 reads as every wheel, while the scrolling column under a mouse still marks rows as it scrolls; scoping it to a coarse primary pointer would match its evidence.
  • DEC-2's decider date, the PickerWheel.test.tsx header, the tap-spin wording in wheel-uikit-picker-values.md, and a few comments that restate the code could be tidied.

[Automated review]

| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). | `spec:AST-094` FR43; current source |
| FR16 | Enter in the pointer field's text input commits typed text that has not been committed and is then held back, so a form never submits the value from before the commit. With no typed text pending, Enter is not prevented, so a form submits as it does from any text field, except on a disabled or busy field, where Enter does nothing (`spec:AST-085` FR2). |
| FR17 | In a sheet wheel, Arrow, Page, Home and End move through the wheel's enabled values in time order, skipping disabled ones, and stop at the first or last enabled value; a key with no enabled value in its direction commits nothing, under every presentation. On a 12-hour hour wheel, time order follows the full time (12 PM comes before 1 PM), not the column's row order. | Owner decision (cixzhang, 2026-11-02); current source |
| FR18 | While a wheel coasts, snaps or turns, no row carries the selection band's accent mark; the mark returns on the row that comes to rest, as UIPickerView does (`spec:AST-094` FR3). | Owner decision (cixzhang, 2026-11-02); UIDatePicker on the iOS 26.2 simulator; current source |

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.

Non-blocking: as written FR18 covers every wheel, but the scrolling column under a mouse still marks the row under the band on each scroll frame (Wheel.tsx:453-483) and marks a key’s target before its smooth scroll (Wheel.tsx:499). Scoping FR18 to a coarse primary pointer would match its only evidence row.

'@astryxdesign/core': patch
---

[chore] Internal: UIPickerView's values measured on iOS 26.2 join the touch motion tables as internal constants: the row pitch, the picker's height and its rubber band, the coast's stop speed and velocity floor, the snap at rest, the tap spin and the selection tick. The pure math a wheel moves with comes with them. It projects the row a release rests on as UIScrollView sums its deceleration, coasts with the rate re-solved to stop on that row, snaps, bounces at an end, rubber-bands, and spins to a tapped row. It is checked against UIKit's recorded frames. Nothing public changes: none of it is exported from the package entry point, and themes do not set these values.

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.

Non-blocking: the tap spin is registered as the internal motion role picker.tapSpin.durationMs (picker.ts:114-116), so listing it among the internal constants reads inaccurately.

* dispatched through the gesture system with a fake clock
* @output Proves what a wheel commits and when: a drag, a release onto a row,
* past the bounds, a fling, a tap, a finger catching a moving wheel, the
* keyboard, a click, a programmatic spin, a custom rest rule

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.

Non-blocking: this file no longer tests a programmatic spin or a custom rest rule; dropping them from the header keeps it accurate.

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 90f8d8a

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

@astracat-bot review

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

Ready to approve: the record changes in component:DateInput and spec:AST-094, and a finger's tap committing when its turn ends, carry an engineering owner's approval for this head, and nothing blocks.

Optional follow-ups:

  • FR18 reads as covering every wheel, while the scrolling column under a mouse still marks rows as it scrolls; scoping it to a coarse primary pointer would match its evidence.
  • In PickerWheel.test.tsx, "ignores fingers while hidden" compares a row's style.transform, which the wheel never writes, so its movement check cannot fail; comparing distanceOf(option('10')) would.
  • The PickerWheel.test.tsx header, the tap-spin wording in wheel-uikit-picker-values.md, two orphaned test doc comments, and a few comments that restate the code could be tidied.

[Automated review]

Comment on lines +692 to +696
const before = option('10').style.transform;
drag(column, 150, 60, {steps: 10, interval: 16});
advance(3000);
expect(onChange).not.toHaveBeenCalled();
expect(option('10').style.transform).toBe(before);

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.

Non-blocking: the wheel never writes a row's style.transform (it moves the strip's translate), so this half of the test cannot fail if a hidden wheel moves. expect(distanceOf(option('10'))).toBeCloseTo(0, 4) would check it.

Comment on lines +62 to +63
/** A slow one-finger drag: no fling, so the wheel rests where it is let go. */
/** A mouse case runs under a fine primary pointer: the page agrees. */

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.

Non-blocking: the first doc comment belongs to drag below, but now sits above expectFinePointer; move it down to drag.

vjeux added 16 commits November 3, 2026 11:14
Under a coarse primary pointer, the TimeInput, DateTimeInput and
DateInput sheet wheels move like UIPickerView: a flick coasts at UIKit's
deceleration onto the row UIKit's projection picks, a finger landing on a
moving wheel stops it where it is drawn, a tap on a row turns it onto the
band, and past the first or last row a drag rubber-bands and bounces
back. The value commits when the wheel comes to rest, by the rest rule
(component:DateInput FR13), and the coast keeps running while the main
thread is busy (spec:AST-094 FR5): one strip moves the rows on the shared
animator, with no Layout or Paint per frame.

A value changed from outside while the sheet is open turns the wheel onto
its row instead of jumping there (component:DateInput FR14); a second
change during that turn turns on from where the wheel is drawn.

The rows lie flat (component:DateInput DEC-2): today's 28 px pitch,
fading toward the column's edges without tipping or shrinking. Under
a mouse the scrolling column and its tilt are unchanged. Under Reduce
Motion a flick still coasts, as scrolling does; a tap, a key or a slow
release cross-fades onto its row (design:reduced-motion DR2).

useOwnScrollGesture now also keeps the pointer a touch lands with, for
a scroller that pans through the gesture system, so the wheel adds no
listener of its own for the sheet's pull-down
(architecture:react-component-runtime INV16).

The touch-support bundle check lists DateInput, TimeInput and
DateTimeInput as gesture users, with PowerSearch and Table, whose value
editors reach TimeInput's wheels (architecture:react-component-runtime
INV13). The wheel's proof stories sit under Core/Wheel.

The tap spin's 300 ms is an internal motion role,
`picker.tapSpin.durationMs` (spec:AST-094 FR4), which every turn reads
under its column; the snap at rest keeps UIKit's measured rate as a
constant, because a motion role is a spring, a duration or a curve.

The record: DEC-2 states that under a finger the rows fade without
tipping or shrinking; FR14 names the tap spin's role and FR13 records the
snap rate's FR4 gap; the FR13 and FR14 verification rows cite the touch
wheel's cases; FR15's gap sentence holds for both pointers. The generated
motion role table lists the new role.

Combines the wheel's values and math, its browser proofs, one listbox
hook for both presentations, and the owner's rest rule and spin; looping
stays out until a record covers it.
The flick proof fitted the coast against UIKit's plan for velocities
around the scripted 900 px/s. In WebKit the page reads the release
velocity from the timestamps of the inputs it receives, and WebKit stamps
each input as it dispatches it, in whole milliseconds. Under load the
intervals of the 120 Hz script jitter from 3 to 17 ms while each step
stays 7 or 8 px, so one interval reads anywhere from 580 to 1,750 px/s,
and a short last interval lifts the release past twice the script. The
coast then followed UIKit's plan for that faster release, but no fit in
the scripted range matched it.

The proof now records the inputs a finger's pan reads (its touchstart,
one sample per touch pointermove, and its touchend) on the host's clock,
holds each time at the latest as the gesture arena does, and runs the
pan's own velocity tracker over them. The whole coast must follow UIKit's
plan for that velocity, within the same 0.75 px. In Chromium, which
replays the script's timestamps, the measured velocity must also be
within 5% of the script.
…ch turns it one row

component:DateInput FR15: under a coarse primary pointer, the sheet
wheels' pan takes a trackpad's two-finger scroll, as spec:AST-094 FR43
allows a record to grant (the pan allows `continuous` scroll input): the
rows follow the fingers, coast on at the lift with their last speed, and
rest on a row by the rest rule (FR13). The platform's momentum after the
lift never moves the wheel.

A mouse wheel's notch turns the wheel one row, as an arrow key does, with
no coast. The gesture host leaves a mouse wheel's notches to what is
under the pointer; the wheel steps on a wheel the host reads as a notch
(`scrollTypeOf`, now exported, so the reading has one owner) and leaves
a wheel the pan took, a ctrl-wheel (the page's zoom) and a sideways
scroll alone. The wheel's own pixel scroll handling goes.

The record's FR15 loses its implementation and evidence gaps and its
current-state line; its verification row cites the unit cases and the
browser proofs (notches in Chromium; the trackpad flick and slow stopped
drag in WebKit, whose driver sends a trackpad's precise deltas, on the
touch column opened alone in a desktop context).

Red first: the notch, three-notch, line-mode and trackpad flick cases
fail on the wheel's previous pixel scrolling; the flick rests 80 px past
the lift there, 426 px with this change.
…, which nothing here calls

`spinTo` on a Wheel's ref (`WheelHandle`) and the `resolveRest` prop have
no caller in this change: the time wheels that turn a column from script
land with TimeInput's sheet, and no column passes a rest rule. Both go,
with their tests and Changeset; the touch wheel always rests on the
nearest enabled row (`component:DateInput` FR13), and today's column is
Wheel's fine-pointer branch again, with no handle around it.
…eet wheels look like and tick

The strip reaches the animator from a callback ref: attaching is work the
element's attaching causes, and the ref lets the animator go as the strip
detaches or is replaced (architecture:react-component-runtime INV6). The
motion's layout still follows the rows in an Effect.

component:DateInput DEC-2 now says one thing: under a finger the rows lie
flat at the scrolling column's pitch and fade toward its edges without
tipping or shrinking, and under a mouse the scrolling column tips and
fades its rows on a scroll timeline; its title and the cylinder non-goal
match. Current-state impact records that the selection tick vibrates
where the browser has the Vibration API, as on Android, and is silent in
iOS Safari (spec:AST-094 FR23). The Changeset names the vibration and
says the wheels keep today's column under a fine primary pointer; the
wide-range story no longer says rows tip.
… one, under a finger and a mouse

The spec owners' rulings, recorded in component:DateInput:

- FR17: in a sheet wheel, Arrow, Page, Home and End move to the next
  enabled row in the key's direction and stop at the last enabled row; a
  key with no enabled row in that direction commits nothing, under every
  presentation. The touch wheel already did; the scrolling column under a
  mouse stopped at a disabled row instead. Both now share one rule
  (`steppedIndex` and `endIndex` in wheelOptions.ts). The new cases at a
  12:00–17:00 bound and across a disabled row fail for the mouse column
  before this change.
- FR18: while a wheel coasts, snaps or turns, no row carries the
  selection band's accent mark; the mark returns on the row that comes to
  rest, as UIPickerView does (spec:AST-094 FR3). A new case covers the
  snap and a key's turn beside the fling's coast.
…M comes before 1 PM

The spec owners' FR17 for component:DateInput, verbatim: in a sheet
wheel, Arrow, Page, Home and End move through the wheel's enabled values
in time order, skipping disabled ones, and stop at the first or last
enabled value; a key with no enabled value in its direction commits
nothing, under every presentation. On a 12-hour hour wheel, time order
follows the full time (12 PM comes before 1 PM), not the column's row
order.

A wheel option may now say where it falls when a key steps the wheel
(`order`); without it, rows step in list order. The 12-hour hour columns
of TimeInput's and DateTimeInput's sheets set the full hour, so at a
12:00–17:00 bound ArrowDown and End from 5 PM commit nothing, and
ArrowDown from 12 PM commits 1 PM, under a finger and a mouse. Before
this change both walked row order: ArrowDown from 5 PM skipped 6 to 11
and committed 12 PM. The field-level cases and the Wheel cases on a
12-hour column fail without it. The mouse column's change is a defect
fix: a key could not reach 12 PM there, against WCAG 2.1.1.
Comments, the story and two Changesets said "today's column" for the snap-scrolling column under a mouse, which names nothing once this lands. They now say the scrolling column. The listbox hook's Changeset no longer claims that no behavior changes: this pull request changes how keys step.
…h on its row

Under a finger, a key stopped the wheel's motion before it asked where to
go. With no allowed row in its direction, as a second ArrowDown during the
turn onto the last allowed row finds, the key then did nothing more: the
wheel stayed between rows while the field showed the committed value.
The key now finds its row first and leaves the turn alone when there is
none. The new case fails before this change, 21.5 px off its row.
… shows

The column faded its rows with a mask on itself, the focusable listbox.
A mask clips everything the element paints, its outline included, so a
wheel focused from the keyboard under a finger matched :focus-visible and
showed no ring (architecture:interaction-modality INV1). The fade moves to
an inner layer around the band and the strip; the rows fade as before.
A jsdom case checks that layer, and a Chromium case checks the focused
column's computed mask and outline and the fade layer's gradient.

Two comments still said "Today's column" and "Today's selection band";
they now name the scrolling column and the selection band.
…ementation gap

The spec owners' ruling on the sheet picker wheel's snap rate: it is an
implementation gap of spec:AST-094 FR4, in the record's own words. "The
sheet picker wheel's snap rate (UIKit's measured picker deceleration)
plays from an internal constant and is not yet a named role; a role kind
for rates comes with a later change." The gap now lives only in FR4, so
component:DateInput FR13 and its Current-state line no longer state it.
…draws, and a starved page still clamps to the maximum

The frame-by-frame case records the strip's drawn top with each frame's time until a frame shows the wheel at rest, then checks the turn with expectAnimatedTurn: no frame count and no fixed wait. A new case taps PM from 10 AM under 8x CPU throttling and waits until the wheels are at rest; the field must read 5:00 PM with the hour wheel on 5. On main's scrolling column a late settle can commit a stale AM and end on 9:00 AM or 12:00 PM.
…de's own timeline

The scrolling column's dip now lands its rows when the fade-out ends, and plays the fade-in after that. Its unit cases finish the fake fade-out instead of advancing a timer, read the two halves, and add the late-frame and unmount cases. The sheet's browser proofs keep this branch's touch-column versions.
…e column rests

The rebase took back the fixed 45-frame window. The proof now keeps main's polled recorder, which reads frames until a frame shows the rows moved and the column fully visible, and samples this branch's committed row and the strip that fades.
main renamed motion/animator's createAnimator to createMotionAnimator. The wheel strip creates its animator through the new name; nothing else changes.
main lists PowerSearch and Table through AlertDialog's Tokenizer, and this branch listed them again for TimeInput's wheels. The list keeps one entry each, and its comment names both reasons.
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 2c6622c

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

Ready to approve. The record changes in component:DateInput and spec:AST-094 carry an engineering owner's /approve-spec on this head, every other behavior change is covered by current records, and the wheel's Chromium browser proofs pass in CI.

Optional follow-ups, none blocking:

  • Scope FR18 to the touch column, since the scrolling column under a mouse still marks the row under its band as it scrolls.
  • Add a sentence to FR14 saying a finger's tap commits when its turn ends, while a click commits at once.
  • Make "ignores fingers while hidden" compare where the strip draws a row, since the wheel never writes a row's transform.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 2c6622c8 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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