Skip to content

spec(AST-094, AST-057): gestures work on the web for every pointer (FR43) - #8646

Merged
cixzhang merged 23 commits into
mainfrom
spec/gestures-on-web
Oct 27, 2026
Merged

cixzhang merged 23 commits into
mainfrom
spec/gestures-on-web

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Decided (Cindy, 2026-10-26 20:37, approving her 15:39/15:40 direction: "a lot of these gestures work well in web as well, so we can likely keep it enabled on web", "aside from the 44px tap size requirement").

User impact

Desktop users get the same direct manipulation as phone users: drag a sheet or a card, pan with a trackpad, pinch with ctrl-wheel, long-press or right-click (or Shift+F10) for the same context actions, and press-tracking on buttons and menus. Gestures that would fight the browser stay off: swiping rows with a mouse would steal text selection and native drag.

Records

  • spec:AST-094 FR43 (new):
    • Gestures attach for every pointer: mouse, pen, and finger.
    • These stay off: row swipe actions under a mouse (a trackpad swipes rows), pen pinch, the screen-edge pan under a mouse, trackpad, or pen, and the touch target size (coarse-only, never overlapping a neighbor).
    • Web adaptations: a pan takes wheel and trackpad scrolling; a pinch takes ctrl-wheel; one shared context request answers long press, right click, and Shift+F10; the press model gets a fine-pointer profile with no delay; a reorder starts on a handle drag without a hold.
    • Only components that use a gesture listen under a fine pointer, and the change adds no stylesheet bytes (AST-080).
  • FR19: recognizers attach for every pointer, with FR43's exceptions.
  • FR24: a component with no gesture registers nothing. Under a fine pointer a gesture user keeps one listener on its element and press-time document listeners, removed at release. That's the action-group pattern from spec(family): action groups — press, slide across, act on lift (AST-094 FR26–FR28) #8622, now the general rule.
  • FR11: a third, fine-pointer press profile with no delay.
  • Scope: menus' FR26–FR28/FR30 apply under every pointer, and "a mouse keeps today's behavior" is gone.
  • Non-goals: only pointer presentations' timing and easing stay with theme tokens.
  • DEC-21 records the decision, and supersedes the coarse-only press controller (a mouse press now uses the press model instead of :active).
  • spec:AST-057 (amended throughout): a mouse drag never swipes a row and hover never uncovers it; a trackpad's horizontal scroll (and a mouse's horizontal wheel, which sends the same events) reveals or fires row actions as a finger does. Under reduced motion a trackpad-started swipe arrives at once (DR3). The amendment covers the intent, ownership, non-goals, FR4, AR2 (a trackpad-opened row follows AR1), the platform line, the AR row, and DEC-4, and adds DEC-7. AST-094 FR43 matches.
  • Consistency amendments:
    • module:DropdownMenu/useMenuPress FR1, FR2, FR7, FR9 and the delay row: a finger or pen follows AST-094 FR26–FR30 under every primary pointer, and a mouse keeps the module's rules.
    • AST-094's menu scope says a mouse keeps useMenuPress FR1–FR3/DEC-3, so a release outside closes the menu.
    • design:user-states DR1/DR3 and family:buttons' visual-state row use FR11's profiles under every pointer.
    • FR11 is retitled "Three press profiles".
    • The FR24 and INV13 rows use the new budget, and the shared press controller counts once per page.
  • architecture:react-component-runtime INV13: the gesture host drops its touch-only query; pointer and hover queries live with the environment queries.

Reconciliation: #8622's action-group FR19/FR24 exception becomes the general rule here (whichever merges second takes the general wording). #8628's scroll-edge exception is about cancelling moves and is unaffected. DEC-21's number assumes DEC-15–DEC-20.

Writing rubric results

  • R1 present tense: pass. R2 research: none in the records.
  • R3 verification: a FR43 row.
  • R4 rejected alternatives: one line in DEC-21. R5 change references: none in the records. R6 open questions: none.

@vercel

vercel Bot commented Oct 27, 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:53pm UTC

Request Review

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 10d85dd

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 3:20 PM UTC

@github-actions

github-actions Bot commented Oct 27, 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/Button 43.3KB 43.3KB 0B 9.2KB 9.2KB 0B
core/ContextMenuItem 16.6KB 16.6KB 0B 5.2KB 5.2KB 0B
core/DropdownMenu 72.7KB 72.7KB 0B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.1KB 0B 5.6KB 5.6KB 0B
core/DropdownMenuDivider 6.6KB 6.6KB 0B 3.6KB 3.6KB 0B
core/DropdownMenuGroup 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/DropdownMenuItem 15.4KB 15.4KB 0B 5.2KB 5.2KB 0B
core/DropdownMenuRadioGroup 6.0KB 6.0KB 0B 3.2KB 3.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.2KB 0B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.1KB 58.1KB 0B 9.5KB 9.5KB 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 504 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

…ller at once; the scroll brake stays touch-only
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec c037c2f

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026
@vjeux

vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Questions the FR43 / DEC-21 text leaves open, collected from the lanes building on it, one line each, the reason first. None of these hold the open pull requests; they shape the FR43 follow-ups built on the host pieces. Where a lane already has a reading, it's in brackets; "yes, that" is a fine answer.

