Skip to content

fix(TimeInput, DateTimeInput): under a finger the AM/PM wheel follows the hour - #8179

Merged
cixzhang merged 3 commits into
meta:mainfrom
vjeux:uikit/c22-time-wheels
Nov 4, 2026
Merged

cixzhang merged 3 commits into
meta:mainfrom
vjeux:uikit/c22-time-wheels

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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

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

Depends on #8178. A pure rebase of 12a0c16 onto main: its own product diff is byte-identical, so the phone check that passed there carries. Review only this pull request's change: vjeux/astryx@fdc1ee1...894ea24

User impact

Under a finger, the hour wheel in the TimeInput and DateTimeInput sheets moves without the AM/PM column. Turning 11 AM to 12 gives 12 AM, where UIDatePicker gives 12 PM.

Problem and solution fit

  • Cause: each sheet column is an independent wheel, so an hour turn never reaches the AM/PM column.
  • Fix: under a finger, the sheets render TimeWheels, which applies TimeInput FR7 (timeWheelRules.ts) and turns the AM/PM column on the Wheel's spin before the commit.
  • A bound between whole minutes commits the nearest whole minute inside it under any pointer (clampTime).
  • A turn the wheels start by themselves commits nothing on arrival, even when the owner keeps the old time.
  • With a mouse, the columns stay independent, with rows from the same rules.
  • Each hour row carries the hour it stands for, so keys follow the full time (DateInput FR17): 12 PM comes before 1 PM.

Expected behavior and authority

  • TimeInput FR7, as recorded here: "On the non-looping hour column, 12 counts as next to 1. Moving between 12 and 1–6 keeps the half of the day; any other move flips it when it passes between 11 and 12." It cites DateInput FR14; the spec owners approve it at this head with /approve-spec.
  • component:DateInput FR13, FR14 and FR17 for the rest, the turn and the keys. A looping hour column follows once a record covers it.

Smallest restoration

Its first commit, by #8159's author, restores the Wheel's internal spinTo, which only these wheels call. Then TimeWheels.tsx and timeWheelRules.ts (new, internal) behind DateInput/datePickerTouch.ts; TouchTimeField.tsx and TouchDateTimeField.tsx; clampTime in utils/timeParser.ts; TimeInput FR7 and its verification row; the BottomSheetWheels story.

Writing rubric results

Over FR7's changed sentence and its verification row: R1 narration, R2 research, R4 rejected alternatives, R5 change references, R6 open questions: none. R3: test names only.

Evidence

  • Before: on the parent head, 11 to 12 under a finger stays AM.
  • After: in TimeWheels.interaction.browser.spec.ts the AM/PM column turns onto PM frame by frame, also through a blocked main thread, under Reduced Motion and caught mid-turn.
  • Unchanged path: "with a mouse each column turns on its own: 11 to 12 stays AM".
  • Regression tests: TimeWheels.test.tsx "keeps PM from 12 to 1" and "keeps PM from 1 back to 12", and the timeWheelRules.test.ts crossing cases.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

This head, e51ebcc, rebases 894ea24 onto main ed33727, where #8178 merged as 826d893. The only content change is the one #9811's lint rule asks for: TimeWheels.interaction.browser.spec.ts imports expect and test from @astryxdesign/a11y-spec/playwright. git range-diff shows commits 1 and 3 equal and commit 2 changed only by hunk context and the spec's import; the added and removed lines match 894ea24's in every other file.

  • At e51ebcc: pnpm install --frozen-lockfile 0; pnpm -F @astryxdesign/core typecheck 0; CI=true pnpm lint 0; check:knowledge 0; check:changesets 0; pnpm review:pre 0; the DateInput, TimeInput, DateTimeInput, Calendar and utils unit tests: 1,980 passed.
  • TimeWheels.interaction.browser.spec.ts against Storybook built at e51ebcc: Chromium 6 passed; WebKit 5 passed, 1 skipped (the blocked-main-thread case: WebKit's screencast clock runs about 100 ms off the page's, so the iOS simulator is that case's WebKit evidence).

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 4, 2026 8:07am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 8:29 AM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · e51ebcc

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
PowerSearch 237.3 kB (+1.3 kB) · 21.8 kB CSS (0) — — · — · —
DateTimeInput · Storybook 169.0 kB (+1.3 kB) · 17.2 kB CSS (0) 3,564 (−73) ✓ · ✓ · ✓
TimeInput · Storybook 132.6 kB (+1.3 kB) · 14.5 kB CSS (0) 1,606 (+355) ✓ · ✓ · —
1 more component
Component Size LoC Visual · A11y · RTL
DateInput · Storybook 147.1 kB (+135 B) · 15.5 kB CSS (0) 5,102 (+68) ✓ · ✓ · ✓

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

API changes: none in this pull request

Committed API report behind this build: cli 8 rows, theme-butter 1 row, theme-chocolate 1 row, theme-gothic 1 row, theme-matcha 1 row, theme-neutral 1 row, theme-stone 1 row, theme-y2k 1 row; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision first: no current spec owns the sheet time wheels' rules (AM/PM following the hour, looping columns, the increment / timeIncrement minute interval, the turn onto a bound), and the diff adds draft spec:AST-094, whose FR1–FR23 have no owner approval. An engineering owner can record that decision by commenting /approve-spec b62f76f7eaa53408eab928ea2d987249376b6f48 on this pull request, and the author may do so as an engineering owner; otherwise, please say which rule or requirement should change. Whatever the decision, these need fixing:

  • TimeWheels.tsx and timeWheels.ts build to dist/TimeInput/TimeWheels.js and timeWheels.js, which collide on case-insensitive file systems and fail the registry-contract (preview) check, so the rules module needs another name.
  • With timeIncrement, DateTimeInput shows 11:58 PM as 12:00 AM and the next minute turn commits 12:15 AM on the same date, and with hasSeconds an interval resets the seconds column to 00 and the next turn drops the seconds.
  • The diff is 31,267 authored lines from the whole stack, so the earlier pull requests should land first and leave this one as the 1,780-line TimeWheels change.

[Automated review]

type TimeColumn,
type TimeParts,
type TimeRow,
} from './timeWheels';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

./timeWheels and this file differ only by case once compiled: dist/TimeInput/TimeWheels.js and dist/TimeInput/timeWheels.js (plus their .d.ts and maps). They cannot coexist on the default macOS and Windows file systems, and CI's registry-contract (preview) consumer build already fails with "No matching export in …/dist/TimeInput/TimeWheels.js for import clampToBounds". Renaming timeWheels.ts and its test fixes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the rules live in timeWheelRules.ts, so no file differs from TimeWheels.tsx only by case.

Comment on lines +134 to +136
const minutes = time.hour * 60 + time.minute + time.second / 60;
const rounded = Math.round(minutes / interval) * interval;
return partsOf(rounded * 60);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With an interval above 1 this always returns seconds of 0, even with hasSeconds, while the doc comment says seconds drop only "when the picker has none". With hasSeconds and increment={15}, 10:15:30 shows :00 and the next hour or minute turn commits 10:xx:00. Rounding only the minutes and keeping time.second would match the comment.

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 (see the later thread): seconds are kept with a seconds column; head 6e34dbc.

time={parsedWheelTime}
hourFormat={hourFormat}
hasSeconds={hasSeconds}
minuteInterval={minuteIntervalOf(timeIncrement)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With timeIncrement={15}, displayedTime shows 23:53–23:59 as 00:00, TimeWheels commits relative to that, and commitWheelTime joins the result to the unchanged date: Mar 15, 11:58 PM becomes Mar 15, 12:15 AM after one minute turn. Keeping the rounding inside the day here (or carrying the date) avoids the jump.

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 (see the later thread): the rounding stays inside the day; head 6e34dbc.

Comment on lines +179 to +181
useIsomorphicLayoutEffect(() => {
latestRef.current = {shown, bounds, hourFormat, hasSeconds, onCommit, rows};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3 asks every new Effect to name the external system it synchronizes; the comment at line 164 does not. Naming the Wheel's rest callbacks, which fire from gesture and animation events outside render, would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TimeWheels has no Effect at 6e34dbc.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision first: no current spec owns the sheet time wheels' rules (AM/PM following the hour, looping columns, the increment / timeIncrement minute interval, the turn onto a bound), and the diff adds draft spec:AST-094, whose FR1–FR23 have no owner approval. An engineering owner can record that decision by commenting /approve-spec a5e1f03a9e505b90e8a7fcde069c5b9f28af5df0 on this pull request, and the author may do so as an engineering owner; otherwise, please say which rule or requirement should change. Whatever the decision, these need fixing:

  • With timeIncrement, DateTimeInput shows 11:58 PM as 12:00 AM and the next minute turn commits 12:15 AM on the same date, and with hasSeconds an interval resets the seconds to 00 on the next turn (displayedTime in timeWheelRules.ts).
  • The stack removes dateInputTouchSizes.wheelItemSize and dateInputTouchGeometry.wheelEdgePadding from the released @astryxdesign/core/DateInput/tokens.stylex export under patch changesets, so they need to stay (deprecated) until a minor.
  • The wheel's mouse-press guard and its six regression tests were deleted with no mouse test replacing them.
  • The diff is 31,267 authored lines from the whole stack, so the earlier pull requests should land first and leave this one as the 1,780-line TimeWheels change.

[Automated review]

}
const minutes = time.hour * 60 + time.minute + time.second / 60;
const rounded = Math.round(minutes / interval) * interval;
return partsOf(rounded * 60);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With an interval above 1 this always returns seconds of 0, even with hasSeconds, while the doc comment says seconds drop only "when the picker has none". With hasSeconds and increment={15}, 10:15:30 shows :00 and the next hour or minute turn commits 10:xx:00. Rounding only the minutes and keeping time.second would match the comment.

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 (see the later thread); head 6e34dbc.

: composeWheelTime({hour12: hour}),
)
onCommit={time =>
commitWheelTime(formatISOTime(time, hasSeconds))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With timeIncrement={15}, displayedTime shows 23:53–23:59 as 00:00, TimeWheels commits relative to that, and commitWheelTime joins the result to the unchanged date: Mar 15, 11:58 PM becomes Mar 15, 12:15 AM after one minute turn. Keeping the rounding inside the day here (or carrying the date) avoids the jump.

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 (see the later thread); head 6e34dbc.

Comment on lines +179 to +181
useIsomorphicLayoutEffect(() => {
latestRef.current = {shown, bounds, hourFormat, hasSeconds, onCommit, rows};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3 asks every new Effect to name the external system it synchronizes; the comment at line 164 does not. Naming the Wheel's rest callbacks, which fire from gesture and animation events outside render, would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TimeWheels has no Effect at 6e34dbc.

* column of values, not as a stack of buttons with air between them. At
* this height ~9 options are on screen.
*/
wheelItemSize: '28px',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@astryxdesign/core/DateInput/tokens.stylex is a published subpath (since 0.6.7), so removing wheelItemSize here, and wheelEdgePadding below, breaks any consumer that reads them, under patch changesets. Keeping both, marked @deprecated, until a declared minor, and moving wheelGeometry to a module that is not on the exports map, avoids the break.

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.

At 6e34dbc DateInput/tokens.stylex.ts exports the same names as on main.

});
});
});
// A finger or a mouse dragging a wheel goes through the gesture system: see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wheel.test.tsx drives fingers, keys and clicks but no mouse drag, so the six deleted mouse tests have no replacement, including the one for a mouse press escaping the BottomSheet's pointer capture. With usePointerDragScroll gone, nothing else stops that press; a mouse drag and a wobbling mouse click on a wheel row inside the open sheet would keep the fixed defect covered.

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.

