Skip to content

fix(core): gesture recognizer and arena fixes from review - #8320

Closed
vjeux wants to merge 1302 commits into
meta:mainfrom
vjeux:uikit/f2-arena-fixes
Closed

vjeux wants to merge 1302 commits into
meta:mainfrom
vjeux:uikit/f2-arena-fixes

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8336. Review only this pull request's change: vjeux/astryx@a0c57a3...a8b174a

Why

Reviews of Astryx's gesture foundation found defects a person would hit on a phone. In Safari before 26, which sends no scrollend, a finger's scroll never ended, so the next tap on a list counted as part of the scroll. After a long task blocked the page, a long press could begin after the finger had lifted. And WebKit read a steady drag's release at 533 px/s instead of 625, so flicks fell short.

What led to it

This is part of the UIKit-parity work that makes Astryx's touch components feel as they do on iOS. The reviews of the foundation pull requests (#8058–#8061, #8071–#8074) listed these defects and a few smaller ones; this pull request collects their fixes.

Options and tradeoffs

  • One pull request per defect: each is easier to read alone, but they touch the same arena, recognizers and host, so a dozen pull requests would conflict with each other and with the stack.
  • Fix only the visible ones now: the rest (an idle cancel() disabling a recognizer for good, a two-finger double tap that never recognized) would wait.
  • One pull request with each fix in its own commit, its failing test first.

Why this choice

The fixes share one arena and one sampling path, so landing them together keeps them consistent, and the commits still read one defect at a time. Release velocity now comes from one tracker for drags and scrolls (spec:AST-100 FR2), and a pan claims as it begins, at UIKit's 10 pt (spec:AST-094 FR17, FR19, as the owner ruled).

How to check it

  1. In Safari 25 on an iPhone, scroll a list of tappable rows, let it stop, then tap a row. Expected: the row activates.
  2. On a phone, start a long press on a context-menu trigger while the page is busy, and lift before it opens. Expected: nothing opens after the lift.
  3. Flick the same sheet in WebKit and in Chromium. Expected: it travels about as far in both.
  4. Double-tap with two fingers on a two-finger double-tap target. Expected: it recognizes.

Evidence

  • Every fix has a unit case that fails before the change and passes after.
  • On Mobile Safari 26.2, fed the exact touch streams UIKit was measured with, release velocity matches UIKit within its run-to-run spread, and a lift 40 ms or more after the last move releases at 0 (spec:AST-100 FR2, FR4).
  • Chrome on an Android emulator releases a flick at the same speed whether or not the main thread is busy, because velocity reads the events' own timestamps.

@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 Oct 27, 2026 2:44pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 29, 2026, 2:42 PM UTC

@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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner decides whether the draft spec:AST-094 (FR1–FR23, DEC-1 and DEC-2) governs the gesture and motion work this stack adds; an engineering owner records that decision by commenting /approve-spec followed by the full head commit SHA, and the author may do so if they are an engineering owner. Whatever that decision, these changes are needed before approval:

  • Build the presentation="adaptive" compact test from the theme's compact definition (compactDefinition() on main, spec:AST-012 FR9) instead of a hard-coded 744 px width, which sends coarse-pointer screens 744 to 767 px wide to the popover.
  • Drop the platform adaptation condition until spec:AST-012 FR2 adds it to its closed vocabulary.
  • Keep reduced-motion durations and springs out of the themable motion roles, as design:reduced-motion DR2 and DR4 require.
  • Run pr-interaction under an existing CI owner whose join it can fail, per spec:AST-030 FR10 and FR11.
  • Remove or qualify the new astryx docs motion section that says sheets and menus already play these springs, since no component reads them yet.
  • Against main the pull request carries about 29,500 authored lines from the unmerged stack, so landing the predecessors first would leave only this commit to review.

[Automated review]

export const TOUCH_QUERY = '(pointer: coarse)';

/** The viewport is compact: layout behaviours. */
export const COMPACT_QUERY = '(width < 744px), (height < 500px)';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-012 FR9 (and architecture:public-component-api INV17) on main say the touch presentation resolver uses the theme's one compact definition, width below the effective md point (768 by default) or height below 500, and keeps no size numbers of its own. With 744 here, a coarse-pointer screen 744 to 767 px wide (an iPad mini in portrait) gets the popover where FR9 gives the sheet. main exports compactDefinition() from theme/themeAdaptations.ts; building COMPACT_QUERY and COMPACT_TOUCH_QUERY from it would conform.

* 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

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 condition vocabulary ("Future environment fields require explicit additions to this closed vocabulary"), and neither main nor this pull request amends it, so platform needs that amendment first or should come out of this change.

* Cross-Fade Transitions on (opacity CABasicAnimation 0.5 s,
* (0.42, 0, 0.58, 1)), Oct 23 2026
*/
reducedMotionCrossFadeMs: 500,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This 500 ms value becomes the theme role --motion-timing-reduced-motion-cross-fade-ms. design:reduced-motion DR2 requires a reduced-motion cross-fade of 200 ms or less under a coarse pointer, and DR4 says theme values offer no way to keep a longer animation or springs under reduced motion. Excluding the reduced-motion entries from the themable roles (as press.scrollBrakeMs is excluded below) would keep the theme API within DR4.

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-030 FR10 and FR11: a new test type is not a new surface, and new coverage must join an existing owner rather than add a separately routed lane. This job routes itself and is not in the test join's needs, so a failing interaction proof fails no required context.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No component reads the motion roles or the animator yet, so sheets, alerts, menus and navigation do not play these springs today and retuning springs.sheetPresent has no visible effect. Could this section say so, or wait until a component ships its touch presentation (as AST-094's current-state impact suggests)?

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.2KB +294B 12.4KB 12.4KB 0B
core/AppShell 57.9KB 58.1KB +242B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 39.9KB 40.2KB +303B 6.4KB 6.4KB 0B
core/Banner 47.8KB 48.1KB +289B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.3KB 54.6KB +251B 9.9KB 9.9KB 0B
core/BreadcrumbItem 66.4KB 66.6KB +297B 11.0KB 11.0KB 0B
core/Button 43.3KB 43.5KB +273B 9.2KB 9.2KB 0B
core/Calendar 54.9KB 55.2KB +290B 10.0KB 10.0KB 0B
core/Carousel 50.2KB 50.5KB +316B 10.2KB 10.2KB 0B
core/ChatComposer 60.6KB 60.9KB +270B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.7KB 56.0KB +264B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.7KB 56.0KB +264B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.5KB 46.8KB +264B 9.5KB 9.5KB 0B
core/ChatLayout 48.8KB 49.1KB +273B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.4KB 46.6KB +255B 9.6KB 9.6KB 0B
core/ChatSendButton 45.8KB 46.1KB +265B 9.2KB 9.2KB 0B
core/CheckboxInput 49.9KB 50.2KB +273B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.0KB 54.3KB +244B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.5KB 60.8KB +278B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 50.9KB 51.2KB +281B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 55.8KB 56.1KB +318B 10.6KB 10.6KB 0B
core/ContextMenu 77.7KB 78.0KB +275B 14.6KB 14.6KB 0B
core/ContextMenuItem 16.6KB 16.8KB +280B 5.2KB 5.2KB 0B
core/DateInput 99.9KB 100.1KB +243B 14.8KB 14.8KB 0B
core/DateRangeInput 69.4KB 69.6KB +266B 11.2KB 11.2KB 0B
core/DateTimeInput 110.1KB 110.3KB +248B 15.2KB 15.2KB 0B
core/DialogHeader 48.8KB 49.1KB +280B 9.8KB 9.8KB 0B
core/DrawerHeader 48.9KB 49.2KB +249B 11.2KB 11.2KB 0B
core/DropdownMenu 72.7KB 73.0KB +278B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +284B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +287B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +275B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.1KB 58.3KB +270B 9.5KB 9.5KB 0B
core/FileInput 54.1KB 54.4KB +262B 10.3KB 10.3KB 0B
core/IconButton 43.3KB 43.6KB +273B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.3KB 63.6KB +288B 10.1KB 10.1KB 0B
core/Lightbox 51.2KB 51.4KB +249B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.8KB +288B 7.6KB 7.6KB 0B
core/ListItem 53.5KB 53.7KB +268B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.7KB +247B 17.4KB 17.4KB 0B
core/MobileNav 51.0KB 51.3KB +264B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.5KB +259B 9.4KB 9.4KB 0B
core/MoreMenu 72.9KB 73.2KB +278B 14.7KB 14.7KB 0B
core/MultiSelector 76.2KB 76.4KB +270B 14.6KB 14.6KB 0B
core/NumberInput 55.7KB 56.0KB +278B 10.7KB 10.7KB 0B
core/Outline 28.6KB 28.9KB +290B 4.0KB 4.0KB 0B
core/Pagination 87.7KB 88.0KB +263B 15.3KB 15.3KB 0B
core/Popover 49.8KB 50.1KB +238B 12.1KB 12.1KB 0B
core/PowerSearch 155.1KB 155.3KB +263B 18.6KB 18.6KB 0B
core/RadioListItem 52.1KB 52.4KB +286B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 79.1KB 79.4KB +268B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 14.8KB +251B 5.1KB 5.1KB 0B
core/SideNav 58.0KB 58.3KB +272B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.9KB +242B 9.5KB 9.5KB 0B
core/SideNavHeading 54.7KB 55.0KB +248B 10.1KB 10.1KB 0B
core/SideNavItem 54.5KB 54.8KB +266B 10.1KB 10.1KB 0B
core/Step 32.4KB 32.7KB +279B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.7KB +265B 9.6KB 9.6KB 0B
core/Switch 48.4KB 48.7KB +285B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.6KB 51.9KB +276B 10.0KB 10.0KB 0B
core/Table 84.0KB 84.2KB +253B 15.1KB 15.1KB 0B
core/TableCell 78.8KB 79.1KB +267B 14.8KB 14.8KB 0B
core/TableHeaderCell 78.6KB 78.8KB +272B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.4KB 52.6KB +278B 11.2KB 11.2KB 0B
core/TextInput 53.5KB 53.8KB +281B 10.5KB 10.5KB 0B
core/Thumbnail 50.7KB 50.9KB +274B 11.0KB 11.0KB 0B
core/TimeInput 86.8KB 87.1KB +266B 13.8KB 13.8KB 0B
core/Timestamp 27.5KB 27.5KB -1B 9.5KB 9.5KB 0B
core/Toast 59.8KB 60.1KB +258B 9.7KB 9.7KB 0B
core/ToggleButton 44.1KB 44.4KB +243B 9.3KB 9.3KB 0B
core/Token 29.6KB 29.9KB +289B 4.5KB 4.5KB 0B
core/Tokenizer 64.8KB 65.1KB +306B 11.7KB 11.7KB 0B
core/TopNav 51.9KB 52.1KB +277B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.4KB +231B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.4KB +268B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.8KB +258B 10.1KB 10.1KB 0B
core/TreeList 32.5KB 32.8KB +274B 4.7KB 4.7KB 0B
core/Typeahead 61.5KB 61.7KB +268B 11.3KB 11.3KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@vjeux
vjeux force-pushed the uikit/f2-arena-fixes branch from eaa20fb to b72a600 Compare October 25, 2026 01:44
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
vjeux added 10 commits October 27, 2026 00:37
…an the edge band decides at 10 px again, DEC-22) into the experimental touch exports
…ouch decides at 15 px on an element within the band and at 10 px beyond it, DEC-22; meta#8641 bec2283) into meta#8270
…8641's moving-touch claim kept on this line's internals delivery) into meta#8315

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
…xposes no ontouchstart is taken once

Chromium fixes ontouchstart when the page is created, so turning touch
emulation on later sends Touch Events without it: the host reads Pointer
Events for the finger and must not read its Touch Events too. A one-finger
long press fails here, the finger tracked twice.
…from their pointer events

install() reads a finger from its pointer events when the window has no
ontouchstart, but the four touch handlers still took the Touch Events a
browser sends anyway (Chromium after touch emulation turns on), tracking
the finger twice, so a one-finger long press never recognized. Each touch
handler now returns while usesTouchEvents is false, after noting the
fingers down, as install()'s comment promised.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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. perEventPointer and actionGroup attach touch work where the primary pointer is fine, which spec:AST-094 FR19 and FR24 currently forbid, and the exceptions they rely on exist only in #8469 and #8622. An engineering owner records that decision by commenting /approve-spec 3d7fa2ebcbcb2089a5cf9b0cf63b19e641f3ff3c on this pull request, and the author may do so if they are an engineering owner; otherwise the two options wait for those pull requests. Whatever the decision, these changes are needed:

  • Remove the tracker.offsetBy call in the rotation setter (RotationGestureRecognizer.ts:56), which doubles a re-based rotation's release velocity, and assert the release value in its tests.
  • Add the real-touch browser cases that AST-094's FR18–FR22 verification row names: an edge pan on an element wider than the band, and a still finger with sub-pixel jitter over a scroll view.
  • Reduce the 24,598-line compare by landing the predecessors and moving the motion, build-plugin, harness and finger-scroll pieces to their own pull requests.
  • Fix the fine-pointer warning for action groups and the stale GestureHost.ts comments, and name the treeshakeCompensation default in the build changeset.

[Automated review]

this.rotationValue = value;
// The velocity continues from the re-based value, without a jump: the
// samples shift with it (as the pinch's scale setter scales them).
this.tracker.offsetBy(delta, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The tracker samples the raw lastAngle (lines 90 and 106), so shifting its samples here makes every re-base read as extra turn. A listener that sets rotation = 0 on each change releases at about twice the true speed: 4.363 rad/s for a 2.182 rad/s turn, against AST-100 FR2. Dropping this call (and delta) keeps the comment at 87–89 true. The tests at 62, 95 and 118 should then assert the release value, not only > 1.5.

if (options.perEventPointer === true) {
this.perEventRecognizers.add(recognizer);
} else if (!this.isCoarse) {
this.warnFinePointer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An actionGroup attach also reaches this branch when the pointer is fine. The warning then says the attachment "stays inactive until the pointer is coarse", although the host serves the group (746–764). If the option survives, skip the warning for action groups.

* input selectable); restored when the contact ends. Only where the
* primary pointer is coarse: a mouse never takes this lock, nor a finger
* or a pen where the primary pointer is fine.
* - Each element with a recognizer that can claim a touch while it moves (a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Nothing else is ever cancelled" no longer holds. The kept press and the scroll-edge hold (1649–1659) cancel moves of a touch no recognizer has claimed yet, and a cancelled gesture now keeps a moving finger claimed until it lifts. Name FR19's three exceptions and the cancelled-claim keep here.

<div id="hscroll" style="height: 300px; overflow-x: auto; overscroll-behavior: contain">
<div style="width: 3000px; height: 300px; background: linear-gradient(90deg, #eef, #cce)">wide</div>
</div>
<div id="edge" style="position: fixed; left: 0; top: 0; width: 40.2px; height: 300px"></div>`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This fixture's edge element is 40.2 px wide, within the band. AST-094's FR18–FR22 verification row also asks for a real-touch case on an element that reaches beyond the band (DEC-22: touch-action left at auto, a claim at 10 px), and for a still finger with sub-pixel jitter over a scroll view.

'@astryxdesign/build': patch
---

[fix] `astryxStylex` resolves `defineConsts` values read from another file. With the `{stylexOptions}` form (the one Storybook uses), a file that read only constants from a `.stylex.ts` module lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. In either form, a constant defined on the other side of the layer split, or in an Astryx package (compiled with the library class-name prefix) and read from an app file (compiled with its own), stayed unresolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please also say that the {stylexOptions} form now defaults treeshakeCompensation to true, and that passing treeshakeCompensation: false restores the previous output (AST-017 FR3).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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 press tracker's perEventPointer and actionGroup attach touch work where the primary pointer is fine, which spec:AST-094 FR19 and FR24 rule out as they stand on main. An engineering owner needs to update FR19 and FR24 for the press-feedback exception (proposed in #8469) and provide the governing spec for action groups (proposed in #8622), or approve both here by commenting /approve-spec <full head SHA>, which the author may do as an engineering owner. Whatever the decision, these changes are needed:

  • Cancel a claimed finger's touchmove where the host now skips Touch Events (no ontouchstart): cancelIfClaimed still finds the finger only through touchIds, and the second-finger check sits behind the same early return.
  • Remove the tracker.offsetBy call in the rotation setter, which doubles a re-based rotation's release velocity, and assert the release value in its tests.
  • Add the FR18–FR22 real-touch cases for an edge pan on an element wider than the band and for a still finger with sub-pixel jitter.
  • Reduce the 24,648-line compare by landing the predecessors and splitting the separable pieces, name the treeshakeCompensation default in the build changeset, and fix the action-group warning that says the attach stays inactive.

[Automated review]

for (const touch of moved) {
const id =
this.touchIds.get(touch.identifier) ??
(this.inputMode === 'pointer-events'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On the path this head adds (no ontouchstart, so onTouchStart returns before touchIds is filled), this lookup finds no finger and a claimed finger's touchmove is never cancelled, against spec:AST-094 FR19 ("A recognizer that begins MUST claim its touches by cancelling touchmove"). Keying the fallback on the channel the host actually reads covers both pointer-sourced paths:

const id =
  this.touchIds.get(touch.identifier) ??
  (!this.usesTouchEvents ? this.pointerFingerAt(touch) : undefined);

this.rotationValue = value;
// The velocity continues from the re-based value, without a jump: the
// samples shift with it (as the pinch's scale setter scales them).
this.tracker.offsetBy(delta, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The tracker is fed the raw unwrapped angle (lines 90 and 106), so shifting its samples here reads every re-base as extra turn: 8 steps of 2° every 16 ms release 2.182 rad/s untouched and 4.363 rad/s when a listener sets rotation = 0 on each change, against spec:AST-100 FR2. Without this line (and delta) the setter no longer touches the tracker, so the re-based release equals the untouched one; a test asserting a release near 2.18 rad/s would catch it.

'@astryxdesign/build': patch
---

[fix] `astryxStylex` resolves `defineConsts` values read from another file. With the `{stylexOptions}` form (the one Storybook uses), a file that read only constants from a `.stylex.ts` module lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. In either form, a constant defined on the other side of the layer split, or in an Astryx package (compiled with the library class-name prefix) and read from an app file (compiled with its own), stayed unresolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/build/src/vite.ts 675 also moves a default: the {stylexOptions} form now passes treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3 asks the changeset to name the old and new default and the option that restores the old one, for example: "The {stylexOptions} form now defaults StyleX's treeshakeCompensation to true, as the zero-config form does; pass treeshakeCompensation: false to keep the previous output."

if (options.perEventPointer === true) {
this.perEventRecognizers.add(recognizer);
} else if (!this.isCoarse) {
this.warnFinePointer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With actionGroup, this warning fires although the host serves the group where the pointer is fine (746–764, 2080–2227), and its text says the attach "stays inactive until the pointer is coarse" (1085–1087). If the option survives the spec decision, skipping the warning when options.actionGroup is set keeps the message true.

vjeux added 3 commits October 27, 2026 05:05
… a mouse press

At the document, an event from inside an open shadow root is retargeted to
the shadow host, and so is a touch's target, so the host looked for
participants from the shadow host and found none inside the root: the
browser then sent its own mousedown and click. Both cases, a finger and a
mouse, attach a tap to an element in the shadow root. Fail until the host
resolves targets through the composed path.
…composed path

Inside an open shadow root the document's listeners see the shadow host,
in event.target and in a touch's target alike, so a recognizer on an
element in the root never got its finger, and nothing claimed it. landedOn
takes the composed path's first element for the event's own target and for
a touch whose target is the event's (another finger keeps its own): a
touch's start, a pointer's press and its pairing, the last touch a scroll
reads, a cancelled pointer's touch and hover.
…he host resolves targets through the composed path) into meta#8320

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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: usePressTracker's perEventPointer and actionGroup attach touch work where the primary pointer is fine, which spec:AST-094 FR19 and FR24 rule out as they stand on main. An engineering owner needs to update FR19 and FR24 for the press-feedback exception (proposed in #8469) and provide the governing spec for action groups (proposed in #8622), or approve both here by commenting /approve-spec <full head SHA>, which the author may do as an engineering owner. Whatever the decision, these changes are needed:

  • Match the targets landedOn now resolves into a shadow tree through the flat tree in onScroll and pointerFingerAt: Node.contains stops at the shadow boundary, so a flick that starts inside a web component no longer counts as the finger's scroll, and an ancestor's claim under inputMode: 'pointer-events' is lost.
  • Cancel a claimed finger's touchmove where the host skips Touch Events (no ontouchstart), and run the second-finger check before that early return.
  • Remove the tracker.offsetBy call in the rotation setter, which doubles a re-based rotation's release velocity, and assert the release value in its tests.
  • Add the FR18–FR22 real-touch cases for an edge pan on an element wider than the band and for a still finger with sub-pixel jitter.
  • Reduce the 24,707-line compare by landing the predecessors and splitting the separable pieces, name the treeshakeCompensation default in the build changeset, and fix the action-group warning that says the attach stays inactive.

[Automated review]

if (
last != null &&
time - last.time < UIKIT_PRESS.momentumMemory.value &&
scroller.contains(last.target)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

last.target is now landedOn(event), the node inside an open shadow root, and Node.contains does not look into a shadow tree: a light-DOM scroller never contains it, so a flick that starts on a web component's row no longer records the finger's scroll and isScrollBraking stays false. scroller.contains(touch.target) below (FR20's scroll route) misses the same finger. Comparing through the flat tree (parentAcrossShadow, as participantsAt does) keeps both working.

for (const touch of moved) {
const id =
this.touchIds.get(touch.identifier) ??
(this.inputMode === 'pointer-events'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where the window has no ontouchstart, usesTouchEvents is false and onTouchStart returns before touchIds is filled, so this fallback never runs and a claimed finger's touchmove is never cancelled (spec:AST-094 FR19). Keying it on !this.usesTouchEvents covers that path; pointerFingerAt then also needs a flat-tree match for a finger that landed in a shadow root.

if (options.perEventPointer === true) {
this.perEventRecognizers.add(recognizer);
} else if (!this.isCoarse) {
this.warnFinePointer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An actionGroup attach under a fine pointer lands here and is told it "stays inactive until the pointer is coarse" (line 1103), while refresh gives the group its pointerdown listener and serves it there (763–781).

this.rotationValue = value;
// The velocity continues from the re-based value, without a jump: the
// samples shift with it (as the pinch's scale setter scales them).
this.tracker.offsetBy(delta, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The tracker samples the raw unwrapped angle (lines 90 and 106), which this setter does not change, so shifting the samples by delta makes the next interval read the re-base as extra turn: with rotation = 0 set on every change, a steady 2.18 rad/s turn releases at about 4.36 rad/s (spec:AST-100 FR2). Removing this line and delta keeps the velocity continuous, as the comment at 87–89 says.

vjeux added 3 commits October 27, 2026 07:08
…adow root; a claimed touch's moves are cancelled where fingers come from pointer events

With targets from the composed path, a finger inside an open shadow root
sits below the scroller only along the flat tree, which Node.contains does
not follow: a scroll under it did not take it, and its flick did not arm
the scroll brake. And where the host reads fingers from pointer events
(inputMode 'auto' with no ontouchstart), the blocking touchmove found no
finger for a claimed pan's touch, so its moves went to the page. Three
cases fail until the host compares along the flat tree and finds that
finger; a fourth pins a finger scroll inside a shadow root.
… tree; a claimed touch finds its pointer finger wherever fingers come from pointer events

containsAlongFlatTree walks parentAcrossShadow, so a finger inside an open
shadow root, now that targets come from the composed path, counts as under
a scroller outside it: a scroll there takes it, its flick arms the scroll
brake, and a touch there brakes. cancelIfClaimed matches a touch to its
pointer finger whenever the host reads fingers from pointer events, not
only under inputMode 'pointer-events', so a claimed pan's moves are
cancelled where a browser that exposes no ontouchstart still sends Touch
Events.
… flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8320

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

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: usePressTracker's perEventPointer and actionGroup attach touch work where the primary pointer is fine, which spec:AST-094 FR19 and FR24 rule out as they stand on main. An engineering owner either updates FR19 and FR24 for the press-feedback exception (proposed in #8469) and provides the governing spec for action groups (proposed in #8622), or approves both here by commenting /approve-spec <full head SHA>, which the author may do as an engineering owner. Whatever the decision, these changes are needed:

  • Compare a finger along the flat tree in isFingerDownIn and pointerFingerAt too: in Chromium, a finger inside an open shadow root is never read as the person's scroll, and its claimed moves are never cancelled where fingers come from pointer events.
  • Remove the tracker.offsetBy call in the rotation setter, which doubles a re-based rotation's release velocity, and assert the release value in its tests.
  • Run the second-finger check before onTouchStart's early return, so a one-finger pan cannot block the page's pinch where the window has no ontouchstart.
  • Add the FR18–FR22 real-touch cases for an edge pan on an element wider than the band and for a still finger with sub-pixel jitter.
  • Reduce the 24,827-line compare by landing the predecessors and splitting the separable pieces, name the treeshakeCompensation default in the build changeset, and fix the action-group warning that says the attach stays inactive.

[Automated review]


/** @internal Whether a finger that landed inside `node` is down now. */
isFingerDownIn(node: Node): boolean {
return this.fingerTargets.some(target => node.contains(target));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Node.contains stops at the shadow boundary, and for a tracked finger the last listener to update fingerTargets is the one watchTarget puts on the inner node, where the browser retargets the touch to that node. A light-DOM scroller (or document) then never contains it: in Chromium with Touch Events, 0 of 19 scroll events under a finger on a recognizer inside an open shadow root read as the finger's in fingerScroll.ts, against 19 of 19 in light DOM. Recording each finger's landed target (composedPath()[0]) and comparing with containsAlongFlatTree would match onScroll.

Comment on lines +1654 to +1658
!(
finger.target === touch.target ||
finger.target.contains(touch.target) ||
touch.target.contains(finger.target)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cancelIfClaimed now reaches this wherever fingers come from pointer events, but finger.target is the node inside an open shadow root while the claiming ancestor's listener sees the touch retargeted to the shadow host, and neither contains the other. In Chromium under inputMode: 'pointer-events', a claimed pan over shadow-root content cancelled 0 of 9 moves (light DOM: 9 of 9), against spec:AST-094 FR19. containsAlongFlatTree in both directions would match it.

Comment on lines +1461 to +1466
if (!this.usesTouchEvents || !this.takeEvent(event)) {
return;
}
if (this.fingerTargets.length > 1) {
this.failUnclaimedLoneFingerPans();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where the window has no ontouchstart this returns before the second-finger check, and onPointerDown has none, so an unclaimed one-finger pan survives a second finger; now that cancelIfClaimed matches such touches to their pointer finger, that pan can begin mid-pinch and cancel its moves (spec:AST-094 FR10). Running failUnclaimedLoneFingerPans before the early return would keep the page's pinch.

this.rotationValue = value;
// The velocity continues from the re-based value, without a jump: the
// samples shift with it (as the pinch's scale setter scales them).
this.tracker.offsetBy(delta, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The tracker samples the raw unwrapped angle (lines 90 and 106), which this setter does not move, so shifting the samples makes each interval read the re-base as extra turn: on the test's stream (2° every 16 ms) the release is 2.182 rad/s untouched and 4.363 rad/s with rotation = 0 on each change. Deleting this line and its delta, and asserting a release near 2.18 rad/s instead of > 1.5, would keep spec:AST-100 FR2.

if (options.perEventPointer === true) {
this.perEventRecognizers.add(recognizer);
} else if (!this.isCoarse) {
this.warnFinePointer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An actionGroup attach under a fine pointer lands here and is told it "stays inactive until the pointer is coarse" (1116–1117), while refresh gives the group its pointerdown listener and serves it there (776–794). If the option survives the spec decision, skipping the warning for an action group would keep the message true.

'@astryxdesign/build': patch
---

[fix] `astryxStylex` resolves `defineConsts` values read from another file. With the `{stylexOptions}` form (the one Storybook uses), a file that read only constants from a `.stylex.ts` module lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. In either form, a constant defined on the other side of the layer split, or in an Astryx package (compiled with the library class-name prefix) and read from an app file (compiled with its own), stayed unresolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/build/src/vite.ts 675 also moves a default: the {stylexOptions} form now passes treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3 asks the changeset to name the old and new default and the option that restores it (treeshakeCompensation: false).

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Closing: carried by #8360 (this head, a8b174a, is in it by hash). The review items:

This branch was successfully deployed

1 active deployment
Preview — a8b174a7 Deployed Oct 27, 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.

2 participants