Skip to content

feat(Wheel): the touch column looks like UIPickerView - #8480

Draft
vjeux wants to merge 1879 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-look
Draft

vjeux wants to merge 1879 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-look

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8527. Review only this PR's change: vjeux/astryx@1d7ee3e...4f789d9

User need

On a phone, the date and time sheets' wheels move like the iOS picker (#8159) but still look like the desktop column, flat rows on an accent plate. People know the iOS picker by its look as much as its motion. This draws the touch wheels as UIPickerView does; a mouse keeps today's column.

Problem and solution fit

What led to it: Astryx's UIKit-parity work (spec:AST-094) makes its phone presentations behave like iOS, and the stack below matched the wheel's motion to UIKit's. Measuring UIKit's picker on the iOS 26.2 simulator gave the rest, drawn here:

  • rows on UIKit's cylinder at the default look's 28 px row (FR42), in the secondary text colour off the band (4.5:1);
  • one capsule band across a sheet's columns, its row magnified;
  • columns where UIDatePicker places them, mirrored right to left;
  • under Reduce Motion, a flick still coasts and every other turn cross-fades.

Current specification

spec:AST-094 FR1, FR5, FR42; design:reduced-motion DR2. The look and its element exception sit in the record that owns each sheet: component:DateInput DD1 and PR3 for its month and year wheels, and component:TimeInput DD1 and PR2 and component:DateTimeInput DD1 and PR2 for their sheets' wheels. All three records' owner is cixzhang; component:TimeInput is current, so its rows are a proposed change to a current record, and the others are draft. Whether they stand is the owner's decision.

Public delta and caller burden