This PR deletes no tests from DateInputTouch.test.tsx at 6e34dbc; with a mouse the sheets keep today's column and its tests.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision first: no current spec owns the sheet time wheels' rules (AM/PM following the hour, looping columns, the increment / timeIncrement minute interval, the turn onto a bound), and the diff adds draft spec:AST-094, whose FR1–FR23 have no owner approval. An engineering owner can record that decision by commenting /approve-spec with this pull request's full head SHA, and the author may do so as an engineering owner; otherwise, please say which rule or requirement should change. Whatever the decision, these need fixing:

  • With timeIncrement={15}, DateTimeInput turns Mar 15, 11:58 PM into Mar 15, 12:15 AM after one minute row, and with hasSeconds an interval drops the seconds on the next turn (displayedTime in timeWheelRules.ts).
  • The stack removes dateInputTouchSizes.wheelItemSize and dateInputTouchGeometry.wheelEdgePadding from the released @astryxdesign/core/DateInput/tokens.stylex export under patch changesets, so they need to stay, deprecated, until a minor.
  • With an interval of 10, 15 or 30 the minute column does not loop, although the changeset says it does.
  • The wheel's six mouse tests were deleted with nothing replacing them, and the new Effects need comments naming what they synchronize.
  • The diff is 31,115 authored lines from the whole stack, so the earlier pull requests should land first and leave this one as the 1,780-line TimeWheels change.

[Automated review]

}
const minutes = time.hour * 60 + time.minute + time.second / 60;
const rounded = Math.round(minutes / interval) * interval;
return partsOf(rounded * 60);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With an interval above 1 this always returns seconds of 0, even with hasSeconds, while the doc comment says seconds drop only "when the picker has none". With hasSeconds and increment={15}, 10:15:30 rests the seconds column on 00, and one hour row commits 11:15:00. Rounding only the minutes and keeping time.second would match the comment.

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 (see the later thread); head 6e34dbc.

: composeWheelTime({hour12: hour}),
)
onCommit={time =>
commitWheelTime(formatISOTime(time, hasSeconds))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With timeIncrement={15}, displayedTime shows 23:53–23:59 as 00:00, TimeWheels commits relative to that, and commitWheelTime joins the result to the unchanged date: Mar 15, 11:58 PM becomes Mar 15, 12:15 AM after one minute row. Keeping the rounding inside the day here (or carrying the date) avoids the jump.

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 (see the later thread); head 6e34dbc.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@astryxdesign/core/DateInput/tokens.stylex is a published subpath (since 0.6.7), and 0.7.4's types declare dateInputTouchSizes.wheelItemSize and dateInputTouchGeometry.wheelEdgePadding, which this file no longer has, under patch changesets. Keeping both, marked @deprecated, until a declared minor, and moving wheelGeometry to a module that is not on the exports map, avoids the break.

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.

At 6e34dbc DateInput/tokens.stylex.ts exports the same names as on main.

Comment on lines +179 to +181
useIsomorphicLayoutEffect(() => {
latestRef.current = {shown, bounds, hourFormat, hasSeconds, onCommit, rows};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3 asks every new Effect to name the external system it synchronizes; the comment at line 164 does not. Naming the Wheel's rest callbacks, which fire from gesture and animation events outside render, would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TimeWheels has no Effect at 6e34dbc.

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

/** Whether a looping wheel of `rowCount` rows can hide its seam. */
function canLoop(rowCount: number): boolean {
return (rowCount * ROW_HEIGHT) / 2 > PARKED_ANGLE * GEOMETRY.radius;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs at least 11 rows to loop, so a minute column with an interval of 10, 15 or 30 (6, 4 or 2 rows) stops at 00 and at its last step, while the time-wheels-follow-uidatepicker changeset says the minute column loops. Repeating a short column's rows, or stating the exception in the changeset and docs, would make them agree.

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.

Right: short minute lists loop once the wheel does (#8366, which matches UIDatePicker's measured behaviour); a small follow-up PR stacks TimeWheels on it.

* event — including the click. Measured before this: a click on a wheel row
* that wobbled more than a pixel selected nothing at all.
*/
it('keeps the press away from the sheet, which would capture the pointer', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wheel.test.tsx drives fingers, keys and clicks but no mouse, so the six deleted mouse tests have no replacement, including this one for a mouse press escaping the BottomSheet's pointer capture. A mouse drag and a wobbling mouse click on a wheel row inside the open sheet would keep the fixed defect covered.

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.

No mouse tests are deleted at 6e34dbc.

Comment on lines +173 to +175
// Typed-field only: the wheels step by their own options and the sheet
// opens on tap, so neither reaches this surface.
increment: _increment,
increment,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment still says increment does not reach the sheet, but it now sets the minute wheel's rows; the matching comment in TouchDateTimeField.tsx ("Desktop-only: mobile uses wheels instead of typed arrow stepping") has the same problem for timeIncrement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the comment now says the minute wheel steps by increment under a finger; head 6e34dbc.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision first: no current spec owns the sheet time wheels' rules (AM/PM following the hour, looping columns, the increment / timeIncrement minute interval, the turn onto a bound), and the diff adds draft spec:AST-094, whose FR1–FR23 have no owner approval. An engineering owner can record that decision by commenting /approve-spec with this pull request's full head SHA, and the author may do so as an engineering owner; otherwise, please say which rule or requirement should change. Whatever the decision, these need fixing:

  • With timeIncrement={15}, DateTimeInput turns Mar 15, 11:58 PM into Mar 15, 12:15 AM after one minute row, and with hasSeconds an interval drops the seconds on the next turn (displayedTime in timeWheelRules.ts).
  • The stack removes dateInputTouchSizes.wheelItemSize and dateInputTouchGeometry.wheelEdgePadding from the released @astryxdesign/core/DateInput/tokens.stylex export under patch changesets, so they need to stay, deprecated, until a minor.
  • The wheel's six mouse tests were deleted with nothing equivalent, and after a commit onto a min / max that is off the interval the wheels rest on a rounded time outside the bounds.
  • The diff is 31,351 authored lines from the whole stack, so the earlier pull requests should land first, leaving the 2,016-line TimeWheels commit.

[Automated review]

: composeWheelTime({hour12: hour}),
)
onCommit={time =>
commitWheelTime(formatISOTime(time, hasSeconds))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With timeIncrement={15}, displayedTime shows 23:53–23:59 as 00:00, TimeWheels commits relative to that, and commitWheelTime joins the result to the unchanged date: Mar 15, 11:58 PM becomes Mar 15, 12:15 AM after one minute row. Keeping the rounding inside the day here (or carrying the date) avoids the jump; a 23:58 test would hold it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 662b5c5: displayedTime keeps the rounding inside the day and inside the bounds, so with timeIncrement={15} 23:53–23:59 shows 23:45 and the wheels rest on a row that commits on the same date (timeWheelRules.test.ts).

}
const minutes = time.hour * 60 + time.minute + time.second / 60;
const rounded = Math.round(minutes / interval) * interval;
return partsOf(rounded * 60);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With an interval above 1 this always returns seconds of 0, even with hasSeconds, while the doc comment says seconds drop only “when the picker has none”. With hasSeconds and increment={15}, 10:15:30 rests the seconds column on 00, and one hour row commits 11:15:00. Rounding only the minutes and keeping time.second would match the comment.

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 662b5c5: the seconds are kept when the picker has a seconds column and dropped only when it has none (timeWheelRules.test.ts: 10:22:30 shows 10:15:30).

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@astryxdesign/core/DateInput/tokens.stylex is a published subpath (since 0.6.7), and this file no longer has dateInputTouchSizes.wheelItemSize or dateInputTouchGeometry.wheelEdgePadding, under patch changesets. Keeping both, marked @deprecated, until a declared minor, and moving wheelGeometry to a module that is not on the exports map, avoids the break.

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.

At 662b5c5 DateInput/tokens.stylex.ts exports the same names as on main. The wheel geometry stays in #8159's wheel files, and #8206's toggle constants live in an unexported monthYearToggle.stylex.ts.

() => ({min: secondsOfISO(min), max: secondsOfISO(max)}),
[min, max],
);
const shown = displayedTime(time, minuteInterval, hasSeconds);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The shown time is rounded without regard to min / max. With increment={15} and max="16:55", an AM/PM turn from 11:30 AM that clamps to 4:55 PM re-renders as 5:00 PM, a disabled hour row over a column of disabled minutes, while the field reads 4:55 PM. Rounding to the nearest step inside the bounds keeps the wheels on an enabled row.

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 662b5c5: the rounding also stays inside min/max (16:55 under a 16:55 maximum shows 16:45), so an AM/PM turn lands on a row that can commit (timeWheelRules.test.ts).

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

/** Whether a looping wheel of `rowCount` rows can hide its seam. */
function canLoop(rowCount: number): boolean {
return (rowCount * ROW_HEIGHT) / 2 > PARKED_ANGLE * GEOMETRY.radius;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs at least 11 rows to loop, so a minute column with an interval of 6 or more (10, 15 and 30 among DateTimeInput's timeIncrement values) stops at 00 and at its last step, while the time-wheels-follow-uidatepicker changeset says the minute column loops. Repeating a short column's rows, or stating the exception in the changeset and docs, would make them agree.

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.

Right: with an interval of 6 or more, the minute column stops at its ends until #8289's short-column looping lands. The body says so.

});
});
});
// A finger or a mouse dragging a wheel goes through the gesture system: see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wheel.test.tsx drives fingers, keys and clicks but no mouse, so the six deleted mouse tests have no equivalent, including the one for a mouse press escaping the BottomSheet's pointer capture (useSheetGestures.ts still captures non-touch pointers). A mouse drag and a wobbling mouse click on a wheel row inside the open sheet would keep that fixed defect covered.

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.

This head deletes no tests from DateInputTouch.test.tsx. With a mouse the sheets keep today's column, and its mouse tests stay as they are on main.

onCommit,
rows,
});
useIsomorphicLayoutEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3 asks every new Effect to name the external system it synchronizes; the comment at line 164 does not. Naming the Wheel's rest callbacks, which fire from gesture and animation events outside render, would satisfy it (the same applies at Wheel.tsx:361 and :494).

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.

TimeWheels has no Effect at 662b5c5.

