Skip to content

docs(List): a story draws each compact row's touch target - #9894

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c11-touch-targets-story
Nov 5, 2026
Merged

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

Conversation

@vjeux

@vjeux vjeux commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Reader impact

Under a finger, a List row's touch target reaches past the row's box where nothing is near (#8085), but nothing on screen shows how far. Someone checking that behavior on a phone can't tell whether a tap landed inside the target or past it. This story draws each compact row's target and names the row a tap fires.

Source of truth

The rows' own touch hit layer: each interactive row's ::after, grown by the insets the shared hit area measures (utils/touchHitArea.stylex.ts, utils/hitArea.ts). The story reads the layer as the browser lays it out and hard-codes nothing.

Correction

A new Storybook story, List › Compact Touch Targets: two compact lists, Row 1–2 and Row 3–4, 8 px apart. Each row's touch target past its box is a striped band, with its reach in px under the lists. "Last tapped:" names the row a tap fires, or "none". A tap that fires no row reads "none", including one outside the lists and one the shared hit area drops. With a mouse there are no bands, and the story says so.

Evidence

On a phone-sized touch page, in Chromium and WebKit, the bands equal the rows' hit insets: Row 1 5.5 px above, Row 2 4 px below, Row 3 4 px above, Row 4 6 px below. Each band's center hit-tests to its own row. In WebKit a tap in each band fires its row. In Chromium, the browser's touch adjustment can move a small tap in the 8 px gap onto the other list's row. The shared hit area then drops it, so it fires nothing (never the wrong row) and the readout says "none". A finger-sized touch fires its row.

Scope

  • This PR changes documentation only, or names every necessary generated artifact.
  • Runtime, API, visual, and policy changes were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. On a phone, open List › Compact Touch Targets. Tap the striped band just below Row 4: "Last tapped: Row 4". Tap a fingertip lower, below the band: "none".
  2. Tap the band just above Row 1: "Row 1".
  3. Between the lists, tap the upper half of the gap: "Row 2"; the lower half: "Row 3".

What was run at this head (on main 7b71d71): review:pre and its checklist, check:repo, changeset coverage (nothing ships), strict lint (0 errors), Prettier, the root and Storybook builds, Storybook's type check (0 errors), and the accessibility audit scoped to this story (no new violations). test:ui (832 files, 20,820 tests) and test:node (913 files) pass with no unhandled errors. In Chromium and WebKit under a phone profile, the story draws four painted bands (5.5, 4, 4 and 6 px); a tap on Row 2 reads "Row 2", a tap on the band above Row 1 reads "Row 1", and a tap far below the lists reads "none".

@vercel

vercel Bot commented Nov 4, 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 5, 2026 4:13am UTC

Request Review

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

astracat-bot Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

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

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 971e97c

Storybook · Sandbox · built from this PR's head

API changes: none in this pull request

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

Not run on this PR: add ci:run-visual, ci:run-a11y 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.

Changes requested.

Compared with main, this pull request also carries the touch hit layer from #8085, so its diff is 2,470 authored lines. Blocking:

  • The diff is 2,470 authored lines, 2,239 of them the separable feature from #8085, and neither body declares the stack: add one identical Stack: #8085 → #9894 line to both bodies, or rebase onto main after #8085 merges so only the story commit remains.
  • In useClickableContainer.ts, the marking Effect now depends on marksContainer but never removes data-pressable-container when it turns false, so a row that stops being a delegate keeps the marker and a mouse click in its padding no longer reaches a clickable card around it; render the marker on Item's root for delegate rows instead and leave the public hook's Effect as it is on main.

The story and the touch hit layer match the List touch-target requirement, and the unit tests of the changed files pass. A few added comments in Core narrate the code; trimming them is optional.

[Automated review]

el.setAttribute(PRESSABLE_CONTAINER_ATTRIBUTE, 'true');
}
}, [containerRef]);
}, [containerRef, marksContainer]);

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.

When marksContainer turns false on the same element this Effect re-runs and does nothing, so data-pressable-container stays and a clickable container around the row keeps treating it as a control. Rendering the marker on Item's root when isDelegate lets React add and remove it; the public hook's Effect can then stay as it is on main.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #8085 at 5ab35de: Item renders the marker on a delegate row's root, so React removes it when the row stops delegating, and the public hook's effect is back to main's. A new test covers a row that stops delegating. This pull request restacks on that head.

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

#8085: changes requested.

  • Miss: InfoTip keeps its own watch and ::after hit layer (InfoTip/InfoTip.tsx lines 109-123 and 153-161) beside the shared useTouchHitArea and touchHitAreaStyles.layer this pull request adds, and the two already differ, so move InfoTip onto the shared hook and layer within this stack, either by adding #9916 to the stack declaration in every body or by folding its change into #8085.

#9894: approved on its own, pending CI. The story changes no Core code and draws exactly the layer #8085 adds.

Whole stack: not yet. A pull request lands only when it and every pull request below it are approved, so #9894 waits on #8085; CI has not run on this head (a draft from a fork), so a maintainer adds ci:run once it is ready for review.

[Automated review]

