Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
874085f
feat(core): one click guard for the menu press and the gesture host
vjeux Oct 31, 2026
61ec301
fix(gestures): a touch handled late goes to what was drawn where the …
vjeux Oct 31, 2026
68ba414
spec(AST-094): a touch on moving content goes to what is drawn under …
vjeux Oct 31, 2026
a9c100b
fix(gestures): content that slid under a late touch takes nothing; re…
vjeux Oct 31, 2026
c23a02a
test(motion): the busy-window proof reads the control's clock from th…
vjeux Oct 31, 2026
9b5337d
fix(core): the haptic switch's private attribute has its owner module…
vjeux Nov 1, 2026
faf3330
refactor(core): one iOS WebKit check and one flat-tree walk, shared b…
vjeux Nov 1, 2026
b5cbf78
chore(changeset): the input stream's fix line covers only what ships
vjeux Nov 1, 2026
2742894
feat(core): a pan takes wheel and trackpad scrolling, and a pinch a c…
vjeux Oct 31, 2026
13b5bc9
feat(gestures): a trackpad scroll ends where its fingers lift, uncert…
vjeux Oct 31, 2026
bf5485d
feat(gestures): a followed trackpad pan keeps moving at the fingers' …
vjeux Oct 31, 2026
2317641
fix(gestures): a pan follows a trackpad only where its owner allows i…
vjeux Oct 31, 2026
1defe9a
chore(core): the sheet's release rule, measured on iOS 26.2
vjeux Oct 31, 2026
0910039
test(core): BottomSheet detents, proven with the interaction harness
vjeux Oct 31, 2026
68b0fdf
fix(core): BottomSheet releases a drag the way an iOS sheet does
vjeux Oct 31, 2026
554129c
fix(core): a BottomSheet drag tracks the finger the way an iOS sheet …
vjeux Oct 31, 2026
9a3e62b
fix(core): a BottomSheet's dim follows the sheet the way an iOS sheet…
vjeux Oct 31, 2026
c7291f5
feat(core): one interactive-transition driver, with BottomSheet on it
vjeux Oct 31, 2026
f83dacd
refactor(core): BottomSheet's drag runs on the gesture recognizers
vjeux Oct 31, 2026
f5bb79b
test(interaction-harness): a WebKit touch gesture keeps its scripted …
vjeux Oct 31, 2026
ebcca12
feat(core): motion/progress.ts, the progress curves the driver maps
vjeux Oct 31, 2026
f0579d5
test(BottomSheet): test-side catches name what they skip
vjeux Oct 31, 2026
ceb35dc
test(BottomSheet): the settle proof writes its frames as JSON evidenc…
vjeux Oct 31, 2026
c72d8f0
fix(core): a sheet hosting another layer in its content leaves that l…
vjeux Oct 31, 2026
018b4a0
test(BottomSheet): main's newest sheet tests on the travel channel an…
vjeux Nov 1, 2026
0a6ac57
feat(core): a trackpad pulls a BottomSheet and a mouse wheel never do…
vjeux Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-catch-no-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A finger that catches a settling BottomSheet and holds it no longer starts selecting the sheet's text. Where a long press fires `selectstart`, the sheet cancels it and changes no style, so nothing repaints; on iOS, whose long press fires none, selection and the touch callout are off on the sheet while a finger holds or drags it.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-drag-restore-height.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A BottomSheet dragged up from a detent stays under the finger while it restores its full height, instead of drawing lower for the frames before that render lands.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-gesture-recognizers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A BottomSheet's drag runs on the shared gesture recognizers under every pointer, a finger, a pen and a mouse: it begins after 10 px of travel and moves the sheet by the travel past that, as UIKit's does; a slow pull down from the top of the sheet's content moves the sheet instead of rubber-banding the content; and a second finger that lands during a drag no longer sends the sheet back to its detent. A mouse, like a finger, pulls the sheet down from the top of its content and up from its end while a taller detent exists, and its release dismisses the sheet or picks a detent by the same rule as a finger's. A mouse, and a pen or finger where the primary pointer is fine, keep the pointer's tracking and the theme's release timing.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-scroll-decided-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A drag that begins in a BottomSheet's scrolling content stays the content's for its whole length: content scrolled to its end mid-drag rubber-bands instead of handing the rest of the drag to the sheet, and the sheet grows only from a drag that begins with the content at its end.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-uikit-dim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Under a finger, on every touch device, a BottomSheet's scrim follows the sheet the way an iOS sheet's dim does. Dragged below its shortest detent, the dim thins in a straight line with the sheet's position and clears only when the sheet reaches the bottom of the screen; it used to clear 40 % of the shortest detent's height below it. A standalone sheet's dim follows the finger at once instead of easing behind it over the theme's medium duration (410 ms by default). When the sheet settles or is flung away, the dim rides the same spring frame for frame, on the compositor. Under a mouse the dim follows the same line, clearing only once the sheet is gone, and the host still eases it. A peek detent's lighter scrim is unchanged.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-uikit-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A BottomSheet let go under a finger moves the way an iOS sheet does, on every touch device. It settles on the stop nearest where its speed carries it, a quick flick going at least to the next stop, with gone below the screen counting as a stop (one moving back toward where the drag began returns there), and on iOS's own release spring, starting at the finger's speed instead of from a standstill; under reduced motion it fades in place instead. Under a mouse a release goes by the same rule and keeps the theme's duration and easing, on the same release speed as a finger's: a mouse held still for more than 40 ms before the release no longer throws the sheet. Everywhere, the settle runs on the compositor, so a busy page no longer stalls it, and a finger can catch the sheet while it settles, on iOS too. The finger's speed is read from when each touch happened rather than when the page got to it.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-uikit-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A BottomSheet drag follows the finger the way an iOS sheet does. The sheet stays exactly under the finger between detents, instead of being pulled toward a nearby detent in the last 40 px; the release picks where it lands. Pulled up past its tallest detent, it stretches by UIKit's rubber band, no farther than the space it reserves below. This applies to a finger or a pen where the primary pointer is coarse; a mouse, and any drag where the primary pointer is fine, is unchanged: it still eases onto a nearby detent and stretches a little past the tallest one.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/bottom-sheet-wheel-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] On the web, a trackpad scrolling over a BottomSheet's handle, or past its content, pulls the sheet as a drag does, at most the sheet's height below where it rests. A mouse wheel never moves the sheet: it scrolls the content. A sheet a swipe can't dismiss (a `form` or `required` sheet) now stretches on a rubber band when a finger or a trackpad pulls it below its rest, and springs back; a mouse or a pen drag keeps tracking it one to one.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/click-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture recognizers and the menu press model share one guard for the browser's click after a gesture that already acted, swallowing it for 400ms or until the next press. Keyboard and assistive-technology clicks (`detail` 0) always pass.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-hook-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture hooks call nothing back after unmount; `usePressTracker` brakes with the host by default and keeps one tracker, so an option change or a new ref never drops a press in progress; the recognizer's arena plumbing is `protected`. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-host-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: gesture host fixes from review. A claimed pen holds only its own stylus touches, and a finger is matched to its scroller, its target and a moving element along the flat tree, through slots and shadow roots. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-host-owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture host becomes the one document-level touch owner: the finger count, touch gates, and a kept press for continuous-tracking controls, whose element gets `touch-action: manipulation`. A scroll lock's page hold and the finger-scroll watch keep their own listeners and ask the host only about the touches it owns. A second finger fails a one-finger pan that has not claimed, so the page can pinch. A scroll-edge pan holds a lone finger from the first move that pulls away from an edge every scroller under it is at, so those moves are cancelled before it begins; with a second finger anywhere on the screen the page keeps its pinch. A press tracker whose hold acts (a stepper's hold-repeat) can bring a selection lock for its presses, as a recognizer can. Internal, for the touch components that adopt them; not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-recognizer-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: gesture recognizer and arena fixes from review; a pan claims nothing before it begins at 10 pt; one velocity tracker for every gesture (`spec:AST-100`: UIKit's measured blend, a scrolling surface's release through the pan's `scrollVelocity()`, and FR2's release rules). Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-watcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: a recognizer created with `claimsTouches: false` is a watcher: passive listeners only, simultaneous with every recognizer, and it follows a touch the browser takes for a scroll to the lift. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gestures-every-pointer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: with the gesture host serving every pointer, a press tracker takes a mouse press as a press, painting at once with no scroll brake (AST-094 FR11); a finger's or a pen's hold that does something keeps text unselected under any primary pointer, a touchscreen laptop's included, while a mouse keeps its released behavior (FR41); a finger is read once where the browser sends Touch Events without exposing `ontouchstart`; and `perEventPointer`, from before FR43, is accepted and does nothing. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gestures-wheel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: a pan follows a trackpad's two-finger scroll where its owner allows it (`allowedScrollTypes: ['continuous']`, UIKit's `allowedScrollTypesMask`; none by default), along its own axis and after the scrollers under the pointer, and never a mouse wheel's notches, which scroll what is under the pointer; the screen-edge pan takes none; and a pinch takes a ctrl-wheel from a trackpad or a mouse (AST-094 FR43, DEC-24). A watcher pan never takes the wheel, and a wheel another handler already prevented stays the page's. Input the deltas cannot place, Shift+wheel included, reads as a mouse wheel; a trackpad scroll a pan follows keeps moving at the fingers' speed until their lift is read, ends there still moving, and the momentum never moves it. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/input-stream-recognizers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture recognizers hear page-wide input through the same shared stream on the window's capture phase, so an app's window-capture `stopPropagation`, or a document-capture `stopImmediatePropagation` registered before Core, no longer hides a touch from them; a recognizer that must claim a touch mid-gesture still needs the touch to reach its element. Each event type's listener is passive unless a subscriber cancels it (the gesture host's `touchend` and `contextmenu` while a claiming recognizer or press tracker is attached, the page hold's `touchmove` while it holds, and the click guard's `click` while it is armed, and from the menu press model's first arm on), so a page of pressables registers no listener that can hold a scroll.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/input-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Touch press feedback now hears scrolls and page hides on the window's capture phase, where its pointer listeners already were. Under an app's window-capture `stopPropagation`, or a document-capture `stopImmediatePropagation` registered before Core, a finger's pressed look now ends when the content under it scrolls, and every pressed look ends when the page is hidden.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/long-press-one-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: `useLongPress` takes its default duration and tolerance, 500ms and 10px on either axis, from the same source as the long-press gesture recognizer, so ContextMenu's long press behaves as before. A finger held on a DropdownMenu trigger passes its own 13px allowance instead (see the trigger drift fix).

@vjeux
7 changes: 7 additions & 0 deletions .changeset/menu-trigger-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A finger held on a DropdownMenu trigger may drift up to 13px on each axis and still open the menu under every primary pointer, a phone's, a tablet's and a touchscreen laptop's alike, as AST-094 FR29 allows; it cancelled past 10px. No menu, row or trigger reads the pointer query.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/motion-interactive-transition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] BottomSheet's releases, entrance, exit and dim run on one interactive-transition driver inside core's motion foundation, with no change in how they look or move. Its rules decide a release once, from where it projects (spec:AST-094), on the release velocity of spec:AST-100's tracker. Nothing is exported.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/presented-hit-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] On iOS, a touch on an element the compositor animator is moving reaches what is drawn: the animator reports each moving element's offset from its layout box, and the gesture host sends the touch, press bounds and the tap's click there instead of to the layout position iOS WebKit hit-tests.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/touch-selection-lock-gestures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture host applies a recognizer's own `selectionLock` to that recognizer's contact and imports no lock itself; a long press brings one only when it has a hold (a `minimumPressDuration` above 0), and `selectionLock: null` opts out. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/touch-selection-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Where the primary pointer is fine (a mouse, a trackpad, a touchscreen laptop, a desktop pen tablet), the press feedback controller no longer registers a touch listener: it reads the primary pointer at the first finger or pen press and keeps its `touchcancel` listener only while that pointer is coarse.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/uikit-sheet-release-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the rule a BottomSheet release will follow, measured from UIKit on iOS 26.2, as a pure function. It picks the stop nearest the sheet's offset plus its velocity × 0.1 s (below 250 pt/s, the stop nearest its offset), counting gone below the screen as a stop; from 1008 pt/s a flick goes at least to the next stop in its direction, on the 0.8-damped release spring. A release moving back toward where its drag began returns to the detent nearest that start, and never dismisses. It lands where UIKit did on 149 of 164 measured releases; the other 15 moved back toward their start, where UIKit settled 14 on the stop nearest them and flicked one to large. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/use-finger-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: `useFingerScroll(scroller)` tells a finger's scroll from the program's, and the gestures README gains a guide to migrating a raw-touch drag onto the recognizers. Internal to the touch components: not exported from the package entry point or the gestures barrel.

@vjeux
2 changes: 1 addition & 1 deletion .github/scripts/ci-interaction-webkit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ describe('WebKit interaction proof routing', () => {
for (const untouched of [
'packages/core/src/Toast/Toast.tsx',
'packages/core/src/Selector/useSelectorPresentation.ts',
'packages/core/src/utils/clickGuard.ts',
'packages/core/src/utils/focusReturn.ts',
'packages/core/src/internal/focus.ts',
'packages/core/src/DateInput/DateInput.tsx',
'packages/core/locales/en.json',
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2047,7 +2047,7 @@ jobs:
id: scope
uses: ./.test-scope/.github/actions/test-scope
with:
paths: '^(packages/core/src/gestures/|packages/core/src/motion/|packages/core/src/internal/touch|packages/core/src/utils/(press|touch|primaryClick|isApplePlatform)|packages/core/src/hooks/(useMenuPress|menuPress|useLongPress|usePressFeedback)|internal/interaction-harness/|internal/a11y-spec/|playwright\.interaction\.config\.ts$|packages/themes/[^/]+/src/|apps/storybook/|\.github/workflows/ci\.yml$|package\.json$|pnpm-lock\.yaml$|\.github/actions/test-scope/)'
paths: '^(packages/core/src/gestures/|packages/core/src/motion/|packages/core/src/internal/touch|packages/core/src/utils/(press|touch|primaryClick|isApplePlatform|flatTree|inputStream|clickGuard|selectionLock|coarsePointer)|packages/core/src/privateAttributes/hapticSwitch\.stylex\.ts$|packages/core/src/hooks/(useMenuPress|menuPress|useLongPress|usePressFeedback|isRtlElement)|internal/interaction-harness/|internal/a11y-spec/|playwright\.interaction\.config\.ts$|packages/themes/[^/]+/src/|apps/storybook/|\.github/workflows/ci\.yml$|package\.json$|pnpm-lock\.yaml$|\.github/actions/test-scope/)'
outside-pull-request: skip
requires-head-file: playwright.interaction.config.ts

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/react-component-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ those contracts.
the modules through an internal hook only. A component's own touch logic is
small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query; under a fine pointer only a component that
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The modules are not public exports and are not
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The gesture host, press feedback, page hold, finger-scroll watch and click guard hear document-wide input through one capture-phase listener per event type on the window, present while a subscriber needs it. They run in subscription order, before any document or element listener; it is passive unless a subscriber cancels the event. The modules are not public exports and are not
documented for consumers; Core and Lab components reach them through
`@astryxdesign/core/internal/touch`, a subpath reserved for Astryx's own
packages: undocumented, outside the compatibility promise, versioned in
Expand Down
4 changes: 2 additions & 2 deletions docs/specs/AST-094-touch-presentations/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ a measured, sourced difference exists.
activation stays the browser's click, except that an action a
`family:action-groups` member tracks activates at the lift (FR26), and a toggle
under `spec:AST-020` FR9's slide-off exception (Switch, Checkbox,
CheckboxListItem) may also complete on a lift after the finger slid off.
CheckboxListItem) may also complete on a lift after the finger slid off. Where the engine hit-tests an element a compositor animation moves at the motion's start (iOS WebKit), a touch on moving content goes to what was drawn under the finger at touch-down, and a tap there (no recognizer claimed it, the browser did not take it, and it stayed within 45 pt) MUST activate it: the browser's own click for that touch is cancelled or swallowed, and that element receives one click, an editable focused first.
Under a fine pointer a control uses a third profile with no delay: its
press paint starts at once on `pointerdown`, through the same press
controller and release fade as a finger's, in place of CSS `:active`; the
Expand Down Expand Up @@ -312,7 +312,7 @@ a measured, sourced difference exists.
recognizer that must claim mid-gesture registers that blocking listener; an
element whose recognizers decide only at the lift keeps its `touchmove`
listener passive, and its `touchend` listener stays cancelable so the
gesture can cancel the click (FR21). Known limit: a native scroll that starts on an element with a
gesture can cancel the click (FR21). While an element that holds, or sits inside, an element with a recognizer that claims moving touches moves on the compositor (iOS WebKit), the document MAY carry a non-passive `touchmove` listener; it cancels only the moves of a touch a recognizer owns under this rule and comes off when the motion ends. While it is on, a native scroll that starts anywhere on the page waits for a free main thread. Known limit: a native scroll that starts on an element with a
blocking listener waits for a free main thread in iOS Safari and Chromium.
That is never worse than UIKit, whose scrolling stops on a busy main thread,
and a passive listener, or one added at `touchstart`, would hand diagonal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
"data-astryx-press",
"data-astryx-press"
],
"packages/core/src/gestures/haptics.ts": ["data-astryx-haptic-switch"],
"packages/core/src/gestures/touchActionRule.ts": ["data-astryx-touch-action"],
"packages/core/src/hooks/useAnnounce.ts": [
"data-astryx-live-region",
Expand Down
Loading
Loading