// Typed-field only: the wheels step by their own options and the sheet
// opens on tap, so neither reaches this surface.
increment: _increment,
increment,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The comment above still says increment does not reach this surface, but it now sets the minute wheel's rows (line 412); the Desktop-only comment above timeIncrement in TouchDateTimeField.tsx has the same problem.

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 662b5c5: the comment now reads "Under a finger the minute wheel steps by it (UIDatePicker's minute interval); with a mouse today's wheels list every minute." TouchDateTimeField says the same for timeIncrement.

cixzhang pushed a commit that referenced this pull request Oct 24, 2026
…st (#8276)

## User impact

When a Popover, or anything built on `usePopover` such as a collapsed SideNav submenu, is dismissed within a frame of opening, focus jumps into the hidden popover's content. A quick Escape after a click or Enter is enough. Keyboard and screen-reader users are left on a control they can't see. The same race makes SideNav's "returns focus to the trigger when the collapsed submenu closes" test flaky on loaded runners, as seen on #8179: it took 1112 ms and ended with focus on the submenu's "General" link.

## Problem and solution fit

- Why is focus lost? `usePopover` focuses its content in a `requestAnimationFrame` after it opens and never cancels that frame.
- Why does the close not fix it? The close runs first. `useFocusTrap` leaves focus on the trigger because focus never entered the trap. Then the frame runs and focuses the first control in content that is no longer shown.
- Why does it matter? Focus sits inside a closed layer, where nothing announces it and Escape no longer applies.

The fix ties the frame to the open: closing or unmounting cancels it.

## Expected behavior and authority

`component:Popover` (current):
- Automatic focus moves focus into the content "for each accepted opening".
- Dismissal returns focus to the trigger; `Popover.test.tsx` covers "returns focus to the trigger when closed via Escape".

An opening that has already closed has no content to focus.

## Smallest restoration

- `usePopover`'s auto-focus effect returns `() => cancelAnimationFrame(frame)`.
- The per-open reset of the skip-auto-focus preference is unchanged.
- No API, timing or visual change.

## Evidence

- Reproduction before the change: hold animation frames, open the collapsed SideNav submenu, press Escape, then run the held frame. Focus lands on the "General" link inside the closed submenu. The Popover equivalent lands on its content button.
- Result after the change: in both cases the trigger keeps focus.
- Representative unchanged path: opening still focuses the first content control in the next frame. Escape after that still returns focus to the trigger, now asserted without a timing wait.
- Regression tests:
  - "does not move focus into a popover that closed before its first frame" (Popover);
  - "keeps focus on the trigger when the submenu closes before its first frame" (SideNav).
  
  Both fail with the fix reverted.

## Scope

- [x] One defect is restored.
- [x] Separable contradictory or unsettled tagalongs were removed or split.
- [x] No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
- [x] Public text and artifacts contain no internal Meta context.

## Testing

- `pnpm vitest run --project ui` on Popover, SideNav, and every directory with a `usePopover` consumer: Breadcrumbs, Chat, ComplexSelector, DateInput, DateRangeInput, DateTimeInput, DropdownMenu, hooks, PowerSearch, Selector, TabList, TopNav, Typeahead, lab Schedule. 115 files, 3183 tests passed.
- With `usePopover.tsx` reverted, the two new tests fail and the reworked SideNav focus-return test still passes. That test is now deterministic: it flushes the open frame, asserts focus is on "General", presses Escape, and asserts the trigger has focus immediately, with no `waitFor`.
- `pnpm -F @astryxdesign/core typecheck`, eslint and prettier on the touched files, `check:changesets`, and pre-commit `check:repo`.
@vjeux
vjeux force-pushed the uikit/c22-time-wheels branch from a7b3537 to ed7552d Compare November 3, 2026 00:36
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Size. This pull request's own change is 2,684 authored lines, 1,734 of them tests, including the 235-line spinTo commit it needs (vjeux/astryx@1a1edee...ed7552d), in batch B4 · Wheel / picker (DateInput, TimeInput, DateTimeInput). The AM/PM rule, the rows and bounds it reads, and their tests are one intent. A split would ship the rule apart from its proofs. It stacks on #8178 and rebases onto main once that lands.

The DEC-2 wording and the haptic gap are fixed in #8159 at 69695d1. The row builders are shared here at ed7552d: the mouse columns build their rows from timeWheelRules.ts. Each thread has its answer.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's decision about this head's spec changes, and four fixes are needed regardless.

An engineering owner needs to approve or amend, for this exact head, the record text it adds or changes: TimeInput FR7's rule that the AM/PM wheel follows the hour under a finger, the rewrite of DateInput DEC-2 (flat rows under a finger, tilted rows under a mouse) and its Non-goals line, DateInput FR14's tap-spin role sentence, and new DateInput FR17 (keys step in time order) and FR18 (no accent mark while a wheel moves). The earlier approval covered the previous head only. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking:

  • Under a finger, a focused wheel paints no keyboard focus ring, because the column's mask-image clips its own outline (PickerWheel.tsx:90-91, :733); move the mask to an inner wrapper so keyboard users can see focus.
  • The layout Effect in TimeWheels.tsx:137-146 copies render values into a ref that restOn does not need, since both wheels already call the latest onChange; delete it and read the render values directly.
  • The size report compares only against the parent branch; report each changed component against main and justify the touch-only JavaScript the stack adds, as the component size rule asks.
  • The diff is 10,066 authored lines, over the 8,000-line limit for an owner-directed batch, and 7,082 of them come from #8178; land #8178, rebase, and keep this change under 2,000 lines or show why it cannot be split.

[Automated review]

// to fully opaque at the band and back. The scrolling column fades them the
// same way under reduced motion. A static mask: nothing paints it again as
// the strip moves.
maskImage:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This mask is on the focusable column, so it also clips the column's own focus outline (2 px at a 3 px offset, outside the border box). Under touch emulation the focused Hour wheel matches :focus-visible and paints no ring, against interaction-modality INV1. Moving the mask to an inner wrapper around the band and the strip keeps the fade and lets the ring show.

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.

At 986b0bb: Fixed in #8159 at 94d18e1: the mask moved to an inner wrapper around the band and the strip, so the column's focus ring shows.

// Synchronizes the Wheels' rest callbacks with this render. The Wheel calls
// them from gesture and animator events, outside render. So they read the
// latest time, bounds and rows through this ref.
useIsomorphicLayoutEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both wheels already call the latest onChange at rest (PickerWheel reads it from its own latest-props ref, the scrolling column refreshes its settle callback every render), so restOn's closure holds the same values this Effect copies. Deleting latestRef and this Effect, and reading the render values in restOn, removes an Effect that synchronizes no external system.

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.

At 986b0bb: Fixed at 986b0bb: the ref and its layout Effect are gone. restOn reads the render's values, since both wheels call the latest onChange.

const RUBBER_BAND_DIMENSION = picker.geometry.rubberBandDimension * UIKIT_SCALE;

const styles = stylex.create({
// Today's column (Wheel.tsx): the same box, band and rows, moved by the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: "Today's column" names nothing once this merges; "The scrolling column (Wheel.tsx)" says the same. The same applies at line 95 and in useWheelListbox.ts line 44.

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.

At 986b0bb: Fixed in #8159 at 94d18e1: the comments name the scrolling column.

@vjeux
vjeux force-pushed the uikit/c22-time-wheels branch from ed7552d to 986b0bb Compare November 3, 2026 02:33
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 986b0bb

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

This head. Rebased onto main through #8159 at 94d18e1. Fixed here: TimeWheels drops its ref and layout Effect, and one 12-hour conversion pair serves the rules and both sheets. Fixed in #8159: the focus ring and the scrolling column's name. Each thread has its answer.

Size (INV6), main cae1f66 → this head: TimeInput 90.1 → 130.5 KB, DateTimeInput 114.9 → 156.3 KB, PowerSearch 183.6 → 225.0 KB, MobilePowerSearch 168.5 → 209.6 KB, DateInput 103.0 → 144.2 KB. Most of it is #8159's shared touch runtime, which a page pays once (spec owners' ruling, Nov 2). This pull request's own part, #8178 → this head: TimeInput +1.2 KB, DateTimeInput +1.3 KB, PowerSearch +1.3 KB, MobilePowerSearch +1.0 KB, DateInput +0.2 KB, each under INV6's 2 KB flag for touch-only JavaScript; CSS unchanged.

Lines. This pull request's own change is 2,684 authored lines, 1,750 of them tests, including the 235-line spinTo commit it needs (vjeux/astryx@d7474ad...986b0bb). It merges after #8159 and #8178 in B4's order, so its range against main is then this change alone, within the batch's 8,000 lines. The AM/PM rule, its rows and their tests are one intent.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. An engineering owner has approved the record changes at this head, so what remains is scope and four fixes:

  • Land #8178 and rebase so this diff holds only the time-wheel change: it is 9,429 authored lines, over the 8,000-line limit for an owner-directed batch, and about 6,400 of them are #8178's separable commits.
  • The changeset's second bullet (.changeset/time-wheels-follow-uidatepicker.md:8) announces the rest past min/max as new, but the base sheet already travels to the bound, turns the other columns onto it and leaves an untouched value alone, so drop the bullet or narrow it to a bound that falls between whole minutes.
  • Complete the component-style-authoring INV6 size report: add each changed component's CSS delta against main, and justify the wheel's own touch-only JavaScript (PickerWheel, wheelPhysics, wheelStrip, about 5 KB gzip apart from the shared touch modules), which the size comment attributes only to the shared runtime.
  • restingTime and clampToBounds in timeWheelRules.ts re-implement clampTime from utils/timeParser.ts, and the two already disagree for a bound between whole minutes (with min="09:00:30" the finger path commits 9:01 and the mouse columns commit nothing), so extend clampTime and call it from both paths.

The AM/PM rule itself works: under a finger, 11:30 AM plus one hour row now commits 12:30 PM, and the date and time input suites pass.

[Automated review]

[fix] Under a finger, the time wheels in the TimeInput and DateTimeInput bottom sheets now follow iOS's date picker:

- The AM/PM column follows the hour. Turning the hour from 11 to 12 now gives 12 PM, not 12 AM, and the AM/PM column turns onto PM. From 12 PM to 1 it stays PM.
- A wheel that comes to rest past `min` or `max` goes on to the nearest time inside them and commits it there, and the other columns turn onto it. Opening the sheet never changes the value: a time outside the bounds stays as it is, and an empty field stays empty.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The base sheet already does this under a finger: a wheel that rests past min travels to the bound and commits it, the other columns turn onto it, and opening never changes an untouched value (the base WheelRest.a11y.chromium.spec.ts and Wheel.test.tsx cover all three). Drop this bullet, or narrow it to what changed: under a finger, a bound that falls between whole minutes now commits the nearest whole minute inside it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At 05396c7: Fixed at b70ae13: the bullet now says only what changes. A bound between whole minutes commits the nearest whole minute inside it, with a finger as with a mouse.

}

/** `time` brought inside the bounds: past a bound, onto it. */
export function clampToBounds(time: TimeParts, bounds: TimeBounds): TimeParts {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This re-implements clampTime (utils/timeParser.ts), which the mouse columns and the finger path's commit still use, and the two already disagree: with min="09:00:30" and no seconds column, restingTime gives 9:01 while clampTime gives 09:00, which isTimeInRange rejects. Extend clampTime with the rounding onto the nearest whole minute inside the bounds, call it from restingTime, and delete this copy.

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.

At 05396c7: Fixed at b70ae13: without seconds, clampTime now gives the nearest whole minute inside a bound between minutes, and restingTime calls it. The copy is gone. With min="09:00:30" both paths now commit 9:01, with a test for each pointer.

});
}, [hourFormat, max, min, parsedWheelTime.hour]);

// The mouse columns' rows come from the same rules as the finger wheels'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: this comment narrates the imported calls below it; consider deleting it (the same line sits in TouchDateTimeField.tsx).

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.

At 05396c7: Fixed at b70ae13: the comment is gone from both sheets.

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 05396c7

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. An engineering owner approved the AM/PM rule at this head, and the tests and an emulated-phone check show 11:58 AM turning to 12:58 PM under a finger. Two things remain:

  • The diff against main is 9,501 authored lines, about 6,400 of them from the still-open #8159 and #8178, which is over the 8,000-line limit for an owner-directed batch; land those two, rebase so this pull request holds only its own ~3,100 lines, and then show it qualifies or split it.
  • The size report that component-style-authoring INV6 requires gives no CSS delta against main (PickerWheel.tsx adds StyleX rules to all three date and time inputs) and no justification for the wheel's own touch-only JavaScript; post both, or rely on your own-part report once the rebase leaves only this change.

Non-blocking: the comments at TouchTimeField.tsx:224, TouchDateTimeField.tsx:558 and Wheel.tsx:241-242 restate the isTouch branch below them and can go.

[Automated review]

const mergedInputRef = useMergedRefs(ref, inputRef);
const inputGroup = useInputGroup();
const [isSheetOpen, setIsSheetOpen] = useState(false);
// A finger gets UIDatePicker's wheels; a mouse gets the scrolling columns.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: this comment restates the isTouch ? <TimeWheels … /> : <>…</> branch below (architecture:knowledge-contracts INV29). Consider deleting it.

);
const [activePanel, setActivePanel] = useState<TouchDateTimePanel>('date');
const [isWheelOpen, setIsWheelOpen] = useState(false);
// A finger gets UIDatePicker's time wheels; a mouse gets the scrolling columns.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: as in TouchTimeField.tsx, this restates the isTouch branch below (INV29). Consider deleting it.

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Size (INV6), main cae1f66 → 05396c7, gzip. JavaScript: TimeInput 90.1 → 130.5 KB, DateTimeInput 114.9 → 156.3 KB, PowerSearch 183.6 → 224.9 KB, MobilePowerSearch 168.5 → 209.6 KB, DateInput 103.0 → 144.2 KB. CSS: TimeInput +73 B, DateInput +51 B, DateTimeInput +49 B, PowerSearch +47 B, MobilePowerSearch +0 B, all from #8159's wheel styles.

Why. Most of that JavaScript is #8159's shared touch runtime, which a page pays once. The spec owners kept touch code eager, with no lazy loader, so a gesture never waits for a load (Nov 2), as for Slider. This pull request's own part, #8178 → this head: TimeInput +1.2 KB, DateTimeInput +1.3 KB, PowerSearch +1.3 KB, MobilePowerSearch +1.0 KB, DateInput +0.2 KB, each under INV6's 2 KB flag; CSS unchanged.

Lines. After #8159 and #8178 land, this pull request is its own 2,684 added and 374 deleted lines, 1,750 of the additions tests, within B4's 8,000-line batch.

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 12a0c16

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. An engineering owner has approved the record changes on this exact head. The AM/PM rule, the whole-minute clampTime fix and their tests are ready.

What remains:

  • Scope: against main this pull request is 9,497 authored lines, and about 6,420 of them are the still-open #8159 and #8178. That puts it over the 8,000 lines an owner-directed batch allows. Merge those two, then rebase so this diff is its own change, about 3,100 lines.

Two smaller points: the INV6 size report should be re-posted for this head once the rerun lands. datePickerTouch.ts:8-9 says the sheets import touch behaviour only from that module, but they also reach PickerWheel through Wheel. Reword or drop that line (non-blocking).

[Automated review]

Comment on lines +8 to +9
* @position The pickers' touch layer. The sheets import touch behaviour from
* here only, so a desktop build can replace this one module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the sheets also reach the touch column through Wheel (Wheel.tsx imports PickerWheel and renders it under a finger). So this module is not their only touch import, and replacing it alone would not drop the touch code from a desktop build. Suggest: "The time wheels the sheets render only under a finger."

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec c821d04

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one scope fix remains. An engineering owner has approved the record changes on this exact head, and the AM/PM rule and the whole-minute clampTime fix otherwise look ready.

  • Scope: against main this pull request is 9,558 authored lines, about 6,486 of them from the still-open #8159 and #8178, which is over the 8,000 lines an owner-directed batch allows; land those two and rebase so this diff is its own change (about 3,100 lines), or declare the three as one stack with the same Stack: #8159 → #8178 → #8179 line in each description.

Non-blocking: the header of packages/core/src/DateInput/datePickerTouch.ts says the sheets import touch behaviour only from that module, but Wheel.tsx imports PickerWheel directly (inline note). After a rebase, the records need a fresh /approve-spec on the new head.

[Automated review]

