Skip to content

feat(core): a checkbox's or radio option's hit area grows toward 44 px - #8241

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c06-touch-targets
Nov 4, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c06-touch-targets

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Implements spec:AST-094 FR42 for CheckboxInput and RadioList (owner decision, 2026-11-04).

User need

On a phone, a standalone CheckboxInput's target is its 24 px row, and RadioList rows are 24 px with 8 px dead gaps. iOS controls are 44 × 44 pt; fingers miss.

Evidence

With Chromium and WebKit touch (TouchTargets): a tap just above a CheckboxInput row toggles it, also when its click lands on the row's edge, a tap in a RadioList row's half of the gap selects it, and no box changes.

Problem and solution fit

Under a coarse pointer, a labelled, enabled, editable CheckboxInput and each enabled RadioList option get an invisible hit band. It grows toward 44 × 44 px, never shrinks the target below the row, and stops at half the gap to a closer neighbour. Nothing changes size; a mouse sees no change.

  • As in InfoTip, a memoized callback ref starts watchHitArea on the row, and its cleanup stops it.
  • utils/touchHitArea.stylex.ts is the band: the row's coarse-only ::after, grown by the insets, with the row's box clipped out, so the label, its links and the input keep their taps.
  • A press that begins on a checkbox's band toggles it, judged from its pointerdown point, so a click moved onto the row's edge counts and a click from code does not. Radio rows already hand taps to the radio.
  • A label-hidden checkbox keeps its composer's row.

Current specification

spec:AST-094 FR42, component:CheckboxInput FR14 and component:RadioListItem FR13 carry the owner's sentence verbatim: "Under a coarse pointer, the touch target grows toward 44 × 44 px, up to halfway to a neighboring target, and is never smaller than the control's own box (owner decision, 2026-11-04; spec:AST-094 FR42)."

Public delta and caller burden

No API, token or default change. Under a finger a RadioListItem row's ::after is the band, so decorate a row with a child. Both import utils/hitArea, already loaded by Switch and InfoTip. Under a coarse pointer a CheckboxInput row is position: relative, so an absolutely positioned child resolves against it.

Design and alternatives

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.

Writing rubric results

R1 narration: none; the dated parenthetical is the owner's verbatim attribution.
R2 research: none.
R3 evidence: the new verification rows name tests.
R4 rejected alternatives: none in a record.
R5 change references: none.
R6 open questions: none changed.

Testing

Rebuilt on main 35985f2 as one commit, ff1b675 (11 files): no #8085 snapshot, no token, no List change. It differs from 178420b only in the three record lines that carry the owner's sentence; every run below was made at 178420b, whose code and tests are byte-identical. It merges cleanly into main 273031c, which has not touched these files.

  • Red first: with main's CheckboxInput.tsx and RadioListItem.tsx, 5 of the 9 cases in Indicator/touchTarget.test.tsx fail: the band, its press, and "grows a 24 px checkbox row (radio option) to 44 px, and leaves a 60 px one at its own box", which drive the real watchHitArea. The 4 layout guards pass. At this head 9/9.
  • The adjusted tap: with the previous head's click-point check, the unit case for a press that begins on the band and clicks on the row's edge fails, and so does the browser case that delivers that press and click in the engine. Both pass here, and a real finger-sized tap (Chromium touch radius 11 px) toggles too.
  • TouchTargets on this head's Storybook build: Chromium 7/7 and WebKit 7/7 (container --cpus=4 -e CI=1 -e LP_NUM_THREADS=4), no retries. It covers a 60 px row (insets 0; the point 2 px above is not its own) and the mouse control for CheckboxInput and RadioList.
  • Chromium a11y contracts: Checkbox, CheckboxList, CheckboxListErrorStatus, ListGroupDisabled and RadioGroup, 71/71, on a build carrying the astryx-build-sha.txt stamp CI writes.
  • Units: full test:ui, 809 files, 20,125 tests.
  • Gates: core tsc 0; ESLint 0 errors on the changed files; Prettier clean; INV29 0; check:knowledge, check:repo, check:use-client, check:token-docs, check:motion-roles, check:changesets, check:i18n-runtime, lab:readiness:check, verify-exports, sync-exports --check and CLI api-types --check; review:pre 0 findings; public text clean.

@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 5:54pm 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, 6:25 PM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · ff1b675

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
MultiSelector 🔴 75.9 kB (+2.7 kB) · 15.5 kB CSS (+159 B) — — · — · —
TableHeaderCell 🔴 78.8 kB (+2.6 kB) · 16.3 kB CSS (+161 B) — — · — · —
Table 🔴 87.3 kB (+2.6 kB) · 16.8 kB CSS (+165 B) — — · — · —
18 more components
Component Size LoC Visual · A11y · RTL
TableCell 🔴 79.2 kB (+2.6 kB) · 16.5 kB CSS (+166 B) — — · — · —
ContextMenu 🔴 78.0 kB (+2.6 kB) · 16.1 kB CSS (+166 B) — — · — · —
Markdown 🔴 148.7 kB (+2.6 kB) · 19.0 kB CSS (+156 B) — — · — · —
CheckboxInput · Storybook 🔴 51.3 kB (+2.5 kB) · 8.8 kB CSS (+161 B) 424 (+52) — · ✓ · —
CheckboxListItem 🔴 56.6 kB (+2.5 kB) · 10.6 kB CSS (+159 B) — — · — · —
richtext/RichTextEditor 🔴 281.0 kB (+2.7 kB) · 2.6 kB CSS (0) — — · — · —
ListItem 🔴 55.9 kB (+2.5 kB) · 10.6 kB CSS (+159 B) — — · — · —
richtext/RichTextView 🔴 261.4 kB (+2.6 kB) · 2.3 kB CSS (0) — — · — · —
lab/CommentThread 🔴 262.5 kB (+2.6 kB) · 2.4 kB CSS (0) — — · — · —
lab/Comments 🔴 267.8 kB (+2.5 kB) · 2.5 kB CSS (0) — — · — · —
RadioListItem 🔴 54.3 kB (+2.3 kB) · 9.5 kB CSS (+144 B) — — · — · —
PowerSearch 244.8 kB (0) · 22.4 kB CSS (+165 B) — — · — · —
MoreMenu 74.3 kB (+1 B) · 16.2 kB CSS (+156 B) — — · — · —
DropdownMenu 74.1 kB (0) · 16.2 kB CSS (+156 B) — — · — · —
CheckboxIndicator 2.7 kB (0) · 813 B CSS (0) — — · — · —
CheckIndicator 8.0 kB (0) · 681 B CSS (0) — — · — · —
RadioIndicator 2.5 kB (0) · 725 B CSS (0) — — · — · —
RadioList 43.4 kB (0) · 6.9 kB CSS (0) — — · ✓ · —

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

API changes: none in this pull request

Committed API report behind this build: cli 8 rows, core 2 rows; run pnpm api:report.

Wide change (shared styles: packages/core/src/utils/touchHitArea.stylex.ts): add ci:run-visual or ci:run-rtl to run them.


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.

This needs an engineering owner's decision before it can be approved: the 44 px coarse-pointer targets for CheckboxInput and RadioList, and the List touch rows, press layer and holdSelection export carried in from the stacked pull requests, rest only on the draft spec:AST-094, which no owner has approved yet. An engineering owner records that decision by commenting /approve-spec c99f34a8cd361498a635ae8dc918e42f458296c4 on this pull request (the author may do so as an engineering owner), or by saying which of these behaviours should wait for AST-094 to be approved on main. Whatever that decision, please also:

  • keep the 44 px row off the CheckboxInputs that other components embed, since on a touch screen MultiSelector options grow from 36 to 56 px, compact TableSelection rows from 33 to 53 px and Markdown task items from 32 to 52 px, or document and test that growth;
  • rebase onto main once #8045, #8052 and #8085 land, so this pull request's diff is only the touch-target commit (it is 15,494 authored lines today, and pr-a11y fails because the branch predates a spec file that main's workflow runs);
  • fix the references to files and records that do not exist: utils/touchPresentation.ts, sources.test.ts, packages/core/src/gestures, and AST-094's citation of an AST-090 record.

[Automated review]

{...(isDisabled ? undefined : pressable)}
{...stylex.props(
// Under a finger the row is at least as tall as an iOS control.
touchTargetStyles.row,

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.

On a touch screen this floor applies to every CheckboxInput, including the ones MultiSelector, TableSelection and List task markers embed: their rows grow from 36 to 56 px, 33 to 53 px and 32 to 52 px. Could it apply only where this row is the target (not for isLabelHidden, inert or read-only marker uses), with those composers in the touch tests?

});

/**
* The touch presentation's query (utils/touchPresentation.ts TOUCH_QUERY),

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.

utils/touchPresentation.ts and TOUCH_QUERY don't exist at this head (the same reference is in List/listRow.stylex.ts:36). Point at the real source of the query or drop the reference.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

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.

There is no AST-090 record on main or in this pull request, so this amendment (and the one at line 235) has nothing to amend yet. Cite it once it exists, or drop the reference.

@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/Indicator/touchTarget.stylex.ts:36 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.
  • Reduced motion. core/src/utils/selectionHold.ts:268 handles reduced motion locally. Per design:reduced-motion (DR1–DR4), the animator is the one place that resolves it (touch cross-fades in 200 ms or less with no travel, a fine pointer changes at once), and core has no opt-out, so reducedMotion: 'keep' should be removed.

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 44 px floor keys on the touch resolver's TOUCH_QUERY alone, spelled the way utils/touchPresentation.ts asks StyleX keys to be. It moves to the resolver's importable constant when that lands; this PR's base, main, doesn't carry the resolver yet.
  • Reduced motion: utils/selectionHold.ts comes from the stacked selection-hold PR, feat(List): a row tapped to push stays selected until the pop deselects it #8082, and its reducedMotion: 'keep' is removed there, in its reduced-motion restack.

@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 settle the governing spec: the 44 px coarse-pointer targets for CheckboxInput and RadioList, and the List touch rows, press layer and holdSelection export carried in from the stacked pull requests, rest only on the draft spec:AST-094, and whether the targets ship on their own coarse-pointer query or wait for the shared touch resolver is still open. An engineering owner records that decision by commenting /approve-spec e877f6946b2b99a33f002fada63d412e4046bb28 on this pull request (the author may do so as an engineering owner), or by naming which of these must wait for AST-094 to be approved on main. Whatever that decision, please also:

  • remove reducedMotion: 'keep' from utils/selectionHold.ts (lines 268 and 283), because under reduced motion the deselect still runs 350 ms with a finger and with a mouse, where design:reduced-motion allows at most 200 ms on touch and an immediate change otherwise;
  • rebase onto main once #8045, #8052 and #8085 land, so the diff is only the touch-target commits (15,545 authored lines today);
  • conform AST-094 FR8 to design:reduced-motion or name that amendment, and fix the references to utils/touchPresentation.ts, sources.test.ts, packages/core/src/gestures and spec:AST-090, which do not exist.

[Automated review]

? {}
: {velocity: {opacity: -timing.velocity}}),
},
{at, reducedMotion: 'keep'},

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 reducedMotion: 'keep' (here and at line 283) the deselect plays its full 350 ms under prefers-reduced-motion, with a finger and with a mouse alike. design:reduced-motion asks for an opacity change of 200 ms or less on a coarse pointer and an immediate change otherwise, with no opt-out in core (DR2–DR4). Could the hold drop 'keep' and resolve the deselect that way, with a browser test under reduced motion?

on: a transition that travels becomes a cross-fade carrying the same change
of state. An entrance lands in place and fades in, an exit fades out in
place, and any other move fades through. A navigation push or pop
cross-fades over 0.5 s on the ease-in-out curve; a sheet, a full-screen

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.

A 0.5 s navigation cross-fade is longer than the 200 ms design:reduced-motion DR2 allows on a touch screen, and the pending-review note under FR8 names only the draft design:motion DR5. Could FR8 follow DR2, or name the amendment to design:reduced-motion so an owner sees it?

});

/**
* The touch presentation's query (utils/touchPresentation.ts TOUCH_QUERY),

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.

utils/touchPresentation.ts and TOUCH_QUERY don't exist on main or at this head. Point at the real source of the query, or drop the reference until the resolver lands.

@vjeux
vjeux force-pushed the uikit/c06-touch-targets branch from e877f69 to 9282c28 Compare October 25, 2026 01:35
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in 9282c28, restacked on #8085. The 44 px floor keeps the resolver's literal (pointer: coarse) key, because the StyleX build gives an imported at-rule key a different priority. #8331's test pins that literal to the resolver once it is in this base.

@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 settle the governing spec: the 44 px coarse-pointer targets for CheckboxInput and RadioList rest only on the draft spec:AST-094, which no owner has approved for this head. An engineering owner records that decision by commenting /approve-spec 9282c287898d26192284c5f6a251282ce3ae1624 on this pull request (the author may do so as an engineering owner), or by saying the targets should wait until a 44 px touch target is approved on main. Whatever that decision, please also:

  • remove the theme platform adaptation condition from this stack, or land an approved amendment to spec:AST-012 FR2 first, because FR2 closes the when vocabulary to width, height, pointer, contrast and motion;
  • rebase onto main once the stacked pull requests land, so the diff is only the touch-target commits (33,535 authored lines today, 429 of them this pull request's);
  • align AST-094 FR8, FR4, DEC-1 and FR13 with design:reduced-motion DR2 and with the code that ships, and fix the references to spec:AST-090 and sources.test.ts, which do not exist.

[Automated review]

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

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.

spec:AST-012 FR2 closes the when vocabulary: "A when object may contain width, height, pointer, contrast, and motion … Future environment fields require explicit additions to this closed vocabulary." No record admits platform, and the theme authoring guide and template already teach it. Could this axis come out of the stack, or wait for an approved FR2 amendment?

measures (0.55 for a scroll view and a sheet), and a release MUST pick its
rest point by projecting its velocity the way its UIKit counterpart does:
with a deceleration rate, or with a measured projection time.
- **FR4 — Physics are not themable in touch presentations.** A touch

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.

FR4 (and DEC-1) say touch physics are not themable, but this stack ships theme motion roles (defineTheme({motion: {springs, durations, curves}})) and holdSelection reads its deselect clock and curve from them. Could FR4 and DEC-1 describe what the roles allow, or the roles wait, so an owner approving this record approves what ships?

@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 44 px coarse-pointer target that CheckboxInput and RadioList get here: no record states that size, and the draft spec:AST-094 this branch carries names none and has no owner approval for this head. An engineering owner records that decision by commenting /approve-spec 10c2d49a4fa0c19a3e7fbcbae4505ee855728aa3 on this pull request (the author may do so as an engineering owner), or by saying the targets should wait until a record that states a 44 px coarse-pointer target is approved on main. Whatever that decision, please also:

  • keep the new row press layer off fine pointers, since its unconditional ::before hides a caller's ::before decoration on interactive Item, ListItem and TreeList rows with a mouse too, which spec:AST-017 FR59 does not allow for touch work;
  • drop the platform axis from the theme template (packages/cli/assets/theme.template.ts), because defineTheme now rejects it and spec:AST-012 FR2 does not admit it;
  • make the failing pr-interaction case pass: the theme-spring proof in animator.interaction.browser.spec.ts fails because nothing registers the springs table, so a theme's springs.* roles reach no animation unless something registers that table;
  • rebase onto main once the stacked pull requests land, or retarget onto #8085, so the diff is only the touch-target commits (35,149 authored lines today, 433 of them this pull request's);
  • align AST-094 FR8, FR4, DEC-1 and FR13 with design:reduced-motion DR2 and with the code that ships, and fix the references to spec:AST-090 and "AST-094 OQ4", which do not exist.

[Automated review]

Comment on lines +399 to +412
isolation: 'isolate',
'::before': {
content: '""',
position: 'absolute',
inset: 0,
zIndex: -1,
borderRadius: 'inherit',
pointerEvents: 'none',
backgroundColor: colorVars['--color-overlay-pressed'],
// On at once while the press is believed, and while a selection hold
// keeps the row selected (its own animations move it from there);
// nothing transitions it.
opacity: {
default: 0,

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.

isolation and this ::before (with opacity: 0 by default) apply at every pointer, so on an interactive Item, ListItem or TreeList row a caller's ::before from xstyle is hidden at rest with a mouse too. spec:AST-017 FR59 lets touch work change fine-pointer appearance only to fix a defect mouse users hit or for a named cross-device consistency change. Could the layer's content and isolation sit under @media (pointer: coarse)?

Comment thread packages/cli/assets/theme.template.ts Outdated
Comment on lines +361 to +362
* are primary-pointer precision, contrast preference, motion preference, and
* platform (`ios` for the browsers of iPhone and iPad, `other` for the rest).

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 stack removed the platform condition: themeAdaptations.ts accepts only the closed axes and throws … .platform is not supported., and spec:AST-012 FR2 closes the when vocabulary. A theme author who follows this template gets a build error; could the platform clause come out?


export {createAnimator} from '../animator';
export {solveSpring, toSpring} from '../spring';
export {springs} from '../uikit/springs';

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 page never calls registerSpringsTable(), and reading a table no longer registers it, so in animator.interaction.browser.spec.ts:321 the themed box plays the table's spring and pr-interaction fails (111.7 px off its themed curve). No shipped module registers the springs table either. Could the table be registered here, or spring roles resolve without an explicit registration?

Comment thread scripts/sync-exports.js Outdated
@@ -177,6 +190,13 @@ const STATIC_EXPORTS = {
types: './dist/utils/interactionOverlay.stylex.d.ts',
default: './dist/utils/interactionOverlay.stylex.js',
},
// Proposed; pending owner review (spec:AST-094 OQ4): the Reduce Motion

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.

AST-094 has only OQ1 and OQ2, so this new stable ./motion/reducedMotion.stylex export points at an open question that does not exist. Could it cite the record that governs it, or ship under an experimental subpath?

@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 44 px coarse-pointer target that CheckboxInput and RadioList get here: no current record states that size, and the pull request itself marks it as proposed and pending owner review. An engineering owner records that decision by commenting /approve-spec 1f32db965954a61a6e29b435510203f160f0fba9 on this pull request (the author may do so as an engineering owner), or by saying the targets should wait until a record that states a coarse-pointer target size is approved on main. Whatever that decision, the diff also needs narrowing:

  • rebase onto main once the stacked pull requests land, or retarget onto #8085, so the diff carries only the touch-target commits (34,549 authored lines today, 433 of them this pull request's).

[Automated review]

@cixzhang

cixzhang commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Cindy's decision on target size: 44 px, for touch only. Requirements:

  1. Touch only. Apply the 44 px target only under a coarse primary pointer (spec:AST-094 FR19). Mouse and desktop stay exactly as today.
  2. Scope: only standalone CheckboxInput, RadioList and CheckboxList. Rows inside dense composites keep their density (Table selection, MultiSelector options, and checkboxes embedded in other components), and other controls such as NumberInput's spin buttons are out of scope.
  3. Markdown task items must not grow. In the touch captures their rows move to a 44 px pitch (about 30 px over three items), which spaces them out too much. Keep today's pitch on touch, matching this PR's own claim that label-hidden checkboxes keep their composer's rows. Add a test that asserts it.
  4. A theme metric, not a constant. The 44 px value comes from a theme metric, so a dense theme can tune it.
  5. Prefer a bigger hit area over a bigger look. Where possible, expand the hit area without changing the visible size or the layout, and keep neighbouring targets from overlapping. Note where that isn't possible.

vjeux, could you update the PR to match these?

@vjeux
vjeux force-pushed the uikit/c06-touch-targets branch from 1f32db9 to 2a302ea Compare October 25, 2026 19:32
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Fixed at 1fc0c9e:

  • --size-touch-target (its comment, the generated size docs and the theme template) now says how far a hit area grows under a coarse pointer before it stops at half the gap to a neighbour, and where 44 px isn't reached: a middle RadioList option reaches 32 px at the default 8 px gap. The target is 44 px; a larger value raises it and a smaller one never lowers it (feat(List): a row's touch target grows toward 44 px where its neighbors leave room #8085).
  • The changeset adds that case, and that under a finger a RadioListItem row's ::after becomes its hit layer.
  • RadioList's docs say a row's ::after is its hit area under a finger, so a row is decorated with a child.

utils/touchHitArea.ts is #8085's: it now resolves px, rem or em and reads the token again on a pointer or theme change, and the token's comment says 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.

Changes requested: the CheckboxInput and RadioList hit areas follow spec:AST-094 FR42 and the target-size decision on this pull request, keep every layout unchanged, and the token now reads px, rem or em, but this branch still carries the whole #8085 chain (21,418 of its 22,247 authored lines). Please land #8085 and the pull requests beneath it on their own, then rebase so only the 12 touch-target files remain.

  • The size report on this pull request flagged CheckboxInput (+2.8 KB) and RadioListItem (+3.2 KB) past the touch-only JavaScript budget; please justify the hit-area cost in the description, as architecture:component-style-authoring INV6 asks.

[Automated review]

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Reading --size-touch-target moved here from #8085, since only this pull request admits the token: the hit area takes max(44 px, token) in px, rem or em and reads it again on a pointer or theme change, through one shared pointer listener; #8085 keeps a plain 44 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.

Changes requested: the touch-target work itself, the CheckboxInput and RadioList hit areas and the --size-touch-target token, matches spec:AST-094 FR42 and the target-size decision, but this branch still merges all of #8085, so 21,368 of its 22,369 authored lines belong to that pull request. Please land #8085 first, then rebase so this pull request carries only its own change; nothing else in it blocks approval.

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

Changes requested: the CheckboxInput and RadioList hit areas and the --size-touch-target token match spec:AST-094 FR42 and the target-size decision on this pull request, and they leave every layout and mouse interaction unchanged. This branch, though, still merges a snapshot of #8085 that makes up 30,787 of its 31,801 changed lines and that #8085 has since moved past. Please land #8085 first, then rebase so this pull request carries only its own 16 files; nothing else found blocks approval.

[Automated review]

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:34
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
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
utils/pressFeedback.ts is taken byte for byte from git's clean merge of
those two heads; nothing in this pull request changes it.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
@vjeux
vjeux force-pushed the uikit/c06-touch-targets branch from 49c8e40 to 378f996 Compare November 3, 2026 22:56

@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. The CheckboxInput and RadioList hit areas and the --size-touch-target token follow spec:AST-094 FR42 and DEC-20 and leave every layout unchanged, but five things need fixing first.

  • This branch still merges a snapshot of #8085 that makes up 30,787 of its 31,801 changed lines and that #8085 has since replaced with a rebuild on main, and the branch now conflicts with main, so please land #8085 and then rebase so only this pull request's 16 files remain.
  • On a phone-sized Chromium page, a touch tap 9 px above a labelled CheckboxInput box toggles it on main but does nothing here, because the browser's touch adjustment moves that click onto the row's edge and handleHitAreaClick (CheckboxInput.tsx:365-378) treats the edge as inside, so please decide the forwarded click from where the press began (the pointerdown point) and add a browser case for it.
  • useTouchHitArea adds its own module-level (pointer: coarse) listener (utils/touchHitArea.ts:87-110) beside the one watchHitArea already keeps in utils/hitArea.ts, so please let watchHitArea re-read the size in its own pointer-change handler (for example through a minSize getter) and delete listenForPointer.
  • The merged snapshot sets isolation: 'isolate' on every List under every pointer (List.tsx:154), which changes mouse stacking against spec:AST-017 FR59, while #8085's current head gates it to (pointer: coarse).
  • The title says a radio option reaches a 44 pt target, but at the default 8 px gap an option reaches at most 38 px and a middle one 32 px, as the changeset says, so please retitle it to say the hit area grows toward 44 px.

Two new comments could also be shortened (see the inline notes); those are suggestions, not blockers.

[Automated review]

Comment thread packages/core/src/utils/touchHitArea.ts Outdated
Comment on lines +87 to +110
const rewatches = new Set<() => void>();
let pointerQuery: MediaQueryList | null = null;

function onPointerChange(): void {
for (const rewatch of [...rewatches]) {
rewatch();
}
}

/** Runs `rewatch` on each change of the primary pointer; returns the stop. */
function listenForPointer(view: Window, rewatch: () => void): () => void {
rewatches.add(rewatch);
if (pointerQuery == null) {
pointerQuery = view.matchMedia?.(TOUCH_QUERY) ?? null;
pointerQuery?.addEventListener('change', onPointerChange);
}
return () => {
rewatches.delete(rewatch);
if (rewatches.size === 0) {
pointerQuery?.removeEventListener('change', onPointerChange);
pointerQuery = null;
}
};
}

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 is a second module-level change listener on (pointer: coarse): watchHitArea in utils/hitArea.ts already keeps one for every watch (onPointerChange), and with both, one labelled CheckboxInput registers two. Could watchHitArea re-read the size in its own pointer-change handler (for example minSize as a getter, the way WatchHitAreaOptions is already being extended in #8085), with useTouchHitArea passing () => touchTargetSize(element)? Then rewatches, pointerQuery and listenForPointer can go, and the base module's "listens to no media query of its own" test can come back.

Comment thread packages/core/src/List/List.tsx Outdated
// band and a swipeable row (utils/touchHitArea.stylex.ts, ListItem) order
// above the rows after them and stay below a page-level sticky header at
// the same z-index (`spec:AST-027` DEC-1, DEC-2).
isolation: 'isolate',

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 isolates every List under every pointer, so under a mouse a row's raised content now stacks below positioned content after the list, although the touch band is the only reason for it (spec:AST-017 FR59) and the changeset says a mouse is unchanged. #8085's current head gates it: isolation: {default: null, '@media (pointer: coarse)': 'isolate'}. A rebase onto #8085 picks that up.

Comment on lines +214 to +223
// How far a control's hit area grows under a coarse primary pointer
// (`spec:AST-094` FR42): a labelled checkbox and a radio option reach it
// through an invisible hit area that stops at half the gap to each
// neighbour, so a target between close neighbours stays smaller (a middle
// RadioList option reaches 32 px at the default 8 px gap). Rows and
// controls keep their drawn size. 44 px, the platform's control size:
// Apple's Human Interface Guidelines give iOS controls a hit target of at
// least 44 × 44 pt. A larger value, in px, rem or em, raises the reach; a
// smaller one never lowers it. It is read again when the pointer or the
// theme changes.

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 second sentence runs past 40 words and repeats the changeset. Suggested: "How far a hit area grows under a coarse pointer. It stops at half the gap to a neighbour. A smaller value never lowers it below 44 px."

Comment on lines +361 to +364
// The hit layer is the container's own ::after, so a tap on it lands on
// the container outside its box, where nothing else answers: it toggles
// the checkbox, as a tap on the box does. A click inside the box (the gap
// between box and label) stays as it was.

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: one 36-word sentence with several topics. Suggested: "The hit layer is the container's ::after. A tap on it lands on the container, outside its box. Forward that tap to the input."

@vjeux vjeux changed the title feat(core): a checkbox and a radio option reach a 44 pt target under a finger without growing feat(core): under a finger, a checkbox and a radio option hit area grows toward 44 px without growing the control Nov 3, 2026
@vjeux vjeux changed the title feat(core): under a finger, a checkbox and a radio option hit area grows toward 44 px without growing the control feat(core): under a finger, the hit area of a checkbox or a radio option grows toward 44 px without growing the control Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
@vjeux vjeux changed the title feat(core): under a finger, the hit area of a checkbox or a radio option grows toward 44 px without growing the control feat(core): a checkbox or radio option's touch target is at least 44 px Nov 4, 2026
@vjeux
vjeux force-pushed the uikit/c06-touch-targets branch from 378f996 to d816209 Compare November 4, 2026 16:36
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec d816209

@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. The CheckboxInput and RadioList hit bands follow spec:AST-094 FR42 and the approved record text, keep every control's size, and add nothing for a mouse, but four things need fixing first.

  • On a phone-sized Chromium page, a touch tap 9 px above a labelled CheckboxInput toggles it on main but does nothing here, because the browser's touch adjustment moves the click onto the row's top edge and handleRowClick (CheckboxInput.tsx:436-449) counts the edge as inside, so please decide from the pointerdown point (or treat the edge as outside) and add a browser case for an adjusted tap.
  • CheckboxInput.tsx:426-432 and RadioListItem.tsx:319-325 start watchHitArea from a layout effect, but the watch is work caused by the row attaching, so please start it from a memoized callback ref merged into each row's existing ref, as InfoTip.tsx:157-162 does, and delete both effects.
  • The title says a radio option's touch target is at least 44 px, but at the default 8 px gap an outer option reaches 38 px and a middle one 32 px, so please retitle it to say the hit area grows toward 44 px.
  • The new FR14 and FR13 verification rows in the CheckboxInput and RadioListItem records fill only two of five cells, while the verification-map contract asks each row for its representative states and the failure that must make it fail, so please fill those cells and the requirement rows' basis.

One comment could also be shortened (see the inline note); that is a suggestion, not a blocker.

[Automated review]

Comment on lines +441 to +445
const isOnBand =
event.clientX < box.left ||
event.clientX > box.right ||
event.clientY < box.top ||
event.clientY > box.bottom;

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.

On a phone-sized Chromium page, a touch tap 9 px above the box lands on the band, and the browser's touch adjustment then moves the click onto the row's top edge (clientY equals box.top). These strict comparisons count that edge as inside, so the click is dropped and nothing toggles, while main toggles the same tap. Deciding from the pointerdown point (which the browser does not adjust), or counting the edge as outside, keeps it; a browser case for an adjusted tap would pin it.

Comment on lines +426 to +432
useIsomorphicLayoutEffect(() => {
const row = rowRef.current;
if (row == null || !hasHitArea) {
return;
}
return watchHitArea(row, insets => writeHitInsets(row, insets));
}, [hasHitArea]);

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 watch is work caused by the row attaching. architecture:react-component-runtime INV6 gives that to a callback ref, and the row already has one. Could it be a useCallback ref keyed on hasHitArea that returns watchHitArea(...), merged with disabledMessageTooltip.interactionRef, as InfoTip.tsx:157-162 does? Then this effect and rowRef can go.

Comment on lines +319 to +325
useIsomorphicLayoutEffect(() => {
const row = rowRef.current;
if (row == null || isDisabled) {
return;
}
return watchHitArea(row, insets => writeHitInsets(row, insets));
}, [isDisabled]);

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 in CheckboxInput: a memoized callback ref keyed on isDisabled, passed to useMergedRefs(ref, …) in place of rowRef, can own this watch, and its cleanup stops it.

const rowRef = useRef<HTMLDivElement>(null);
const mergedRowRef = useMergedRefs(ref, rowRef);
// Syncs the band's insets with the layout around the row, through
// utils/hitArea's observers. A disabled option watches nothing.

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.

Suggestion (non-blocking): "A disabled option watches nothing." restates the isDisabled guard below; the first sentence is enough.

| FR10, AR3 | `CheckboxInput.test.tsx` focus-ring ownership suite | Built-in and prop-dropping replacement indicators; blur | A ring on the wrong element, or none, fails. | `audit:CheckboxInput/a11y` |
| FR11 | `CheckboxInput.test.tsx` pressed-state suite and its suite for an indicator that paints its own press | Enabled press; disabled; touch fade | A missing overlay, or one on a disabled box, fails. | `audit:CheckboxInput/visual` |
| FR12, AR5 | `CheckboxInput.test.tsx` coarse-pointer, RTL, and forced-colors suites | `sm`, `md`; RTL; forced colors | A misplaced hit target or a mark lost in forced colors fails. Evidence gap: no test asserts field `width`. | `audit:CheckboxInput/layout` |
| FR14 | `touchTarget.test.tsx` hit-band, band-tap and minimum cases; `TouchTargets.interaction.browser.spec.ts` checkbox cases |

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 row fills two of the table's five cells. The verification-map contract (architecture:component-test-sufficiency) asks each row for its representative states and the failure that must make it fail, for example: labelled, label-hidden, disabled and read-only; 24 and 60 px rows; a mouse; and a band where none belongs, a row that changes size, or a band under a mouse fails. The FR14 requirement row at line 123 also leaves its basis cell empty.

| FR8 | `RadioList.test.tsx` pressed-state and bare-row cases; `RadioListItem.press.source-build.test.mjs` | Enabled, disabled, touch | Press paint on a disabled option, or a default row background, fails. Evidence gap: no test asserts the hover tint. | `audit:RadioListItem/visual` |
| FR9, FR10, AR3, AR4 | `RadioList.test.tsx` coarse-pointer, RTL, and forced-colors cases | Coarse pointer; RTL; forced colors | Evidence gap: the forced-colors case passes on any matching rule (the checkbox mark supplies one), so it does not prove the dot; the coarse-pointer and RTL cases check only the direction-symmetric centering class, not the 24 by 24 minimum or RTL placement. | `audit:RadioListItem/accessibility` |
| FR11, FR12 | `RadioList.test.tsx` theme-target and rest-forwarding cases | Sizes; selected; disabled | A target off the painted row, or a missing state, fails. Evidence gap: no test asserts `ref`, `className`, `style`, or `xstyle` on the row. | `audit:RadioListItem/theming` |
| FR13 | `touchTarget.test.tsx` hit-band and minimum cases; `TouchTargets.interaction.browser.spec.ts` RadioList case |

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 here: please add the states (enabled and disabled options, both orientations, a mouse), the failure (a band on a disabled option, a row or gap that changes size, a band past half the gap), and the audit section; the FR13 row at line 129 also needs its basis.

Under a coarse pointer, a labelled CheckboxInput and each enabled
RadioList option get an invisible hit band from utils/hitArea, as
InfoTip does. The band grows toward 44 x 44 px, never makes the
target smaller than the row's own box, and stops at half the gap to
a closer neighbour. Nothing changes size, and a mouse sees no change.

Red first: with main's CheckboxInput and RadioListItem, 5 of the 9
cases in Indicator/touchTarget.test.tsx fail; the 4 layout guards pass.
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ff1b675

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

Approved pending CI: CI has not run on this head yet. This pull request comes from a fork, so its build, lint and test jobs stop at "Waiting for a maintainer's ci:run label"; a maintainer adds the ci:run label so CI runs.

The CheckboxInput and RadioList hit bands follow spec:AST-094 FR42 and the record text approved for this head: under a finger, a labelled checkbox and each enabled radio option take taps in a band clamped to half the gap to each neighbour, and a mouse sees no change. The four points from the previous round are fixed, including the tap 9 px above a checkbox, which toggles it again.

  • Non-blocking: on a full-width checkbox row the band also covers the strip above and below the empty space right of the label, while that space itself still ignores taps; worth a look on a device.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — ff1b6751 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) ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) 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