Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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/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
2 changes: 1 addition & 1 deletion .changeset/gesture-arena-moving-cancel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---

[chore] Internal: a gesture that cancels itself on a finger's move (a long press dragged past its tolerance) keeps that finger claimed until it lifts, so the page never takes the drag over midway; a resting finger is still given back when its gesture is cancelled. Not exported from the package entry point.
[chore] Internal: a claimed gesture cancelled while one of its fingers is moving keeps that moving finger claimed until it lifts, so the page never takes the drag over midway; a resting finger is still given back when its gesture is cancelled. Not exported from the package entry point.

@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. A disabled recognizer brings no listener; where a finger comes from its pointer events, a claimed finger's touchend is cancelled too, a touchscreen laptop's included and inside an open shadow root, a hold's that claims with its lift included; and a pen or a pointer-read finger the window's blur cancelled clicks nothing at its later lift, while a mouse keeps its click. 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/input-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] The touch press feedback and the gesture recognizers now hear page-wide input through one 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/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/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/scroll-lock-keeps-the-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] `useScrollLock`, and so Dialog, BottomSheet, Drawer and Lightbox, keeps the page where it is while an overlay is open. The lock used to pin `<body>` with `position: fixed` at minus the scroll offset and scroll back on release: the page's offset read 0 for the whole lock, and on iOS Safari the jump to the top brought the toolbar back, where it stayed after the overlay closed. Now the page gets `overflow: hidden` (on the root, or on `<body>` when the body's own overflow is the one the viewport uses), and on touch screens a one-finger pan that would scroll the page is cancelled, which iOS Safari before 26.4 needs because it scrolls an `overflow: hidden` page under a finger. Pans that a scroller inside the overlay can take, drags on a native range input, pinch-zoom, and one-finger drags on a pinch-zoomed page (they pan the zoomed view) still work. The lock no longer pins `<body>`, and `window.scrollY` keeps its value while the lock holds. The lock cancels those pans itself, leaving a touch a gesture recognizer owns to it, and judges each pan after a small slop so a swipe's first wobble does not fix the wrong axis. It follows the browser's scroll chain, so an overlay rendered inside a scrolling container still holds the page: a modal dialog's pans go to the page, not to that container. It also sets `overscroll-behavior-y: none` on the root and `<body>`, which turns off pull-to-refresh (Chrome on Android) while an overlay is open. MobileNav's own clip goes through the same page scroll-style claims, so closing or unmounting it no longer releases another overlay's lock.

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

[chore] Internal: a gesture touch carries the modifier keys (`ctrlKey`, `metaKey`, `shiftKey`, `altKey`) held when its press began, read from the pointer or touch event, so a group of links can leave a Ctrl-, Cmd- or Shift-click on a member to the browser. Not exported from the package entry point.

@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/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 docs/architecture/react-component-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New INV13 text from #8360's stack: the touch modules hear document-wide input through one capture-phase window listener per event type, in subscription order. Current INV13 does not state it. It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New normative text from #8360's stack: a MUST that a tap on compositor-moving content activates what was drawn under the finger with one click, the browser's own click cancelled or swallowed. Current FR11 says a control's activation "stays the browser's click" with two exceptions, so this adds a third. It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New MAY from #8360's stack: a document-level non-passive touchmove listener while such content moves, during which every native scroll on the page waits for the main thread. Current FR19 says "Only an element with a recognizer that must claim mid-gesture registers that blocking listener". It needs an engineering owner's approval, recorded with /approve-spec <full head SHA>.

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
11 changes: 11 additions & 0 deletions internal/test-utils/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
/// <reference types="@testing-library/jest-dom" />
import '@testing-library/jest-dom/vitest';
import {configure} from '@testing-library/react';
import {afterEach} from 'vitest';
import {__resetClickGuardForTest} from '../../../packages/core/src/utils/clickGuard';

// Text queries (getByText/findByText/…) target VISIBLE text. Astryx's
// `useAnnounce` renders a visually-hidden aria-live region
Expand All @@ -25,6 +27,15 @@ import {configure} from '@testing-library/react';
// Keeps the jsdom defaults (script, style).
configure({defaultIgnore: 'script, style, [data-astryx-live-region]'});

// The click guard (packages/core/src/utils/clickGuard.ts) is armed per
// document by a gesture that already acted and disarms itself 400 ms later.
// A test that ends inside that window would leave the timer to fire after
// the environment tears its window down, during a later file. Disarm every
// guard as each test ends.
afterEach(() => {
__resetClickGuardForTest();
});

// Polyfill for matchMedia (not supported in jsdom).
//
// `(hover: hover)` reports TRUE: jsdom models a desktop browser driven by a
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/AlertDialog/AlertDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import userEvent from '@testing-library/user-event';
import {AlertDialog} from './AlertDialog';
import {useImperativeAlertDialog} from './useImperativeAlertDialog';

// The scroll lock's page hold reads the primary pointer as it starts
// (gestures/pageHold.ts). It is stubbed here so that every `matchMedia` call
// in this file is AlertDialog's own: its button layout reads the width (and
// `(hover: none)`), never the pointer.
vi.mock('../gestures/pageHold', () => ({
holdPageScroll: () => () => {},
}));

function stubAlertDialogMedia({
smallScreen,
coarsePointer,
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/BottomSheet/BottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -576,12 +576,12 @@ describe('BottomSheet', () => {
expect(dialog).toHaveAttribute('aria-hidden', 'true');
expect(dialog).not.toHaveAttribute('aria-modal');
expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '0'});
expect(document.body.style.position).toBe('fixed');
expect(document.documentElement.style.overflow).toBe('hidden');

finishSheetExit();

expect(dialog).not.toHaveAttribute('open');
expect(document.body.style.position).not.toBe('fixed');
expect(document.documentElement.style.overflow).not.toBe('hidden');
});

it('does not dismiss when the sheet surface itself is clicked', () => {
Expand Down Expand Up @@ -1024,7 +1024,7 @@ describe('BottomSheet', () => {

expect(dialog).toHaveAttribute('open');
expect(dialog).toHaveAttribute('inert');
expect(document.body.style.position).not.toBe('fixed');
expect(document.documentElement.style.overflow).not.toBe('hidden');

finishSheetExit();

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/BottomSheet/BottomSheetReplace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
getBottomSheetStackForTests,
resetBottomSheetStackForTests,
} from './bottomSheetStack';
import {isScrollLockActive} from '../hooks/rootScrollStyles';
import {__resetInteractionModalityForTest} from '../utils/interactionModality';

let nativeLog: {
Expand Down Expand Up @@ -1455,7 +1456,7 @@ describe('BottomSheet replace: non-modal sheets', () => {
'hidePopover:One',
]);
// The page stayed interactive: no scroll lock, no focus steal.
expect(document.body.style.position).not.toBe('fixed');
expect(isScrollLockActive()).toBe(false);
});

it('a non-modal reversal re-shows the returning sheet above the other', () => {
Expand Down
Loading