Comment on lines +8 to +9
* @position The pickers' touch layer. The sheets import touch behaviour from
* here only, so a desktop build can replace this one module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the sheets also render Wheel, which imports the touch column PickerWheel itself (Wheel.tsx:54, :248), so this module is not their only touch import. Shorter and true: "The time wheels the sheets render only under a finger."

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 89e3bb3

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommend approval. An engineering owner has approved the record changes on this exact head. The AM/PM rule and the whole-minute clampTime fix come with tests that fail before the change and pass after it, under a finger and with a mouse.

  • This diff includes #8178's commits, so it merges after #8178 or together with it.
  • The Stack: line in #8209, #8200 and #8206 lists #8159 first and differs from this description's, and #8209 does not contain this head. With identical lines, the stack could be reviewed as one.
  • Two optional comment fixes are noted inline, in datePickerTouch.ts and PickerWheel.tsx.

[Automated review]

Comment on lines +8 to +9
* @position The pickers' touch layer. The sheets import touch behaviour from
* here only, so a desktop build can replace this one module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: the sheets also render Wheel, which imports and renders the touch column PickerWheel itself (Wheel.tsx:54, :248), so replacing this module would not remove the touch path. Suggested: "The sheets import TimeWheels from here. Wheel.tsx imports the touch column itself."

Comment on lines +599 to +601
// A spin turns only onto an enabled row (canSpinTo). So its arrival commits
// that row, and nothing turns on past it. It reports the row it was asked
// for, as the scrolling column does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: spinTo resolves a boolean, not a row. Suggested: "It resolves true only when it arrives on that row, as the scrolling column does."

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 894ea24

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommend approval. An engineering owner approved this head's TimeInput FR7 and DateInput record text, and the code matches it: under a finger the AM/PM wheel now follows the hour (11:58 AM to 12:58 PM), and the mouse sheet is unchanged.

  • The finger and whole-minute clamp tests fail on main and pass here, and an emulated Pixel 7 shows 12:58 PM where main gives 12:58 AM.
  • This diff still carries #8178, so it merges after #8178 or together with it.
  • Optional: the datePickerTouch.ts header and the spinTo comment in PickerWheel.tsx still describe the code inaccurately (see the earlier inline suggestions), and FR7 could say how the AM/PM change reaches a screen reader.

[Automated review]

vjeux added 3 commits November 3, 2026 23:39
The time wheels turn a column by themselves when another column's change
moves its value, so the Wheel offers that turn on its ref: `spinTo(index)`
(`WheelHandle`). Under a finger the wheel turns onto the row on the tap
spin, then commits it as a rest would; under a fine pointer the scrolling
column commits the row at once, and the new value scrolls it there. Each
reports whether the row was committed: a disabled row or an index outside
the rows resolves false at once, and a finger, another turn or the
wheel's unmounting cutting the turn short resolves false.
the hour, as UIDatePicker's does

Under a finger the TimeInput and DateTimeInput sheets render TimeWheels,
through the pickers' touch layer (datePickerTouch.ts). The half of the
day
changes only when the hour's move passes between 11 and 12: 11 AM turned
to 12 gives 12 PM, and the AM/PM column turns onto the new half on the
wheel's own spin (component:DateInput FR14). The hour column does not
loop, so 12 counts as next to 1: 12 PM turned to 1 stays 1 PM, and 1 PM
turned to 9 stays 9 PM. A column that comes to rest past min or max goes
on to the nearest time inside them and commits it there, the other
columns turning onto it; an untouched time outside the bounds stays as
it
is. A turn the wheels start by themselves is not a person's rest: its
arrival commits nothing, even when the owner kept the old time. With a
mouse the sheets keep today's independent columns.

The columns do not loop, and the minute wheel lists every minute.
…column

TimeInput FR7 records the rule the spec owners asked for at the B4 head.
On the non-looping hour column, 12 counts as next to 1. Moving between 12
and 1–6 keeps the half of the day; any other move flips it when it passes
between 11 and 12. It cites DateInput FR14's turn-onto rule, and the FR7
verification row names the tests.
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e51ebcc

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommend approval, pending CI. An engineering owner approved this head's component:TimeInput FR7 text, and the code matches it: under a finger the AM/PM wheel now follows the hour (11:58 AM to 12:58 PM), and the mouse sheet is unchanged.

  • Approved pending CI: CI has not run on this head yet. The pull request is a draft on a fork branch, so CI runs once it is marked ready for review and a maintainer adds the ci:run label.
  • The finger and whole-minute clamp tests fail on main and pass here, and an emulated Pixel 7 shows 12:58 PM where main gives 12:58 AM.
  • Optional: the datePickerTouch.ts header and the spinTo comment in PickerWheel.tsx describe the code inaccurately (see the inline suggestions), and FR7 could say how the AM/PM change reaches a screen reader.

[Automated review]

Comment on lines +8 to +9
* @position The pickers' touch layer. The sheets import touch behaviour from
* here only, so a desktop build can replace this one module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (comment accuracy): Wheel.tsx imports and renders the touch column PickerWheel itself, so replacing this module would leave the touch path in. Suggested:

 * @position The pickers' touch layer. The sheets import TimeWheels from
 *   here. Wheel.tsx imports the touch column itself.

Comment on lines +597 to +599
// A spin turns only onto an enabled row (canSpinTo). So its arrival commits
// that row, and nothing turns on past it. It reports the row it was asked
// for, as the scrolling column does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (comment accuracy): spinTo resolves a boolean, not a row. Suggested:

  // A spin turns only onto an enabled row (canSpinTo). So its arrival commits
  // that row, and nothing turns on past it. It resolves true only when it
  // arrives on that row, as the scrolling column does.

This branch was successfully deployed

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

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants