Skip to content

fix(DateInput, DateTimeInput): the month scroller's steps follow design:reduced-motion - #8496

Draft
vjeux wants to merge 850 commits into
meta:mainfrom
vjeux:uikit/month-scroller-reduced-motion
Draft

vjeux wants to merge 850 commits into
meta:mainfrom
vjeux:uikit/month-scroller-reduced-motion

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: https://github.com/vjeux/astryx/compare/0947061930..cdbe48196b

Depends on #8076 and #8534 (its flex host keeps WebKit re-rendering in a presented sheet); the rest are merges.

User impact

With Reduce Motion on, the DateInput and DateTimeInput sheet calendars' month arrows, Reset and keyboard moves still slide the months sideways, with a mouse and on a touch screen.

Problem and solution fit

  • Why it happens: the month scroller's steps call scrollTo({behavior: 'smooth'}) whatever the preference.
  • Why it harms: the whole grid slides for every step.
  • Why that matters: design:reduced-motion asks for a change without travel, at once with a mouse or a keyboard (DR3) and a short cross-fade on a touch screen (DR2).

Now the month changes without travel: at once with a mouse or a keyboard, through a 200 ms opacity dip on a touch screen.

Expected behavior and authority

design:reduced-motion DR2 and DR3, through core's reducedMotionFade, as every touch surface does.

Smallest restoration

  • MonthScroller.tsx: a step asks reducedMotionFade, then jumps (mouse, keyboard) or dips (touch screen).
  • The jump lands once the mounted window has moved to the target, as a far jump does, so nothing moves the window afterwards (WebKit re-snapped it one month further every frame).
  • Any newer step (another arrow, a jump, a step at once) replaces a running dip; its timer and animation end with the scroller.
  • A landing clears an earlier steer that never arrived, so a later trackpad scroll updates the month.
  • No API, visual or default change; without Reduce Motion nothing changes.

Evidence

  • Before the change (feat(core): the internal touch foundation: press tracking and the animator #8076's tree, the same tests): with a mouse a smooth scroll was still running after the click; on a touch screen the offset travelled 368 px over about 300 ms.
  • After the change: with a mouse the offset changes within the click and stays; on a touch screen the opacity dips below 0.5 within 200 ms and the offset changes once, at the dip's middle.
  • Unchanged path: without Reduce Motion an arrow still scrolls (new browser test).
  • Regression tests: MonthScroller.test.tsx (a second step within the dip lands once; a jump or a step at once made within it stays; an unmount within it scrolls nothing; a later trackpad scroll updates the month) and the browser spec's two Reduce Motion cases, all failing before.
  • Proved in Chromium and WebKit with mouse and touch emulation; no real device was available, so a real-phone check is the release gate.

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

  • The full core suite (--maxWorkers=16): 541 files, 14,863 tests.
  • MonthScroller.interaction.browser.spec.ts (3 tests): Chromium 9/9 and WebKit 9/9 over three runs each.
  • Size (INV6, analyze-pr.js), import gzip, base → head: DateInput 101.5 → 102.8 KB, DateTimeInput 111.7 → 113.0 KB; component CSS unchanged.
  • pnpm build then the root pnpm typecheck; CI=true pnpm lint; check:changesets; the CI duration guard.
  • scripts/touch-support-bundle.test.mjs "is absent from the root entry" fails for every touch component under main's current test; test(core): a touch component's import carries its gesture code, and nothing else does #8671 replaces it with the per-component check, which lists DateInput, DateTimeInput, PowerSearch and TimeInput.

@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 Ready Ready Preview Oct 29, 2026 8:47pm 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
🟡 Reviewed (for maintainers only) Oct 29, 2026, 9:07 PM UTC

…hing is attached

From CodeBunny on meta#8073 (spec:AST-094 FR24), fixed once here: the
(pointer: coarse) change listener was added when the host was created,
on a fine pointer too, and never removed. It is now added with the first
attachment and removed when the last one goes, so nothing touch-related
remains after the last instance unmounts. The test fails on the previous
head (a listener at creation, none removed).
- The detach's landing commit can promote the element again (a coast
  still moving on another axis), so attach demotes after it, as dispose()
  orders them: no will-change stays on an unmounted or replaced node.
- A new element taking over an exit by role hands the outgoing element its
  stylesheet's opacity, as for a borrowed dip (the same test stop() uses).
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026
vjeux and others added 11 commits October 26, 2026 00:29
Lab imports no new core-private path until Lab's module-sharing decision, so
the hooks/isRtlElement entry of the internal-module resolver, its two Lab
tsconfig paths and its resolver tests lose their only users and go. A Lab
component that needs the check keeps a local helper.
…t syncs, and the stale headers and changesets match the code

- useGestureRecognizer and usePressTracker re-attach when the element behind
  the ref is replaced and detach when it goes, checked after every render at
  no render cost (architecture:react-component-runtime INV6); each Effect
  names the system it syncs (INV3).
- A control's profile releases on the deselect clock the shared press styles
  play, so a painter's `fading` never outlasts the fade it shows; UIButton's
  250 ms arrives with a control's own release layer.
- PressTracker's and GestureHost's headers describe the cell's delay inside
  scroll views only, and no delegated consumers or scroll handoff.
- One changeset for the press tracking, the one press writer and the hooks.

Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
src/utils backs the public @astryxdesign/core/utils entry, where a
deliberately experimental API may carry the tag, so the guard reads only its
package-private touchPresentation.ts there. A test pins that boundary, and
the guard checks that every file it names exists.
…e, and a backward end delay is not progress

waitForAnimation's fallback finish time (read before the finish event is
delivered, right after finish()) now goes through the playback rate's speed,
and for a run going backward is its startTime; it was a whole run late for a
reverse()d run and 500 ms late at rate 2. minProgressMs no longer counts a
backward run's end delay, which plays first and presents nothing. Self-tests
for both; the finish case failed by 1,000 ms before (Chromium, 27/27 and 5x
repeat pass).
…ing is attached) into UIControl press tracking

The host has no delegated consumers at this layer, so the listener follows
the registry alone.
…animator wrote is owned

Under Reduce Motion on a touch screen:
- A detach and re-attach, or a hand-over to an element with no stylesheet
  opacity, dropped a running borrowed dip, so the move passed in full view:
  the rebuild wrote the dip's end value (1, the identity) inline first, and
  releaseIdentity took that for the dip's end. releaseIdentity now hands a
  borrow back only once the dip has ended, and attach keeps a running dip
  in the model for any element, giving only the outgoing one its
  stylesheet's opacity back; a dip that ended while detached is handed back
  on the next attach.
- An opacity the animator left inline (a `set` over a stylesheet that hides
  or dims the element) counts as owned, so a dip returns to it instead of
  borrowing the stylesheet and stripping it.
… an opacity the animator wrote is owned)

One behaviour for a detach mid-dip, meta#8055's: the dip stays in the model
through any attach, the move lands at its bottom, and only the outgoing
element's inline opacity goes. landPending() remains for dispose() alone.
The plan takes meta#8055's rule that an inline opacity the animator wrote is
owned (for dips and fades by role). releaseIdentity keeps the plan's
pending-landing check: under the plan the end value of a running dip is
not written by a rebuild before the dip ends, so meta#8055's running-dip check
is not needed and would strand the hand-back. meta#8055's new tests run against
the plan, stepping the clock to the landing; this branch's replacement
tests now expect the dip to go on on the new node.
… until it finishes

While a rendering trace runs, waitForRest waits on timers for each curve's
time left, computed as its end less its current time. Going backward
(reverse(), a negative playback rate) the current time falls from the end
time to 0, so that difference is the time already played: a reversed move
with a 200 ms end delay settled within 50 ms, before it began to move, and
one without waited about twice its length.

Backward, the time left is now the current time over the rate's magnitude:
the run is waited for until it finishes, as the untraced path waits it out.
A self-test pins both: the delayed reversed move holds the traced wait for
its 800 ms, and a reversed move is waited for no longer than the same move
forward.
… AST-094 DEC-11) into the motion clock and Reduce Motion plan

scroll.ts takes main's note.
…into the experimental touch exports

main's meta#8494 rewrites the scroll-to-top note's Reduce Motion sentences; the
note takes main's wording as it stands, as the motion clock chain's own merge
of main does: a tap-started decay is a transition under Reduce Motion (DR2 on
a coarse pointer, DR3 elsewhere), and a component checks for Reduce Motion
before it plays it.
…into the motion-role animator docs

main's meta#8494 rewrites the scroll-to-top note's Reduce Motion sentences; the
note takes main's wording as it stands, as the motion clock chain's own merge
of main does.
vjeux added 9 commits October 26, 2026 23:57
…(spec:AST-094 FR18, DEC-13)

A screen-edge pan attached to a stack container, as wide as the screen,
with a touch 4 px from its edge: inside the 40.2 px band the pan decides at
UIKit's 15 px whatever the element's width, and then tracks the travel past
it. From several edges too. Both fail while a wider element decides at
FR19's 10 px.
…'s width (spec:AST-094 FR18, DEC-13)

FR18: inside the 40.2 px band a screen-edge pan decides at UIKit's 15 px
screen-edge hysteresis; outside it the author's scrolling is untouched and
FR19's 10 px governs every other claim. The pan decided at 10 px on any
element wider than the band, so NavigationStack's edge pan, attached to the
stack container, began 5 px early. The edge zone still decides only the
touch-action; a touch outside every band still fails at once. The two
cases that pinned the 10 px decision go.
… 15 px whatever the element's width) into the experimental touch exports
…er's move keeps that finger until it lifts) into the experimental touch exports
…than the band (spec:AST-094 FR18, DEC-22)

FR18 as amended in meta#8631 (DEC-22): the band's pan-x claim and 15 px start
hold only where the screen-edge pan is attached to an element within the
band; on an element that reaches beyond it, whose touch-action cannot
claim per touch, a touch in the band decides at FR19's 10 px like every
other claim. This restores the 10 px decision 3d85c30 removed and keeps
15 px on an element within the band. The cases pin DEC-22 both ways (a wide
element with a touch at x=4 begins at 10, an in-band strip at 15, and from
several edges at 10 on a wide element); the 10 px cases fail on 3d85c30.
…an the edge band decides at 10 px again, DEC-22) into the experimental touch exports
…xperimental touch exports

The motion README's Files table is the union: main's rows, with meta#8663's
resolveSnapPoint.ts and selectPage.ts, plus this branch's, in main's
order.

# Conflicts:
#	packages/core/src/motion/README.md
The gesture README promises that usePressTracker rebuilds its tracker
when trackingExpansion, highlightDelay, cancelTravel or quickTapFlash
change, from the next touch. That fix (c248459) was lost in a later
merge of the press-tracking line: the tracker was built once, so a
changed option never applied. It is memoized on those four again; the
node attachment, keyed on the tracker, moves to the new one and disposes
the old. The callbacks keep being read through the ref on every touch.

The new test fails before this change (the same tracker after each
option change) and passes after it.

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

Changes requested: the month-scroller fix does what Reduce Motion asks, but this branch also carries about 16,000 lines of the still-open #8076 and #8534, so please land those on their own and update this branch from main until it holds only the four fix files. One fix is still needed in MonthScroller.tsx: the Reduce Motion land() does not clear steeredRowRef, so after the month wheels close and a step lands, a trackpad scroll stops updating the header month; clearing it at the top of land(), with a test for that sequence, resolves it.

[Automated review]

… s of wall clock

The two proofs that a Reduce Motion dip held before its first frame
still lands (after the hold is finished, and after it is resumed) polled
the box for 2 s of wall clock. On a loaded machine the page's main
thread ran the dip's next step late and the poll ran out first: one
WebKit run read {opacity: 0, translate: 'none'} with four WebKit
containers on the box, then passed in 530 ms on a rerun. They now wait
until the box runs no animation, on the animations' own finished
promises (including the fade back in the dip starts when its fade out
ends), and then assert the landed state. The test's own timeout still
bounds them.

With a 2.5 s main-thread block induced right after the release, both
proofs failed before this change (Timeout 2000ms exceeded while waiting
on the predicate) and pass after, 3 runs each in Chromium.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux and others added 9 commits October 28, 2026 10:39
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts:
it takes main's merged meta#8046 text (the finger and button slots, the mouse
and key press) and keeps this pull request's own additions, the one
writer of the press attribute (writePress, paintPress, readPressPaint)
and the header line naming them.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
Main's button slot, merged with meta#8046, ends a press's release at once
when the next press starts, by clearing the press attribute directly. On
a row another painter holds, that clear erased the hold, which this pull
request's one writer refuses. A mouse press on a held row, released, then
a press elsewhere must leave the row held. Fails until the early end goes
through the one writer.
finish() writes none through writePress, as this pull request's
controller paints, so a held row stays its owner's when the next press
ends a release early, under the finger and the button slots alike.
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
…bute

From the review of meta#8076: the README said paintPress in
utils/pressFeedback.ts is the one writer of data-astryx-press, but the
action-group tracker on main (meta#8346) writes its groups' presses itself.
The sentence now says what holds at this head.
…clears an earlier steer

The month scroller ignores the scrolls of a steer until it arrives. The re-assert when the month wheels close steers onto the month already in view, so its scroll never arrives; a step that then lands under Reduce Motion left that steer in place, and every later scroll, a trackpad's included, stopped updating the header month. A landing is the newest steer, so it clears the earlier one; its own scroll lands on the centre row and reports nothing.
@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

The Reduce Motion land() now clears steeredRowRef at its top (9ea2bcb). A new MonthScroller.test.tsx case runs the sequence: a steer onto the month already in view whose scroll never arrives, a step that lands, then a trackpad scroll, which now updates the month. It failed before the change.

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

Changes requested for scope: the month-scroller fix does what Reduce Motion asks (the month changes at once with a mouse and through a 200 ms opacity dip on touch, and the landing now clears the earlier steer, with a test), but this branch still carries about 14,200 lines of the open #8076 and #8534. Please land those through their own pull requests, then update this branch from main so it holds only MonthScroller.tsx, its two tests and its changeset.

[Automated review]

@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
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026

This branch was successfully deployed

1 active deployment
Preview — cdbe4819 Deployed Oct 29, 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants