Repository navigation
Conversation
Touch behaviours key on a coarse primary pointer, layout behaviours on a compact viewport, and presentations that need both (a sheet in place of a popover) ask for both. utils/touchPresentation.ts holds the three queries, SSR-safe hooks (useTouchPresentation; useMotionPresentation for components with no interaction to infer from: UIKit motion under a finger, theme tokens under a mouse) and a one-moment read. Styles write the same queries as StyleX media keys, and a test keeps core's compact-touch keys spelled as the shared query. A compact viewport is narrower than 744 px or shorter than 500 px, the closest a size gets to UIKit's compact size class: an iPad mini in portrait keeps the anchored popover and a phone in landscape gets the sheet; a Max-size iPhone in landscape is the known miss. The adaptive popover-or- sheet presentation reads the shared query; its docs, stories and tests follow.
A touch presentation is now also an idiom: ios for WebKit on iOS or iPadOS
under a coarse pointer (CSS.supports('-webkit-touch-callout: none'), which
no other engine implements), default everywhere else, Android included,
which keeps today's behaviour until a Material variant exists.
useTouchPresentation and readTouchPresentation report it, useTouchIdiom
reads it alone, and useMotionPresentation now chooses UIKit motion only in
the iOS idiom. Styles gate the same way with two nested StyleX keys, the
media query outside and the supports query inside (IOS_TOUCH_STYLE_KEYS),
as core's 16px input floor already does; a test keeps every such key in
core spelled and nested that way. A real-engine spec reads Chromium as an
Android phone and WebKit as default.
…documented behavior change - The resolver answers the same on every engine: the iOS idiom (useTouchIdiom, useMotionPresentation, the idiom field) is dropped, so nothing chooses a platform; touch motion reads the theme's motion roles. The iOS style-gate constants stay, internal, for a style iOS WebKit itself requires (core's 16px input floor), and their spelling test stays. - The changeset names the old and new default (sub-menu drill-in included) and the presentation prop that pins a component's presentation.
Adds internal/interaction-harness (@astryxdesign/interaction-harness) and playwright.interaction.config.ts: phone-sized touch projects in Chromium and WebKit for *.interaction.browser.spec.ts specs. Touch scripts are data (tap, drag with hold and rest, flick with an exact release velocity and velocity profiles, hold-then-drag, pinch, rotate, any path, a second finger mid-gesture), played as real engine input: CDP Input.dispatchTouchEvent with scripted timestamps in Chromium, WebKit's own input command in WebKit. The harness's self-test proves the timestamps, a native fling and multi-touch delivery; a pr-interaction CI job runs the proofs against each PR's Storybook build, and a container script runs the WebKit project where host libraries are missing.
… Chromium The Chromium driver sent a lift of one finger of several as a touchMove listing the fingers still down, assuming CDP releases a point a move omits. It does not: the lifted finger stayed down. A touchEnd that lists the lifting fingers releases exactly those while the others keep moving, so the driver sends that, and a self-test lifts one of two fingers and checks the release, the other finger's moves and the touchend's remaining touches. Playwright's WebKit sends that touchend with no remaining touches, which iOS never does. The harness does not work around it: drivers expose partialLifts, and PARTIAL_LIFT_SKIP is the reason a proof cites when it skips; the README lists it with the other engine limits.
… lift profiled flicks at their velocity - pr-interaction now always reports and the `test` join requires its success (AST-030 FR7, FR11), shaped like test-native-swift: a Linux interaction-scope job decides, with the base's scope helper and failing closed, whether the harness, a component, Storybook, internal/a11y-spec, the workflow, package.json or pnpm-lock.yaml changed. Only then do the proofs take a 4-core runner; otherwise, and in merge groups (as with pr-a11y), the job passes without work. - A profiled flick's last move runs at velocity times profile(1), so the finger lifts at the documented velocity, and releaseVelocity reads the last move instead of a 100 ms window mean. - Docs: drop the /math subpath this package does not export yet; scripted timestamps arrive to 0.1 ms; the job comment describes the self-test it runs.
Brings in the theme compact definition (spec:AST-012 FR9) this branch's resolver reads.
…ed its previous position A sample that repeats a position is not a move: frames that merge() inserts for another finger, where this finger did not move, made the last move look shorter, so a 10 Hz drag merged with a tap read 375 px/s instead of 300 and a 60 Hz drag whose other finger lifted 5 ms before its last move read about 3.6x too fast. The previous sample is now the first one at the previous position, as the page sees it, with a merged case in scripts.test.ts.
COMPACT_QUERY and COMPACT_TOUCH_QUERY are the theme's compact definition (spec:AST-012 FR9: width below the md point, 768 by default, or height below 500, the same on every platform), so useAdaptivePresentation and the sub-menu drill-in keep no size numbers of their own. Tests, docs, stories and the changeset follow: a coarse-pointer screen exactly 768 wide keeps the popover, and a phone in landscape gets the sheet.
…t width The previous commit's formatter pass rewrapped the whole file; this keeps the file as it was and changes only 744px to 768px in both languages.
Pause every Web Animation, seek it to a time and read the presented transform and opacity, then compare the curve with oracles that share no code with the motion under test: cubic-bezier, the SwiftUI spring (response, dampingFraction, initial velocity), UIScrollView deceleration and UIKit's rubber band. Storyboards photograph seeked animations at t = 0, 16.7, 33.3 ... ms or each step of a scripted touch, and write a PNG contact sheet plus the JSON trace a spec asserts, under the gitignored test-results/interaction-evidence. A timer gate holds a component's own timers while a filmstrip is taken. pnpm storyboards runs the tagged tests.
…sented values An animation that keeps its end value in a run of identical final keyframes (held so a later interruption can read it) reported its whole duration as the curve: storyboards filmed the hold, waitForAnimation's interval reached past the landing, and waitForRest, which also required every animation to leave the running state and read only transform, timed out. The curve now ends where that run starts (HeldAnimation.curveEndMs, waitForAnimation's to, the default storyboard times), and rest is no animation pending or before its curve's end plus the presented transform, translate, rotate, scale and opacity (and ::backdrop opacity) still for 3 frames, whatever the playState. sampleCurve composes translate, rotate and scale with transform, so curves of the individual properties are sampled.
…film no endless curve, let the page cancel gated callbacks - holdAnimations holds only running document-timeline animations: a finished fill animation keeps its end value, an animation the page paused stays paused (release resumes only what was held), and a scroll-driven one is not seeked. Each held animation keeps its offset from the earliest start, so staggered starts stay staggered. - A sample or storyboard frame at or past an end reports the time it was read at (presentedAt, half a millisecond inside the end), and curveOf reads oracles there. - An animation that repeats forever no longer sets a storyboard's default length (it ran the worker out of memory); with nothing else to film, animationStoryboard asks for times. - The timer gate queues held callbacks by id, so the page's clearTimeout and cancelAnimationFrame still cancel them; a held frame callback gets the time it runs; frames(), waitForRest and touchStoryboard use the real requestAnimationFrame, so they resolve while frozen. - The storyboard self-tests carry the @Storyboard tag that `pnpm storyboards` selects.
…s AST-094 copy - useTouchPresentation, useAdaptivePresentation and the sub-menu drill-in read the active theme's compact definition (useCompactDefinition, from its effective width points), so a theme that moves md moves compact with it (spec:AST-012 FR9). The exported constants stay the default-point values a StyleX media key spells. A test covers a theme with md 1000. - The changeset names the edge moving with md and the sub-menu pin (presentation="flyout" on DropdownMenuSubMenu). - The stacked copy of docs/specs/AST-094-touch-presentations/spec.md is removed: the size rule here rests on current spec:AST-012 FR9 and the Selector record, and AST-094 itself is proposed separately.
The size rule here rests on current spec:AST-012 FR9 and the Selector record; AST-094 itself is proposed separately, so this branch no longer carries an older revision of it.
The adaptive policy's width edge follows the theme's md breakpoint (768px by default, spec:AST-012 FR9), so the docs (en and zh) and the two story descriptions say that instead of a fixed 768px.
…ation Nothing reads it: an adaptive overlay decides through useAdaptivePresentation and the compact definition's coarse-pointer query. useTouchPresentation and readTouchPresentation answer isTouch and isCompact.
Four frames sized at the compact edges (767 and 768 px wide, 499 and 500 px
tall) report what useAdaptivePresentation('adaptive') picks: the bottom
sheet at 767 and 499 under a finger, the popover at 768 and 500 and
everywhere under a mouse, in Chromium and WebKit.
…s from the element's own animations, resume endless ones
- animationStoryboard's default times run from the earliest start to the
latest curve end among the animations that end, each seek at
origin + t: beside a spinner that started earlier the animation under
test no longer shows its start in every frame, and a second animation
that starts later is filmed to its end.
- A sample's presentedAt (per channel: transform, opacity) comes from the
sampled element's own animations, by keyframe property, not from any
animation on the page.
- releaseAnimations({finish: true}) resumes an animation that repeats
forever instead of finishing it (which throws), and always clears the
held set.
- The interaction proofs' scope includes packages/themes/*/src/.
…before it looked waitForAnimation polled getAnimations() after the action, so an entrance shorter than the first look (a cold page answers its first evaluate late) had already finished: "no animation started". The page now records every animation from the first call into curves.ts on it, and on every later document: a frame loop on its own requestAnimationFrame, the CSS animation and transition events, and Element.animate as it is called, pending animations included (WebKit can start and finish a short script animation between two frames of a fresh page). waitForAnimation also reports an animation that started after the last waitForRest or waitForAnimation and has finished, with finishedAt. watchAnimations(locator) arms the record before an action that may be the page's first contact with the harness.
…uest This pull request keeps the curve-sampling instruments: holding and seeking animations, sampling presented values, the oracles, waiting for an animation and for rest. The storyboards (storyboard.ts, the evidence output, the timer gate and the `pnpm storyboards` script) and their tests move to a pull request stacked on this one.
`holdAnimations` replaced the held set on each call, so a second hold left the first set paused after `releaseAnimations` (it resumed only the newest set), and `waitForRest` then reported the frozen frame as rest. A later hold now adds to the set: everything held stays held until the release, and offsets count from the earliest start of all of it. The self-test holds two elements in turn, releases, and checks the first runs to its end; on the old code it stays frozen 50 px into a 300 px move. The self-test's header and the `curves.ts` SYNC line no longer describe the storyboard and timer-gate layer, which moved to its own pull request.
animationStoryboard films held animations by seeking them (t = 0, 16.7, 33.3 … ms, from the earliest start to the latest curve end among the animations that end, each at its own offset); touchStoryboard takes a frame per scripted touch event. Each writes a PNG contact sheet and a JSON trace of its measurements to the evidence directory and attaches them to the report; the spec asserts on the trace. The timer gate (installTimerGate, freezeTimers, thawTimers) holds the page's timers and frame callbacks while a storyboard photographs, so a component that finishes an animation from a timer cannot do it behind the held animation's back; the page can still cancel a held callback, and the harness's own frame waits (and the animation recorder) keep the page's real requestAnimationFrame. `pnpm storyboards` runs only the tests tagged @Storyboard.
`touchStoryboard` said velocity-dependent behavior is unchanged in Chromium. That holds for velocity read from event timestamps, which keep their scripted times, but a handler that reads `performance.now()` or `Date.now()` (Item's and Toast's swipes) sees one event per photograph, about 100 ms apart, so a flick that dismisses when played can settle back when filmed. The doc and the README now state that limit and point such gestures at a screencast, and a self-test shows both clocks: scripted 16.7 ms gaps in `timeStamp`, more than two frames in the handler's clock.
Timestamped composited frames (Chromium's screencast and WebKit's own), a marker the frames can find, and the busy-main-thread proof: block the page's main thread mid-flight and show the element still moving every frame, with a requestAnimationFrame animation that freezes as the negative control. Chromium trace counts of Layout, Paint and style work inside an animation window, and long tasks. The exemplar spec proves the BottomSheet drag to a detent with all of it: the drag follows the finger, a flick dismisses, the settle matches its timing function, keeps advancing through a blocked main thread in Chromium and WebKit, does no Layout or Paint, and gets a drag and settle storyboard.
…ed machine The proof asserted a stall under 40 ms and near-exact frame counts, so a dropped screencast frame or two on a loaded runner failed it, and frames delivered out of order read as the element stepping backwards. busyWindowProof now returns a verdict that tolerates what load does to a screencast and nothing a frozen animation does: a median frame interval of at most 20 ms, no stall longer than three screencast intervals, and at least 90% of the window's whole frame intervals presenting a new position (one miss always allowed), with the raw numbers in a summary. Screencast frames are sorted by presentation time, the cadence comes from the frames around the window, and blockMainThread can block from inside the page in the frame an animation has run, so a short settle is still judged at length on a busy machine. The exemplar judges its settle until it is within 2 px of rest, since the screencast rounds to whole pixels. A unit test covers the limits; the rAF negative control still fails every criterion.
…ading them from the animation The settle-curve oracle took its easing and duration from the animation under test, so it passed a 900 ms ease-in or a 150 ms linear settle as readily as the real one. The spec now asserts the theme's values (the default --ease-standard, cubic-bezier(0.24, 1, 0.4, 1), over the neutral theme's --duration-medium, 300 ms) and builds the oracle from them; the header no longer claims 410 ms.
…ebKit frames on its clock - `blockMainThread`'s `during` path ran its loop inside the rAF callback of the frame that saw the animation. WebKit hands a newly started animation to its compositor in that frame's rendering update, which runs after rAF callbacks, so the loop held the hand-off and even a compositor animation froze (2 of 29 frames moved). The loop now runs from a task queued in that frame. - WebKit's screencast frames were rebased onto the wall clock by taking the first frame as the start, which shifted every frame earlier by however long that frame took to come (60–100 ms). They are now rebased by the measured offset between the wall clock and CLOCK_MONOTONIC, which WebKit stamps them on. - `busyWindowProof` fails a judged part shorter than four frames (the animation under test was not moving during the block) instead of passing it, takes the end value from `settledAt` when given (WebKit can present a stale position once an accelerated animation ends while the main thread is blocked), and reports where the longest stall starts. - The BottomSheet busy case arms the block before the release and reads where the settle ends from the page.
…cription They ran under their own Playwright config, which no CI job picks up; spec:AST-030 FR11 asks new coverage to join its existing owner. Their results (Chromium and WebKit, and Mobile Safari on the iOS 26.2 simulator) stay in the pull request description, and the unit suites keep the resolver's answers.
Nothing pinned useMenuDrillIn's theme read: a fixed default query left the suites green. A 900 px coarse screen now flies the sub-menu out under the default md (768) and drills in under a theme whose md is 1000, as the root sheet does (spec:AST-012 FR9).
… reads Its default ignored a theme that moves md; nothing calls it yet, so the compact definition is now required and its first caller passes the theme's (useCompactDefinition()). The changeset names ContextMenu's sub-menus, whose drill-in changes too.
…'s browser proofs
…ction harness They ran under their own Playwright config, which no CI job picked up (spec:AST-030 FR11). As *.interaction.browser.spec.ts they join the interaction harness's phone projects, which CI runs: the resolver's answers and the iOS style gate under a finger, and an adaptive menu taking the sheet only below the compact edges (767 px wide, 499 px tall), under a finger and never with a mouse. The one-moment read passes its compact definition.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
Changes requested, for scope rather than for the proofs themselves. Because this branch merges #8045 and #8062, its diff is 7,105 authored lines across 49 files, including the whole interaction harness and #8062's presentation="adaptive" behavior change, not only the three proof files the description covers. Please land #8045 and #8062 first (or retarget onto them) and rebase, so this pull request carries only adaptiveEdges.interaction.browser.spec.ts, touchPresentation.interaction.browser.spec.ts and fixture.ts; until then, the description's "no product change" and "only test files" statements do not match the diff. The proofs look right: they pass in Chromium and fail at the 768 px edge when the adaptive hook gets the old (max-width: 768px) query.
[Automated review]
|
Thanks. The proofs stay as they are.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNo new or modified components detected. Bundle Size SummaryGzip figures measure each package's entry file, not the code an import pulls in.
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
…n resolver as they landed) into the touch presentation proofs # Conflicts: # .github/scripts/ci-interaction.test.mjs # .github/scripts/ci-test-routing.test.mjs # .github/workflows/ci.yml # internal/interaction-harness/README.md # internal/interaction-harness/package.json # internal/interaction-harness/scripts/webkit-container.mjs # internal/interaction-harness/src/curves.ts # internal/interaction-harness/src/harness.interaction.browser.spec.ts # internal/interaction-harness/src/index.ts # internal/interaction-harness/src/math.test.ts # internal/interaction-harness/src/math.ts # internal/interaction-harness/src/scripts.ts # internal/interaction-harness/src/storyboard.ts # package.json # playwright.interaction.config.ts
… on main's harness Main's interaction harness landed without the busy-thread, frame and trace modules and the CI wiring this branch carried from the harness's own pull request, and the BottomSheet detent proof belongs to another branch. This pull request now changes only its three files: the adaptive-edges and touch-presentation proofs and their fixture.
There was a problem hiding this comment.
Recommended for approval: the branch now carries only the three proof files on main's interaction harness, and they match spec:AST-012 FR9, spec:AST-094 FR13 and spec:AST-030 FR11. The proofs pass in Chromium and WebKit, and they catch an inclusive FR9 edge, an adaptive hook with its own size numbers or without its pointer check, and an iOS gate that matches outside iOS. Optionally, refresh the description: it still lists #8045 as a dependency and asks for a review of one earlier commit, and its testing command needs --project=chromium, because with a space Playwright reads the path as a second project name.
[Automated review]
|
Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed. |
Review only this pull request's three test files: vjeux/astryx@39ed04d...f9638b4
Why
Nothing in CI checks what a real browser engine answers for Astryx's touch presentation: whether a phone reads as touch and compact, whether the iOS-only styles stay off elsewhere, and where an adaptive menu switches to a bottom sheet. A regression there would show a desktop menu on phones, or a sheet on a laptop, and only a person holding a device would notice.
What led to it
This is part of the UIKit-parity work that makes Astryx's touch components behave as they do on iOS. #8062 added the touch presentation with real-engine proofs, but those ran under their own Playwright config, which no CI job runs, so #8062 dropped them. The interaction harness (#8045) has since landed, and CI runs its phone projects.
Options and tradeoffs
Why this choice
The harness is where real-engine coverage lives (
spec:AST-030FR11: new coverage joins its existing owner), so the proofs run in CI with no new job and no product change.How to check it
pnpm exec playwright test -c playwright.interaction.config.ts --project chromium packages/core/src/utils/__tests__/browser/. Expected: 3 passed.mcr.microsoft.com/playwright:v1.61.1-noble. Expected: 3 passed.spec:AST-012FR9).Evidence
*.interaction.browser.spec.tsfiles and their fixture. The edges are checked in four iframes on one page, because a frame's media queries read its own size.