No API change. Size (INV6, gzip, the size report's own measure):

Import, gzip JavaScript: main → this head This PR's step (from #8527) CSS: main → this head This PR's step
DateInput 101.4 → 133.8 KB +1.6 KB 14.9 → 15.3 KB +0.34 KB
TimeInput 88.6 → 119.5 KB +1.6 KB 13.9 → 14.3 KB +0.31 KB
DateTimeInput 111.6 → 143.6 KB +1.7 KB 15.4 → 15.7 KB +0.33 KB

Against main the imports carry the whole stack below (the gesture system, the wheel's physics, player and rest), whose pull requests report their own parts. This pull request's own step stays under INV6's 2 KB JavaScript and 1 KB CSS flags for touch-only code.

Design and alternatives

  • Keep today's look. Consistent with desktop, but iOS motion in a desktop look.
  • One drawing per row, turned in 3D. Fewer elements, but the band can't magnify its row, nor show a row crossing its edge at both sizes.
  • Chosen: UIKit's two drawings (dimmed rows on the cylinder, a magnified copy in the band), the only way to match what people see on iOS. Costs: each label drawn three times (DateInput PR3, TimeInput PR2, DateTimeInput PR2) and the size above.

Evidence

iOS 26.2 simulator, this head; UIKit is Safari's own picker, Before is today's look:

UIKit Before After
UIKit Before After
Drag the hours, flick the minutes Reduce Motion, tap, main thread blocked: before After
Clip Before After
Flick Reduce Motion: flick Reduce Motion: tap
Flick RM flick RM tap

The look the design owner approved, and this head (28 px rows, secondary colour):

Approved This head
Light Before After
Dark Before After

Scope

  • One visual direction, recorded as a design decision in each sheet's record (DateInput, TimeInput and DateTimeInput DD1).
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Focused tests and evidence ship with it.
  • Public text and artifacts contain no internal Meta context.

Writing rubric results

Over DateInput.spec.md, TimeInput.spec.md and DateTimeInput.spec.md: R1 narration, none; R2 research, none (the element counts are the sheets' own); R3 evidence, no pull requests or commits; R4 rejected alternatives, one (DateInput DEC-1), one line; R5 change references, none; R6 open questions, none. pnpm check:knowledge passes.

Testing

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

  1. Open the sheet: three columns read as one picker with one band, as in Safari's picker above.
  2. Drag the hours, flick the minutes: rows travel on the cylinder, the band's row magnified. Catch a wheel bouncing past its last row: it holds where it is.
  3. With Reduce Motion on, tap the row below the band: the rows cross-fade onto it, even while the page is busy.
  4. With a mouse: today's column.

Automated tests hold the 28 px rows, the 4.5:1 contrast in light and dark, and the catch, in jsdom and in Chromium; each fails without its change. At this head the core suite (15,356 tests) passes, and the date, time and wheel browser specs pass in Chromium (89 accessibility, 11 interaction, including the one-second busy-page proof). Not this change: the root-entry bundle check fails on main's test, which #8671 fixes.

Since b4b2032: the stack below moved to its new heads; the look's two browser proofs follow it (the clamp proof taps the PM row where the cylinder draws it, and the one-second busy proof follows rows in turn, since the cylinder hides a row as it crosses the band); each sheet's record now holds its wheels' look and exception; and the band-magnification note in picker.ts says what UIKit draws, each band label magnified about its own centre.

@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Error Error Oct 30, 2026 3:41am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 4:01 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This review is on hold for an owner decision. No current spec governs the touch wheel's UIPickerView look, because spec:AST-094 covers touch behavior only. An engineering or design owner needs to provide or identify the record that governs the look, its --color-neutral band fill and the INV7 exception its band copy needs, and to approve or reject the draft rest rule in component:DateInput FR13/DEC-1 and component:DateTimeInput FR8. Any engineering owner records the decision by commenting /approve-spec <full head SHA> once the head carries that record, and as an engineering owner the author can do this too. These points need fixing whatever the decision:

  • Land #8366 and its predecessors first, so this diff carries only the look (about 2,960 lines instead of 42,176), then split it or show why it has to stay together.
  • PickerWheel.tsx draws every label again through content: attr(data-label) pseudo-elements, which architecture:component-style-authoring INV7 rules out unless an owner records an exception.
  • A drag re-renders the wheel through flushSync each time it leaves the mounted rows, which spec:AST-094 FR9 rules out ("no component render per move").
  • The PickerWheel.tsx header says the touch layer loads lazily, but Wheel.tsx imports it statically, and INV6 asks for the component size report for this change.

[Automated review]

// technology meet only the options.
rowLabel: {
'::before': {
content: 'attr(data-label)',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This draws the label again as generated content, and stripLabel (line 257) draws it a third time, over the transparent option (line 211) that already holds it. architecture:component-style-authoring INV7 says content renders once and a pseudo-element never repeats content, and it allows an exception only as a recorded owner decision with element counts. Either record that exception where the look is approved, or draw the band and drum without repeating the label.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The repetition is the look itself. Each label is the listbox's transparent option (for assistive technology and taps), a row on the drum, and a row in the band's magnified copy: a row crossing the band's edge is drawn both magnified and unmagnified, as UIKit draws two table views, so one rendering cannot show it. The description now asks the design owner to record the INV7 exception with the look, with the counts: 265 elements for the TimeInput sheet under a finger, 288 under a mouse.

// A motion's animations are baked per mounted row as it starts, and
// a drag frame writes the mounted rows: both need the rows now.
// eslint-disable-next-line @eslint-react/dom-no-flush-sync, @eslint-react/set-state-in-effect -- see above; never called from an effect with `now`
flushSync(() => setRowWindow(next));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The pan's began and changed handlers (lines 707 and 720) call this with now, so every time the offset leaves the mounted window, about every two rows, the whole column renders synchronously inside a touch move. spec:AST-094 FR9 requires no component render per move. Recycling the mounted drum and strip nodes inside WheelMotion on the drag path would move the window without a render.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, a real gap: the mounted window is React state, so a drag that leaves it renders the column inside a touch move. The fix is to recycle the mounted drum and strip nodes imperatively in WheelMotion, so the drag path never renders. It lands once the design owner rules on this look; the description lists it as a known issue until then.

* programmatic turns
* @output Exports PickerWheel — one UIPickerView column
* @position Internal component; the Wheel under a finger (Wheel.tsx picks
* it and loads it lazily), drawn standalone or inside a WheelGroup

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wheel.tsx imports PickerWheel statically (line 73), so the touch layer is not loaded lazily. Please correct this comment; INV6 then asks for the size report, and a justification if the touch layer is flagged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b84acba: the header says Wheel.tsx imports PickerWheel statically. The description now gives INV6's size report for this pull request against #8366: +0.3 KB gzip JavaScript and +0.3–0.4 KB CSS each for DateInput, TimeInput and DateTimeInput (DateInput 118.8 → 119.1 KB), under INV6's 2 KB and 1 KB flags.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This review is on hold for an owner decision: no current spec governs the touch wheel's UIPickerView look, since spec:AST-094 covers touch behavior only, and the head also adds the draft rest rule in component:DateInput FR13/DEC-1 and component:DateTimeInput FR8. An engineering or design owner needs to provide or identify the record that governs the look, its --color-neutral band fill and the INV7 exception its band copy needs, and to approve or reject the rest rule; any engineering owner, the author included, records that decision by commenting /approve-spec <full head SHA> once the head carries that record. Whatever the decision, these points need changes:

  • Land #8366 and its predecessors first, so this diff carries only the look (2,999 authored lines instead of 37,149), then split it or show why it has to stay together.
  • On short lists a finger now renders more elements than a mouse (1,031 against 1,012 on BottomSheetNarrowRange with the month and year wheels open), past the 5 that spec:AST-017 FR60 allows, so trim the touch wheel's fixed nodes or include these counts in the INV7 exception.
  • A drag still re-renders the wheel through flushSync each time it leaves the mounted rows (PickerWheel.tsx:463), which spec:AST-094 FR9 rules out.

[Automated review]

styles.stripRowAt(`${position * ROW_HEIGHT}px`),
option.isDisabled && styles.rowDisabled,
)}>
<span

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On short lists each touch wheel's fixed nodes (the options wrapper, drum, band copy, strip, and a row plus this span for up to seven strip positions) outnumber what they save: the head's BottomSheetNarrowRange story counts 1,031 elements under a coarse pointer against 1,012 under a fine one with the month and year wheels open, past the 5 that spec:AST-017 FR60 allows an open layer. Trim these nodes for short lists, or include these counts in the INV7 exception the description asks the design owner to record.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Trimmed in e47456a: the narrow sheet's month and year wheels went from 1,030 to 1,019 elements under a finger (1,011 under a mouse), pixel-identical. The remaining 8 over spec:AST-017 FR60's 5 are in the INV7 exception the description asks the design owner to record, with these counts.

@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

The touch wheel's look and its rest rule decide what every bottom-sheet date and time wheel draws and commits under a finger, and neither can merge without an owner's record. One question each, with our reading and evidence.

1. The look's record. Will you record the look in the touch wheel's record, with its band filled by --color-neutral and an INV7 exception for the band copy?

  • The band: UIKit fills it with quaternarySystemFill (#F4F4F5 on white, #151517 on black), which has no token; --color-neutral renders #E6EBEE and #2D2D2E.
  • The band copy: each label is drawn three times (a transparent option for assistive technology, a drum row, a band copy), because a row crossing the band's edge shows magnified and unmagnified at once, as UIKit's two table views do.
  • What remains after e47456a dropped the wrappers with no job of their own (pixels identical, 11 elements fewer): DateInput's narrow-range sheet, month and year open, draws 1,019 elements under a finger and 1,011 under a mouse, 8 more against spec:AST-017 FR60's 5. That is the drum rows and band copies (20) in place of the scrolling column's 14 label spans, and per column a drum (its perspective, and the band's hole), a band clip and a moving strip (3) in place of a wrapper and a band (2). The TimeInput sheet draws 250 under a finger and 288 under a mouse.

2. The rest rule. Will you approve DateInput FR13 / DEC-1 and DateTimeInput FR8 as they read: a wheel that stops on a disabled row continues to the nearest enabled row (a tie toward the committed row) and commits it there, and a rest on the committed row commits nothing, even outside the bounds?

  • UIDatePicker on iOS 26.2 continues the same way; returning to the previous row makes the person scroll again to reach the bound.
  • Evidence: Wheel.test.tsx (three cases); PickerWheel.test.tsx, "rests back on a committed value outside the bounds and commits nothing", which fails without its fix; WheelRest.a11y.chromium.spec.ts (real touch drags below the minimum and inside the bounds). Both columns now agree.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This review is on hold for an owner decision: no current spec governs the touch wheel's UIPickerView look, since spec:AST-094 covers touch behavior only, and the head adds the draft rest rule in component:DateInput FR13/DEC-1 and component:DateTimeInput FR8. An engineering or design owner needs to provide or identify the record that governs the look, its --color-neutral band fill and the INV7/FR60 exception its band copy and extra touch elements need, and to approve or reject the rest rule; any engineering owner, the author included, records that decision by commenting /approve-spec <full head SHA> once the head carries that record. Whatever the decision, these points need changes:

  • Land #8527 and its predecessors first and merge main, where seven files now conflict with the landed animator, so this diff carries only the look (3,121 authored lines instead of 34,966), then split it or show why it has to stay together.
  • A drag re-renders the wheel through flushSync whenever it leaves the mounted rows (PickerWheel.tsx:460, called from the pan's moves at :722), which spec:AST-094 FR9 rules out.
  • Under reduced motion the cross-fade lands its rows on a main-thread timer (wheelMotion.ts:421-427), so a busy page fades back in on the old rows, against FR5.

[Automated review]

// A motion's animations are baked per mounted row as it starts, and
// a drag frame writes the mounted rows: both need the rows now.
// eslint-disable-next-line @eslint-react/dom-no-flush-sync, @eslint-react/set-state-in-effect -- see above; never called from an effect with `now`
flushSync(() => setRowWindow(next));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This flushSync runs from the pan's began and changed handlers (lines 709 and 722) whenever the offset leaves the mounted window, so the column renders synchronously inside a touch move. spec:AST-094 FR9 requires "no component render per move". Moving the window inside WheelMotion by recycling the mounted drum and strip nodes would keep the drag path to style writes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Recorded under the description's Known issue (spec:AST-094 FR9): the mounted window is React state, so a drag that leaves it renders the column. The fix recycles the drum and strip nodes imperatively in WheelMotion, once the design owner rules on this look; it is the same rework as the landing below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4e810c2, with the thread above: the flushSync is gone; a drag recycles the mounted drum and strip rows imperatively (wheelRows.ts), so its path is style writes only (0 renders through a ten-row drag, 3 before).

Comment on lines +421 to +427
motion.timers.push(
window.setTimeout(() => {
if (this.motion === motion) {
onMidpoint?.();
this.render(to, true);
}
}, durationMs / 2),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The rows land at the fade's midpoint on a main-thread timer, and onMidpoint mounts them, through flushSync when they lie outside the mounted window. If the main thread is busy then, the opacity fade comes back in on the old rows and they jump once the timer runs, which spec:AST-094 FR5 rules out ("MUST keep advancing at display rate through a main-thread block"). Mounting the destination rows before the fade and landing them inside the same Web Animations would keep the landing on the compositor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Recorded under the description's Known issue (FR5). Today's look already lands on the compositor (#8526 bakes the destination into the rows' own animations, and #8162 proves it through a 700 ms block); the look adopts that with the node recycling FR9 needs, once the design owner rules. Its busy-landing browser case is marked to fix there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4e810c2: the wheel mounts the rows around both ends of the fade before it starts (not the rows between), and the player bakes the landing as #8526 does: the destination inline first, each row and the strip holding the origin in an animation that steps onto it at the midpoint, on the compositor, timed from when the animations start. The Chromium busy-main-thread proof is no longer fixme: before, minute 01 reached the band only when a 700 ms block ended; after, every frame from the midpoint shows it there (6 of 6). On the iOS simulator, with the main thread blocked 600 ms after the tap, the fade and the landing now play through the block.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This review is on hold for an owner decision: no current spec governs the touch wheel's UIPickerView look, since spec:AST-094 covers touch behavior only, and the head adds the draft rest rule in component:DateInput FR13/DEC-1 and component:DateTimeInput FR8. An engineering or design owner needs to provide or identify the record that governs the look, its --color-neutral band fill and the INV7/FR60 exception its band copy and extra touch elements need, and to approve or reject the rest rule; any engineering owner, the author included, records that decision by commenting /approve-spec <full head SHA> on the pull request once the head carries that record. Whatever the decision, these points need changes:

  • Land #8527 and its predecessors first and merge main, where three internal/interaction-harness files still conflict, so this diff carries only the look (3,281 authored lines instead of 34,606), then split it or show why it has to stay together.
  • A drag re-renders the wheel through flushSync whenever it leaves the mounted rows (PickerWheel.tsx:460, called from the pan's moves at :722), which spec:AST-094 FR9 rules out.
  • Under reduced motion the cross-fade lands its rows on a main-thread timer (wheelMotion.ts:421-427), so a busy page fades back in on the old rows, against FR5.

[Automated review]

vjeux added 11 commits October 29, 2026 13:45
…o their own changesets

In the same release meta#8179's time columns loop, so "no shipped column loops
yet" would contradict it; the columns that loop say so themselves.
…heirs) into uikit/c21-wheel-rest-and-spin
…tep a frame from the change that ends it

Against the control, a frame with no new component position was a miss. On an easing tail the component moves under one cast step a frame, which the screencast cannot show on every frame: Lightbox's dismiss-cancel proof in WebKit missed one of the control's 12 to 14 frames in 7 of 10 runs, each time at its spring's last plateau (210, 210, then 208 on WebKit's 2 px grid). Such a run of unchanged frames now counts as moved when the change that ends it is a single cast step. A freeze ends in a catch-up of more than one step and stays a miss, as does a run that no change ends, and the component must still show a new position on at least minJudgedFrames of the control's frames, so a frame-counted animation that stands still through the block and resumes one step a frame fails too. The cast step is read from the tracked value's changes in the judged part rather than from its positions: the photo's position once the block ended (207) sat off WebKit's 2 px grid and made the step read as 1 px.

Red first: the tail, pooled tail, off-grid commit and frame-counted freeze cases fail on the previous busy.ts; the single-miss, slow freeze and stop-for-good cases still fail. Lightbox's five busy proofs with this judgment: 30 of 30 in Chromium, 39 of 40 in WebKit (the other presented 3 frames in its judged part, under the 4 a window needs). The harness's own proofs still pass a compositor animation with no miss and fail the rAF one (0 of 39 to 47 control frames) in both engines.
…awn PM row, and the 1 s busy proof follows rows in turn

- The clamped-turn proof taps the PM row where the cylinder draws it.
- UIPickerView's cylinder foreshortens a row as it turns away from the band
  and leaves the band's row to the band's copy, so no one row stays in
  sight, moving a pixel a frame, for the whole 1 s block. Rows take turns:
  each is followed from three and a half rows below the band until it
  reaches the band's edge, and the harness judges each stretch, which
  together cover the whole block.
…ng curve only when it is at most a frame longer than the run before it

astracat found that the sub-step credit let pooledBusyProof pass a rAF loop that steps a fixed 1 or 2 px a frame: in each 70 ms block the cast still shows the loop's last steps, then it stands still and goes on one step a frame without catching up, so its run ended in a single step. With the previous busy.ts the self-test's frame-counted loop passed the pooled proof in 5 of 5 Chromium runs.

A slowing curve lengthens its runs a frame at a time, so a run now counts as moved only when it is at most one frame longer than the component's run before it; a stall is longer than the runs before it. A judged part with no change reads the finest cast step, 0.5 px, so a larger jump never passes for one step.

The self-test adds the frame-counted loop, at 1 and 2 px a frame, to the pooled proof's mutants: it fails every pool (Chromium 10 of 10, moved on 2 to 8 of 7 to 12 control frames; WebKit 10 of 10, 0 or 1 of 11 or 12). The timed rAF mutant can read as moved on one control frame, presented as the block starts, so its assertion takes 0 or 1 and also requires its stall to exceed the control's (Chromium showed 1 of 40 once).

Red first: the frame-counted stall, the pooled frame-counted loop and the finest-step cases fail on the previous busy.ts. Lightbox's five busy proofs with this judgment: 30 of 30 in Chromium; 33 of 40 in WebKit with no miss, the other 7 presenting too few control frames to judge.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:36
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…k's fixes) into uikit/c21-wheel-fr43

The two harness files that conflicted take the clean merge of meta#8340 and
meta#8676, the heads that carry them.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
… turn through the 1 s block) into uikit/c21-wheel-fr43

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request waits on an owner decision: approval needs an engineering or design owner to provide or identify the governing spec for the touch wheels' UIPickerView look (component:DateInput DD1), for its exception to architecture:component-style-authoring INV7 and spec:AST-017 FR60 (PR3), and for the rest rule in component:DateInput FR13/DEC-1 and component:DateTimeInput FR8, none of which has current spec authority, and the earlier design approval predates DD1 and PR3. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Whatever the decision, these changes are needed:

  • Land #8527 first and rebase, since this diff carries 37,949 authored lines, 36,258 of them that stack.
  • Record the TimeInput sheet's look and INV7 exception in component:TimeInput, which owns that sheet, or narrow DD1 and PR3 to DateInput's own wheels.
  • Report INV6 sizes against the base, and restate the band-magnification note in motion/uikit/picker.ts to match the drawing.

[Automated review]

- **PR2 — Scroll work.** Scroll-driven measurement and selection updates remain
animation-frame throttled or settle-driven; listeners, frames, timers, and observers
are cleaned up on deactivation or unmount.
- **PR3 — The touch wheel draws each label three times.** Under a finger, a sheet's

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR3 names TimeInput's counts, but component:TimeInput is current and owns "the bottom-sheet time wheels", and architecture:component-style-authoring INV7 records an exception "in the component's current record or, when the component has none, in this record". This record is draft and its own Owns line (line 90) gives the time wheels to TimeInput and DateTimeInput. Recording the TimeInput sheet's exception in component:TimeInput, or narrowing PR3 and DD1 to DateInput's month and year wheels, would put each exception where INV7 looks for it.

*/
bandInset: 9,
/**
* The band's copy of the rows is magnified about the picker's centre:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This note says the band copy is magnified about the picker's centre, with band digits about 11 pt outward, but PickerWheel.tsx magnifies each label about its own centre and wheelColumns.ts records UIKit doing exactly that. Restating the note from the UIDatePicker measurement keeps the value's source in line with the drawing (spec:AST-094 FR2).

…its wheels' look and exception; the band note matches the drawing

- DateInput's DD1 and PR3 now cover its own month and year wheels; the
  TimeInput and DateTimeInput sheets record their wheels' UIPickerView look
  (DD1) and the INV7 exception for the labels drawn three times (PR2) in
  their own records, which own those sheets. TimeInput's sheet has 250
  elements under a finger against 286 under a mouse; DateTimeInput's open
  sheet 377 against 470.
- The band-magnification note in picker.ts says what UIKit draws and what
  the wheel draws: each band label magnified about its own centre, as
  wheelColumns.ts records. The value is unchanged.
… long a frame takes

The proof's guard compared the longest frame gap of two openings under CPU
throttling, and the load on the machine running it moved that gap more than
the wheel did: at one head the 200-year opening's worst gap ran from 266 to
516 ms, against 183 to 233 ms for two years, so the guard failed now and
then on a busy machine. It now counts the year wheel's elements the opening
writes or adds, from the tap until the wheel rests: 1 for two years and 19
for 200, the same on every run. A cost per year shows as 200 or more.
Counting needs no CPU throttling, so the proof runs in WebKit too. It waits
for the wheel to rest instead of a fixed 1.5 s, and the sheet's wait no
longer swallows a failure.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request waits on an owner decision: the touch wheels' UIPickerView look (DD1 in the DateInput, TimeInput and DateTimeInput records), the exceptions for labels drawn three times (DateInput PR3, TimeInput PR2, DateTimeInput PR2) and the rest rule (DateInput FR13/DEC-1, DateTimeInput FR8) have no current spec authority, so approval needs an engineering owner to provide or identify the governing spec for them by approving or rejecting them as written, including where DateInput's and DateTimeInput's exceptions are recorded while those records are draft. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; a design owner's exact-head approval covers the Design decisions rows. Whatever the decision, these changes are needed:

  • Land #8527 first and rebase: this diff carries 37,979 authored lines, 36,249 of them that stack.
  • Report the INV6 component sizes against the base rather than against #8527.
  • Add FR13's and FR8's "turn onto the new row rather than jump" sentence to DEC-1 for approval, or mark it pending, instead of calling it settled by DEC-1.

[Automated review]

| FR10 | Standalone mode MUST compose Field; admitted InputGroup mode MUST remove competing Field chrome while preserving name, description, value, keyboard, focus, and editing semantics. | `family:input-fields/FR3`; current source and InputGroup tests | Settled shared rule and verified adoption |
| FR11 | Clear MUST emit `undefined` through the ordinary change pipeline. The pointer and bottom-sheet surfaces restore field focus using their shipped timing; the native surface MUST NOT reclaim focus because doing so reopens the OS picker. Implementation gap: when the field becomes disabled or busy while its clear control holds focus, the control unmounts and focus falls to the document body; `spec:AST-085/FR4` requires focus to stay or move to a destination this record names. | Current source and focused clear tests; `spec:AST-085/FR4` | Verified shipped behavior |
| FR12 | No surface renders native `disabled`. A disabled field MUST stay focusable with `aria-disabled`, block typing, picking, clearing, and opening, describe its disabled reason when one is given, and take disabled paint. While busy (`isLoading` or a pending Action) the pointer and bottom-sheet surfaces expose `aria-disabled` and `aria-busy`, stay focusable, and block new commits; the native surface does the same for `isLoading`, and during a pending Action stays editable so the OS picker remains attached but rejects additional commits until the Action settles. A busy field that is not disabled keeps its ordinary paint and Spinner, shows no hover ring while commits are blocked, and keeps its keyboard focus ring. | `spec:AST-085/FR1–FR6`; `family:input-fields/FR4–FR6`; `family:buttons/FR4`; current source and focused tests | Settled shared rule and verified adoption |
| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. A shown 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. | DEC-1; UIDatePicker on the iOS 26.2 simulator; wheel unit and real-browser tests | Settled by DEC-1 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This row is marked "Settled by DEC-1", but its last sentence ("a shown wheel … MUST turn onto the new row rather than jump to it") is not in DEC-1, which decides only the continue-to-nearest rest and the untouched value. component:DateTimeInput FR8 repeats it as settled too. Adding the rule to DEC-1 for the owner to approve, or marking that sentence pending owner approval, keeps the row from reading as already decided.

vjeux added 7 commits October 29, 2026 17:27
…il cancelled, and its compositor run is blocked only once on the compositor

C23 saw the control self-test fail under load at eight workers: "660 ms judged at 16.6 ms/frame: 28 frames presented, 0/28 moved". Its animations ran once, for 1,600 ms from before the screencast started, so on a loaded runner, where starting the cast takes long, the compositor's animation had ended before the block and nothing moved in the judged part.

Both animations now go back and forth until cancelled, as the pooled self-test's do, and the compositor's block uses `during`, so it starts only once that animation runs on the compositor.

Red first: with the setup stretched to 1.8 s, as a loaded runner takes, the previous test fails with exactly that read (0/39 moved) and this one passes. Normal runs at eight workers: 10 of 10 in Chromium and 10 of 10 in WebKit.
…pending owner approval [skip ci]

FR13 and DEC-1 settle where a wheel rests. The turn of a shown wheel onto a
value changed from outside, as when a bound clamps the time after another
wheel turned, sat inside FR13 and DateTimeInput FR8, so both read it as
settled by DEC-1, which doesn't decide it. It is now DateInput FR14, the
shared wheel's own requirement, a verified observation pending owner
approval; DateTimeInput FR8 keeps only the rest, and the two tests that prove
the turn cite FR14.
…nst a compositor-only control [skip ci]

The 1 s busy proof marked one row and asked that every frame the compositor
presented show it moved. Two things made it fail on a busy machine. Late in
the block the coast moves about a pixel a frame, under the screencast's
step, so a frame delivered early shows no change; and over a whole second
the coast travels further than the column shows, so the marked row was out
of sight at one end, turned and faded at the other.

It now follows markers that ride the strip itself, every three rows in three
colours, placed before the flick so the block meets them already on screen.
They move with the coast and never turn or fade, and each is judged while
within reach of the band; together they cover the block. The judgment is
busyWindowProof's against a compositor-only control recorded in the same
frames (meta#8906): the coast moves on every frame the control moves, a run
under one step that a single step ends counts as moved, and its longest
stall is no longer than the control's.
…wheel-uikit-look

# Conflicts:
#	packages/core/src/DateInput/DateInput.spec.md
… into uikit/c21-wheel-uikit-look

# Conflicts:
#	packages/core/src/DateInput/__tests__/Wheel.interaction.browser.spec.ts
#	packages/core/src/DateInput/__tests__/WheelActivation.interaction.browser.spec.ts
…R14 [skip ci]

meta#8131 made the turn its own requirement, DateInput FR14, pending owner
approval; the wheel's park note and its test said FR13.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request waits on an owner decision: the touch wheels' UIPickerView look (DD1 in the DateInput, TimeInput and DateTimeInput records), the exceptions for labels drawn three times (DateInput PR3, TimeInput PR2, DateTimeInput PR2), the rest rule (DateInput FR13/DEC-1, DateTimeInput FR8) and the new DateInput FR14 have no current spec authority, so approval needs an engineering owner to provide or identify the governing spec by approving or rejecting them as written, including where the two draft records' exceptions belong. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; a design owner's exact-head approval covers the Design decisions rows. Whatever the decision, the diff needs to shrink: it carries 39,085 authored lines, 37,357 of them the still-open stack under #8527, so land that stack first and rebase until this pull request is the look alone.

[Automated review]

- **PR2 — Scroll work.** Scroll-driven measurement and selection updates remain
animation-frame throttled or settle-driven; listeners, frames, timers, and observers
are cleaned up on deactivation or unmount.
- **PR3 — The touch wheels draw each label three times.** Under a finger, the sheet's

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This exception sits in a draft record, so it passes no spec-owner gate on its own: architecture:component-style-authoring INV7 records an exception "in the component's current record or, when the component has none, in this record". Until an owner approves it at an exact head and says where it lives (this record made current, or the exception moved into INV7's record), "an owner exception" overstates it. The same holds for component:DateTimeInput PR2.

This branch had an error being deployed

1 failed deployment
Preview — 075ce3da Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants