Skip to content

refactor(InfoTip): the button's touch target uses the shared touch layer - #9916

Closed
vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:uikit/c11-infotip-touch-layer
Closed

vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:uikit/c11-infotip-touch-layer

Conversation

@vjeux

@vjeux vjeux commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8085 → #9916, #9894

Stacked on #8085: review only this pull request's last commit, vjeux/astryx@3d0e824...ced35b3. It moves to main once #8085 merges.

Primary intent

InfoTip's button takes taps under a finger through the same touch hook and layer as List rows, so one path serves both. This is a refactor that also changes InfoTip's touch target a little (below). So neither the maintenance template (no behavior change) nor the bug-fix template (one restored defect) fits.

User or maintainer impact

Maintainers keep one copy of the coarse-pointer touch target instead of two. On a phone, InfoTip's button target still grows toward 44 px where its neighbors leave room, and never into a neighbor's. It now clears its insets when the button stops taking taps, it waits for motion around the button to end before it grows, and its layer exists only under a coarse pointer. A mouse sees no change.

Problem and solution fit

#8085 adds useTouchHitArea and touchHitAreaStyles.layer for List rows. InfoTip kept its own copy: a callback ref over watchHitArea and its own ::after layer. Two copies drift: the shared layer's stop-time clearing, motion wait and border offset never reached InfoTip. This change deletes InfoTip's copy and uses the shared hook and layer. The button is already position: relative with no border, so it needs no border variables.

Authority

component:InfoTip FR1 and AR1, both current: under a coarse pointer the button reaches 44 px through an invisible hit area where its neighbors leave room, and the hit area never overlaps a neighbor's. No record changes.

Change and evidence

  • InfoTip.tsx: useTouchHitArea(true) replaces the callback ref, and touchHitAreaStyles.layer replaces the ::after style.
  • utils/touchHitArea.ts and utils/touchHitArea.stylex.ts: their headers name InfoTip as a user.
  • Red-first: the InfoTip test "clears the button's insets when its watch stops" fails on feat(List): a row's touch target grows toward 44 px where its neighbors leave room #8085's head, where the insets stay at 12 px, and passes here.
  • A new story, InfoTip › In A Divided List, and a browser test in InfoTipTouch.a11y.chromium.spec.ts: inside a divided List row the button's layer reaches its measured inset and no further. It fails while the row declares its divider width on itself, which feat(List): a row's touch target grows toward 44 px where its neighbors leave room #8085 now keeps on the row's own layer.
  • The motion wait is the shared hook's own watch (watchAtRest in utils/touchHitArea.ts), tested in touchHitArea.test.tsx ("drops a measurement taken while the element’s parent moves, and watches afresh once the motion ends"). It now covers InfoTip's button through the hook.
  • Size (gzip, feat(List): a row's touch target grows toward 44 px where its neighbors leave room #8085's head → this head): InfoTip +307 B JS and +164 B CSS, for the shared hook's motion wait and the coarse-only layer's CSS. No other component changes size.

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. On a phone, open InfoTip › Touch. Tap a few pixels to the right of the "i", outside the icon: the help pins open. Tap well away from it: the help closes.
  2. With a mouse, hover and click the "i": the help previews, then pins, as before.

What was run at this head: InfoTipTouch 4/4 in Chromium, including the new divided-List case. That case fails when the row declares its divider width on itself (the button then inherits 1px). Lint, prettier, tsc and changeset coverage pass, and the public text check passes on the commit and this body. The rest of the tree is #8085's head, gated there.

@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 4, 2026 10:29am 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
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review 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 4, 2026, 10:41 AM UTC

@github-actions

github-actions Bot commented Nov 4, 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.

Requesting changes on both pull requests in this stack. A pull request lands only when it and every pull request below it are approved.

#8085: request changes

  • On its own it adds useTouchHitArea and touchHitAreaStyles.layer but leaves InfoTip's own callback ref and ::after beside them, so land it together with #9916 or fold #9916 into it.

#9916: request changes

  • Inside a divided List row, InfoTip's button inherits the row's --_hit-border-bottom, so the shared layer moves its touch band 1 px down (bottom at −11.5 px against a 10.5 px measured inset under touch in Chromium), giving the strip right under the icon to the row and reaching 1 px past the half-gap clamp in component:InfoTip AR1; keep that variable off descendants, for example by declaring it on the row's own ::after, and add an InfoTip-in-divided-List test.

Whole stack: fails on the InfoTip finding above. Every other delta matches component:List FR10, spec:AST-094 FR42 and InfoTip AR1, and mouse behavior is unchanged. Two optional comment trims are noted inline.

[Automated review]

focusOutlineStyles.focusVisible,
styles.trigger,
styles.hitArea,
touchHitAreaStyles.layer,

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.

Inside a divided List row this button inherits --_hit-border-bottom from the row (ListItem.tsx sets it on the <li>, and custom properties inherit). This layer's bottom edge then becomes -(inset + 1px): under touch in Chromium it measures −11.5 px against a 10.5 px inset. The strip right under the icon goes to the row, and the band runs 1 px past the half-gap clamp in InfoTip AR1.

Comment thread packages/core/src/List/ListItem.tsx Outdated
},
// The touch hit layer's band starts past the divider, the edge of the box
// its insets are measured from (utils/touchHitArea.stylex.ts).
'--_hit-border-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.

Declared on the <li>, this reaches every descendant, so an InfoTip's shared layer inside the row adds the divider width to its own band. Declaring it on this row's own ::after, its only intended reader, keeps it local.

Comment thread packages/core/src/InfoTip/InfoTip.tsx Outdated
Comment on lines +139 to +140
// Under a coarse pointer the button takes taps out toward 44 px, on the
// shared touch layer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: this restates useTouchHitArea's own JSDoc, and the call says it. Suggest deleting the comment.

vjeux added 3 commits November 4, 2026 01:32
…rs leave room

Under a coarse primary pointer, an interactive List row gets an
invisible hit layer on its ::after. The shared touch hit area measures
each row and grows the layer toward 44 px, stopping at half the gap to
the nearest neighboring row or control. Rows look and lay out as before,
and a mouse is unchanged.

A List link row's padding and band follow its link under a finger,
through the shared clickable-container guards: a control inside the
row and the click that ends a text selection keep their own behavior.
A standalone Item keeps its anchor as its only link target. Under a
finger a List isolates its rows, so a raised band or swipeable row
stays below a page-level sticky header.

A row's watch of the shared hit area owns the touches on its content, so
a tap on a swipe panel's entry beside an open row stays the entry's.
Other watches, such as Switch's grown input, keep the nearer-neighbour
rule. A stopped watch clears the insets it wrote. A divided row declares
its divider's width on its own layer, so content inside the row never
inherits it.

A delegate row renders its pressable-container marker itself, so a row
that stops delegating drops it.
The With end controls story's drill-in chevrons take rtlStyles.mirror,
so they point to the row's end under right-to-left. Story only.
InfoTip's button kept its own copy of the List rows' touch target: a
callback ref over the shared hit area and its own ::after layer. It now
uses useTouchHitArea and touchHitAreaStyles.layer, so one path serves
both. A stopped watch clears the button's insets, a measurement waits
for motion around the button to end, and the layer exists only under a
coarse pointer.

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

Approving #9916 pending CI; #8085 below it still needs to land together with it. A pull request lands only when it and every pull request below it are approved.

#9916: approve

  • The InfoTip band inside a divided List row now ends at its measured inset, and the new story and browser test cover that case.

#8085: request changes

  • On its own it adds useTouchHitArea and touchHitAreaStyles.layer but leaves InfoTip's own callback ref and ::after beside them, so land it together with #9916 or fold #9916 into it.

Whole stack: passes; every delta matches component:List FR10, spec:AST-094 FR42 and component:InfoTip AR1, and mouse behavior is unchanged.

Approved pending CI: CI has not run on this head yet, because the pull request is a draft from a fork; it runs once it is ready for review and a maintainer adds the ci:run label. One optional comment trim is noted inline.

[Automated review]

Comment on lines +25 to +54
// 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). It declares it on its own `::after`, so content inside the host
// never inherits it. 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 (comment style): this 30-line block repeats much of the file header. It could keep only the constraints, for example: "The insets start at the host's border box. The offsets start at its padding box. A host with a border declares its width on its own ::after. Under a finger the host's ::after is this layer."

This branch was successfully deployed

1 active deployment
Preview — ced35b36 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

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.

1 participant