Skip to content

feat(SelectableCard): under a finger the selection ring and border change in one frame - #8246

Closed
vjeux wants to merge 2 commits into
meta:mainfrom
vjeux:uikit/c06-card-mark
Closed

vjeux wants to merge 2 commits into
meta:mainfrom
vjeux:uikit/c06-card-mark

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8226. Review only this PR's change: vjeux/astryx@4707533...3161c95

User need

On a phone, tapping a SelectableCard changes its selection mark in two steps. The inset ring appears or vanishes at once, while the border fades over the theme's fast duration (125 ms), so for several frames the card that lost the selection still shows a gray outline. UIKit changes a selection mark in the frame the selection lands; under a finger the card now does too.

Evidence

A tap moving the selection from Basic to Pro, ¼ speed, recorded on this PR's earlier code: iOS 26.2 Mobile Safari (the magenta ring is the finger; before is #8226) and Android 15 Chrome (before is main). Before, Basic's border fades after Pro's ring appears; after, both cards change in one frame. The engine runs in Testing prove this head:

Before After
iOS before after
Android before after

With a mouse under Reduce Motion, Chromium storyboards of a click moving the selection to Pro. Before (main), the borders fade; after, both cards change in one frame:

A mouse click under Reduce Motion before this change

A mouse click under Reduce Motion after this change

Problem and solution fit

Current specification

Public delta and caller burden

Under a finger only, the card's ring and border change in one frame. No API changes; the press overlay, colours and elevation are unchanged.

Design and alternatives

The card's touch press paint is separate press-layer work (#8057, #8118).

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at 1d2f527c7f, which differs from this head (3161c95) only in the isSelected doc sentence, on #8226's 4707533:

  • pnpm build, pnpm api:report --check, pnpm typecheck and pnpm review:pre: clean.
  • Strict ESLint on the changed files: no errors. Prettier: clean, except SelectableCard.doc.mjs, which main also leaves unformatted. check:repo, check:changesets, check:knowledge and check:i18n-runtime: clean.
  • Units: Indicator, motion and SelectableCard, 879 passed. SelectableCardMark.test.tsx fails on feat(core): under a finger a selection mark changes in one frame #8226's SelectableCard.
  • Chromium, pinned to one physical core with no other test process on it: SelectableCardMark and SelectionMarks ×10 at 4 workers, 120 of 120.
  • WebKit in the CI container profile (4 CPUs, 1 worker): both specs ×10, 120 of 120.
  • Accessibility contracts in Chromium (Checkbox, CheckboxList, RadioGroup): 66 of 66, once the Storybook build carried its source stamp.
  • Full unit suite: 18,991 of 18,992. The one failure, DropdownMenuLazySheet's "focuses the first action when a keyboard open beats the sheet load", is outside this change and passes alone 3 of 3 at this tree.
  • Repo-wide strict lint: 0 errors.

@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 3, 2026 7:51am 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
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@github-actions
github-actions Bot requested review from cvkxx and ernestt October 24, 2026 13:45
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 3, 2026, 8:06 AM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@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: the touch timing here rests only on the draft spec:AST-094 FR1, FR2 and FR4 that this pull request adds, and FR4 would narrow the current theme-tokens INV3 (components consume motion roles) to pointer presentations. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and you may do so yourself as an engineering owner. Independent of that decision, these need changes:

  • Land or drop the #8226 stack and rebase, so this pull request is the 367-line SelectableCard commit rather than 13,990 authored lines (the rebase also clears the pr-a11y "No tests found" failure).
  • Make the new pr-interaction job part of the existing UI test owner and its test join, or leave it out, since AST-030 FR11 and FR7 rule out a separately routed lane whose failure fails no join.
  • Add the writing-rubric results for spec:AST-094 to the description, as the knowledge-contracts record requires for a new record.
  • Describe the touch arm as following a coarse primary pointer, not "a finger" versus "a mouse", in the Indicator docs and both changesets.

[Automated review]

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

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 job routes itself (Check interaction scope) and is not in the test join, so a failing interaction proof fails none of the required joins. AST-030 FR11 asks new coverage inside an existing surface to join that surface's test owner instead of adding a separately routed lane, and FR7 asks an owned lane's failure to fail its join.

// do (motion/uikit/selection.ts). The ring cannot interpolate with the
// card's resting shadow, so a fade would split it from the border. A
// mouse keeps the theme's duration.
'@media (pointer: coarse)': MARK_CHANGE,

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.

(pointer: coarse) follows the device's primary pointer, not the input that made the selection, so a trackpad on a coarse-primary device also gets 0 ms and a finger on a fine-primary laptop keeps the fade. Whether touch marks may leave --duration-fast at all is the open decision on draft AST-094 FR1 and FR4.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Touch gating. core/src/SelectableCard/SelectableCard.tsx:88 writes its own (pointer: coarse) check. This conflicts with AST-012 FR2 (touch is resolved once, through the closed adaptation vocabulary) and public-component-api INV16. Resolve through the shared touch resolver key so behavior matches the spec. If this behavior genuinely can't be expressed through the resolver, please raise it here as a spec question instead of shipping a separate check.

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

  • Touch gating: the card's selection mark now keys on the touch resolver's TOUCH_QUERY alone, the same on every touch device, with no platform gate. It moves to the resolver's importable constant when that lands. Commit a782f5b, in the next push.

@vjeux
vjeux force-pushed the uikit/c06-card-mark branch from c68e13e to a782f5b Compare October 24, 2026 23:41
@vjeux vjeux changed the title fix(SelectableCard): under a finger the selection ring and border change in one frame feat(SelectableCard): under a finger the selection ring and border change in one frame Oct 24, 2026
@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in a782f5b: the card's mark keys on TOUCH_QUERY alone, the same on every touch device.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in 7bb7354: the card's touch and Reduce Motion arms now import the shared keys (touchKeys.touch, reducedMotionKeys.instant) instead of spelling them.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in c30794b, on #8331. The card's touch and Reduce Motion keys stay literal, and a test pins each one to the resolver's constant. The StyleX build gives an imported at-rule key a different priority than a literal one.