Shared: wheel, trackpad and ctrl-wheel

  • A scroll has no lift: does a wheel or trackpad pan end after an idle gap with a release velocity, and does macOS momentum count toward that velocity, end the pan, or get ignored? (PickerWheel coast vs snap, Toast dismiss, pagers' release rule, Lightbox paging)
  • A ctrl-wheel stream has no lift either: what ends a ctrl-wheel pinch, for the rules that decide at the release? (Lightbox pinch close)
  • Wheel deltas arrive in jumps and mixed axes: does a wheel pan claim its axis by FR16's |dx| ≥ |dy| over the accumulated deltas at 10 pt, so a vertical wheel over a horizontal pager keeps scrolling the page? (pagers)
  • A page scroll would otherwise change values and minimize bars: does every pan take the wheel, or are value controls (Slider's thumb, as native range inputs ignore the wheel) and scroll watchers (TabBar's minimize on scroll down) exempt? [lanes read: exempt]
  • A scroll-edge pan shares its axis with the scroller under it: does it take a wheel only while every scroller under the pointer is at its edge and the scroll pulls away (FR19's rule for a wheel), and does a wheel over a sheet's handle move the sheet? (BottomSheet, the F2 host)

Shared: a mouse on a pan

  • A mouse has no slop: does a fine pointer's pan claim at once, or at the 10 pt FR17 gives a finger? (Slider thumb, BottomSheet handle 1:1 today) Answered by 848021a: FR19's 10 px governs every claim outside FR18's edge band.
  • macOS has no coast: does a mouse release use UIKit's projection, or stop where the pointer let go? (Slider, BottomSheet's release rule, Lightbox zoom's rubber band and coast)
  • A mouse drag over content takes text selection and native drag, which is AR2's reason for rows: besides row swipes, which drags stay off under a mouse: a toast's swipe, a screen's content back swipe, a pager's track, Lightbox's drag to dismiss, TabBar's lens? [Toast, NavigationStack, pagers read: off]
  • A pen is precise but held like a finger: does it take the touch path (a hold before reorder, the touch hold timeline) or the fine-pointer path? (List reorder, context request)

Shared: press model under a fine pointer

  • "clears at the release": at once with no fade, like the :active it replaces, or on the control and cell release clocks (UIButton 250 ms, cell 200 ms)? (press controller, selection controls, List rows) [lanes read: at once] Answered by 848021a: the same press controller and release fade as a finger's (FR11).
  • Rows are cells, which highlight 150 ms after touch-down: does a cell take the no-delay profile under a mouse too? [List reads: yes]
  • Some controls paint no press under any pointer (SearchField's buttons, TabBar's tabs): does DEC-21 make every control adopt the press model, or only replace :active where a control paints one? [lanes read: only replace]
  • The text names only the mouse press: does a Space press on a focused button move to the press model on every device? (the two-slot press controller)
  • AST-080 FR8's "adds no stylesheet bytes": zero net bytes per component, or for the package stylesheet as a whole? (retiring the :active arms is near neutral, not exact for every variant)

Shared: touch-only rules on a touchscreen laptop

  • FR41's text still says "real touch input" while the host checks a coarse primary pointer: does a held finger or pen on a fine-primary touchscreen take the selection lock, decided by the press's pointer type? (reorder handle, NumberInput's hold-repeat) [F2 reads: by the press]
  • FR18's touch-action: manipulation for a kept press is written "where the pointer is coarse": is it written under every pointer now? (NumberInput asserts auto under a fine pointer today) Answered by 848021a: FR18 names no pointer and FR19 attaches recognizers for every pointer; the FR43 pull request writes it under every pointer.
  • FR13's touch presentation and FR43's gestures now diverge: do the touch presentation's pieces stay coarse-only while their gestures take every pointer: List's update, edit-mode and expansion motion; PageControl's ten-slot window; RefreshControl's drawing; Switch's thumb lift and springs; Slider's lift; ContextMenu's lift (FR33 vs FR13); DropdownMenu's growth and outside-pan close (FR31, FR32)? [lanes read: coarse-only]
  • spec(AST-094): every touch presentation holds pointer input while it enters (FR35) #8618's FR35 kept the mouse unchanged and spec(AST-094, AST-057): gestures work on the web for every pointer (FR43) #8646 drops that sentence from the FR31–FR35 scope: does the entrance hold apply under a mouse now, so a mid-entrance backdrop click no longer closes? (BottomSheet, Dialog, Drawer)
  • holdSelection is null under a mouse ("part of the one touch model"): does a row that pushed a screen under a mouse stay selected until the pop? (List, NavigationStack)
  • FR24's "keeps one listener on its element": per component, or per gesture? (TabBar's lens on the capsule and its minimize watcher on the page's scroller)

Context request

  • A mouse hold fights text selection and native drag, FR43's reason for mouse swipes: does a mouse long press make no request, leaving right click and Shift+F10? [ContextMenu and F2 read: no request]
  • ContextMenu focuses the first row for a keyboard open and lights nothing for a pointer open: does the request report which input made it? [F2 reads: yes]
  • spec(AST-094, AST-057): gestures work on the web for every pointer (FR43) #8646 drops "a mouse keeps today's behavior" only from FR26–FR28 and FR30: does a finger under a fine primary pointer get UIKit's hold timeline (commit 400 ms, menu 735 ms), or today's 500 ms? [F2 reads: UIKit's]
  • FR33's handoff names only "a held finger": does releasing the secondary button over a row after a right-click press pick that row (macOS press-drag-release)?
  • The trigger's action is the request, not a click: does a mouse press on a ContextMenu trigger paint under the fine profile?
  • A long press on a stepper is its repeat (FR41): on an element whose press tracker keeps the press, do a long press, a right click and Shift+F10 stay its own, with no shared request and a right click starting no press? [F2 reads: yes]

Per component

  • Switch, FR6: AST-020 FR9's exception only says "may", and the shared contract presses with a mouse: does a mouse press slid off and released outside still toggle, or cancel like a native checkbox?
  • Slider: the desktop convention and today's mouse set the value at press-down, while the touch tap sets it at release (WCAG 2.5.7): which does a mouse track click do?
  • SegmentedControl: it's spec(family): action groups — press, slide across, act on lift (AST-094 FR26–FR28) #8622's named exception with "Under a finger" in its own record (spec(SegmentedControl): touch presentation — slide, overlay on press, general pressed-thumb lift (D33) #8596 FR7–FR10): does drag-to-select extend to a mouse and a pen?
  • List reorder: AST-103's claim follows the 148 ms content-touch delay so a flick on the handle scrolls: does "starts on a drag from its handle without a hold" also cover a finger under a coarse pointer, which would need touch-action: none on the handle?
  • NavigationStack: FR18's 15 px band (DEC-13) still describes the finger's edge pan and yields to Safari's own: does "the screen-edge pan on every web input" turn off a finger's edge back swipe too, or only mouse, pen and trackpad?
  • NavigationStack: DR3 has a fine pointer's push and pop change at once: does a horizontal trackpad scroll drive the interactive pop, and finish on the pop spring?
  • Lightbox: FR43 names no double-click: does it map to the double tap (zoom about the clicked point), or keep main's 2× about the centre?
  • Lightbox: Photos toggles its controls on a tap: does a mouse click on the photo toggle them, or keep them as main does?
  • Carousel: its paging is its own presentation (public-component-api INV16): under a fine pointer, does a hasSnap carousel keep native scroll-snap, or move to the pager's one-child settle?
  • DropdownMenu: FR26–FR28 apply under every pointer with no popover carve-out, while FR25 names only FR29 and FR33 as what popover keeps: does a popover menu clear a cancelled finger's lit row?
  • SearchField: its focus tap claim exists only to stop Mobile Safari's reveal pan, and under a mouse it would take caret placement: may it stay touch-only (and pen?), despite FR19's "recognizers attach for every pointer"?
  • ChatLayout: there is no on-screen keyboard under a mouse, and a wheel would blur the composer: does keyboardDismissMode stay finger-only? [ChatLayout reads: yes]
  • AppShell and NavigationStack's scroll edge effect: UIKit treats pointer scrolling as a user scroll: does a wheel or trackpad scroll fade the veil like a finger's?
  • RefreshControl: FR13 gives its drawing to touch devices only: does a trackpad pull at the scroll top refresh, and with which drawing?

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

Requesting changes: the every-pointer direction is owner-approved, but it leaves current records that now contradict it, so please amend them in this pull request.

  • useMenuPress.spec.md FR1, FR2, FR7, FR9 and its long-press-delay row still keep a fine-pointer finger on the module's own rules by citing the old FR19, and the menu scope no longer says whether a mouse keeps the module's FR3 and DEC-3.
  • spec:AST-057 AR2's trackpad reveal contradicts the same record's DEC-4, non-goals, intent, platform line and AR1–AR3 verification row, and AR1 and AR2 disagree about a row a trackpad leaves open.
  • FR11 is still titled "Two press profiles", and the user-states design (DR1, DR3) and the buttons family still hand FR11 their timing only under a finger.
  • The FR24 row and the INV13 row of architecture:react-component-runtime still fail a touch listener under a fine pointer.

[Automated review]

views and activates by the browser's click on a lift inside. On the web a
control's press ends when the browser takes the touch (FR20), and its
activation stays the browser's click.
Under a fine pointer a control uses a third profile with no delay: its

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 adds a third profile, but FR11 is still titled "Two press profiles" and says "Press feedback MUST follow one of two native profiles" (line 185). The records that own the press look, the user-states design (DR1, DR3) and the buttons family's visual-state row, still hand FR11 their timing only under a finger, so they need the fine-pointer profile too.

Comment on lines +312 to +313
and Selector's listbox, whose options count as rows) under every pointer
(FR43); FR29 applies to DropdownMenu's trigger under the same

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 FR26–FR30 under every pointer, packages/core/src/DropdownMenu/modules/useMenuPress.spec.md now contradicts this: its FR1, FR2, FR7 and FR9 keep a fine-pointer finger on the module's rules "(spec:AST-094 FR19)", and its delay row says "500 ms where it is fine", while FR29 opens on press. Dropping "A mouse keeps today's behavior" also leaves open whether a mouse follows FR28 and FR29 or keeps the module's FR3 and DEC-3 (a mouse release outside closes the menu).

Comment thread docs/specs/AST-057-row-actions/spec.md Outdated
Comment on lines +252 to +253
a mouse drag MUST NOT reveal the panel, and hover MUST NOT uncover it. A
trackpad's horizontal scroll swipe MAY reveal the panel, as a pan that takes

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 MAY contradicts the rest of the record, which is unchanged: DEC-4 ("On a pointer that can hover, swipeActions does nothing"), the non-goal "two-finger trackpad swipes", the intent ("touch-only … changes nothing on any other input"), the platform line and the AR1–AR3 verification row ("any effect on a fine pointer fails"). AR1 also makes a resting-open panel's entries focusable buttons while this line keeps the gesture out of the tab order on a hovering pointer.

- **FR24 — Gesture work costs nothing where it is not used.** A component
that uses no gesture registers no gesture listener, observer or timer, and
while a touch presentation is disabled none is active. Under a fine pointer
a component that uses a gesture keeps one listener on its element and adds

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 FR24 verification row (line 559) still fails "a touch listener on a fine pointer", but FR43 gives a finger gestures under every primary pointer and FR19 claims a finger through a non-passive touchmove listener on the element, so a conforming touchscreen-laptop implementation fails that row. Please also say whether the shared document-level press controller, which components install on mount, counts as gesture work under this budget.

(`spec:AST-094` FR19, FR24). The modules are not public exports and are not
small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query, and pointer and hover queries live
with the environment queries; under a fine pointer only a component that

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 INV13 verification row (line 342) still fails "a touch listener under a fine primary pointer"; it needs the new budget this paragraph states.

mouse, a pen, and a finger, not only a coarse primary pointer. These stay
off: swipe actions under a mouse, which would take the browser's text
selection and native drag (`spec:AST-057` AR2); a pinch under a pen; the
screen-edge pan on every web input; and the 44 px touch target, which stays

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 screen-edge pan on every web input" reads as covering a finger in a browser too, which would switch off FR18's screen-edge pan; "under a mouse, a trackpad or a pen" says what DEC-21 means. "the 44 px touch target" is also not defined by any current record yet.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 0329096

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 91bc491

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

Requesting changes: the every-pointer direction is owner-approved at this head, but a few current claims still contradict it, so please amend them in this pull request.

  • useMenuPress.spec.md FR6 and FR11 (with FR3, the Behavior row and the compatibility line) still limit the finger's menu rules to a coarse primary pointer, against the module's own FR2 and AST-094 FR33.
  • FR43's "a reorder starts on a drag from its handle without a hold" names no pointer, so it overrides spec:AST-103 FR3's finger hold; scope it to a mouse, a trackpad or a pen, or amend AST-103.
  • The INV13 verification row, and AST-094's FR24 row, fail the shared press controller that FR24 admits; add an exception for it.

The Current-state impact, the FR43 row's "web edge pan", AST-057's trackpad identification and ctrl-wheel page zoom each need a line too (details inline).

[Automated review]

| FR3 | A press that began inside a menu and is released outside it MUST act on nothing. Under a mouse the menu MUST then close; under a finger or pen it MUST stay open (where the primary pointer is coarse, as `spec:AST-094` FR28 states). | DEC-3; `useMenuPress.test.tsx`, `DropdownMenu.test.tsx`, `Selector.test.tsx` | verify |
| FR4 | A menu whose rows fit MUST declare `touch-action: pinch-zoom`; one that scrolls MUST declare `touch-action: pan-y pinch-zoom` and `overscroll-behavior: contain`. Neither admits a one-finger pan the menu does not scroll, and both leave a two-finger pinch to the browser, so the page stays zoomable over an open menu. The model MUST treat `pointercancel` and a second pointer as the end of the gesture with nothing acting, and MUST NOT re-implement scrolling. | Browser scroll ownership; WCAG 2.2 SC 1.4.4 under `spec:AST-020` FR1; `DropdownMenu.test.tsx` touch-action case, `useMenuPress.test.tsx`, `MenuPinchZoom.a11y.chromium.spec.ts` | verify |
| FR5 | The click the browser reports for a gesture this model tracked MUST NOT act on any row; only the release acts. A click with no tracked gesture behind it, or one whose `detail` is 0, MUST act on its target as before. The swallower arms at a tracked release and disarms at the next `pointerdown` or after the stray-click window. | One gesture, one activation; `useMenuPress.test.tsx` FR5 suite | verify |
| FR6 | At most one row of a menu is lit. Under a finger the row a press began on MUST NOT keep a pressed or hovered look once the pointer left it, so menu rows paint `:active` only under `(hover: hover)`. Where the primary pointer is coarse, `spec:AST-094` FR27 and FR11 govern a finger's lit row and its press look instead. | `DropdownMenuItem.tsx` styles | verify |

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 still gives a finger AST-094 FR27 and FR11 only where the primary pointer is coarse, while FR2 (line 157) now applies them "under every primary pointer". On a touchscreen laptop the two disagree: :active at once under (hover: hover) here, FR11's cell profile there. Suggest: "A finger or pen follows spec:AST-094 FR27 and FR11 instead, under every primary pointer."

| FR9 | Where the primary pointer is coarse, opening a menu from its trigger under a finger and the opening finger's handoff are governed by `spec:AST-094` FR29. Where it is fine, a finger keeps the released behavior (`spec:AST-094` FR19): a tap on the trigger opens the menu through its click, and a press held on the trigger for the long-press delay opens the menu with the finger down, prevents the default of the finger's later `touchmove`s until the gesture ends, and continues under FR1–FR4. | `spec:AST-094` FR29 | as `spec:AST-094` states |
| FR9 | Opening a menu from its trigger under a finger and the opening finger's handoff are governed by `spec:AST-094` FR29, under every primary pointer (`spec:AST-094` FR43); a mouse opens the menu as `component:DropdownMenu` FR8 states. | `spec:AST-094` FR29 | as `spec:AST-094` states |
| FR10 | A finger released on a sub-menu's row MUST open that sub-menu, as the row's own click would (the lift acts, `spec:AST-094` FR26; on a compact touch screen an `adaptive` row drills in, FR34); a finger resting on the row MUST NOT open it. A finger that slides from a row onto the rows of an open flyout picks there under ORD1. | DEC-8; `DropdownMenuSubMenu.test.tsx` press cases | verify |
| FR11 | A pointer that went down elsewhere, such as a context menu's long press, and is handed to the menu as it opens MUST be the opening press. Where the primary pointer is coarse, a finger picks only after the handoff distance, counted from the first position the menu sees after it opened as `spec:AST-094` FR33 counts it, and its lift before that acts on nothing and leaves the menu open. | DEC-8; `useMenuPress.test.tsx` continuation suite | verify |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AST-094 FR33 now reaches a finger under every primary pointer, so "Where the primary pointer is coarse" lets a laptop finger handed to a ContextMenu pick with no handoff travel here and only after 10 pt there. FR3's parenthetical (line 158), the Behavior row (line 41) and the compatibility line (lines 84–87) carry the same coarse-only wording.

takes wheel and trackpad scrolling; a pinch takes a ctrl-wheel; one shared
context request answers a long press, a right click, and Shift+F10; the
press model has a fine-pointer profile with no delay (FR11); and a reorder
starts on a drag from its handle without a hold. Under a fine pointer only 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.

This names no pointer, so with "Gestures attach for every pointer" it covers a finger too, which contradicts spec:AST-103 FR3 ("under a finger, a hold on the handle picks the row up … and a flick still scrolls"), not amended here. Scope it to a mouse, a trackpad or a pen, or amend AST-103 FR3.

| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV12 | Source boundary check plus real-browser tests on a throttled phone profile over a 10-level by 50-sibling tree and two changes in one frame | A per-frame height or layout, a walk past the area or into a subtree other than `display: contents`, an element moved twice for one frame, an added element, or an exported reporting function fails |
| INV13 | Source boundary check plus bundle analysis of a page with several touch components, and real-browser tests on a throttled phone profile, under a fine pointer, and on a hybrid device | A second copy of a touch module on one page, a component bundling its own copy of a module's primitive, an `import()` of a module in a component, a public export or doc entry for the modules, an install call or installed check in the public API, a gesture listener in a component that uses no gesture, or an idle document listener outside a press, or an import of `@astryxdesign/core/internal/*` outside Core and Lab fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR24 admits the shared press controller, whose window and document listeners stay installed between presses (packages/core/src/utils/pressFeedback.ts lines 396–409), but this row fails "an idle document listener outside a press" with no exception, so a page that follows FR24 fails it. Suggest "… outside a press, other than the one shared press controller"; AST-094's FR24 row (line 564) needs the same.

| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse | a drill-in that leaves the parent's rows in place, a change between levels that does not play on its roles, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path fails |
| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity, or an action with no keyboard or mouse path fails |
| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, or a change to native selection elsewhere or to mouse, keyboard, or assistive-technology selection fails |
| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse swipe action, a pen pinch, a web edge pan, a press delay under a fine pointer, an idle listener in a component with no gesture, or added stylesheet bytes, fails |

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 row's inputs include a finger, so "a web edge pan … fails" reads as failing FR18's finger edge pan. FR43 now says "under a mouse, a trackpad, or a pen"; say the same here.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 874ee55

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

A question on FR43 and Reduce Motion. Today a touch drag's release cross-fades under Reduce Motion whatever the primary pointer (design:reduced-motion DR2), and any other call on a fine pointer completes at once (DR3). The animator reads that from the caller: a call started by a touch drag's release says so.

With FR43 a pan also takes a mouse drag, wheel and trackpad scrolling. Under Reduce Motion, should a release from one of those cross-fade like a touch drag's release, or complete at once like any other fine-pointer call? The answer decides whether the animator's release flag means any drag's release or only a finger's.

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

Requesting changes: the every-pointer direction is owner-approved at this head, but three current claims still contradict it, so please amend them in this pull request.

  • FR43's "a reorder starts on a drag from its handle without a hold" names no pointer, so it overrides spec:AST-103 FR3's finger hold; scope it to a mouse, a trackpad or a pen, or amend AST-103.
  • useMenuPress.spec.md FR3, FR6 and FR11, with its Behavior row and compatibility line, still limit the finger's menu rules to a coarse primary pointer, against the module's own FR2 and AST-094 FR33.
  • spec:AST-057 FR5 cross-fades a trackpad-started swipe "because the swipe's own touch pointer started it", while design:reduced-motion DR3 completes it at once on a fine primary pointer; state which one applies.

The trackpad-versus-mouse-wheel signal in AST-057, a shared-press-controller exception in the INV13 and FR24 verification rows, and AST-094's Current-state impact list each need a line too (details inline).

[Automated review]

takes wheel and trackpad scrolling; a pinch takes a ctrl-wheel; one shared
context request answers a long press, a right click, and Shift+F10; the
press model has a fine-pointer profile with no delay (FR11); and a reorder
starts on a drag from its handle without a hold. Under a fine pointer only 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.

This names no pointer, and FR43 opens with "Gestures attach for every pointer, a mouse, a pen, and a finger", so it also covers a finger on a phone. spec:AST-103 FR3 (not amended here) says "under a finger, a hold on the handle picks the row up at spec:AST-094's values, and a flick still scrolls". Suggest scoping it: "… and under a mouse, a trackpad or a pen a reorder starts on a drag from its handle without a hold", or amend AST-103 FR3 in this pull request.

| FR3 | A press that began inside a menu and is released outside it MUST act on nothing. Under a mouse the menu MUST then close; under a finger or pen it MUST stay open (where the primary pointer is coarse, as `spec:AST-094` FR28 states). | DEC-3; `useMenuPress.test.tsx`, `DropdownMenu.test.tsx`, `Selector.test.tsx` | verify |
| FR4 | A menu whose rows fit MUST declare `touch-action: pinch-zoom`; one that scrolls MUST declare `touch-action: pan-y pinch-zoom` and `overscroll-behavior: contain`. Neither admits a one-finger pan the menu does not scroll, and both leave a two-finger pinch to the browser, so the page stays zoomable over an open menu. The model MUST treat `pointercancel` and a second pointer as the end of the gesture with nothing acting, and MUST NOT re-implement scrolling. | Browser scroll ownership; WCAG 2.2 SC 1.4.4 under `spec:AST-020` FR1; `DropdownMenu.test.tsx` touch-action case, `useMenuPress.test.tsx`, `MenuPinchZoom.a11y.chromium.spec.ts` | verify |
| FR5 | The click the browser reports for a gesture this model tracked MUST NOT act on any row; only the release acts. A click with no tracked gesture behind it, or one whose `detail` is 0, MUST act on its target as before. The swallower arms at a tracked release and disarms at the next `pointerdown` or after the stray-click window. | One gesture, one activation; `useMenuPress.test.tsx` FR5 suite | verify |
| FR6 | At most one row of a menu is lit. Under a finger the row a press began on MUST NOT keep a pressed or hovered look once the pointer left it, so menu rows paint `:active` only under `(hover: hover)`. Where the primary pointer is coarse, `spec:AST-094` FR27 and FR11 govern a finger's lit row and its press look instead. | `DropdownMenuItem.tsx` styles | verify |

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 still gives a finger AST-094 FR27 and FR11 only where the primary pointer is coarse, while FR2 (line 157) now applies them "under every primary pointer" and AST-094 FR11 amends this :active sentence. On a touchscreen laptop the two disagree: :active under (hover: hover) here, FR11's cell profile there. Suggest: "A finger or pen follows spec:AST-094 FR27 and FR11 instead, under every primary pointer." Please also say whether a menu row keeps :active under a mouse, since DEC-21 says a mouse press now uses the press model while FR11 gives the no-delay profile only to a control.

| FR9 | Where the primary pointer is coarse, opening a menu from its trigger under a finger and the opening finger's handoff are governed by `spec:AST-094` FR29. Where it is fine, a finger keeps the released behavior (`spec:AST-094` FR19): a tap on the trigger opens the menu through its click, and a press held on the trigger for the long-press delay opens the menu with the finger down, prevents the default of the finger's later `touchmove`s until the gesture ends, and continues under FR1–FR4. | `spec:AST-094` FR29 | as `spec:AST-094` states |
| FR9 | Opening a menu from its trigger under a finger and the opening finger's handoff are governed by `spec:AST-094` FR29, under every primary pointer (`spec:AST-094` FR43); a mouse opens the menu as `component:DropdownMenu` FR8 states. | `spec:AST-094` FR29 | as `spec:AST-094` states |
| FR10 | A finger released on a sub-menu's row MUST open that sub-menu, as the row's own click would (the lift acts, `spec:AST-094` FR26; on a compact touch screen an `adaptive` row drills in, FR34); a finger resting on the row MUST NOT open it. A finger that slides from a row onto the rows of an open flyout picks there under ORD1. | DEC-8; `DropdownMenuSubMenu.test.tsx` press cases | verify |
| FR11 | A pointer that went down elsewhere, such as a context menu's long press, and is handed to the menu as it opens MUST be the opening press. Where the primary pointer is coarse, a finger picks only after the handoff distance, counted from the first position the menu sees after it opened as `spec:AST-094` FR33 counts it, and its lift before that acts on nothing and leaves the menu open. | DEC-8; `useMenuPress.test.tsx` continuation suite | verify |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AST-094 FR33's handoff rule has no pointer condition and now reaches a finger under every primary pointer, so "Where the primary pointer is coarse" lets a laptop finger handed to a ContextMenu pick with no handoff travel here and only after 10 pt there. FR3's parenthetical (line 158), the Behavior row (line 41), the compatibility line (lines 84–87) and DEC-8 carry the same coarse-only wording.

Comment thread docs/specs/AST-057-row-actions/spec.md Outdated
| FR3 | `swipeBehavior` MUST be one enum with the closed values `reveal` and `commit`, default `reveal`. `commit` with more than one entry on a side MUST warn in development, because the other entries have no touch path. | DEC-2; `spec:AST-002` FR15 | Proposed |
| FR4 | A sideways drag on a coarse pointer MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required |
| FR4 | A sideways drag on a coarse pointer, or a trackpad's horizontal scroll, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required |
| FR5 | Under `reveal`, a release past the panel's width MUST leave the row resting open with every entry tappable, and a drag past the commit point or a fling MUST fire the **outermost** entry and no other. Under `commit`, a release past the commit point or a fling MUST slide the row out and fire the outermost entry; nothing MUST rest. After any entry fires, under either model, the row MUST spring back unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2), because the swipe's own touch pointer started it. `onActivate` MUST fire at activation and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DEC-7 lets a trackpad start this swipe, but the reduced-motion sentence still says the row "cross-fades (DR2), because the swipe's own touch pointer started it". design:reduced-motion DR2 covers only a coarse primary pointer or "the release of a touch drag", and DR3 makes every other transition on a fine primary pointer complete at once, so a trackpad swipe gets two answers. Please state which applies (DR3 conforms to the current design record; covering trackpad releases needs a DR2 amendment).

Comment thread docs/specs/AST-057-row-actions/spec.md Outdated
Pointer type is read from the pointer event.
- Unsupported behavior: a device that never produces a coarse pointer gets
no swipe and nothing else changes.
- Unsupported behavior: a device with neither a touch screen nor a trackpad

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A trackpad's two-finger scroll reaches the page as wheel events, and WheelEvent has no pointerType; a mouse's tilt or thumb wheel (and Shift+wheel on macOS) produces the same horizontal wheel events. As written, "Pointer type is read from the pointer event" cannot identify the trackpad path, and a mouse-only device with a horizontal wheel would swipe rows despite AR2 and DEC-7. Suggest defining the path by the input the page receives (a horizontal wheel or scroll gesture), saying what a horizontal mouse wheel does, and updating the modality row (line 220) to match.

| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV12 | Source boundary check plus real-browser tests on a throttled phone profile over a 10-level by 50-sibling tree and two changes in one frame | A per-frame height or layout, a walk past the area or into a subtree other than `display: contents`, an element moved twice for one frame, an added element, or an exported reporting function fails |
| INV13 | Source boundary check plus bundle analysis of a page with several touch components, and real-browser tests on a throttled phone profile, under a fine pointer, and on a hybrid device | A second copy of a touch module on one page, a component bundling its own copy of a module's primitive, an `import()` of a module in a component, a public export or doc entry for the modules, an install call or installed check in the public API, a gesture listener in a component that uses no gesture, or an idle document listener outside a press, or an import of `@astryxdesign/core/internal/*` outside Core and Lab fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AST-094 FR24 admits "the shared press controller, installed once per page", whose document-level scroll and visibilitychange listeners stay installed between presses while any pressable surface is mounted (packages/core/src/utils/pressFeedback.ts lines 395–409). This row fails "an idle document listener outside a press" with no exception, so a page that follows FR24 fails it. Suggest "… an idle document listener outside a press, other than the one shared press controller …"; AST-094's FR24 row (line 569) needs the same.

| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse | a drill-in that leaves the parent's rows in place, a change between levels that does not play on its roles, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path fails |
| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity, or an action with no keyboard or mouse path fails |
| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, or a change to native selection elsewhere or to mouse, keyboard, or assistive-technology selection fails |
| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse swipe action, a pen pinch, a web edge pan, a press delay under a fine pointer, an idle listener in a component with no gesture, or added stylesheet bytes, fails |

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 row's inputs include a finger, so "a web edge pan … fails" reads as failing FR18's finger edge pan. FR43 says "under a mouse, a trackpad, or a pen"; say the same here, and in DEC-21's "the screen-edge pan".

selection and native drag, while a trackpad's horizontal scroll swipes a
row (`spec:AST-057` AR2, DEC-7); a
pinch under a pen; the screen-edge pan under a mouse, a trackpad, or a pen;
and the touch target size, which stays coarse-only and never overlaps 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.

No current record defines "the touch target size" (DEC-21 names a 44 px target; the draft control-rhythm design record leaves target intent open as its OQ1). Please name the size here, apart from AST-020's WCAG 2.5.8 minimum, which holds under every pointer.

Comment thread docs/specs/AST-057-row-actions/spec.md Outdated
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public contract | `Item` carries touch swipe actions. `swipeActions` declares, per side, the verbs a sideways drag uncovers as `ItemSwipeAction[]` — `label`, `icon`, `onActivate`, `variant`, `hasRemoval` — and `Item` renders the panel from them (DEC-1). `swipeBehavior` is `reveal` (rest open) or `commit` (fire on release) (DEC-2). The capability is `Item`'s, available on any row whose role permits interactive descendants — a `listitem`, a role-less row — and out of scope where it does not (`option`, `menuitem*`, a radio) (DEC-3). No hover reveal and no mouse swipe (DEC-4); a trackpad's horizontal scroll swipes as a finger does (DEC-7). No new DOM: the row's root translates and its panels counter-translate (DEC-5). |
| Behavior | On a coarse pointer a sideways drag, or a trackpad's horizontal scroll, moves the row and uncovers the side's panel. Under `reveal` (default) the row rests open so each action is tappable and a long drag fires the outermost; under `commit` the row slides out and the outermost fires; nothing rests. After an action fires the row springs back, or holds out when the action has `hasRemoval` — one bit the component cannot infer, independent of the model. The component waits for nothing afterwards. A short release springs back under both. A mouse never starts the drag; a mostly vertical drag stays the scroller's; a pointer anywhere outside a resting row closes it. |
| End-user impact | A person on a phone gets the flick they expect on a mail row or a settings row, with the row's own look. Nothing changes for anyone else: a mouse, a keyboard, or a screen reader meets the row exactly as today. |

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 DEC-7, trackpad users now swipe rows, so "Nothing changes for anyone else" no longer holds; AST-094's Current-state impact list also needs an entry for this record, the INV13 rewrite, and the fine-pointer profile in the user-states and buttons entries.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 848021a

@cixzhang
cixzhang merged commit c01521c into main Oct 27, 2026
74 checks passed
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…t morph's source copy

AST-057's record takes main's text throughout (meta#8646 drops two-finger
trackpad swipes from the non-goals); this branch keeps only its own
change there: the row's slide and spring motion plays on theme motion
roles (spec:AST-094 FR4), so its durations leave the behavior constants.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…nter gestures) into the theme motion knobs
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…any primary pointer, a mouse's never (spec:AST-094 FR41)

Merged FR41 makes a pen's hold a finger's and keeps a mouse's released
behavior, with no coarse-pointer condition: that carve-out was FR19's,
and meta#8646 removed it. locksSelectionFor still required a coarse primary
pointer, so on a touchscreen laptop a finger's hold on a stepper, a slider
thumb or a ContextMenu trigger still selected text. Fails until the coarse
gate goes.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…FR43 current) under the press release

Conflict in packages/core/src/Button/Button.spec.md, resolved on main's
text: FR15 is meta#8646's rewrite (a press paints through the press
controller, at once under a mouse or a pen, on FR11's control profile
under a finger), with this pull request's own additions kept: under a
coarse primary pointer the overlay paints on Button's own ::before and
a touch press fades over UIButton's measured release (250 ms), a Space
press keeps the 200 ms fade (spec:AST-017 FR59), and under reduced
motion a touch release under a coarse primary pointer cross-fades within
200 ms (design:reduced-motion DR2, DR3). The table is re-padded by
Prettier; no other row changes.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
… through c02-8124-base (main's meta#8646 Button.spec.md conflict)
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…9c0e, meta#8665 6b3e60d) into uikit/c02-switch-every-pointer (main's meta#8646 Button.spec.md conflict)
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…d, main ad427c2 with meta#8646's FR43 current) under the button group's tracking
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

A question on FR43's wheel scrolling, for RefreshControl (#8697). FR43 has a pan take wheel scrolling with no notch exception, so a wheel notch at the top now pulls through a finger's mapping. One 100 px notch maps to about 76 px of overscroll: on a scroll view shorter than about 369 px, where the trigger is its 73 px floor, a single notch starts a refresh; on a taller one (the trigger is 19.8% of the height) it takes more, three on a 714 px page. Should a notch pull less there, or should RefreshControl leave a wheel's notches off? Either needs an amendment; until then #8697 follows FR43 as written.

@github-actions
github-actions Bot deleted the spec/gestures-on-web branch October 28, 2026 06:17
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…e Popover touch presentation

AST-057 takes the dependencies' text, which is main's (meta#8646): the tuning bullet this branch once restored is superseded, and the Popover touch presentation does not touch row actions.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…ws the primary pointer

spec:AST-094 FR43, current since meta#8646: under reduced motion a release
from a mouse, a trackpad, or a pen follows design:reduced-motion DR2 on
a coarse primary pointer and DR3 otherwise. The pager marks only a
finger's release as a touch release, so these pass as they stand:

- a mouse drag's release on a fine primary pointer lands at once, with
  no cross-fade (DR3);
- the same release on a coarse primary pointer cross-fades in at most
  200 ms (DR2);
- a trackpad scroll's release on a fine primary pointer lands at once.

The comment that touch reaches the pager only under a coarse primary
pointer goes: the pager takes every pointer.
cixzhang pushed a commit that referenced this pull request Oct 30, 2026
…ter proofs (#8666)

## Why

Gesture proofs could drive only a finger. The every-pointer requirement (`spec:AST-100` FR4: the same motion from touch, pen and mouse) and #8646, which has gestures attach for every pointer, need a pen stroke and a wheel step as real browser input. A hand-made wheel step was also wrong on phones: Chromium divides it by the device-pixel ratio, so a 120 px step reached a phone page as 40.

## What led to it

We are bringing Astryx's touch components to the behavior of native controls, and their proofs drive real browser input through the interaction harness. With gestures now accepting every pointer, the harness had to drive a pen and a wheel the same way it drives a finger.

## Options and tradeoffs

- Synthetic DOM events (`new WheelEvent`, `new PointerEvent`): untrusted, and a component can tell (`isTrusted`), so they prove nothing about real input.
- Playwright's own wheel as is: a third of the delta in a phone context, and no pen at all.
- The browser's own input commands (this pull request): in Chromium, its DevTools input, with the wheel delta scaled back and pen strokes carrying pressure, tilt and their scripted times; in WebKit, its own wheel in a desktop context. WebKit's phone emulation has no wheel and Playwright has no WebKit pen, so the driver says so with flags and proofs skip with a stated reason.

## Why this choice

It is the only option whose events are trusted and go through the browser's own input pipeline, and it states each engine's limits instead of hiding them.

A run of wheel steps, as a trackpad page swipe sends, goes through `wheelBurst({x, y, steps, everyMs, maxGapMs})`: it sends every step on its schedule without waiting for the one before, resolves once the page has the run's whole delta, and, past `maxGapMs`, throws `WheelPageStallError` when the page's own wheel listeners held the main thread on the step before (WebKit dispatches the next step only once the page is free), and `WheelPacingError`, naming the gaps, when the dispatch stalled before the page. `retakeOnWheelPacing` re-takes a proof once, only on `WheelPacingError`, and lists the re-take; a stall the page caused fails the proof.

## How to check it

1. `pnpm exec playwright test --config playwright.interaction.config.ts internal/interaction-harness/src/harness.interaction.browser.spec.ts --project chromium -g "wheel and pen input"`.
2. Expected: a wheel step is on the page by the time `wheel()` resolves, as exactly its deltas, trusted, with Control when held, whether the page's wheel listeners are passive or not; a pen stroke arrives as trusted pen pointer events with its pressure and its scripted 100 ms between the first and last points.
3. With `--project webkit`, the desktop wheel cases pass and the phone wheel and pen cases skip, each naming why.
4. `pnpm exec vitest run internal/interaction-harness/src/webInput.pacing.test.ts`: the re-take stays red when both attempts are off pace, never re-takes a product failure, and passes and is listed when an off-pace attempt is followed by a good one.
5. `-g "burst"`: a burst paced at 180 ms against a 100 ms bound throws `WheelPacingError` in both engines, as does one paced at 125 ms over a page whose listener takes 30 ms (the page is free before each step arrives); a page that blocks 150 ms on each step is never re-taken (`WheelPageStallError` in WebKit, where a gap is the page's only when the step reached it within a frame of its listeners finishing the one before; in Chromium every gap is the dispatch's).
6. Two earlier mistakes now have their own cases: reading the page right after `wheel()` missed the step when every listener was passive, and a stroke sent right after a navigation pressed at 41 px for 100.

## Evidence

- Gestures for every pointer: #8646
- WebKit's one red in the harness self-test, "a run going backward is dated from when it began", is main's own check, which #8535 fixes: at a CI runner's size (four CPUs), main fails it in 4 of 6 full harness runs.

This branch was successfully deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants