Repository navigation
feat(DateInput, TimeInput, DateTimeInput): the sheets' wheels draw as one picker under a finger - #8161
feat(DateInput, TimeInput, DateTimeInput): the sheets' wheels draw as one picker under a finger#8161vjeux wants to merge 546 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewModified ComponentsContextMenu (@astryxdesign/core) · View in Storybook
DateInput (@astryxdesign/core) · View in Storybook
DateTimeInput (@astryxdesign/core) · View in Storybook
MoreMenu (@astryxdesign/core) · View in Storybook
MultiSelector (@astryxdesign/core) · View in Storybook
TimeInput (@astryxdesign/core) · View in Storybook
Component Size
Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.
|
| Component | JavaScript | CSS |
|---|---|---|
| core/DateInput | +26.7KB |
+313B |
| core/DateTimeInput | +26.9KB |
+312B |
| core/PowerSearch | +27.8KB |
+298B |
| core/TimeInput | +26.4KB |
+363B |
Bundle Size Summary
Gzip figures measure each package's entry file, not the code an import pulls in.
| Package | Entry (ESM) | Entry gzip before | Entry gzip after | Delta |
|---|---|---|---|---|
| @astryxdesign/core | 4.9KB | 1.2KB | 1.2KB | 0B |
| Package stylesheet | Gzip before | Gzip after | Delta |
|---|---|---|---|
| @astryxdesign/core | 33.7KB | 33.9KB | +254B |
Accessibility Audit
Status: No accessibility violations detected.
Visual Regression
48 of 502 shot(s) changed.
A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.
| component | story | theme | mode | pixels |
|---|---|---|---|---|
| Selector | Indicator Space Bottom Sheet Narrow End RTL | neutral | light | 753,933 |
| Selector | Indicator Space Bottom Sheet Wide Start | neutral | light | 740,072 |
| RichTextEditor | Markdown parity: side by side | probe | dark | 556,900 |
| RichTextEditor | Markdown parity: side by side | probe | light | 538,816 |
| Chat | Full AI Chat | probe | light | 230,823 |
| Chat | Full AI Chat | probe | dark | 229,805 |
| AppShell | With Mobile Nav | probe | light | 217,010 |
| AppShell | With Mobile Nav | probe | dark | 216,960 |
| Collapsible | Dividers | probe | dark | 66,445 |
| Collapsible | Dividers | probe | light | 66,445 |
| AppShell | Playground | neutral | dark | 65,198 |
| AppShell | Playground | neutral | light | 63,788 |
| Chat | Default | neutral | dark | 36,304 |
| Chat | Default | neutral | light | 35,767 |
| Chat | Empty State | probe | light | 23,531 |
| Chat | Empty State | probe | dark | 22,622 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | dark | 15,378 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | light | 14,107 |
| Chat | With Attachments | probe | light | 13,406 |
| Chat | With Attachments | probe | dark | 11,232 |
and 28 more.
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report
There was a problem hiding this comment.
Approval is on hold until an engineering owner approves or rejects the draft requirements in AST-094 (FR1–FR23) and the iOS picker look and motion this pull request gives the TimeInput, DateInput and DateTimeInput wheels; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and you may do so yourself if you are an engineering owner. Independently of that decision, these changes are needed:
- Split the 29,256 authored lines into smaller pull requests, landing the animator, projection and unused recognizers with their first consumer.
- Keep
dateInputTouchSizes.wheelItemSizeanddateInputTouchGeometry.wheelEdgePaddinginDateInput/tokens.stylexor ship their removal as[breaking], derive the wheel's resting row during render instead of in a layout Effect, and remove the gesture README section that documents atouchPresentationmodule that does not exist.
[Automated review]
| // 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.
This layout Effect stores selectedIndex, a value derived from props, so the listbox renders once with the previous active row and is repaired afterwards; the reason names the component's own motion engine rather than an external system. Derive the resting row during render when selectedIndex changes and the wheel is idle, and keep only motion.setOffset here (architecture react-component-runtime INV1, INV3).
| * column of values, not as a stack of buttons with air between them. At | ||
| * this height ~9 options are on screen. | ||
| */ | ||
| wheelItemSize: '28px', |
There was a problem hiding this comment.
dateInputTouchSizes.wheelItemSize is part of the released public subpath @astryxdesign/core/DateInput/tokens.stylex; removing it breaks consumers and swizzled wheels. Keep it (and wheelEdgePadding) with its released value, or ship the removal as [breaking] with a migration to wheelGeometry (AST-017 FR7).
| ## Touch presentation | ||
|
|
||
| Whether a component presents itself the touch (UIKit) way is answered once, | ||
| in `../utils/touchPresentation.ts`: |
There was a problem hiding this comment.
../utils/touchPresentation.ts, TOUCH_QUERY, COMPACT_QUERY, COMPACT_TOUCH_QUERY, useTouchPresentation(), readTouchPresentation() and useMotionPresentation() do not exist in this pull request; remove this section or land the module with it.
There was a problem hiding this comment.
| --output tooltip-hover-bridge-output | ||
|
|
||
| # A picker wheel dragged onto a row outside its bounds must come to rest | ||
| # on the nearest row inside them. The stop comes from native scrolling, |
There was a problem hiding this comment.
The wheel no longer rests through native scrolling, CSS snapping or scrollend; it rests through its recognizers and motion engine. Reword this comment so the contract step describes what it proves.
There was a problem hiding this comment.
Reworded: on the touch column (a finger) the stop comes from real touch input through the gesture system; on today's column (a mouse) from native scrolling, CSS snapping and the end-of-scroll signal. The step runs both.
25b432c to
712f1ad
Compare
There was a problem hiding this comment.
Approval is on hold until an engineering owner approves or rejects the draft spec:AST-094 requirements (FR1–FR23 and DEC-1/DEC-2, which carry the wheels' UIPickerView look and motion) by commenting /approve-spec <full head SHA> on this pull request; the author may record that decision too if they are an engineering owner. Independent of that decision, these changes are needed:
- Retarget onto the open #8159 stack, or land it first, so this pull request merges only its own one-picker change instead of 29,324 lines.
- Keep the released
dateInputTouchSizes.wheelItemSizeanddateInputTouchGeometry.wheelEdgePaddingtokens, or ship their removal as[breaking]with a migration, and tag the new look[feat](AST-017 FR3, FR7). - Derive the wheel's active row during render instead of repairing it in the layout effect at
Wheel.tsx:430-440(react-component-runtime INV1). - Remove or fix the docs that describe modules missing from the tree (
gestures/README.md:335-356,motion/MEASUREMENTS.md), the stale WheelRest comment inci.yml, and thespec:AST-090citations.
[Automated review]
| 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.
This layout Effect copies the prop-derived selectedIndex into restIndex, so after an outside value change the first render points aria-activedescendant (line 807) at the old row and this Effect repairs it with a second render. architecture:react-component-runtime INV1 forbids rendering a known-wrong state and repairing it in an Effect; the lint reason names the motion engine, but the stored value does not come from it. Keep the rest row together with the value it belongs to and derive the active row during render, leaving only motion.setOffset here.
| * column of values, not as a stack of buttons with air between them. At | ||
| * this height ~9 options are on screen. | ||
| */ | ||
| wheelItemSize: '28px', |
There was a problem hiding this comment.
dateInputTouchSizes.wheelItemSize (and dateInputTouchGeometry.wheelEdgePadding below) ship in 0.7.4 at @astryxdesign/core/DateInput/tokens.stylex, a path exported since 0.6.7 for swizzled components. Removing them under a [fix] patch Changeset contradicts spec:AST-017 FR3 and FR7: keep them as deprecated aliases, or ship the removal as [breaking] with a migration.
| ## Touch presentation | ||
|
|
||
| Whether a component presents itself the touch (UIKit) way is answered once, | ||
| in `../utils/touchPresentation.ts`: |
There was a problem hiding this comment.
../utils/touchPresentation.ts, TOUCH_QUERY, COMPACT_QUERY, COMPACT_TOUCH_QUERY, useTouchPresentation(), readTouchPresentation() and useMotionPresentation() do not exist in this tree, and no test enforces them. Drop this section or land the module first.
There was a problem hiding this comment.
| --output tooltip-hover-bridge-output | ||
|
|
||
| # A picker wheel dragged onto a row outside its bounds must come to rest | ||
| # on the nearest row inside them. The stop comes from native scrolling, |
There was a problem hiding this comment.
The wheel no longer stops through native scrolling, CSS snapping or the end-of-scroll signal: at this head WheelMotion plays the release and snap curves. Describe the engine's stop instead.
There was a problem hiding this comment.
Reworded: on the touch column (a finger) the stop comes from real touch input through the gesture system; on today's column (a mouse) from native scrolling, CSS snapping and the end-of-scroll signal. The step runs both.
| follows. | ||
| - **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the | ||
| exit its UIKit counterpart plays. **Proposed; pending owner review:** this | ||
| departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10 |
There was a problem hiding this comment.
No spec:AST-090 exists on main or at this head, so this departure (and the amendment at line 235) points at nothing. Cite the record meant, or drop the note.
712f1ad to
7e49b08
Compare
There was a problem hiding this comment.
Approval is on hold until an engineering owner approves or rejects the draft spec:AST-094 requirements (FR1–FR23 and DEC-1/DEC-2, which carry the wheels' UIPickerView look and motion) by commenting /approve-spec <full head SHA> on this pull request; the author may record that decision too if they are an engineering owner. Independent of that decision, these changes are needed:
- Retarget onto the open #8159 stack, or land it first, so this pull request merges only its own one-picker change instead of 29,172 lines.
- Keep the released
dateInputTouchSizes.wheelItemSizeanddateInputTouchGeometry.wheelEdgePaddingtokens, or ship their removal as[breaking]with a migration, and tag the new look[feat](AST-017 FR3, FR4, FR7). - Derive the wheel's active row during render instead of repairing it in the layout Effect at
Wheel.tsx:430-440(react-component-runtime INV1). - Remove or fix the docs that describe modules missing from the tree (
gestures/README.md:335-356,motion/MEASUREMENTS.md, thesources.test.tsheaders), the stale WheelRest comment inci.yml, and thespec:AST-090citations.
[Automated review]
| 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.
This layout Effect copies the prop-derived selectedIndex into restIndex, so after an outside value change the first render points aria-activedescendant (line 807) at the previous row while aria-selected (line 829) already marks the new one, and a second render repairs it. architecture:react-component-runtime INV1: "A component MUST NOT render a known-wrong initial state and repair it in a follow-up Effect". The stored value comes from props, not from the motion engine the lint reason names. Keep the rest row with the value it belongs to (for example {index, value} in state), derive the active row during render when they differ, and leave only motion.setOffset here.
| * column of values, not as a stack of buttons with air between them. At | ||
| * this height ~9 options are on screen. | ||
| */ | ||
| wheelItemSize: '28px', |
There was a problem hiding this comment.
dateInputTouchSizes.wheelItemSize (and dateInputTouchGeometry.wheelEdgePadding below) ship in 0.7.4 at @astryxdesign/core/DateInput/tokens.stylex (dist/DateInput/tokens.stylex.d.ts lines 49 and 61). Removing them breaks code that reads them, which spec:AST-017 FR3, FR4 and FR7 make a [breaking] change, while the Changesets declare [fix] patch. Keep both keys with their released values beside wheelGeometry, or declare the removal [breaking] with a migration.
| '@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.
This describes an intentional new look and motion, which spec:AST-017 FR3 tags [feat] ("[fix] when it restores current visual authority and [feat] when it intentionally changes visual direction"); no current record states the UIPickerView look yet. The same applies to .changeset/wheel-group-inputs.md.
There was a problem hiding this comment.
Retagged [feat]: the touch look is a new visual direction (AST-017 FR3). .changeset/wheel-group-inputs.md is [feat] too.
|
|
||
| ## Touch presentation | ||
|
|
||
| Whether a component presents itself the touch (UIKit) way is answered once, |
There was a problem hiding this comment.
../utils/touchPresentation.ts, TOUCH_QUERY, COMPACT_QUERY, COMPACT_TOUCH_QUERY, useTouchPresentation(), readTouchPresentation() and useMotionPresentation() do not exist at this head, and no test enforces the query spelling. Drop this section until the resolver lands, or land the module with it.
There was a problem hiding this comment.
| --output tooltip-hover-bridge-output | ||
|
|
||
| # A picker wheel dragged onto a row outside its bounds must come to rest | ||
| # on the nearest row inside them. The stop comes from native scrolling, |
There was a problem hiding this comment.
At this head the wheel no longer stops through native scrolling, CSS snapping or the end-of-scroll signal: WheelMotion plays the release and snap curves and reports arrival. Reword the comment to describe what the contract now exercises.
There was a problem hiding this comment.
Reworded: on the touch column (a finger) the stop comes from real touch input through the gesture system; on today's column (a mouse) from native scrolling, CSS snapping and the end-of-scroll signal. The step runs both.
| follows. | ||
| - **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the | ||
| exit its UIKit counterpart plays. **Proposed; pending owner review:** this | ||
| departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10 |
There was a problem hiding this comment.
No spec:AST-090 exists on main or at this head, so this departure (and the amendment listed at line 235) points at nothing. Cite the record meant, or drop the note.
7e49b08 to
ca05d39
Compare
There was a problem hiding this comment.
Approval is on hold until an engineering owner approves or rejects the draft spec:AST-094 requirements (FR1–FR23, DEC-1 and DEC-2, which carry the wheels' iOS look and motion) and identifies the record that governs the new motion/experimental and gestures/experimental subpaths and the theme motion roles. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request; the author may record it too as an engineering owner. Independent of that decision, these changes are needed:
- Retarget onto the #8159 branch, or land that stack first, so this pull request merges only its 343-line one-picker change.
- Resolve compact as width below the theme's
mdpoint or height below 500 instead of 744 px (public-component-api INV17, AST-012 FR9). - Remove
when.platformor have it added to AST-012 FR2, and make FR4 and DEC-1 match the themable motion roles or drop the roles. - Derive the wheel's active row during render instead of repairing it in the layout Effect at
PickerWheel.tsx:362-372(react-component-runtime INV1). - Keep the experimental hooks out of stable
astryx searchoutput (AST-017 FR27), make Home and End reach the ends while the wheel moves, tag the new look[feat], and fix the references to missing files andspec:AST-090.
[Automated review]
| 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.
This edge contradicts architecture:public-component-api INV17 ("uses the theme's compact definition: width below the effective md point or height below 500 … It keeps no compact numbers of its own") and spec:AST-012 FR9, which names the web touch presentation resolver. At 760 × 1024 with a coarse pointer, MoreMenu presentation="adaptive" now opens a popover where current spec gives a sheet. Resolve compact from the theme's effective md point (768 by default) or a height below 500, and update touchPresentation.test.ts:328, the docs and the README that repeat 744.
| * is everything else. Pair `ios` with `pointer: 'coarse'` for a touch | ||
| * profile, such as touch motion tuned per platform. | ||
| */ | ||
| platform?: 'ios' | 'other'; |
There was a problem hiding this comment.
spec:AST-012 FR2 lists width, height, pointer, contrast and motion, and says "Future environment fields require explicit additions to this closed vocabulary." Remove platform, or have an AST-012 owner add it to FR2 before it ships on the exported ThemeAdaptationCondition.
There was a problem hiding this comment.
Not in this pull request's commits: themeAdaptations.ts and its platform field come from #8076's stack. The FR2 amendment or the removal belongs to that 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.
After an outside value change the first render points aria-activedescendant (line 757) at the previous row while aria-selected (line 779) marks the new one, and this layout Effect repairs it with a second render; architecture:react-component-runtime INV1 forbids that. Keep the rest row with the value it belongs to (for example {index, value} in state), derive the active row during render when they differ, and leave only motion.setOffset here.
There was a problem hiding this comment.
Fixed in #8159 (adb185f), which this branch sits on: 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.
| 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 a running coast is heading to, so Home pressed while the wheel coasts from row 10 to row 20 lands on row 10, and End misses the same way on an upward coast. Move to the first and last enabled rows directly instead of a delta from restIndex.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] On iOS and iPadOS, under a finger, 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, and the row in the capsule band is 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. A theme can retune the tap spin's duration (`picker.tapSpin.durationMs`) and curve (`timing.curves.easeInOut`) and the bounce at an end (`scroll.bounce`). On Android and with a mouse the wheels are unchanged. |
There was a problem hiding this comment.
This is an intentional new look and motion, which spec:AST-017 FR3 tags [feat] ("[fix] when it restores current visual authority and [feat] when it intentionally changes visual direction"); the same applies to .changeset/wheel-group-inputs.md.
There was a problem hiding this comment.
Fixed: .changeset/wheel-uikit-picker.md and .changeset/wheel-group-inputs.md are both [feat].
| - **FR4 — Physics are not themable in touch presentations.** A touch | ||
| presentation MUST read its motion from the table on every theme. Themes keep | ||
| color, shape, type and spacing; the theme's duration and easing tokens keep | ||
| governing pointer presentations. **Proposed; pending owner review:** this | ||
| narrows `architecture:theme-tokens` (owners: cixzhang, imdreamrunner) to | ||
| pointer presentations, and departs from the draft `design:motion` DR3 | ||
| (owners: ernestt, cixzhang) wherever UIKit settles with an overshoot or | ||
| stretches with a rubber band. |
There was a problem hiding this comment.
This pull request also ships defineTheme({motion: {springs, durations, curves}}) and the wheel reads picker.tapSpin.durationMs, timing.curves.easeInOut and scroll.bounce through the theme, so FR4 (and DEC-1's rejection of per-theme tuning) says the opposite of the code. Make the record state the themable roles, or drop the roles.
| follows. | ||
| - **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the | ||
| exit its UIKit counterpart plays. **Proposed; pending owner review:** this | ||
| departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10 |
There was a problem hiding this comment.
No spec:AST-090 exists on main or at this head, so this departure and the amendment listed at line 235 point at nothing. Cite the record meant, or drop the note.
| @@ -1,4 +1,24 @@ | |||
| Results for "experimental" (3) | |||
| Results for "experimental" (7) | |||
There was a problem hiding this comment.
The four new results are experimental hooks in stable astryx search output with no opt-in. spec:AST-017 FR27 says "An omitted opt-in MUST leave stable defaults, output, routes, data, and behavior unchanged", and experimental-isolation.test.mjs calls a golden diff that names experimental the leak it exists to catch. Keep the hooks out of stable search unless a marked experimental route carries them, and restore this golden.
There was a problem hiding this comment.
Not in this pull request's commits: those search results and the golden come from #8076's stack, and that change carries the fix.
| where a switch outside any scroll view is needed. | ||
|
|
||
| Done (iOS 26.2 simulator, Oct 23 2026; in the switch's own table, | ||
| `uikit/switch.ts`): M-SW02, M-SW03, M-SW04, M-SW05, M-SW06, M-SW09 (UISwitch |
There was a problem hiding this comment.
uikit/switch.ts (and uikit/contextMenu.ts at line 254) are not in this tree; the table headers in uikit/scroll.ts, springs.ts and timing.ts likewise name a sources.test.ts that does not exist (the check is uikit/uikit.test.ts). Fix the references or drop them.
There was a problem hiding this comment.
Not this pull request's lines: the switch and context-menu references and the table headers are #8076's, and that change carries the fix. This pull request adds only the picker section.
ca05d39 to
e16c969
Compare
…the next touch The hook rebuilds its tracker when trackingExpansion, highlightDelay, cancelTravel or quickTapFlash change, but the test that pinned it was lost in a merge. Restored: a control rerendered with a cancel travel cancels a later touch that moves past it. With cancelTravel left out of the rebuild's dependencies, it fails.
Her arena work (the touch depth, a view's delivery only to involved recognizers, the reset count, touchBeganElsewhere's delivery, the simultaneity rule) reaches the recognizer's plumbing through the internal view, as this PR keeps it out of the public type; the new deliverBeganElsewhere joins that view.
Her foundation fixes for the review items this PR also answered take her form: the swipe's segment reading with directions ruled out for good, the tap's lift centroid, the claim release that counts a recognized discrete gesture, Hover's restart and the idle recognizer's comment. Both sides' tests stay: her spacing, 40-degree, re-basing and settling cases beside this PR's 20-degree, re-based release and mid-hover cases.
…ades On a touch screen, or after a touch drag, the wheel's own scrolls cross-fade with the rows landing while they are faded out (design:reduced-motion DR2); otherwise they land at once (DR3). "With a mouse or keyboard" read as if a key step on a touch screen landed at once.
… they add picker.tapSpin.durationMs is a role defineTheme accepts and astryx docs motion lists, a compatible capability, so the entry is a [feat] that names it first (spec:AST-017 FR7) instead of an internal [chore].
…urves.move spec:AST-094 FR4 names the table's roles by what they do: the base now calls Core Animation's ease-in-ease-out timing.curves.move (the same control points, 0.42, 0, 0.58, 1). The tap spin's default curve and the picker table's notes read that role. With the old name the tap spin baked an undefined curve: at the merge, wheelPhysics.test.ts's tap spin and three of wheelDrum.test.ts's looping cases failed.
…e base The base's motion docs topic and this branch's picker role (the tap spin's duration, picker.tapSpin.durationMs) each moved the topic's skeleton; the merge kept the base's golden. Regenerated with `vitest run packages/cli/test/experimental-isolation.test.mjs --update`: only the motion topic's skeleton hash changes, and no experimental surface is named.
…gnizer and compositor curves The picker wheel is a native scroll-snap column. WebKit gives every scroll-snapping scroller the fast deceleration rate (0.99/ms) while UIPickerView decelerates at the normal rate (0.998) onto a row it picks from its own projection, and a native scroll's momentum cannot be caught or held to a curve. Under a finger, on every touch device (one touch model, iOS and Android alike), the Wheel is now UIPickerView's, PickerWheel.tsx, on the gesture system and the picker's values in the UIKit constants table: - A ScrollViewPanGestureRecognizer, the pan a scroll view uses, drags the rows, on UIKit's rubber band past an end. The lift plays UIKit's release plan (wheelPhysics.ts) from a scroll view's release velocity (the pan's last two, blended as UIPickerView's); a finger landing on a moving wheel stops it where it is; a tap turns a row onto the band. Hidden mid-drag, the wheel drops the drag uncommitted and parks the committed row when shown again. - Each motion plays through wheelMotion.ts: per-row Web Animations on the compositor, each row turned onto the cylinder, with a selection tick per row reached where the browser can vibrate. - The tap spin's duration and curve and the bounce at an end are the theme's motion roles (picker.tapSpin.durationMs, timing.curves.easeInOut, scroll.bounce), resolved against the column as each motion starts. - The value commits when the wheel comes to rest; keys and clicks commit at once and turn the wheel. Home and End go to the first and last enabled row wherever the wheel is heading. The listbox's active descendant is the committed row, derived during render. - Under reduced motion (design:reduced-motion) every transition the wheel starts lands without travel: the rows cross-fade onto their row on the animator's Reduce Motion fade (reducedMotionFade, WheelMotion.crossFade), whatever curve it was asked for; the drag itself follows the finger. - isLooping, resolveRest and a spinTo(index) handle for the date and time wheels. - WheelGroup: one band and one vanishing point across picker columns. A Wheel on its own draws its own band. The band is decoration, so the frame's (or the lone column's) ::before draws it, marked data-wheel-band (INV7). The vanishing point follows the shared resize observer. - A held finger never starts text selection or the callout: the pan asks the gesture host for it (preventsTextSelection). The column keeps the pointer of a finger that lands on it, as it keeps the touch: a BottomSheet would otherwise arm its pull-down on the pointer and follow the first 8 px of a downward drag. The switch reads useTouchPresentation. A wheel shows only on a sheet, so a caller who pins the fine surface gets no sheet and no wheel (architecture:public-component-api INV16). Under a mouse or other fine pointer Wheel keeps today's scrolling column, now named ScrollWheel and otherwise unchanged (the released DateInput tokens stay as they are; the touch geometry is in the unexported wheelGeometry.stylex.ts); there spinTo commits at once, isLooping and resolveRest do nothing, and WheelGroup renders its children alone. PickerWheel.tsx is the touch layer, imported by Wheel.tsx alone. The shared types and nearestEnabledIndex move to wheelOptions.ts.
…s coarse The gesture host takes input only where the primary pointer is coarse (spec:AST-094 FR19, the gesture line's coarse-pointer gate), and the touch column draws only there. PickerWheel.test.tsx ran under jsdom's fine pointer, so on that base every touch case saw no input: 11 of 30 failed. Each case now runs under a coarse primary pointer, as on a phone or a tablet; the mouse cases are a mouse on a touch device, a tablet's trackpad.
The base renamed Core Animation's ease-in-ease-out to timing.curves.move (spec:AST-094 FR4, the same control points). The tap spin resolves that role under the column's theme, and the docs and the release note name it. With the old name the spin resolved an undefined curve, and 13 of PickerWheel.test.tsx's cases that turn the wheel failed at the merge.
…nLock The base replaced a recognizer's preventsTextSelection with the selectionLock option (spec:AST-094 FR41): a recognizer whose hold does something passes selectionLock: lockSelection from gestures/selectionLock.ts. The wheel's pan is such a recognizer (a held finger turns the wheel), so it keeps the lock.
… one picker under a finger The month and year wheels of the DateInput sheet and the time wheels of the TimeInput and DateTimeInput sheets sit in a WheelGroup. Under a finger, on every touch device, one selection band spans every column, and each column's dimmed rows lean toward the picker's centre, as UIPickerView draws them. Under a mouse or other fine pointer the group renders the columns alone, so the sheets keep today's layout; the sheets' own styles are unchanged (the DateTimeInput time row keeps its gutter between today's columns: a Chromium check under a mouse holds every gap to 8 px or more).
17a6cee to
9a40046
Compare
|
Nothing is left in this pull request, so it closes. Adding an extra load on mobile is extremely costly, so the touch column stays a static import behind Where its parts went:
|
Depends on #8159 (and its predecessors); review the last commit.
User impact
Under a finger, the TimeInput, DateTimeInput and DateInput sheets show two to four wheels side by side. UIPickerView draws such columns as one picker: one selection band spans them, and each column's dimmed rows lean toward the picker's centre. After #8159 each touch wheel still draws its own band and vanishing point. This PR wraps the sheets' wheels in
WheelGroup, which draws that frame under a finger and nothing under a fine pointer: mouse sheets are unchanged.Visual authority
DDrow reused or amended.Design decisionstable by a team design owner.docs/design/.None of these settles it yet. The geometry is UIKit's, measured for #8156 (one band inset 9 pt across the columns, every column's table centred on the picker), under
spec:AST-094(#8378) FR1. The band's fill is #8159's, proposed; pending owner review.Before and after
A flick in Mobile Safari (iOS 26.2 simulator) under the shared band; the blue dot marks the finger (drawn for the recording):
State matrix
Mobile Safari, iOS 26.2 simulator:
Scope
Testing
presentation="bottom-sheet"on a fine pointer).pnpm exec vitest run packages/core/src/DateInput packages/core/src/TimeInput packages/core/src/DateTimeInput packages/core/src/motion packages/core/src/gestures packages/core/src/utils packages/core/src/BottomSheet: 95 files, 2310 tests.pnpm -F @astryxdesign/core typecheck, ESLint and Prettier on touched files,pnpm check:changesets.Stack (merge in this order): … → #8156 → #8159 → this PR (the full order is in #8156).