Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
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
optionsarray rerunsmotion.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]
ece20fb to
ee9446d
Compare
There was a problem hiding this comment.
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]
ee9446d to
2def93c
Compare
There was a problem hiding this comment.
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.stylexdrops the releasedwheelItemSizeandwheelEdgePaddingkeys in a patch; keep them and movewheelGeometryout 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]
|
|
||
| // 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}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| case 'Home': | ||
| event.preventDefault(); | ||
| moveBy(-activeIndex); | ||
| moveBy(-restIndex); | ||
| break; | ||
| case 'End': | ||
| event.preventDefault(); | ||
| moveBy(options.length - 1 - activeIndex); | ||
| moveBy(count - 1 - restIndex); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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 /> |
There was a problem hiding this comment.
The prop is isLooping; looping is not a Wheel prop.
There was a problem hiding this comment.
Fixed: the example uses isLooping.
| '@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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
2def93c to
1c1e238
Compare
There was a problem hiding this comment.
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"contradictsspec:AST-012FR9, which defines compact as width below the theme'smdpoint or height below 500 px. - Home and End pressed while a wheel is still moving commit the wrong row, and
wheelGeometrybecomes 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]
| case 'Home': | ||
| event.preventDefault(); | ||
| moveBy(-restIndex); | ||
| break; | ||
| case 'End': | ||
| event.preventDefault(); | ||
| moveBy(count - 1 - restIndex); | ||
| break; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)'; |
There was a problem hiding this comment.
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.
| * holds them to the table. Outside the iOS idiom the wheels keep the sizes | ||
| * above. | ||
| */ | ||
| export const wheelGeometry = stylex.defineConsts({ |
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
| '@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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Resolved: this stack no longer contains #8298. The wheel reads useTouchPresentation(), so presentation-resolver.md is not in this pull request.
|
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. |
…nto uikit/c21-wheel-proofs
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on what this pull request writes into the draft component:DateInput and component:DateTimeInput records (FR13 with its second sentence, which keeps an out-of-bounds committed value after a drag back onto it, and its new third sentence, which turns a wheel onto a clamped value; DEC-1; DateInput's ownership of the shared wheel; DateTimeInput FR8), and on whether the 300 ms tap spin and the snap rate may stay constants under spec:AST-094 FR4; an engineering owner records that decision by commenting /approve-spec with the full head SHA, and the author may do so as an engineering owner. Whatever that decision, these still need changes:
- Land the unmerged stack first, or merge
mainagain as each part lands: the diff is 33,990 authored lines, 2,983 of them this pull request's own. - A second clamp during the hour wheel's 300 ms turn leaves it resting on the old target (
PickerWheel.tsx:469-475); re-check the committed row when a turn ends and test two clamps inside one turn. - Play the wheel's motion through the shared animator, or have
architecture:react-component-runtimeINV13 amended to admit a component-owned player. - Add browser proofs of a flick's coast through a main-thread block and of a mid-coast catch (Chromium phone emulation and WebKit), plus real-device recordings, as
spec:AST-094FR5, FR6 and Platform support ask. - Correct the Changeset's "keep today's look" and "With a mouse the wheels keep today's column" (
spec:AST-017FR3).
[Automated review]
| if (dragRef.current != null || motion.isMoving()) { | ||
| return; | ||
| } | ||
| const target = selectedIndex * ROW_HEIGHT; | ||
| if (Math.abs(motion.offset() - target) > 0.01) { | ||
| if (parkedRef.current) { | ||
| move(turnCurve(columnRef.current, motion.offset(), target)); |
There was a problem hiding this comment.
This bails while any motion runs and only runs again when selectedIndex changes, and the turn it starts has no onEnd. In TimeInput's "Bottom Sheet With Min Max" story (9 AM to 5 PM, at 10 AM), PM clamps the time to 5 PM and starts this 300 ms turn from 10 to 5; AM within the turn clamps it to 9 AM, this returns, and the wheel rests on 5, painted under the band, while the field holds 9:00 AM. Re-checking the committed row when a turn ends (or retargeting a running park turn) would fix it; a test with two clamps inside one turn would catch it.
There was a problem hiding this comment.
Fixed at e897a73: a second change during a turn turns on from where the wheel is drawn. PickerWheel.test.tsx "turns on from where the wheel is drawn when the value changes again during that turn" covers it.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Under a finger, on every touch device, the picker wheels in the TimeInput, DateTimeInput and DateInput bottom sheets now move like UIPickerView, and keep today's look: 28 px rows that tip and fade toward the edges of the column, with the row under the band in semibold accent text on its plate. 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. Under reduced motion a flick still coasts, as scrolling does, with the rows fading rather than tipping; a tap, a key or a slow release cross-fades onto its row. `presentation="popover"` keeps the previous surface; no value keeps the native-scroll column under a finger. With a mouse the wheels keep today's column. |
There was a problem hiding this comment.
"Keep today's look" holds only where animation-timeline: view() is supported: elsewhere today's column draws a plain, fully legible list (Wheel.tsx:186-196), while the touch column tips and fades on every engine (wheelLook.ts:81-114). No row is marked while the touch column coasts, snaps or turns, where today's highlight follows the scroll, and Wheel picks the column by the primary pointer, so a mouse on a phone or tablet gets the touch column. spec:AST-017 FR3 asks the Changeset to name the old and new appearance.
There was a problem hiding this comment.
Fixed at e897a73: the changeset names both looks. Under a coarse primary pointer the rows lie flat and fade toward the column's edges in every browser; today's column tilts and fades its rows only where scroll-driven animations exist, and shows a plain list elsewhere. No row is marked while the wheel coasts, snaps or turns.
| } | ||
|
|
||
| /** Play `curve` from the current offset; the rows end at curve.to. */ | ||
| play(curve: WheelCurve, {ticks = false, onEnd}: PlayOptions = {}): void { |
There was a problem hiding this comment.
architecture:react-component-runtime INV13 has a component's own touch logic be "small glue" over the shared modules, the animator among them, and its verification row fails "a component bundling its own copy of a module's primitive". This player bakes, starts, follows, stops and cross-fades curves itself and imports nothing from motion/animator.ts; either a per-row channel in the animator or an INV13 amendment would settle it.
|
Mapped to 161c506:
Open:
|
…nothing else does (#8671) ## Why Every pull request that gives a component a gesture fails `test-node` once merged with main, and the failure names no component: `scripts/touch-support-bundle.test.mjs` requires Core's root and hooks entries to carry no gesture code. At least six open pull requests (#8142, #8190, #8337, #8159, #8098, #8658) fail it this way. ## What led to it This comes from the work to make Astryx's touch components behave as native iOS ones do. Since #8465, a component that uses a gesture imports the touch modules directly (`architecture:react-component-runtime` INV13), and the root entry re-exports every component, so it carries gesture code as soon as one component does. ## Options and tradeoffs - Exempt the root entry: unblocks everyone, but stops catching a component that pulls gesture code it does not use. - Check each public import on its own: catches exactly that, at the cost of a list of the imports allowed to carry gesture code. ## Why this choice The per-import check keeps INV13's promise, a component without gestures ships none of their code, while letting gesture users through by name. It was offered on #8562 and aimed at main's test. ## What changes - Each public import of Core is built the way the size report builds it, and may carry gesture code only if `GESTURE_USERS` names it. A failure lists the import, its gesture modules with their sizes, and the chain of imports that brings them. - The root entry may carry only what a named import carries; the utils entry stays gesture-free. - A strict case keeps Button and TextInput free of gesture code whatever the list says. - `GESTURE_USERS` starts empty, so every import stays as strict as on main. The pull request that gives a component a gesture adds that component's import, and each import that reaches it, with a comment naming the gesture, so the new gesture user shows up in review. ## How to check it 1. Run `pnpm exec vitest run --project node scripts/touch-support-bundle.test.mjs` on main with this branch merged: it passes. 2. On a tree whose component uses a gesture, run it without naming that import in `GESTURE_USERS`: the test fails and names that import and the chain that brings the gesture code. Name it, and the test passes. 3. Import a gesture module from Button: the strict case fails. ## Evidence - With the list empty, the test passes on main: no public import carries gesture code today. - On open trees that give a component a gesture (AppShell's scroll-edge bar, Slider, the List reorder), the per-import case fails naming exactly those imports and their chains, and passes once each is named. - No product code changes. ## Scope - [x] No intended public API, product behavior, visual direction, or policy change. - [x] Product changes discovered during the work were removed or split. - [x] Public text and artifacts contain no internal Meta context.
…wins over touch defaults (#8298) <!-- Template: maintenance.md (an internal resolver; no behavior change until a component reads it). --> Small batch of the merge plan: one commit on main holding only this change. Its paragraph for the gestures README waits for that README, which comes with #8076; the resolver's file header carries the same contract. ## Maintainer impact A component that behaves differently under a finger, such as a menu that tracks the finger or becomes a sheet, must still let a caller pin the desktop behavior. Today components read `presentation ?? 'popover'`, which cannot tell an omitted prop from an explicit `'popover'`, so a component that adopts a touch default would override a caller who asked for the popover. The menu, context menu and picker work that adopts touch modes builds on this resolver (#8104, #8627, #8080, #8125, #8201, #8159, #8162). ## Intended invariant A caller's explicit `presentation` always wins over the touch default (`architecture:public-component-api` INV16), and the surface equals `useAdaptivePresentation`'s for every value on every device, so no caller sees a change: | `presentation` | surface | touch input modes | | ------------------------------- | --------------------------------------------- | ----------------- | | `'popover'` | popover | never | | `'bottom-sheet'` | sheet | under a finger | | `'adaptive'` | sheet on a compact touch screen, else popover | under a finger | | omitted, `whenOmitted: 'touch'` | popover | under a finger | The modes follow the pointer, never the platform. The server and hydrating render never present touch modes, and no hint changes that (`spec:AST-094` FR13). The hook listens only to the queries a request reads (`architecture:react-component-runtime` INV5): none for a pinned `'popover'`, the pointer for `'bottom-sheet'` and the omitted `'touch'` default, and the pointer and compact size for `'adaptive'`. A touch-mode style is applied through `touchModes`, never nested under the iOS-only `@supports` gate. ## Change - `utils/presentation.ts`: `usePresentation` and `readPresentation` (and the pure `resolvePresentation`) return the surface to render and `touchModes`, whether the component's touch input modes apply; `usePresentation` subscribes to each media query only while the request reads it. Internal: no component reads it yet. - `hooks/useAdaptivePresentation.ts`: its header points a component that also switches input modes to `usePresentation`, whose surface is its own. ## Evidence - Failure before the change: an omitted `presentation` and an explicit `'popover'` resolve the same, so a touch default cannot tell them apart; a server render given a touch hint must still answer the popover with no touch modes, a case that failed first. On an iPhone, a resolver that seeded its server snapshot from the live window fails both the server-render case and the `hydrateRoot` case; listening to both queries for every value fails the subscription case. A resolver that gives `'adaptive'` the sheet without the coarse pointer, or reads compact by width only, fails the surface matrix; one that ignores the theme's `md` fails the themed case. - Success after the change: the unit matrix (iPhone, iPad, Android, desktop, Mac Safari, a fine-pointer narrow window, a phone in landscape, and the 767/768 and 499/500 compact edges, plus `'adaptive'` under a theme whose `md` is 1000) covers each row above, a live pointer change and the one-moment read; on an iPhone, the server render and the hydrating render answer the popover with no touch modes, and after hydration the phone's sheet with touch modes; each request holds only the subscriptions it reads. - Product/runtime behavior verified unchanged: every value renders `useAdaptivePresentation`'s surface on every device, and no component reads the resolver yet. ## Scope - [x] No intended public API, product behavior, visual direction, or policy change. - [x] Product changes discovered during the work were removed or split. - [x] Public text and artifacts contain no internal Meta context. ## Testing Local gates at 9f0595f, one commit on main d73908f holding only this change (4 files, +571/−1): - `pnpm test:ui` (core UI): 654 files, 16,459 tests passed. - `pnpm exec vitest run --project node`: 804 files passed; the only failures are main's own story-tree and release-cache reds, tracked in #8509 and #8520. - `tsc --project packages/core/tsconfig.json --noEmit`, strict ESLint and Prettier on every changed file: clean. - `pnpm check:repo`: clean.
161c506 to
e897a73
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on three changes this pull request writes into the current component:DateInput record:
- DEC-2's new sentence that rows lie flat under a finger. Current DEC-2 says a finger and a mouse see the same column, with rows tipping back.
- FR14's new sentence naming the 300 ms
picker.tapSpin.durationMsmotion role. - FR13's new gap, which keeps the snap at rest as a literal rate against
spec:AST-094FR4.
An engineering owner records the decision by commenting /approve-spec with the full head SHA on this pull request; the author may do so as an engineering owner. Whatever the decision, these still need changes:
- The touch wheel's own
wheellistener (PickerWheel.tsx:587-628) bypasses the shared trackpad pan path thatmainnow has (spec:AST-094FR43). Mergemain, passallowedScrollTypes: ['continuous']to the wheel's pan, and keep only a one-row-per-notch handler (component:DateInputFR15). - The title ("in today's look") and two Changesets claim more than the change does:
wheel-rest-and-spin.mdsays time and date-time columns use the new rest rule, andwheel-uikit-picker.mdsays "With a mouse the wheels keep today's column", which is not true on a tablet. resolveRest,spinToandWheelHandlehave no caller. Remove them, with their tests and Changeset, or land them with the column that uses them.component:DateInputmust record that the per-row selection tick has no haptic in iOS Safari, asspec:AST-094FR23 requires.- Attach the strip to the animator from a callback ref instead of the layout effect at
PickerWheel.tsx:353-356(architecture:react-component-runtimeINV6). - At 6,585 authored lines the change is over the size limit. Once the record changes are approved and the unused plumbing is removed, it qualifies as an owner-directed batch; otherwise split it.
Non-blocking: the BottomSheetWideRange story comment says rows tip under a finger, and wheelOptions.ts:36 has a stray doc comment.
[Automated review]
| useEffect(() => { | ||
| const column = columnRef.current; | ||
| if (column == null || !isActive) { | ||
| return; | ||
| } | ||
| let idle: number | undefined; | ||
| const onWheel = (event: WheelEvent) => { | ||
| // A ctrl-wheel is a pinch: a trackpad pinch arrives as one. The wheel | ||
| // has no pinch, so the ctrl-wheel stays the page's zoom and never turns | ||
| // the wheel. | ||
| if (event.ctrlKey) { | ||
| return; | ||
| } | ||
| event.preventDefault(); | ||
| const currentLayout = latestRef.current.layout; | ||
| const delta = | ||
| event.deltaMode === 1 | ||
| ? event.deltaY * ROW_HEIGHT | ||
| : event.deltaMode === 2 | ||
| ? event.deltaY * PAGE_HEIGHT | ||
| : event.deltaY; | ||
| const {min, max} = offsetBounds(currentLayout); | ||
| const shown = motion.isMoving() ? motion.stop() : motion.offset(); | ||
| motion.setOffset(Math.min(max, Math.max(min, shown + delta)), { | ||
| ticks: true, | ||
| }); | ||
| window.clearTimeout(idle); | ||
| idle = window.setTimeout(() => { | ||
| const offset = motion.offset(); | ||
| const target = | ||
| nearestIndex(offset, latestRef.current.layout) * ROW_HEIGHT; | ||
| move(snapCurve(offset, target, 1), {onEnd}); | ||
| }, WHEEL_IDLE_MS); | ||
| }; | ||
| column.addEventListener('wheel', onWheel, {passive: false}); | ||
| return () => { | ||
| column.removeEventListener('wheel', onWheel); | ||
| window.clearTimeout(idle); | ||
| }; | ||
| }, [isActive, motion, move, onEnd]); |
There was a problem hiding this comment.
This listener runs its own trackpad and mouse-wheel path beside the shared one main now has (allowedScrollTypes on PanGestureRecognizer, with the wheel segmenter behind the gesture host), which spec:AST-094 FR43 requires every followed trackpad pan to use. After merging main, pass allowedScrollTypes: ['continuous'] to the wheel-pan recognizer and keep only a discrete-notch handler that calls moveBy(±1) (component:DateInput FR15).
| useIsomorphicLayoutEffect(() => { | ||
| motion.setLayout(layout); | ||
| motion.attach(rowsRef.current); | ||
| }, [motion, layout]); |
There was a problem hiding this comment.
Attaching the strip is work caused by the element attaching (architecture:react-component-runtime INV6). A callback ref on the strip <div> that sets rowsRef and calls motion.attach(node) handles attach, detach and replacement; setLayout can stay here.
There was a problem hiding this comment.
Done at 69695d1: a callback ref on the strip sets rowsRef and calls motion.attach(strip), and its cleanup calls motion.attach(null), so attach, detach and replacement are handled. setLayout stays in a layout Effect.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: the picker wheel takes an owner's rest rule and turns from script, for the time and date-time columns that use them. Under a finger, `resolveRest` names the row a wheel moves on to after resting on another (by default the nearest enabled row), and `spinTo` on the wheel's ref turns it onto a row on the tap spin, then commits it as a rest would; under a fine pointer `spinTo` commits the row at once. Either way it reports whether that row was committed, refusing a disabled row, an index outside the rows and a row the owner's rest moves off, and a turn the wheel's unmounting cuts short reports that it did not arrive. A rest on the committed row still commits nothing. Nothing public changes: the wheel is not exported from the package entry point. |
There was a problem hiding this comment.
No time or date-time column passes resolveRest or a ref at this head (TouchTimeField.tsx, TouchDateTimeField.tsx, MonthYearWheels.tsx), so this note describes code with no caller. Drop it with the plumbing, or land both with the column that uses them.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Under a coarse primary pointer, as on a phone or a tablet, the picker wheels in the TimeInput, DateTimeInput and DateInput bottom sheets now move like UIPickerView. Their rows keep today's 28 px pitch and lie flat, fading toward the edges of the column in every browser; today's column tilts and fades its rows only where the browser supports scroll-driven animations, and shows a plain list elsewhere. The row under the band is in semibold accent text on its plate while the wheel rests and while a finger drags it; no row is marked while the wheel coasts, snaps or turns. 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, and a value changed from outside while the sheet is open, as when a bound clamps the time, turns the wheel onto its row instead of jumping there. 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. Under reduced motion a flick still coasts, as scrolling does; a tap, a key or a slow release cross-fades onto its row. `presentation="popover"` keeps the previous surface; no value keeps the native-scroll column under a finger. With a mouse the wheels keep today's column. |
There was a problem hiding this comment.
"With a mouse the wheels keep today's column" is true only where the primary pointer is fine: Wheel picks by the primary pointer, so a mouse on a tablet gets the touch wheel. Suggest "Under a fine primary pointer, as with a mouse on a desktop, the wheels keep today's column."
There was a problem hiding this comment.
Taken at 69695d1: "Under a fine primary pointer, as with a mouse on a desktop, the wheels keep the scrolling column."
| away from the committed row. Under a finger the rows fade without tipping or | ||
| shrinking; under a mouse they keep main's scroll-timeline tilt. |
There was a problem hiding this comment.
This sentence contradicts the unchanged first sentence of DEC-2 ("looks the same under a finger as under a mouse … rows tip back") and the Ownership non-goal, so it needs an engineering owner's decision. A record also should not name a branch ("main's scroll-timeline tilt").
There was a problem hiding this comment.
Fixed at 69695d1. DEC-2 now says one thing: under a finger, each sheet wheel lays its rows flat at the scrolling column's pitch and fades them toward its edges, without tipping or shrinking; under a mouse, the scrolling column tips and fades its rows on a scroll timeline. Its title and the cylinder non-goal match, and it names no branch. The owners approve the record lines at this head.
e897a73 to
69695d1
Compare
|
/approve-spec 69695d1 |
There was a problem hiding this comment.
This is ready to approve once its browser checks pass. Every behavior change is covered by the current component:DateInput and spec:AST-094 records, and the record changes here carry an engineering owner's /approve-spec for this head. The earlier findings are all fixed: the trackpad path, the unused turn plumbing, the FR23 haptics gap, the strip's attach Effect and the overclaiming notes.
Before merging:
- Let the browser lanes run (they were skipped while the pull request is a draft), and merge after #8131.
Optional tidy-ups:
- Scope
component:DateInputFR18 to a coarse primary pointer, and set DEC-2's decider date to the approval date. - Fill in the
@BROWSER@line in the description, and drop "a programmatic spin, a custom rest rule" fromPickerWheel.test.tsx's header.
[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 | |
There was a problem hiding this comment.
Non-blocking: FR18 names no presentation, but the scrolling column's highlight follows its scroll (Wheel.tsx's scroll listener), so under a mouse a row is marked while the column coasts or scrolls to a key's row. Scoping FR18 to a coarse primary pointer, as FR15 is, keeps the record from reading as a gap in the mouse column.
| ### DEC-2 — The sheet wheels' rows lie flat under a finger | ||
|
|
||
| **Reference:** `component:DateInput/DEC-2` | ||
| **Decider:** cixzhang, 2026-11-01 |
There was a problem hiding this comment.
Non-blocking: DEC-2's decision text changed at this head and was approved on 2026-11-02, the date FR17 and FR18 cite, but this line still dates it 2026-11-01.
| insetBlockStart: `calc(50% - (${dateInputTouchSizes.wheelItemSize} / 2))`, | ||
| pointerEvents: 'none', | ||
| }, | ||
| /** One row: an option at its place along the strip, flat. */ |
There was a problem hiding this comment.
Non-blocking (INV29): this JSDoc and the one on rowAt only restate the style names. Delete both; the same holds for the "Home and End: the first and the last enabled row." comments, which restate moveToEnd/toEnd and endIndex's JSDoc.
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.
|
/approve-spec 94d18e1 |
There was a problem hiding this comment.
This looks ready to approve. Every behavior change is covered by the current component:DateInput and spec:AST-094 records, and the record changes here carry an engineering owner's /approve-spec for this head. No blocking issue was found.
Before merging:
- Let the browser lanes run, the busy-main-thread proof among them; they are skipped while the pull request is a draft.
Optional tidy-ups:
- Scope
component:DateInputFR18 to a coarse primary pointer, since under a mouse the scrolling column still marks the row under its band while it scrolls. - Update DEC-2's decider date, the
@outputline ofPickerWheel.test.tsx, and the line inwheel-uikit-picker-values.mdthat calls the tap spin an internal constant. - Drop the comments that only restate the code (inline notes).
[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 | |
There was a problem hiding this comment.
Non-blocking: FR18 names no presentation, while FR17 says "under every presentation". Under a mouse, the scrolling column's scroll listener (Wheel.tsx:452-476) moves the accent mark with the scroll, so the rule holds only under a coarse primary pointer. Consider scoping it that way.
| ### DEC-2 — The sheet wheels' rows lie flat under a finger | ||
|
|
||
| **Reference:** `component:DateInput/DEC-2` | ||
| **Decider:** cixzhang, 2026-11-01 |
There was a problem hiding this comment.
Non-blocking: DEC-2's text changed in this pull request, after the 2026-11-02 decision that FR17 and FR18 cite, but the decider line still dates it 2026-11-01.
| insetBlockStart: `calc(50% - (${dateInputTouchSizes.wheelItemSize} / 2))`, | ||
| pointerEvents: 'none', | ||
| }, | ||
| /** One row: an option at its place along the strip, flat. */ |
There was a problem hiding this comment.
Non-blocking (INV29): this JSDoc and the one on rowAt (line 144) restate the style names. Delete both.
| return () => column.removeEventListener('wheel', onWheel); | ||
| }, [isActive, moveBy]); | ||
|
|
||
| // Home and End: the first and the last enabled row. |
There was a problem hiding this comment.
Non-blocking (INV29): restates moveToEnd and endIndex. Delete it, and the same line in Wheel.tsx (515).
| ); | ||
| const option = options[next]; | ||
| if (option == null || option.isDisabled) { | ||
| // A key commits the row it moves to and scrolls the column there. |
There was a problem hiding this comment.
Non-blocking (INV29): this narrates moveTo. Delete it. activate below (line 521) has the same body as moveTo, so it can be moveTo itself.
Batch: B4 · Wheel / picker (DateInput, TimeInput, DateTimeInput)
On main after #8131. The change: vjeux/astryx@cae1f66...94d18e1
Supersedes #8156, #8526, #8162, #8361, #8527, #8480 and #8698.
#8156, #8162, #8361 and #8698 are combined 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 distance. This makes them move like UIPickerView; a mouse keeps the scrolling column. Keys step in time order, so 12 PM is reachable.
Current specification
spec:AST-094FR4 and its implementation gaps, FR5, FR6, FR23, FR43.component:DateInputDEC-1, DEC-2, FR13 to FR15, FR17, FR18.design:reduced-motionDR2, DR3;architecture:react-component-runtimeINV6, INV13, INV16;architecture:interaction-modalityINV1.Owner decisions
DEC-2, FR14's tap-spin role, FR17, FR18 and FR4's snap-rate gap carry the spec owners' rulings verbatim; FR13 no longer repeats the gap.
Design and alternatives
decayand turning withplay, faded by a static mask inside the column.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 (gestures and animator), paid once per page, as for Slider.
Writing rubric results
Over DEC-2, the FR13 to FR18 rows, FR4's new gap line, the Current-state lines and the front matter:
Evidence
Testing
Gates at 94d18e1 so far: strict lint, typecheck, check:knowledge, check:motion-roles, check:changesets and check:i18n-runtime; the wheel, gesture and Switch units (99 files, 2,824 tests); the touch-bundle and motion-role node tests. The full test:ui and test:node, the Storybook build, Chromium (a11y, interaction, busy proofs) and WebKit at CI's profile ×10 are running.
On a phone or the iOS simulator, open Storybook's TimeInput › Bottom Sheet With Min Max:
Automated:
PickerWheel.test.tsx,Wheel.test.tsx,wheelStrip.test.ts,wheelPhysics.test.tsand TimeInput'sPresentation.test.tsxin jsdom; the browser specs in Chromium and WebKit.Merge order: this pull request, then #8178, #8179 and #8209.