@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 under spec:AST-094 FR4, which is current on main: under a coarse pointer the SelectableCard mark (and the checkbox and radio marks from #8226) plays a build-time 0 ms constant, while FR4 requires every touch duration to be a theme motion role and forbids a literal. An engineering owner records whether this one-frame mark sits outside FR4 or must become a registered motion role by commenting /approve-spec <full head SHA> on this pull request, and you may do so yourself as an engineering owner. Independent of that decision, these need changes:

  • Land or drop the stack beneath the card commit and rebase on current main, which also clears the conflicts in seven gesture and motion files that keep CI from running, so this pull request is the 598-line card change rather than 31,417 authored lines.
  • Add mouse before-and-after evidence for the card's new Reduce Motion behaviour, as AST-017 FR59 asks.
  • Type the root theme motion duration scale so a partial scale fails type-checking, remove the build README paragraph about the reverted undeclared-variable warning, and correct the checkbox and radio Reduce Motion sentence in the changeset and the isSelected doc.

[Automated review]

// once (design:reduced-motion DR3); a finger's change already is.
// The resolver's and the policy's keys, spelled: an imported at-rule
// key lands in a different StyleX priority layer than a literal one.
'@media (pointer: coarse)': MARK_CHANGE,

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.

Under a coarse pointer this arm plays a build-time 0 ms constant (selection.mark.changeMs through selectionMotionConsts). Current spec:AST-094 FR4 requires every duration a touch presentation plays to be a theme motion role and forbids a literal, and selection is not in the generated role table, so this needs an engineering owner's ruling: either the one-frame mark sits outside FR4, or it becomes a role read through its --motion-* variable.

Comment on lines +55 to +56
export interface MotionConfig
extends Partial<MotionScaleConfig>, MotionRoleValues {}

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.

Partial<MotionScaleConfig> lets a root motion: {fast: 150, medium: 300} type-check, but motionScaleOf throws unless fast, medium and ratio come together (lines 89-92); the base type rejected it at compile time (TS2741). Typing the root input as all-or-none, e.g. MotionRoleValues & (MotionScaleConfig | {fast?: never; medium?: never; ratio?: never; slow?: never; easing?: never}), keeps the error in the editor; the adaptation value can stay partial since it completes from the root.

Comment thread packages/build/README.md

Unsupported declarations fail the build by default with StyleX's replacement guidance. Set `propertyValidationMode` explicitly in `stylexOverrides` (modern API) or `stylexOptions` (legacy API) only when a migration needs the upstream `warn` or `silent` behavior.

A production build also checks the StyleX stylesheet for generated custom properties (`--<prefix><hash>`) that it reads but no stylesheet in the build declares, which browsers drop: a `.stylex.ts` export read in a style that is not a `defineVars`/`defineConsts` value, or one whose module never reached the build. It warns, naming the variable and the rule that reads it. Only stylesheets count as declarations, so a property the app sets at runtime (an inline style) is reported too: give it a name outside the StyleX class-name prefixes.

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 describes the production warning for undeclared generated custom properties, which this branch reverted (vite.ts has no such check), so build users would wait for a diagnostic that never appears. Suggest removing the paragraph.

'@astryxdesign/core': patch
---

[feat] Under a finger (a coarse primary pointer, on every touch device), a checkbox or radio mark now changes in the frame the selection lands, the way UIKit shows a selection mark. The box or circle used to fade between its two colors over the theme duration, so a tap first showed half-filled frames. With a mouse the marks keep the theme duration and easing, and under Reduce Motion a mouse's change is now immediate.

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 already gave CheckboxIndicator and RadioIndicator '@media (prefers-reduced-motion: reduce)': '0s', so a mouse's change under Reduce Motion was already immediate; only the coarse-pointer change is new for these marks.

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: the ring switches at once (SelectableCard.tsx:90-91), and under Reduce Motion neither fades. Suggested: "…with a mouse the border fades over the theme's fast duration, except under Reduce Motion."

@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 under spec:AST-094 FR4, which is current on main: under a coarse pointer the SelectableCard mark (and the checkbox and radio marks from #8226) plays a build-time 0 ms constant, while FR4 requires every touch duration to be a motion role and forbids a literal. An engineering owner records whether this one-frame mark sits outside FR4 or must become a registered motion role by commenting /approve-spec <full head SHA> on this pull request, and you may do so yourself if you are an engineering owner. Whatever that decision, these need changes:

  • Land or drop the stack beneath the card and rebase, so this pull request carries the 612-line card change rather than 23,846 authored lines.
  • Add mouse before-and-after evidence for the card's new Reduce Motion behavior, as spec:AST-017 FR59 asks.
  • Correct the isSelected doc (with a mouse only the border fades, and not under Reduce Motion) and the changeset sentence announcing a mouse Reduce Motion change the checkbox and radio marks already had.

[Automated review]

@@ -71,3 +72,13 @@ export const navigation = {
*/
interactivePopFinishThreshold: 0.5,
} as const;

/**
* Makes each spring, duration and curve above a motion role a theme may

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 table passes no roles, so registerMotionTable registers no role here, and current spec:AST-094 FR4 says a theme cannot override a touch motion role yet (DEC-11). Say the values stay internal constants instead of "a motion role a theme may retune".

@@ -122,3 +124,16 @@ export const scroll = {
*/
scrollToTopRate: 0.99035,
} as const;

/**
* Makes each spring, duration and curve above a motion role a theme may

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.

Same as registerNavigationTable: no roles are listed, so nothing here becomes a role, and FR4 rules out theme retuning for now; the comment should say so.

@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 under spec:AST-094 FR4: under a coarse pointer the SelectableCard mark (and the checkbox and radio marks from #8226) plays a build-time 0 ms constant rather than a registered motion role, and an engineering owner records whether that is acceptable by commenting /approve-spec <full head SHA> on this pull request, which you may do yourself if you are an engineering owner. Whatever that decision, these need changes:

  • Land or drop #8226 and its predecessors and rebase, so this pull request carries the card change instead of 23,099 authored lines.
  • Add mouse before-and-after evidence for the card's new Reduce Motion behaviour, as spec:AST-017 FR59 asks, and correct the isSelected doc and the selection-marks changeset, which misdescribe what a mouse user sees.

[Automated review]

// once (design:reduced-motion DR3); a finger's change already is.
// The resolver's and the policy's keys, spelled: an imported at-rule
// key lands in a different StyleX priority layer than a literal one.
'@media (pointer: coarse)': MARK_CHANGE,

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.

Under a coarse pointer this arm plays MARK_CHANGE, the build-time 0 ms mirror of selection.mark.changeMs, and motion/uikit/selection.ts registers no motion role. Current spec:AST-094 FR4 says "A component MUST read touch motion through these roles and MUST NOT play a literal", so an engineering owner needs to rule whether a one-frame mark is outside FR4 or must become a registered role.

// The resolver's and the policy's keys, spelled: an imported at-rule
// key lands in a different StyleX priority layer than a literal one.
'@media (pointer: coarse)': MARK_CHANGE,
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':

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 arm changes what mouse users see: on the base the card's border fades over 125 ms under Reduce Motion, and here it changes at once (the design:reduced-motion DR3 repair). spec:AST-017 FR59 asks the pull request to show fine-pointer evidence before and after such a change; the PR's evidence covers touch only.

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: the ring switches at once (see SelectableCard.tsx:90-91), and under Reduce Motion nothing fades. Suggested: "with a mouse the border fades over the theme's fast duration (at once under Reduce Motion)".

'@astryxdesign/core': patch
---

[feat] Under a finger (a coarse primary pointer, on every touch device), a checkbox or radio mark now changes in the frame the selection lands, the way UIKit shows a selection mark. The box or circle used to fade between its two colors over the theme duration, so a tap first showed half-filled frames. With a mouse the marks keep the theme duration and easing, and under Reduce Motion a mouse's change is now immediate.

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 CheckboxIndicator and RadioIndicator already set '@media (prefers-reduced-motion: reduce)': '0s' for every pointer, so "under Reduce Motion a mouse's change is now immediate" announces a change readers will not see. Drop that clause.

Comment thread packages/core/src/motion/animator.ts Outdated
Comment on lines +128 to +137
* Roles: a spring or curve that is one of a registered table's motion roles
* plays the value its CSS variable carries on the element's scope, or the
* table's value when none is set (resolveMotion.ts; nothing public sets one
* while touch motion stays internal): `springs.settle` once
* `registerSpringsTable()` has run, the Reduce Motion fade's curve (the
* animator registers that role alone). Every other value, and a table never
* registered, plays its own value, and the fade's length is not a role. A
* role is read once, when the motion starts, from the scope's computed style: no layout, and the start, the promotion
* and the browser-set start time are unchanged. A spring written inline at the
* call site is played as given.

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 paragraph (and the comment at line 1701) says a role plays its CSS variable read from the scope's computed style, but resolveMotion.ts returns the table value and reads no style while touch motion stays internal (spec:AST-094 DEC-11).

Comment on lines +60 to +63
* DELEGATED CONSUMERS. A consumer type registered with an attribute (a
* pressable row) is found at touch-down by walking up from the touch target,
* and an instance is created for that one touch: a list of a thousand rows
* costs nothing until a finger lands on one.

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 host no longer registers consumer types by attribute (only attachConsumer remains), so this "DELEGATED CONSUMERS" paragraph describes a capability that is gone.

@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 under spec:AST-094 FR4: under a coarse pointer the SelectableCard mark (and the checkbox and radio marks from #8226) plays a build-time 0 ms constant rather than a registered motion role, and an engineering owner records whether that is acceptable by commenting /approve-spec <full head SHA> on this pull request, which you may do yourself if you are an engineering owner. Whatever that decision, these need changes:

  • Land or drop #8226 and its predecessors and rebase, so this pull request carries the card change instead of 21,774 authored lines.
  • In the gesture host those predecessors add, intersect the inline touch-action with the element's own value and count a scroll as the browser taking a finger only after 10 px of travel, as spec:AST-094 FR18 and FR20 require.
  • Add mouse before-and-after evidence for the card's new Reduce Motion behaviour, as spec:AST-017 FR59 asks, and correct the isSelected doc and the selection-marks changeset, which misdescribe what a mouse user sees.

[Automated review]

element.style.getPropertyValue(property),
);
}
element.style.setProperty(property, value);

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 inline touch-action overrides the element's own value, and nothing reads or intersects it first. spec:AST-094 FR18 says the derived value "is intersected with the element's own author touch-action and is never wider than it (DEC-8)": an element styled touch-action: none with a tap recognizer would become manipulation here. Reading the element's computed value before writing, keeping the author's value when it omits pinch-zoom, and a test for a narrower author value would bring it in line.

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

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.

Any scroll of a container under a live, unclaimed finger hands that finger to the browser here, with no travel check (the test at GestureHost.test.ts:802 asserts it for a finger that never moved). spec:AST-094 FR20 counts a native scroll "only once that finger has moved the scroll take distance (FR2), so sub-pixel movement under a still finger does not cancel a tap (DEC-8)", and FR2 sets that distance at 10 px.

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

This needs an engineering-owner decision before it can be approved: under a finger, SelectableCard's mark now plays a 0 ms build-time constant, while spec:AST-094 FR4 requires touch motion to read a named motion role and not play a literal. An engineering owner (the author included, if they are one) can accept the one-frame mark as outside FR4 by commenting /approve-spec <full head SHA> on this pull request, or ask for selection.mark.changeMs to become a registered motion role. Whatever the ruling, these need fixing:

  • Narrow the pull request to the card change by landing or dropping #8226 and its predecessors, since it carries 21,497 authored lines today.
  • The gesture host replaces a narrower author touch-action (GestureHost.ts:467) and lets a scroll cancel a still finger's tap (GestureHost.ts:1460), against AST-094 FR18 and FR20.
  • Add fine-pointer before-and-after evidence for the Reduce Motion change, as AST-017 FR59 requires.
  • Fix the isSelected text at SelectableCard.doc.mjs:62 (with a mouse only the border fades) and drop the Reduce Motion clause from .changeset/selection-marks-touch-one-frame.md:5, which describes what the base already did.

[Automated review]

@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 to provide or identify the governing spec for the one-frame (0 ms) selection mark that SelectableCard, CheckboxIndicator and RadioIndicator play under a coarse pointer: accept it as it stands under spec:AST-094 FR4, or require selection.mark.changeMs to become a registered motion role first. 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. Whatever the decision, these changes are needed:

  • Land #8226 and its stack first so this pull request carries only the card change (21,115 authored lines today), and fix GestureHost writing its derived touch-action over the author's (FR18) and cancelling a still finger's tap on any scroll (FR20).
  • Correct SelectableCard.doc.mjs, since a mouse fades only the border, not the ring, and add before-and-after mouse evidence for the Reduce Motion change (spec:AST-017 FR59).

[Automated review]

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: a click starts eight 125 ms border-*-color transitions and none on box-shadow, and the ring switches at once (the comment at SelectableCard.tsx:90–91 says the ring cannot interpolate). Suggest: "with a mouse the border fades over the theme’s fast duration and the ring changes at once."

element.style.getPropertyValue(property),
);
}
element.style.setProperty(property, value);

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 writes the recognizers’ derived touch-action over the element’s own stylesheet value without intersecting it, so an author’s none or pan-y can widen to manipulation or pinch-zoom. spec:AST-094 FR18 requires the derived value to be "intersected with the element’s own author touch-action and … never wider than it (DEC-8)".

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

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.

Every unclaimed touch inside the scroller is handed to the browser here, however far its finger moved, so a sub-pixel or programmatic scroll cancels a still finger’s tap. spec:AST-094 FR20 counts a native scroll "only once that finger has moved the scroll take distance (FR2)", which is 10 px.

@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 to decide whether the one-frame (0 ms) selection mark that SelectableCard, CheckboxIndicator and RadioIndicator play under a coarse pointer is acceptable under spec:AST-094 FR4 as it stands, or whether selection.mark.changeMs must first become a registered motion role. 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. Whatever the decision, these changes are needed:

  • Land #8226 and its stack first so this pull request carries only the card change (17,795 authored lines today).
  • In GestureHost, intersect the derived touch-action with the element's own (FR18) and let a scroll take a touch only after 10 px of that finger's travel (FR20).
  • Add before-and-after mouse evidence for the Reduce Motion change (spec:AST-017 FR59), and correct SelectableCard.doc.mjs, since a mouse fades only the border.

[Automated review]

// once (design:reduced-motion DR3); a finger's change already is.
// The resolver's and the policy's keys, spelled: an imported at-rule
// key lands in a different StyleX priority layer than a literal one.
'@media (pointer: coarse)': MARK_CHANGE,

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.

Under a coarse pointer this arm plays MARK_CHANGE, the build-time 0 ms mirror of selection.mark.changeMs, and motion/uikit/selection.ts registers no motion role. Current spec:AST-094 FR4 says "A component MUST read touch motion through these roles and MUST NOT play a literal", and its non-goals leave each component's touch presentation to its own record, which SelectableCard does not have. This waits on an engineering owner's decision: accept the one-frame mark as it stands, or register the value as a role first.

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: a click starts four 125 ms border-*-color transitions and none on box-shadow (the ring cannot interpolate, SelectableCard.tsx:90–91), and under Reduce Motion nothing fades. Suggested: "with a mouse the border fades over the theme’s fast duration (at once under Reduce Motion) and the ring changes at once."

element.style.getPropertyValue(property),
);
}
element.style.setProperty(property, value);

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 writes the recognizers' derived touch-action over the element's own stylesheet value without intersecting it, so an author's none or pan-y can widen to manipulation or pan-y pinch-zoom. spec:AST-094 FR18: "The derived value is intersected with the element's own author touch-action and is never wider than it (DEC-8)".

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

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.

Every unclaimed touch inside the scroller is handed to the browser here, however far its finger moved, so a sub-pixel or programmatic scroll cancels a still finger's tap (GestureHost.test.ts:802 asserts it). spec:AST-094 FR20 counts a native scroll "only once that finger has moved the scroll take distance (FR2)", which is 10 px.

@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 to provide or identify the governing spec for the one-frame (0 ms) selection mark that SelectableCard, CheckboxIndicator and RadioIndicator play under a coarse pointer: accept it as it stands under spec:AST-094 FR4, or require selection.mark.changeMs to become a registered motion role, with each component's own record stating its touch presentation, first. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever the decision, these changes are needed:

  • Land #8226 and its stack first, so this pull request carries only the 612-line card change (17,370 authored lines today).
  • In GestureHost, intersect the derived touch-action with the element's own (FR18), and let a scroll take a touch only after 10 px of that finger's travel (FR20).
  • Add before-and-after mouse evidence for the Reduce Motion change (spec:AST-017 FR59), and correct SelectableCard.doc.mjs, since a mouse fades only the border.

[Automated review]

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: the ring (box-shadow) never transitions, on the base or this head, and under Reduce Motion nothing fades. Suggested: "with a mouse the border fades over the theme’s fast duration (at once under Reduce Motion) and the ring changes at once."

element.style.getPropertyValue(property),
);
}
element.style.setProperty(property, value);

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 writes the recognizers' derived touch-action over the element's own stylesheet value, so an author's none or pan-y comes back as manipulation or pan-y pinch-zoom. spec:AST-094 FR18 (DEC-8): the derived value is intersected with the author's and is never wider; when the author's value leaves out pinch-zoom, the author's value wins.

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

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.

Every unclaimed touch inside the scroller is handed to the browser here, however far the finger moved, so a sub-pixel or programmatic scroll under a resting finger cancels its tap. spec:AST-094 FR20 (DEC-8) counts a native scroll only once that finger has moved 10 px from touch-down; GestureHost.test.ts:823 asserts the opposite.

// True while an Effect drives the recognizer, so a transition it causes
// reaches no caller callback (react-component-runtime INV2, INV3).
const isSyncingRef = useRef(false);
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: name what this effect synchronizes (the latest onGesture the recognizer's listener reads), as the neighbouring effects do; the same applies to the options ref at :133.

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

The one-frame selection mark under a finger works as described, but approval needs an engineering owner to provide or identify the governing spec for it: spec:AST-094 FR4 requires every touch duration to be a registered motion role, and no current record gives SelectableCard, CheckboxIndicator or RadioIndicator a touch presentation, so an owner must settle whether the 0 ms mark read from the measured selection table stands as it is or first needs a registered role and component records. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as one.

  • Whatever the decision, land #8226's stack first so this diff is the card change alone, and fix GestureHost so it intersects its touch-action with the author's (FR18) and lets a scroll take a touch only after 10 px of that finger's travel (FR20).
  • Add before-and-after mouse evidence for the Reduce Motion change (spec:AST-017 FR59), and correct the isSelected docs, since with a mouse only the border fades.

[Automated review]

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: the ring is a box-shadow that cannot interpolate with the card's resting shadow (as SelectableCard.tsx:90–91 says), and under Reduce Motion nothing fades. Suggested wording: "…with a mouse the border fades over the theme’s fast duration (at once under Reduce Motion) and the ring changes at once."

element.style.getPropertyValue(property),
);
}
element.style.setProperty(property, value);

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 inline write replaces the element's own touch-action instead of intersecting with it, so a tap recognizer turns an author's stylesheet touch-action: none into manipulation. spec:AST-094 FR18: "The derived value is intersected with the element's own author touch-action and is never wider than it (DEC-8)". Read the author's value before the first write and intersect it, keeping the author's value when it leaves out pinch-zoom.

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

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.

Any scroll inside the container hands every unclaimed touch to the browser here, even a finger that has not moved, so a sub-pixel or programmatic scroll cancels a resting finger's tap. spec:AST-094 FR20 counts a native scroll as the browser taking the touch "only once that finger has moved the scroll take distance (FR2)", which is 10 px. Check each touch's travel from touch-down before adding it to taken, and turn GestureHost.test.ts:823 around.

// True while an Effect drives the recognizer, so a transition it causes
// reaches no caller callback (react-component-runtime INV2, INV3).
const isSyncingRef = useRef(false);
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.

This layout effect, and the one at :133, names no external system it synchronizes, unlike the neighbouring effects' "Syncs …" comments. architecture:react-component-runtime INV3: "A new or changed Effect MUST name the external system it synchronizes." Name the listener each ref feeds.

@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 providing or identifying the governing spec for the one-frame (0 ms) selection mark under a coarse pointer: spec:AST-094 FR4 requires every duration a touch presentation plays to be a motion role, and no current component record gives SelectableCard, CheckboxIndicator or RadioIndicator a touch presentation, so an owner either accepts the mark as it stands or asks for selection.mark.changeMs to become a registered role, with the component records stating the presentation, first. 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. Whatever the decision, these changes are needed:

  • Land #8226 and the rest of its stack first and rebase, so this pull request carries the 612-line card change instead of 28,981 lines.
  • Correct SelectableCard.doc.mjs: with a mouse only the border fades (and not under Reduce Motion), while the ring changes at once.
  • Add before-and-after mouse evidence for the Reduce Motion change, as spec:AST-017 FR59 asks.

[Automated review]

name: 'isSelected',
type: 'boolean',
description:
'Controlled selection state. Under a finger (a coarse primary pointer) the selection ring and border change in one frame; with a mouse they fade over the theme’s fast duration.',

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 a mouse only the border fades: the inset ring cannot interpolate with the card's resting shadow, so it changes at once (the comment at SelectableCard.tsx:90–91 says the same), and under Reduce Motion the border changes at once too on this head. Suggested wording: "with a mouse the border fades over the theme’s fast duration (at once under Reduce Motion) and the ring changes at once."

// once (design:reduced-motion DR3); a finger's change already is.
// The resolver's and the policy's keys, spelled: an imported at-rule
// key lands in a different StyleX priority layer than a literal one.
'@media (pointer: coarse)': MARK_CHANGE,

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 0 ms arm is the part that waits on an engineering-owner decision: spec:AST-094 FR4 asks every duration a touch presentation plays to be a motion role read through its role, and selectionMotionConsts.markChangeMs mirrors a table value that is not a registered role. The literal @media (pointer: coarse) key itself conforms to FR13 (DEC-6).

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:34
vjeux added 2 commits November 2, 2026 22:03
iOS has no checkbox or radio control; a choice is marked on its row, and on
iOS 26.2 the mark changes in the frame the selection lands with no animation
of its own: the checkmark accessory of a single-choice list in Settings, the
completion circle in Reminders, a collection-view list's multiselect
accessory, with and without Reduce Motion. CheckboxIndicator and
RadioIndicator faded their box or circle between its two colors over the
theme's fast duration under every pointer, so a tap showed half-filled
frames first. Under a coarse primary pointer the fade now takes the UIKit
table's value, one frame (motion/uikit/selection.ts, mirrored for the
stylesheet in Indicator/selectionMotion.stylex.ts and held to it by
selectionMotion.test.ts). A mouse keeps the theme's duration and easing
tokens; under Reduce Motion a mouse's change is immediate, in the instant
key of the reduced-motion policy (design:reduced-motion DR3), and a
finger's already is.

The browser spec proves both in Chromium and WebKit: every frame of a tap
shows the mark wholly in its old or its new colour, a busy main thread
cannot stretch the change, and a mouse keeps the fade.
…ange in one frame

A selected card's mark is an inset ring, drawn in the card's shadow slot,
and the accent border. The ring cannot interpolate with the card's resting
shadow, so it appeared or vanished in one frame while the border faded over
the theme's fast duration: the mark changed in two steps. UIKit changes a
selection mark in the frame the selection lands (motion/uikit/selection.ts,
selection.mark.changeMs). Under a finger (a coarse primary pointer, the
touch resolver's TOUCH_QUERY, on every touch device) the card's box-shadow
and border-color transition now takes that clock, through the same
build-time mirror the checkbox and radio marks read
(Indicator/selectionMotion.stylex.ts), so ring and border change together.
A mouse keeps the theme's fade, and under Reduce Motion changes at once
(design:reduced-motion DR3).

@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 the one-frame selection mark. Under a coarse pointer, SelectableCard, CheckboxIndicator and RadioIndicator now play 0 ms from a mirror of the measured selection.mark.changeMs, while spec:AST-094 FR4 asks for every touch duration to be a registered motion role and leaves each component's touch presentation to that component's own record, which none of these records states yet. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (you may do so yourself as an engineering owner), or asks for selection.mark.changeMs to become a registered motion role first. No blocking defects were found, and the Reduce Motion fix for mouse users matches design:reduced-motion DR3; if the mark is accepted, please add real-device iOS Safari and Android Chrome evidence for the one-frame claim, as spec:AST-094 asks.

[Automated review]

/** Written by the press controller for the press's release. */
const PRESSED_FADING = '[data-astryx-press="fading"]';

/** How long the selection mark takes to change under a finger: one frame. */

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 (source comments): this restates MARK_CHANGE and copies the table's value, so it goes stale if selection.mark.changeMs changes. Suggest deleting it; the same line sits in CheckboxIndicator.tsx:27 and RadioIndicator.tsx:27.

Comment on lines +91 to +92
// split it from the border. Under Reduce Motion a mouse's change is
// immediate too.

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 (source comments): "Under Reduce Motion a mouse's change is immediate too." narrates the 0s arm below. Suggest dropping the sentence and keeping the reason and the StyleX key quirk; the same sentence is in CheckboxIndicator.tsx:44-45 and RadioIndicator.tsx:48-49.

displayName: 'Checkbox Indicator',
description:
'The checkbox visual: a square box with a checkmark or an indeterminate bar. Decorative (aria-hidden); the owning control keeps the input, role, accessible name, focus, and keyboard behavior.',
'The checkbox visual: a square box with a checkmark or an indeterminate bar. Decorative (aria-hidden); the owning control keeps the input, role, accessible name, focus, and keyboard behavior. Under a finger (a coarse primary pointer) the box changes in one frame, as UIKit selection marks do; with a mouse its colors fade over the theme duration.',

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: under Reduce Motion a mouse's change is immediate, so "with a mouse its colors fade over the theme duration" is not true there. Suggest ending with "; under Reduce Motion it changes at once", as SelectableCard.doc.mjs does. The RadioIndicator description at line 100 has the same sentence.

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: under the owner's ruling, main's indicators already play the theme duration on every pointer and change at once under Reduce Motion; SelectableCard's Reduce Motion arm lands in #8326.

@vjeux vjeux closed this Nov 3, 2026

This branch was successfully deployed

1 active deployment
Preview — 3161c95a Deployed Nov 3, 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.

2 participants