* <li ref={hitAreaRef} {...stylex.props(isInteractive && touchHitAreaStyles.layer)} />
* ```
*/
export function useTouchHitArea(enabled: boolean): RefCallback<HTMLElement> {

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.

Miss: InfoTip still runs its own copy of this hook and its layer: a callback ref over watchHitArea (InfoTip/InfoTip.tsx lines 153-161) and an ::after reading the same inset properties (lines 109-123). That copy neither clears its insets on stop nor waits for nearby motion, as this hook does. Moving InfoTip onto useTouchHitArea(true) and touchHitAreaStyles.layer within this stack (adding #9916 to the stack declaration, or folding it in here) leaves one path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#9916 moves InfoTip onto the shared hook and layer within this stack, and every body in the stack now declares Stack: #8085 → #9916, #9894.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: InfoTip's move is folded into #8085 at f33aedc, and this pull request's stack is now #8085 → #9894.

Comment thread packages/core/src/Item/Item.tsx Outdated
Comment on lines +812 to +819
// Under a coarse primary pointer, a List row's whole touch target
// follows its link. That covers a tap in the row's padding, outside the
// anchor, and a tap in the band past its box. The click goes to the
// row's anchor with its modifier keys, so the link component and the
// browser pick the destination. The primary pointer decides, as it
// decides the band, not the pointer that made the click. With a fine
// primary pointer a click in the padding does nothing. A click with no
// pointer behind it does nothing either.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (comments): these lines narrate the branch below. Keeping only the reason reads: "The primary pointer decides, as it decides the band. WebKit reports a finger's click as a mouse click."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in #8085 at f33aedc.

Comment thread packages/core/src/List/ListItem.tsx Outdated
Comment on lines +412 to +415
// Under a finger, an interactive row's hit target grows toward the touch
// target where its neighbors leave room. Nothing moves or repaints.
// useTouchHitArea keeps the row's insets live, and its invisible layer reads
// them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (comments): this restates the next three lines. Suggested: "Under a finger, the row's hit target grows where its neighbors leave room."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in #8085 at f33aedc.

Comment on lines +25 to +53
// Under a coarse primary pointer, a host's hit target grows toward the touch
// target where its neighbors leave room. Nothing moves and nothing repaints.
// utils/hitArea.ts measures how far on each side. It stops at half the gap to
// the nearest neighboring target, and at 0 where they touch.
//
// The layer is the host's `::after`, grown past the host's edges by those
// insets. The insets are 0 until the first measurement, so first paint and
// server rendering never overlap a neighbor. The host is the layer's
// containing block, so a host that is not positioned sets `position:
// relative`. The insets start at the host's border box, but the layer's
// offsets start at its padding box. So a host that draws a border declares
// its width on that side (`--_hit-border-{top,right,bottom,left}`, 0 by
// default). A divided list row declares its divider's width.
//
// A clip path cuts the host's own box out of the layer. So the host's content
// and controls keep their taps, and a tap in the band goes to the host. The
// layer has no z-index of its own. A later sibling that is positioned with no
// z-index paints and hit-tests over it. Inside a List, which isolates its
// rows, the layer takes `z-index: 1`, so a later row cannot cover it. A host
// outside such a container raises nothing, so its band never passes a
// page-level sticky header. The host itself keeps no isolation and no
// z-index, so at rest it is no stacking context. A swipeable row is a
// stacking context, so it raises itself.
//
// Under a finger the host's `::after` is this layer, so decorate the host
// with a child. The insets are measured in the viewport, so the inline edges
// are physical: a left inset grows the left edge in either direction. StyleX
// evaluates property names at build time, so they are spelled out, and
// touchHitArea.test.tsx checks them against HIT_INSET_PROPERTIES.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (comments): a 29-line block before one style. Keeping the constraints only: "The host must be positioned. Its insets start at the border box and the layer's offsets at the padding box, so a host with a border declares its width in --_hit-border-*. Under a finger the host's ::after is this layer."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Left as is: the block states the layer's constraints in one place, beside the style that depends on them.

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.

Fair point — keeping the layer's constraints together beside the style that depends on them is a reasonable reason to leave the block as is. The suggestion was optional, and the next full review will re-evaluate it.

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

#8085: approved, pending CI. Under a finger, interactive List rows now reach toward a 44 px target through an invisible band that stops halfway to a neighbour, as the List touch-target requirement asks, and InfoTip now uses the same hook and layer, so the shared-path blocker from the last round is fixed. No blocking findings.

  • Optional: the size report has per-component lines but none for the package stylesheet itself; one line for astryx.css (base against head, gzip) completes it.

#9894: approved, pending CI. The story changes no package code and draws exactly the layer #8085 adds; its readout names the tapped row, or "none".

Whole stack: passes. A pull request lands only when it and every pull request below it are approved. CI has not run on either head yet: both are drafts from a fork, so once they are marked ready for review a maintainer adds the ci:run label so CI runs.

[Automated review]

The Compact touch targets story draws each row's touch target under a
finger as a striped band past the row's box, read from the row's own
hit layer. A readout names the row a tap fires, or "none". Story only:
no product change.
@vjeux
vjeux force-pushed the uikit/c11-touch-targets-story branch from 42d2a63 to 971e97c Compare November 5, 2026 04:06
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Nov 5, 2026

@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. The new List › Compact Touch Targets story changes no package code and draws exactly the touch band interactive List rows already have: under touch in Chromium the bands measure 5.5, 4, 4 and 6 px, matching each row's hit insets, and the readout names the tapped row or "none". No blocking findings.

  • Optional: Testing step 3 expects "Row 2" and "Row 3" for taps in the gap between the lists, but in Chromium both halves read "none", as your Evidence section explains; naming the engine in that step keeps the test plan reproducible.

CI has not run on this head yet: it is a draft from a fork, so once it is marked ready for review a maintainer adds the ci:run label so CI runs.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 5, 2026 04:26
@vjeux
vjeux requested a review from josephfarina as a code owner November 5, 2026 04:26
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 5, 2026
@cixzhang
cixzhang marked this pull request as draft November 5, 2026 04:30
@cixzhang
cixzhang marked this pull request as ready for review November 5, 2026 04:31
@cixzhang
cixzhang merged commit bfc1dcc into meta:main Nov 5, 2026
147 of 152 checks passed
@vjeux
vjeux deleted the uikit/c11-touch-targets-story branch November 5, 2026 06:38

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants