Skip to content

chore(gestures): a Storybook harness to check presented hit testing on a phone - #9400

Merged
cixzhang merged 9 commits into
meta:mainfrom
vjeux:uikit/f2-presented-lab
Nov 5, 2026
Merged

cixzhang merged 9 commits into
meta:mainfrom
vjeux:uikit/f2-presented-lab

Conversation

@vjeux

@vjeux vjeux commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Main is merged into this branch. Open this pull request's Storybook preview on an iPhone: /storybook/iframe.html?id=core-gestures--presented-hit-testing&viewMode=story.

Maintainer impact

#8360's presented hit testing (spec:AST-094 FR11, FR19) is proven in CI's WebKit lane and the iOS Simulator. But no story, sandbox or docs page attaches the gesture host, so a phone that opens a preview cannot exercise it. A check on a physical iPhone needs a page that does.

Intended invariant

spec:AST-017 FR21 and architecture:react-component-runtime INV13: Core's internal entries and gesture source stay with Core and Lab. This pull request adds no import of them from outside Core: the harness is a Core file, and the story renders it the way the a11y stories render Core's __tests__ renderings.

Change

  • packages/core/src/__tests__/PresentedHitTestingLab.tsx (not exported): a sheet springs up over a full-page scrim on a spring slowed to a 3 s response, with Done in the sheet and a page behind it that scrolls. It reads out:

    • what the engine hit-tested at each touch-down;
    • Done's clicks and the scrim's taps;
    • the sheet pan's translation;
    • how far the page scrolled in the last touch.

    A native checkbox turns the gesture host's correction off as the control; it is not Core's Switch, which would attach the document's shared gesture host and hide the correction. Replay restarts the motion. Every touch is measured at the page root in the capture phase, because on iOS WebKit a touch on the moving sheet targets the scrim. When Reduce Motion is on, the status line says so, since the sheet then does not move.

  • apps/storybook/stories/PresentedHitTestingLab.stories.tsx: one full-screen story, "Presented hit testing" under "Core/Gestures".

  • No change to internal/touch, eslint.config.js, any public entry, or any record. No changeset: story and test files only.

On a phone

  1. On an iPhone, open the preview's Storybook story above.
  2. Press Replay, then tap Done while the sheet rises. With the correction on, the log shows the engine hit-testing the scrim, then "Done clicked", and the scrim takes nothing.
  3. Press Replay, then drag the sheet down while it rises. The pan catches it where it is drawn, and the page scrolls 0 px.
  4. Untick Correction and repeat. The tap reaches the scrim, and the drag scrolls the page.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

At 5adbd62: main 9e559e9 merged in, and the lab calls createMotionEngine, the name #9754 gave Core's internal animator.

  • Core's tsc --noEmit passes. On the plain merge without the rename, it fails with TS2305 on the old import.
  • The lab's unit tests passed, 3 of 3. Storybook's type check, strict ESLint, Prettier and review:pre: clean.

Earlier evidence:

After the maintainer update (main merged; the harness moved into Core; the review fixes):

  • Chromium with iPhone-sized touch emulation, against a Storybook built from this head:

    • a tap on Done while the sheet rises clicks it once, and the scrim takes nothing;
    • a tap on the scrim reaches the scrim;
    • a touch drag on the moving sheet moves its pan 110 px, and the page scrolls 0 px;
    • no page errors.

    Chromium already hit-tests the drawn sheet, so the phone is the check of the correction itself.

  • PresentedHitTestingLab.test.tsx:

    • a touch that lands on the scrim is logged and measured at the page root (with the earlier sheet-only handlers the readout stayed "–");
    • the Reduce Motion line shows;
    • with the engine check stubbed to iOS WebKit and the sheet drawn 544 px above where its motion started, a scrim-targeted touch at Done's drawn point clicks Done once and the scrim takes nothing. That case fails with Core's Switch as the control. The gesture import-graph and internal-module tests pass.
  • Story tree: story-tree.test.ts passes on a Storybook built from this head (two-segment title).

  • Static checks: strict ESLint (no no-core-internal-import exemption), Prettier, core typecheck and pnpm review:pre (no findings).

/lab/presented-hit-testing, not linked and not indexed, springs a sheet up
over a full-page scrim on a spring slowed to a 3 s response, with Done in
the sheet and a page behind it that scrolls. It reads out what the engine
hit-tested at each touch-down, Done's clicks, the scrim's taps, the sheet
pan's translation and how far the page scrolled during the last touch on
the sheet, and a switch turns the gesture host's correction off as the
control: a phone opening a preview can now exercise the presented hit
testing of spec:AST-094 FR11 and FR19.

The page drives Core's gesture host and animator through
@astryxdesign/core/internal/touch, which now also re-exports GestureHost,
PanGestureRecognizer, createAnimator and isIosWebKit by name (no root or
public export changes); the entry's test checks each is the value Core
imports by path, and failed before them. eslint.config.js lets that one
route import the internal entry; any other docsite file still cannot.
@vercel

vercel Bot commented Nov 1, 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 8:19pm UTC

Request Review

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

astracat-bot Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 8:28 PM UTC

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

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · f14a774

Storybook · Sandbox · built from this PR's head

Package size: @astryxdesign/core 6.62 MB packed (+31 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 23 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: the lab page is a useful instrument for the AST-094 device check, but it needs two fixes first.

  • The ESLint exemption lets a docsite route import @astryxdesign/core/internal/touch, which spec:AST-017 FR21 and architecture:react-component-runtime INV13 limit to Core and Lab, so either keep the harness inside Core or Lab (for example a Lab-owned harness that the preview's Storybook renders) or amend both records with an engineering owner's approval in this pull request.
  • The "page scrolled" readout listens only on the sheet, so on iOS WebKit, where a touch on the moving sheet targets the scrim, it never updates; measure every touch at the page root instead.
  • Smaller: three of the new Effects have handler or ref homes, and the status line should say when Reduce Motion is on, since the sheet then never moves.

[Automated review]

Comment thread eslint.config.js Outdated
ignores: [
'packages/core/**',
'packages/lab/**',
'apps/docsite/src/app/lab/presented-hit-testing/**',

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 exempts the route from @astryx/no-core-internal-import, so a docsite file imports @astryxdesign/core/internal/touch. spec:AST-017 FR21 and architecture:react-component-runtime INV13 both limit that subpath to Core and Lab, and neither is amended here. Either keep the harness inside Core or Lab, or carry an engineering-owner-approved amendment of both records (and their verification rows) in this PR.

Comment on lines +294 to +303
onTouchStart={() => {
scrollAtTouchRef.current = window.scrollY;
}}
onTouchEnd={() => {
const start = scrollAtTouchRef.current;
if (start != null) {
setScrolled(Math.round(Math.abs(window.scrollY - start)));
}
scrollAtTouchRef.current = 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.

On iOS WebKit a finger on the moving sheet targets the scrim, and every later event of that touch goes to the scrim too, so these handlers never run for the drag the FR19 check reads and "page scrolled" keeps its last value. Measuring at the page root (onTouchStartCapture / onTouchEndCapture / onTouchCancelCapture on the outer <div>) would see every touch.

cixzhang and others added 2 commits November 3, 2026 11:02
…at Storybook renders

Per review: a docsite route may not import Core's internal entries
(AST-017 FR21, react-component-runtime INV13), so the device check moves
into Core as a harness that a Storybook story renders, and a phone opens
it on the preview's Storybook. The harness imports the gesture host, the
motion animator and the engine check by relative path, so internal/touch,
its test, eslint.config.js and the changeset are back to main's.

Also from review:
- Every touch is measured at the page root in the capture phase: on iOS
  WebKit a touch on the moving sheet targets the scrim, so the sheet's own
  handlers missed it.
- The engine and Reduce Motion are read on the first render, and the
  animator lives in the sheet's ref callback, instead of three Effects.
- The status line says when Reduce Motion is on, since the sheet then does
  not move.
- A test checks the scrim-targeted touch is logged and measured, and the
  Reduce Motion line.

Co-authored-by: vjeux <vjeux@users.noreply.github.com>
@cixzhang cixzhang changed the title chore(docsite): a lab page to check presented hit testing on a phone chore(gestures): a Storybook harness to check presented hit testing on a phone Nov 3, 2026
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Nov 3, 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.

Changes requested: the harness is the right shape for the AST-094 phone check, but as written it cannot show the correction on an iPhone.

  • The Correction control is Core's Switch, which attaches to the document's shared gesture host; on iOS WebKit that host handles each touch first and resolves the moving sheet, so the harness's own host sees no moving element and every tap and drag still goes to the scrim with Correction on, exactly as with it off; use a native checkbox (or any control that does not use the gesture host) so the harness's host is the only one on the page, and add a test that sends a scrim-targeted touch at Done's drawn point with the engine check stubbed to iOS WebKit and expects "Done clicked".

Everything else checks out: the harness stays inside Core, ships in no package, and its readouts work in Chromium with touch emulation. A small comment fix is noted inline.

[Automated review]

<HStack gap={3} vAlign="center" hAlign="between" wrap="wrap">
<Heading level={1}>Presented hit testing</Heading>
<HStack gap={3} vAlign="center">
<Switch

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 Switch attaches a press consumer to the document's shared gesture host (useSwitchTouch.ts calls getGestureHost) in a layout effect, so that host hears every touch before the host built in the Effect above. On iOS WebKit its presentedTarget resolves the moving sheet first, which zeroes the sheet's presented offset; this harness's host then finds no moving element and keeps the engine's target, the scrim. With an iOS target emulated (engine check stubbed, touches targeted at the scrim), Correction on reads "Done clicked 0 · scrim took 1"; with a native <input type="checkbox"> here it reads "Done clicked 1 · scrim took 0" and a drag catches the pan.

Comment on lines +156 to +158
// The page root sees every touch in the capture phase, before the host
// corrects anything. On iOS WebKit a touch on the moving sheet targets the
// scrim, so a handler on the sheet would miss it.

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 gesture hosts listen in the window's capture phase, so they run before this root handler, not after it. The log is still right because no host changes event.target. Suggested: "The page root logs the engine's own target: no host changes it. On iOS WebKit a touch on the moving sheet targets the scrim, so a handler on the sheet would miss it."

Per review: Core's Switch attaches to the document's shared gesture
host, which on iOS WebKit resolves the moving sheet before the harness's
own host, so the correction never showed. A native checkbox keeps the
harness's host the only one on the page. A test stubs the engine check
to iOS WebKit, sends a scrim-targeted touch at Done's drawn point, and
expects Done to click (it fails with the Switch). A comment says the root
logs the engine's own target.

Co-authored-by: vjeux <vjeux@users.noreply.github.com>

@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 Correction fix works, but the story's title breaks the repository's story-tree check.

  • The title 'Core/Gestures/Presented hit testing' has three segments, which apps/storybook/.storybook/story-tree.test.ts ("never nests a component deeper than /") rejects, so the build-storybook job and the build check fail once CI runs; use a two-segment title such as title: 'Core/Gestures' with the story named 'Presented hit testing', and update the story link in the description.

The native checkbox resolves the earlier finding: with an iOS target emulated, a tap on the rising Done now clicks it with Correction on and reaches the scrim with it off, and the new test guards that. Everything else checks out.

[Automated review]

import {PresentedHitTestingLab} from '../../../packages/core/src/__tests__/PresentedHitTestingLab';

const meta: Meta<typeof PresentedHitTestingLab> = {
title: 'Core/Gestures/Presented hit testing',

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 three-segment title fails apps/storybook/.storybook/story-tree.test.ts ("never nests a component deeper than /": a third segment is allowed only under Hooks, Themes, Internationalization and the chart groups), which the build-storybook job runs. A two-segment title fixes it, for example title: 'Core/Gestures' with name: 'Presented hit testing' on the story (id core-gestures--presented-hit-testing); the story link in the description would change with it.

Per review: the story tree allows <Package>/<Component> only, so the
story is "Presented hit testing" under "Core/Gestures"
(core-gestures--presented-hit-testing). The story-tree test passes on a
Storybook built from this head.

Co-authored-by: vjeux <vjeux@users.noreply.github.com>

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

Recommending approval: the story-tree failure from the last round is fixed, and nothing else needs to change.

  • The two-segment title Core/Gestures with the story named "Presented hit testing" passes the repository's story-tree check, and the story opens at core-gestures--presented-hit-testing as the description says.
  • The harness stays inside Core and ships in no package, and its tests, strict lint and the gesture import-boundary tests pass on this head.

Real-device evidence for presented hit testing follows merge, as spec:AST-094 allows, so the iPhone run is the next step rather than a condition here.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec c80d196

@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 10:20
@cixzhang
cixzhang marked this pull request as ready for review November 5, 2026 10:20

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

Recommending approval: nothing needs to change.

  • The harness stays inside Core and ships in no package. Its tests, strict lint and the gesture import-boundary tests pass on this head, and the iOS-stubbed tap case fails as it should with Correction unticked.
  • The story opens at core-gestures--presented-hit-testing and passes the story-tree check.

spec:AST-094 lets real-device evidence for presented hit testing follow merge, so the iPhone run comes next rather than being a condition here.

[Automated review]

@vjeux
vjeux marked this pull request as draft November 5, 2026 13:25
vjeux added 2 commits November 5, 2026 05:27
… name

meta#9754 renamed Core's internal createMotionAnimator to createMotionEngine,
with no alias. The presented hit-testing lab still called the old name,
so main's type check would fail once it merged.
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 5adbd62

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

Recommending approval, pending CI: CI has not finished on this head yet.

  • The harness now calls Core's motion engine as createMotionEngine, the name main gave it, and Core's type check passes with it.
  • The harness tests, the gesture import-boundary tests, strict lint and the story-tree check pass on this head, and the iOS-stubbed tap case reads the control correctly with Correction unticked.

spec:AST-094 lets real-device evidence for presented hit testing follow merge, so the iPhone run comes next rather than being a condition here.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 5, 2026 13:50

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

Recommending approval: nothing needs to change.

  • Main is merged in with no change to the pull request's own files, and on this head Core's type check passes and the harness tests pass, including the iOS-stubbed tap that reaches the drawn Done.
  • The harness stays inside Core and ships in no package, and the story passes the story-tree check.

spec:AST-094 lets real-device evidence for presented hit testing follow merge, so the iPhone run comes next rather than being a condition here.

[Automated review]

@cixzhang
cixzhang merged commit 4e963df into meta:main Nov 5, 2026
66 checks passed
@vjeux
vjeux deleted the uikit/f2-presented-lab branch November 5, 2026 23:19

This branch was successfully deployed

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