Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This needs an owner decision before it can be approved. Against main, the pull request carries the whole touch-presentation stack, and the draft spec:AST-094 it adds is the only spec for the gesture host, the motion animator, the experimental subpaths and the theme motion roles, though its FR4 and DEC-1 rule out those same theme roles.
An engineering owner can record that decision, including whether touch motion is themable, by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Whatever the decision, please land the predecessors first so this diff is the 550-line click-guard commit, and fix the following:
pressFeedback.tsnow turns off text selection and the touch callout under a held finger on every stable press-feedback surface,Linkincluded, under an[experimental]changeset, whichspec:AST-017FR22 does not allow for stable behaviour.- The touch resolver behind
presentation="adaptive"stays at 768 px instead of the theme's effectivemd(spec:AST-012FR9), although its changeset says it reads the theme. theme.template.tsdocuments aplatformadaptation axis thatdefineThemerejects, andtouch-experimental-subpaths.mdmisstates what is exported.
[Automated review]
| * are primary-pointer precision, contrast preference, motion preference, and | ||
| * platform (`ios` for the browsers of iPhone and iPad, `other` for the rest). |
There was a problem hiding this comment.
themeAdaptations.ts accepts only width, pointer, contrast and motion in when (its CONDITION_AXES), so a rule copied from this sentence with platform makes defineTheme throw "… .platform is not supported." The stack removed the platform condition; please drop it from the template too.
| @@ -283,6 +302,9 @@ function onPointerDown(event: PointerEvent): void { | |||
| gesture: idlePressGesture(), | |||
| pointerId: event.pointerId, | |||
| timer: null, | |||
| // A held finger never starts text selection on the surface (UIKit's | |||
| // controls and cells never do), on any coarse pointer. | |||
| unlock: lockSelection([element], event.target), | |||
There was a problem hiding this comment.
This lock now applies to every stable usePressFeedback surface on a touch device, Link's <a href> included. -webkit-touch-callout: none for the whole hold removes the iOS link callout on Astryx links. spec:AST-017 FR22 keeps stable runtime changes out of an [experimental] changeset; can the lock stay on the experimental press trackers?
| export const COMPACT_TOUCH_QUERY = | ||
| DEFAULT_COMPACT_DEFINITION.coarsePointerQuery; |
There was a problem hiding this comment.
This query is built from DEFAULT_COMPACT_DEFINITION (md 768), so a theme that overrides adaptations.widthBreakpoints.md never moves the sheet/popover boundary. spec:AST-012 FR9 defines compact by the effective md, and its verification row lists "overridden md"; .changeset/touch-presentation.md also says the theme's md is used.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [experimental] Two experimental subpaths for building touch presentations, outside the stable compatibility promise (they may change or be removed in a patch release). `@astryxdesign/core/motion/experimental` exports the compositor animator (`createAnimator`, `useAnimator`), UIKit's spring, rubber-band and cubic-bézier math, `resolveMotion` (a motion role's value under the element's theme), and the UIKit constants table as the read-only `uikit` namespace. `@astryxdesign/core/gestures/experimental` exports UIKit-style gesture recognizers (tap, long press, pan, screen-edge pan, swipe, pinch, rotation, hover), `useGestureRecognizer`, `usePressTracker`, `useTouchPresentation`, `readTouchPresentation`, `playHaptic` and `attachTapHaptic`, with the `GestureArena` and `VelocityStrategy` types their options reach. Nothing is added to the package root or to any other subpath. |
There was a problem hiding this comment.
"Nothing is added to the package root or to any other subpath" does not hold at this head: packages/core/package.json also adds ./motion/reducedMotion.stylex. And VelocityStrategy is listed as exported, but gesture-recognizer-fixes.md removes it and gestures/experimental.ts does not export it.
dd226d0 to
8033d6c
Compare
8033d6c to
cc802bf
Compare
cc802bf to
a3c9619
Compare
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved: nothing current on main governs the gesture host's click handling or most of what this diff carries beneath it (the gesture and motion systems, theme motion roles, the selection lock, the long-press distance and undefinedVariables), so an owner needs to provide or identify that spec, or rule that those parts leave this pull request. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever the decision, please land the predecessors first so this diff is the 549-line click-guard commit, and address these:
undefinedGeneratedVariablesinpackages/build/src/vite.tsflags an app's own--xs-style variables declared outside the StyleX stylesheet, so default builds warn and'error'builds fail.armClickGuardadds and removes its window listener per gesture, whereuseMenuPressPR2 says it is installed once per document..changeset/click-guard.mdand.changeset/reduced-motion-record.mddescribe internal, unexported work as consumer fixes.
[Automated review]
| subscribeInput( | ||
| view, | ||
| 'click', | ||
| event => { | ||
| if (isExempt(event)) { | ||
| return; | ||
| } | ||
| event.preventDefault(); | ||
| event.stopImmediatePropagation(); | ||
| disarmClickGuard(doc); | ||
| }, | ||
| {cancels: true}, | ||
| ), | ||
| // The next press is a new gesture: its click must never be eaten. | ||
| subscribeInput(view, 'pointerdown', () => disarmClickGuard(doc)), | ||
| ); |
There was a problem hiding this comment.
useMenuPress.spec.md PR2 says the window capture click listener is installed once per document and armed per gesture; here it is subscribed at every arm and removed at every disarm, and clickGuard.test.ts asserts that. Either keep one installed listener per document and arm it per gesture, or have PR2 amended.
| const declared = new Set( | ||
| [...css.matchAll(new RegExp(`(--(?:${names})[a-z0-9]+)\\s*:`, 'g'))].map( | ||
| match => match[1], | ||
| ), | ||
| ); | ||
| const read = new Set( | ||
| [ | ||
| ...css.matchAll( | ||
| new RegExp(`var\\(\\s*(--(?:${names})[a-z0-9]+)\\s*[,)]`, 'g'), | ||
| ), | ||
| ].map(match => match[1]), | ||
| ); | ||
| return [...read].filter(name => !declared.has(name)).sort(); |
There was a problem hiding this comment.
Any --x… or --astryx… name of lowercase letters and digits counts as generated here, and only declarations inside the StyleX stylesheet count, so an app style reading var(--xs) that its global CSS declares is reported as dropped (a warning by default, a failed build with 'error'). Could the check use the variable and constant names StyleX actually generated (from rulesById), or the declarations of every emitted CSS asset?
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] The gesture recognizers and the menu press model share one guard for the browser's click after a gesture that already acted. The gesture system now swallows that click for 400ms or until the next press, as menus do, instead of 1000ms. Script-dispatched clicks with a non-zero `detail` are swallowed in that window too; keyboard and assistive-technology clicks (`detail` 0) always pass. |
There was a problem hiding this comment.
No release has the gesture system or its 1000 ms window, and menu users see no change, so this reads as a consumer fix that is not one. The other internal touch changesets use "[chore] Internal: … Not exported from the package entry point"; could this one do the same?
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] The experimental touch animator follows design:reduced-motion. Under Reduce Motion every `animate` and `play` (travel or opacity, on a spring or any curve) becomes an opacity-only cross-fade of 200 ms on a coarse primary pointer, or for a call marked `touchRelease`, and completes at once on any other pointer; nothing moves, scales or springs, and travel lands with a plain style write while the element is transparent, never as an animated transform. `timing.reducedMotionCrossFadeMs` is now 200 ms and is no longer a theme role. The `reducedMotion` and `crossfade` call options are removed; `crossfadeAs` stays. `animator.reducedMotion()` reports the mode for feedback with no fade of its own, `reducedMotionFade()` gives the same fade to code beside the animator, and `reducedMotion.stylex.ts` holds its constants for StyleX transitions. |
There was a problem hiding this comment.
The animator is internal at this head (there is no experimental subpath), so "The experimental touch animator", its removed call options and reducedMotion.stylex.ts are nothing consumers can import. Could this be worded as an internal change like the other touch changesets?
| } from './GestureArena'; | ||
| import type {GestureRecognizer} from './GestureRecognizer'; | ||
| import type {PressBounds} from './PressTracker'; | ||
| import {HAPTIC_SWITCH_ATTRIBUTE} from './hapticSwitch'; |
There was a problem hiding this comment.
HAPTIC_SWITCH_ATTRIBUTE has no use here since onClick moved into utils/clickGuard.ts (an unused-import warning); the headers of hapticSwitch.ts and haptics.ts also still describe the host's own swallower.
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC |
|---|---|---|
| TableCell | 80.5 kB (+808 B) · 15.7 kB CSS (0) | — |
| ContextMenu | 79.2 kB (+775 B) · 15.2 kB CSS (0) | — |
| Pagination | 89.9 kB (+754 B) · 15.7 kB CSS (0) | — |
103 more components
| Component | Size | LoC |
|---|---|---|
| richtext/RichTextEditorToolbar | 217.8 kB (+749 B) · 454 B CSS (0) | — |
| TableHeaderCell | 80.1 kB (+745 B) · 15.4 kB CSS (0) | — |
| Table | 89.1 kB (+734 B) · 15.9 kB CSS (0) | — |
| Markdown | 149.6 kB (+732 B) · 18.2 kB CSS (0) | — |
| PowerSearch | 175.8 kB (+728 B) · 20.3 kB CSS (0) | — |
| lab/CommentThread | 222.1 kB (+722 B) · 2.4 kB CSS (0) | — |
| TopNavMenu | 61.7 kB (+686 B) · 10.5 kB CSS (0) | — |
| lab/Comments | 226.7 kB (+678 B) · 2.5 kB CSS (0) | — |
| SideNavHeading | 62.2 kB (+670 B) · 10.5 kB CSS (0) | — |
| TopNavHeading | 61.6 kB (+660 B) · 10.3 kB CSS (0) | — |
| DropdownMenuSubMenu | 62.4 kB (+659 B) · 9.9 kB CSS (0) | — |
| Selector | 81.3 kB (+658 B) · 15.4 kB CSS (0) | — |
| TabMenu | 59.0 kB (+649 B) · 10.4 kB CSS (0) | — |
| BreadcrumbItem | 71.4 kB (+632 B) · 11.4 kB CSS (0) | — |
| MoreMenu | 77.4 kB (+630 B) · 15.4 kB CSS (0) | — |
| DropdownMenu | 77.1 kB (+628 B) · 15.4 kB CSS (0) | — |
| richtext/RichTextEditor | 273.6 kB (+535 B) · 2.6 kB CSS (0) | — |
| FileInput | 58.1 kB (+526 B) · 10.7 kB CSS (0) | — |
| lab/Schedule | 64.3 kB (+526 B) · 3.9 kB CSS (0) | — |
| lab/ChatReactionBar | 69.7 kB (+525 B) · 1.1 kB CSS (0) | — |
| AvatarGroupOverflow | 43.0 kB (+518 B) · 6.5 kB CSS (0) | — |
| NavHeadingMenu | 14.2 kB (+516 B) · 3.4 kB CSS (0) | — |
| lab/TransferListSelector | 76.7 kB (+514 B) · 1.9 kB CSS (0) | — |
| lab/ChatEmojiPicker | 69.1 kB (+511 B) · 773 B CSS (0) | — |
| DialogHeader | 52.3 kB (+510 B) · 10.1 kB CSS (0) | — |
| DrawerHeader | 52.4 kB (+505 B) · 11.5 kB CSS (0) | — |
| lab/MobileTokenizer | 96.9 kB (+505 B) · 880 B CSS (0) | — |
| lab/MobilePowerSearch | 161.0 kB (+504 B) · 1.2 kB CSS (0) | — |
| lab/TransferList | 71.0 kB (+503 B) · 1.7 kB CSS (0) | — |
| ClickableCard | 14.5 kB (+502 B) · 7.1 kB CSS (0) | — |
| LayerProvider | 67.5 kB (+502 B) · 10.4 kB CSS (0) | — |
| TopNavMegaMenu | 57.9 kB (+499 B) · 11.1 kB CSS (0) | — |
| lab/TourStep | 58.5 kB (+499 B) · 590 B CSS (0) | — |
| Banner | 51.3 kB (+498 B) · 10.2 kB CSS (0) | — |
| SelectableCard | 14.3 kB (+498 B) · 7.3 kB CSS (0) | — |
| lab/CommentPin | 50.8 kB (+498 B) · 1.2 kB CSS (0) | — |
| lab/CommentComposer | 98.4 kB (+497 B) · 1.9 kB CSS (0) | — |
| ContextMenuItem | 57.0 kB (+496 B) · 9.9 kB CSS (0) | — |
| Collapsible | 21.1 kB (+494 B) · 4.5 kB CSS (0) | — |
| DateInput | 104.3 kB (+492 B) · 15.1 kB CSS (0) | — |
| Carousel | 54.0 kB (+491 B) · 10.5 kB CSS (0) | — |
| MobileNavToggle | 49.7 kB (+491 B) · 9.7 kB CSS (0) | — |
| TableSelectionToolbar | 55.7 kB (+491 B) · 11.4 kB CSS (0) | — |
| Typeahead | 66.6 kB (+491 B) · 11.6 kB CSS (0) | — |
| MultiSelector | 77.0 kB (+490 B) · 15.0 kB CSS (0) | — |
| Tokenizer | 73.7 kB (+490 B) · 16.9 kB CSS (0) | — |
| ChatComposer | 64.6 kB (+488 B) · 11.1 kB CSS (0) | — |
| DateRangeInput | 73.5 kB (+488 B) · 11.5 kB CSS (0) | — |
| lab/DiffViewer | 47.6 kB (+488 B) · 2.1 kB CSS (0) | — |
| TopNavMegaMenuItem | 10.6 kB (+487 B) · 4.2 kB CSS (0) | — |
| BaseTypeahead | 59.0 kB (+486 B) · 10.2 kB CSS (0) | — |
| Step | 34.7 kB (+485 B) · 5.6 kB CSS (0) | — |
| ToggleButton | 47.3 kB (+484 B) · 9.5 kB CSS (0) | — |
| ChatSendButton | 49.2 kB (+482 B) · 9.4 kB CSS (0) | — |
| Stepper | 51.0 kB (+482 B) · 9.9 kB CSS (0) | — |
| SideNavItem | 58.2 kB (+481 B) · 10.4 kB CSS (0) | — |
| SelectorOption | 16.4 kB (+480 B) · 5.4 kB CSS (0) | — |
| ChatDictationButton | 49.9 kB (+479 B) · 9.8 kB CSS (0) | — |
| CommandPalette | 54.6 kB (+479 B) · 12.1 kB CSS (0) | — |
| CommandPaletteItem | 9.9 kB (+478 B) · 3.6 kB CSS (0) | — |
| ComplexSelector | 60.0 kB (+478 B) · 11.0 kB CSS (0) | — |
| Popover | 53.1 kB (+478 B) · 12.4 kB CSS (0) | — |
| Tab | 11.2 kB (+477 B) · 4.2 kB CSS (0) | — |
| ChatComposerInput | 59.2 kB (+476 B) · 10.3 kB CSS (0) | — |
| ChatComposerTokenElement | 59.2 kB (+476 B) · 10.3 kB CSS (0) | — |
| Outline | 30.8 kB (+475 B) · 4.2 kB CSS (0) | — |
| CodeBlock | 63.6 kB (+474 B) · 11.0 kB CSS (0) | — |
| SegmentedControlItem | 10.2 kB (+474 B) · 4.0 kB CSS (0) | — |
| lab/ListInput | 79.0 kB (+474 B) · 1.6 kB CSS (0) | — |
| DropdownMenuItem | 17.3 kB (+473 B) · 5.5 kB CSS (0) | — |
| TopNavItem | 10.6 kB (+470 B) · 4.0 kB CSS (0) | — |
| Toast | 63.4 kB (+468 B) · 10.0 kB CSS (0) | — |
| AlertDialog | 60.3 kB (+467 B) · 12.7 kB CSS (0) | — |
| ListItem | 57.5 kB (+467 B) · 11.0 kB CSS (0) | — |
| ChatLayoutScrollButton | 49.9 kB (+466 B) · 9.9 kB CSS (0) | — |
| Item | 13.2 kB (+466 B) · 4.9 kB CSS (0) | — |
| MobileNav | 54.7 kB (+466 B) · 10.4 kB CSS (0) | — |
| ChatLayout | 52.4 kB (+465 B) · 10.2 kB CSS (0) | — |
| DateTimeInput | 115.9 kB (+464 B) · 15.6 kB CSS (0) | — |
| SideNavCollapseButton | 50.1 kB (+461 B) · 9.8 kB CSS (0) | — |
| TreeList | 43.6 kB (+461 B) · 5.0 kB CSS (0) | — |
| richtext/RichTextView | 254.1 kB (+461 B) · 2.3 kB CSS (0) | — |
| DropdownMenuCheckboxItem | 18.1 kB (+459 B) · 5.9 kB CSS (0) | — |
| DropdownMenuRadioItem | 18.1 kB (+459 B) · 5.9 kB CSS (0) | — |
| AppShell | 72.1 kB (+458 B) · 12.0 kB CSS (0) | — |
| Button | 46.5 kB (+457 B) · 9.4 kB CSS (0) | — |
| Token | 32.0 kB (+457 B) · 4.8 kB CSS (0) | — |
| Thumbnail | 54.2 kB (+456 B) · 11.3 kB CSS (0) | — |
| Link | 47.0 kB (+454 B) · 7.9 kB CSS (0) | — |
| RadioListItem | 56.0 kB (+453 B) · 10.0 kB CSS (0) | — |
| Calendar | 58.6 kB (+452 B) · 10.3 kB CSS (0) | — |
| IconButton | 46.5 kB (+448 B) · 9.4 kB CSS (0) | — |
| CheckboxListItem | 58.1 kB (+447 B) · 11.0 kB CSS (0) | — |
| SideNav | 68.3 kB (+445 B) · 10.9 kB CSS (0) | — |
| TextInput | 57.2 kB (+443 B) · 10.8 kB CSS (0) | — |
| lab/EmojiPicker | 66.9 kB (+442 B) · 722 B CSS (0) | — |
| Lightbox | 55.0 kB (+438 B) · 10.1 kB CSS (0) | — |
| CheckboxInput | 53.4 kB (+435 B) · 9.6 kB CSS (0) | — |
| Switch | 51.8 kB (+431 B) · 9.7 kB CSS (0) | — |
| NumberInput | 59.4 kB (+428 B) · 11.0 kB CSS (0) | — |
| TopNav | 55.6 kB (+421 B) · 10.7 kB CSS (0) | — |
| TimeInput | 92.6 kB (+399 B) · 14.1 kB CSS (0) | — |
| Timestamp | 28.4 kB (0) · 9.9 kB CSS (0) | — |
Package size: @astryxdesign/core 5.74 MB packed (+79.2 kB); no new dependencies.
API changes: core +8 · ~20 · −0 (report stale: run pnpm api:report); richtext +0 · ~1 · −0 (report stale: run pnpm api:report)
29 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | core |
. · Types · interface FieldStatusProps |
message: string |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {message: …} |
| + | core |
. · Types · interface FieldStatusProps |
ref?: React.Ref<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {ref: …} |
| + | core |
. · Types · interface FieldStatusProps |
type: InputStatusType |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {type: …} |
| + | core |
. · Types · interface FieldStatusProps |
variant?: FieldStatusVariant = 'attached' |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {variant: …} |
| + | core |
. · Types · interface FieldStatusProps |
xstyle?: StyleXStyles |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {xstyle: …} |
| + | core |
. · Types · interface FieldStatusProps |
inherits AriaAttributes, DOMAttributes, HTMLAttributes |
import type {FieldStatusProps} from '@astryxdesign/core' |
| + | core |
. · Types · interface SearchContext |
interface SearchContext<T extends SearchableItem = SearchableItem> |
import type {SearchContext} from '@astryxdesign/core' |
| + | core |
./Typeahead · Also exported from an earlier entry point · SearchContext |
SearchContext (.) |
import {SearchContext} from '@astryxdesign/core/Typeahead' |
| ~ | core |
. · Components · component ComplexSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<ComplexSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component DateInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateInput statusVariant="attached" /> |
| ~ | core |
. · Components · component DateRangeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateRangeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Field |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Field statusVariant="attached" /> |
| ~ | core |
. · Components · component FieldStatus |
component FieldStatus(props: FieldStatusProps) → component FieldStatus @deprecated |
<FieldStatus /> |
| ~ | core |
. · Components · component FieldStatus |
prop variant?: FieldStatusVariant = 'attached' → prop variant: FieldStatusVariant = 'attached' |
<FieldStatus variant="attached" /> |
| ~ | core |
. · Components · component FileInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<FileInput statusVariant="attached" /> |
| ~ | core |
. · Components · component MultiSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<MultiSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component NumberInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<NumberInput statusVariant="attached" /> |
| ~ | core |
. · Components · component PowerSearch |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<PowerSearch statusVariant="attached" /> |
| ~ | core |
. · Components · component Selector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary/ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary/ghost selectors |
<Selector statusVariant="attached" /> |
| ~ | core |
. · Components · component TextArea |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextArea statusVariant="attached" /> |
| ~ | core |
. · Components · component TextInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextInput statusVariant="attached" /> |
| ~ | core |
. · Components · component TimeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TimeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Tokenizer |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Tokenizer statusVariant="attached" /> |
| ~ | core |
. · Components · component Typeahead |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Typeahead statusVariant="attached" /> |
| ~ | core |
. · Types · interface FieldStatusProps |
interface FieldStatusProps extends BaseProps<HTMLDivElement> (props of FieldStatus) → interface FieldStatusProps extends BaseProps<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core' |
| ~ | core |
. · Types · interface FieldStatusVariantMap |
tooltip: true → tooltip: true @deprecated |
import type {FieldStatusVariantMap} from '@astryxdesign/core'; const x: FieldStatusVariantMap = {tooltip: …} |
| ~ | core |
. · Types · interface SearchSource |
search: (query: string) => Promise<T[]> | T[] → search: (query: string, context?: SearchContext<T>) => Promise<T[]> | T[] |
import type {SearchSource} from '@astryxdesign/core'; const x: SearchSource = {search: …} |
| ~ | core |
. · Types · interface UseInputStatusIconOptions |
statusVariant?: FieldStatusVariant → statusVariant?: FieldStatusPlacement |
import type {UseInputStatusIconOptions} from '@astryxdesign/core'; const x: UseInputStatusIconOptions = {statusVariant: …} |
| ~ | richtext |
. · Components · component RichTextEditor |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusVariant | 'tooltip' = 'attached' |
<RichTextEditor statusVariant="tooltip" /> |
Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.
CI impact: +3.9 job-min per update · pool +5% · standard runners · 1 CI action or script file changed (not measured)
| Before | After | Change | |
|---|---|---|---|
| Job-minutes per PR update | 78.7 | 82.6 | +3.9 |
| Runner pool, runner-minutes a day | 36,690 | 38,522 | +1,832 (+5%) |
| Average concurrent jobs | 25.5 | 26.8 | +1.3 |
| Time to green (median → this run) | 14.6 min | — | — |
| Runs a day | 466 | 466 | 0 |
Changed jobs: pr-interaction-webkit changed: standard
Not measured (the jobs that use these keep their recent minutes above): .github/scripts/ci-interaction-webkit.test.mjs.
Assumptions: before is each job's average over the last 20 pull-request runs; after keeps that for unchanged jobs and uses this run's minutes for added or changed ones, weighted by how often each changed job ran in those runs; runs a day averages the seven whole days before today; time to green compares this run with the median of those runs that ran the same jobs.
Current run: CI concluded failure; parts of this report may be missing.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two carried changes: touches on moving content (on iOS a retargeted tap dispatches its own click, and the document carries a blocking touchmove while a claiming recognizer's element moves), which depart from spec:AST-094 FR11 and FR19 as they stand, and the window-capture input stream, which no current record governs. An engineering owner records the decision by amending or identifying the governing spec and commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. Whatever is decided, two changes are needed:
- Land the click guard on its own (about 1,000 authored lines) and move the host rework and its new modules to their own pull requests, since 15,277 authored lines with no behavior-module declaration block approval.
- Keep cancelling a claimed finger's
touchendwhere the host reads fingers from pointer events (gestures/GestureHost.ts:1431,:1977), as FR21 requires, together with the test that pinned it onmain.
[Automated review]
| unsubscribe(); | ||
| } | ||
| if (view != null) { | ||
| if (this.inputMode === 'auto') { |
There was a problem hiding this comment.
Only 'auto' mode subscribes touchend here, and onTouchEnd returns early whenever usesTouchEvents is false (:1977), so under inputMode: 'pointer-events' (and in 'auto' where Touch Events arrive without ontouchstart) a claimed finger's touchend is no longer cancelled, and its compatibility mouse events reach the page. main cancelled it through claimedLifts; spec:AST-094 FR21 requires it. Could that cancel and its test stay until #8309 replaces them?
|
Fixed in e5e49f4: where fingers come from their pointer events ( |
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two changes this pull request pursues: touches on moving content (on iOS a retargeted tap dispatches its own click, and the document carries a blocking touchmove while a claiming recognizer's element moves), which depart from spec:AST-094 FR11 and FR19 as they stand, and the window-capture input stream, which no current record governs. An engineering owner records the decision by amending or identifying the governing spec and commenting /approve-spec <full head SHA> on the pull request; the author may do so as an engineering owner. Whatever is decided, one fix is needed: where the gesture host reads fingers from pointer events, a claimed lift's record and its target element are never dropped when no element listener consumes the touchend, and the list no longer has the cap main kept (gestures/GestureHost.ts:2560-2570), against FR24's rule that nothing touch-related remains after the last instance unmounts, so push records only while a listener can consume them (or restore the cap) and clear them on uninstall, with a test. The restored touchend cancel addresses the earlier FR21 concern, and the size is accepted under the behavior-module declaration.
[Automated review]
|
Fixed in 1379537: a claimed lift is noted only where an element can take its |
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two changes this pull request pursues: touches on moving content (on iOS a retargeted tap dispatches its own click and the document gets a blocking touchmove, which spec:AST-094 FR11 and FR19 do not allow as written) and the window-capture input stream, which no current record governs. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the pull request; the author may do so as an engineering owner. Whatever is decided, one fix is needed: under a fine primary pointer with fingers read from pointer events, uninstall now clears a claimed lift's note in the microtask after pointerup, so its touchend is no longer cancelled (gestures/GestureHost.ts:1607, FR21), and the case at GestureHost.test.ts:4637 should await a microtask between the two events so it fails first. The new cap and expiry on those notes resolve the earlier leak.
[Automated review]
| this.cancelEverything(this.arena.now()); | ||
| this.clearClaimedLifts(); |
There was a problem hiding this comment.
Under a fine primary pointer this runs in releaseArmedPress's microtask (:1399-1405), which a browser runs between a claimed finger's pointerup and its touchend, so the note onPointerUp just pushed (:2576) is gone when onClaimedTouchEnd (:1992-2021) looks for it, and that touchend is not cancelled (spec:AST-094 FR21): after a tap a recognizer took, the browser's compatibility mouse events follow. The element's touchend listener outlives this uninstall, so the notes could go only when the host goes idle (uninstallIfIdle, once nothing is attached), leaving the rest to the 500 ms timer. The case at GestureHost.test.ts:4637 dispatches both events in one synchronous run; awaiting a microtask between them makes it fail first.
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two changes this pull request pursues: touches on moving content (on iOS a retargeted tap dispatches its own click and the document gets a blocking touchmove, which spec:AST-094 FR11 and FR19 do not allow as written) and the window-capture input stream, which no current record governs. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. Whatever is decided, three fixes are needed:
- Under a fine primary pointer with fingers read from pointer events,
uninstallclears a claimed lift's note in the microtask afterpointerup, so itstouchendis no longer cancelled (gestures/GestureHost.ts:1606, FR21); the case atGestureHost.test.ts:4637fails once it awaits a microtask between the two events. - Under a fine primary pointer,
releaseArmedPressnow waits until the arena holds no lifted touch (gestures/GestureHost.ts:1399), so a press whose tap waits on a double tap leaves the page listeners on until some later press ends (FR24). .changeset/long-press-one-source.mdsays the held DropdownMenu trigger keeps 10 px with no behavior change, which.changeset/menu-trigger-drift.mdcontradicts.
[Automated review]
| this.cancelEverything(this.arena.now()); | ||
| this.clearClaimedLifts(); |
There was a problem hiding this comment.
Under a fine primary pointer with fingers read from pointer events, this runs in releaseArmedPress's microtask (:1398-1403), which a browser runs between a claimed finger's pointerup and its touchend, so onClaimedTouchEnd finds no note and the touchend is not cancelled (spec:AST-094 FR21). Could the notes be cleared only when the host goes idle, leaving the rest to the 500 ms timer? The case at GestureHost.test.ts:4637 fails once it awaits a microtask between the two events.
| return; | ||
| } | ||
| queueMicrotask(() => { | ||
| if (this.isArmedPress && this.arena.touches.length === 0) { |
There was a problem hiding this comment.
arena.touches includes a lifted touch whose click a recognizer may still claim (a tap waiting on a double tap), and nothing re-checks once that resolves, so under a fine primary pointer the page listeners stay on with no contact down until some later pointerup or touchend (spec:AST-094 FR24). main counted only contacts still down here.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: `useLongPress` (ContextMenu's long press, and the held finger that opens a menu from its trigger) takes its default duration and tolerance, 500ms and 10px on either axis, from the same source as the long-press gesture recognizer. No behavior changes. |
There was a problem hiding this comment.
The held finger that opens a menu from its trigger now gets 13 px (.changeset/menu-trigger-drift.md), so naming it here with 10 px and "No behavior changes" contradicts that note (spec:AST-017 FR7). Could this drop "and the held finger that opens a menu from its trigger"?
|
Fixed in d7e6d99: a claimed lift's note now survives the host's uninstall after an armed press, since its |
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two changes this pull request pursues: touches on moving content (on iOS a retargeted tap dispatches its own click and the document gets a blocking touchmove, which spec:AST-094 FR11 and FR19 do not allow as written) and the window-capture input stream, which no current record governs. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. The claimed-lift fix in this push holds; these changes are still needed whatever is decided:
- Under a fine primary pointer,
releaseArmedPresswaits until the arena holds no lifted touch (gestures/GestureHost.ts:1399), so a press whose tap waits on a double tap leaves the page listeners on until some later press ends (FR24). - Claimed-lift notes and their timer now outlive the last detach (
gestures/GestureHost.ts:1606-1608); drop them when the host goes idle (FR24). .changeset/long-press-one-source.mdsays the held DropdownMenu trigger keeps 10 px with no behavior change, which.changeset/menu-trigger-drift.mdcontradicts.spec:AST-013FR10 asks for real-browser evidence of the pointer-fedtouchendtiming, and iOS Safari evidence for presented hit testing if it stays.
[Automated review]
| return; | ||
| } | ||
| queueMicrotask(() => { | ||
| if (this.isArmedPress && this.arena.touches.length === 0) { |
There was a problem hiding this comment.
arena.touches includes a lifted touch whose click a recognizer may still claim (a tap waiting on a double tap), and nothing re-checks once that resolves, so under a fine primary pointer the page listeners stay on with no contact down until some later press ends (spec:AST-094 FR24). Counting only contacts still down, as main did, or re-checking when the deferred click resolves would fix it; a fine-pointer case with a pending double tap would pin it.
| // A claimed lift's note stays: its touchend comes in a later task than | ||
| // the pointerup, often after an armed press's listeners have gone, and | ||
| // the element's own listener still takes it (noteClaimedLift). |
There was a problem hiding this comment.
With uninstall keeping the notes, nothing drops them when the last recognizer detaches inside the 500 ms window: cleanup has already removed the element's touchend handler, so the note and its clock timer outlive the last attachment (spec:AST-094 FR24, "nothing touch-related remains after the last instance unmounts"). Clearing them in uninstallIfIdle's idle branch would keep this fix and close that gap.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: `useLongPress` (ContextMenu's long press, and the held finger that opens a menu from its trigger) takes its default duration and tolerance, 500ms and 10px on either axis, from the same source as the long-press gesture recognizer. No behavior changes. |
There was a problem hiding this comment.
The held finger that opens a menu from its trigger now gets 13 px (.changeset/menu-trigger-drift.md), so naming it here with 10 px and "No behavior changes" contradicts that note (spec:AST-017 FR7). Dropping "and the held finger that opens a menu from its trigger" would fix it.
|
Fixed in f125044, after an audit of every listener, record and timer the host creates (the table is in the description):
The iOS simulator and WebKit evidence is in the description. It covers hit testing on moving content (this PR 18/18 against a control that fails 18/18) and the pointer-fed |
There was a problem hiding this comment.
Approval waits on an engineering-owner decision about two changes this pull request pursues: touches on moving content (on iOS a retargeted tap dispatches its own click and the document gets a blocking touchmove, which spec:AST-094 FR11 and FR19 do not allow as written) and the window-capture input stream, which no current record governs. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner. The fixes from the last round hold: the double-tap listener window, the notes, frames and timers after the last detach, the long-press changeset, and the touchend timing evidence. If presented hit testing stays, spec:AST-094's Platform support also asks for real-device iOS Safari evidence beyond the simulator takes in the description.
[Automated review]
|
Pushed 3be852c, rebased onto current main, which has FR19's fourth exception. The three card sentences are in word for word, as one commit with no other record edit: FR11 and FR19 in AST-094, and INV13 in react-component-runtime. The presented proof now uses Core's animator with a critically damped spring, moving the sheet up into place and down off the screen over the scrim. In both directions, a tap on the drawn Done clicks it once, and a drag on the drawn sheet catches it where it is drawn. With the correction off, both taps and the upward drag fail. It holds under load: Chromium pinned to 2 cores at 4 workers, 60/60; WebKit in a container capped at 2 CPUs at 4 workers, 60/60, and at 3 CPUs at 16 workers, where every tap and drag case passed (256/256; 3 of 128 offset checks missed their tolerance by under 1 px, a test-only check). The load runs found one host gap, fixed in this push. When the main thread handled a touch late, the touch went to what was drawn when the host ran, not when the finger landed. It now goes to what was drawn under the finger at touch-down, as FR11 says. A unit case failed before the fix, and with the fix off, 37 of 384 runs fail in WebKit capped at 3 CPUs. |
|
/approve-spec 3be852c |
There was a problem hiding this comment.
Changes requested: the record sentences for touches on moving content and the shared input listener are approved for this head, so what remains is code and evidence.
- A touch the host handles late, beside a sheet that has since slid under the finger, reaches the sheet's control rather than what was under the finger at touch-down (
GestureHost.ts:842-869); please handle that case and add a test beside the late-touch case. - AST-094's Platform support asks for real-device evidence for iOS Safari claims, and presented hit testing has simulator and Playwright WebKit takes only.
.changeset/input-stream.mdsays press feedback no longer misses touches behind an app's stop, but its pointer listeners were already on the window's capture phase; only its scroll and visibility listeners moved.utils/coarsePointer.tsre-spellsTOUCH_QUERY, whichutils/touchQueries.tsalready exports without React.
[Automated review]
| if (age > 0 && age < 1000) { | ||
| for (const mover of movers) { | ||
| const then = presentedOffsetOf(mover, time); | ||
| if (Math.abs(then.x) < 0.5 && Math.abs(then.y) < 0.5) { | ||
| continue; | ||
| } | ||
| const landed = this.drawnAt(x - then.x, y - then.y); | ||
| if (landed != null && containsAlongFlatTree(mover, landed)) { | ||
| hit = landed; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
This corrects a late touch only when some mover's content was under the finger at time. When the finger landed beside a mover that has since slid under it (a sheet presenting under a busy main thread), nothing matches, hit stays the mover's content drawn now, and isAboutMover returns it: in this file's test harness, a finger on the scrim 10 px above a sheet moving up at 160 px/s, handled 150 ms late, clicks Done instead of the scrim. Could the fallback take the first element of elementsFromPoint(x, y) that is outside every mover whose drawn box at time did not cover the point, with a case beside the late-touch test?
| '@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. |
There was a problem hiding this comment.
Press feedback's pointer and touchcancel listeners were already on the window's capture phase on main, where neither stop reaches them; what moves for consumers is its scroll and visibilitychange listeners. Could this say that, e.g. "Press feedback's scroll brake and page-hide reset now listen on the window's capture phase, so an app's document-level stopImmediatePropagation on scroll or visibilitychange registered before Core no longer stops them"?
| * (`spec:AST-094` FR19, FR24, FR43). The query is TOUCH_QUERY | ||
| * (touchPresentation.ts), spelled here because that module brings | ||
| * React; a test keeps the two equal. The answer can change while the page is |
There was a problem hiding this comment.
utils/touchQueries.ts exports TOUCH_QUERY without React ("the dependency-free half of touchPresentation.ts"), so this module can import it instead of spelling it again and pinning the copy in a test; pressFeedback.ts's stylesheetDroppedActive could then call isCoarsePointer(window) too.
meta#8360's change on the internal touch foundation: one click guard shared by the menu press and the gesture host (its click listener stays once the menu press arms it, as its record has it, and lasts only while armed for the gesture host, FR24), the host as the one touch owner (finger scroll, page holds, attachments, the press tracker hook and a held control's kept touch, the long press's single source, presented hit testing on iOS WebKit), its cases in the gesture browser proof (Chromium and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320 and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an unused import). Rebased onto main after section 2 as one commit; the touch-action rule came with meta#8071. Where fingers come from their pointer events, a claimed finger's touchend is cancelled from its element (FR21), under a fine primary pointer wherever the browser sends Touch Events and inside an open shadow root too; at most eight such lifts wait for their touchend, and each goes once taken or after 500 ms, kept across the host's uninstall, which can run in the microtask after the pointerup, before the touchend that comes later in the same task (FR24). An armed press's page listeners go once the arena has decided its last touch, a tap that waits on a double tap included, and after its last detach the host holds no listener, note, frame or timer (FR24). The animator's busy proof is judged against the harness's compositor-only control (meta#8906), with a main-thread mutant as its negative, and the spring's curve is placed against the control's own clock, so the late frames a loaded runner sends fail nothing. With the host serving every pointer (meta#8071), this change's own pieces follow it: a press tracker takes a mouse press as a press, painting at once and braking no scroll (FR11); a finger's or a pen's hold that does something keeps text unselected under any primary pointer, while a mouse keeps its released behavior (FR41); and `perEventPointer`, from before FR43, changes nothing. These come from meta#8665, which keeps only the undecided rule for text selection during a mouse or pen drag. The hover recognizer and the host's hover routing land with hover's first user. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…finger landed On iOS WebKit the host resolves moving content at touch-down and asks the engine what is under the finger. When a busy main thread hands it the touch later, the content has moved on: the host now asks at the point where what was under the finger at the touch's timestamp is drawn, so a tap on a moving sheet's Done still clicks Done (spec:AST-094 FR11). A presented source reports its offset at a given time for this. The presented browser proof drives the sheet with Core's animator and a critically damped spring, up into place and down off the screen over the scrim: a tap on the drawn Done clicks it once, and a drag on the drawn sheet catches it where it is drawn, in both directions.
…the finger (FR11, FR19); INV13 names the shared input listener Where the engine hit-tests an element a compositor animation moves at the motion's start (iOS WebKit), a touch on that content goes to what was drawn under the finger at touch-down, and a tap there activates that element with one click while the browser's own click is cancelled or swallowed (FR11). While such content around a recognizer that claims moving touches is moving, the document may carry a non-passive touchmove listener that cancels only owned moves and comes off when the motion ends; while it is on, a native scroll waits for a free main thread (FR19). 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 (architecture:react-component-runtime INV13).
…view fixes A touch the host handles late now also leaves alone content that slid under the finger after it landed: the touch goes to what was beneath it then (spec:AST-094 FR11). The input-stream changeset says what moved: the gesture host's listeners, and the press feedback's scroll and visibility listeners, since its pointer listeners were already on the window's capture phase. The coarse-pointer gate reads TOUCH_QUERY from touchQueries.ts instead of spelling it again. The WebKit interaction job's scope covers every module the gesture host now bundles: hooks/isRtlElement and utils' flatTree, hapticSwitch, inputStream, clickGuard, selectionLock and coarsePointer. The scope test's example of a util outside it moves from clickGuard, which the gesture fixture now bundles, to focusReturn. The presented proof checks that the sheet moves on a spring (its speed changes along the animator's curve) and no longer compares the drawn box with the animator's offset: under a 3-CPU cap the two disagree by 3 to 27 px in about half the frames whatever reference is used, while the tap and drag cases go through the host's real path.
…e origin most frames agree with Under the CI lane's WebKit profile (four CPUs, one worker), "a spring keeps advancing every frame through a 1 s main-thread block" failed in two ways, each about once in a few hundred runs, and a failing run's frames show each cause: - The spring drifted 138.6 ms against the control. The control's clock took its origin from the first usable frame alone, reading that frame's direction from the next usable one: the frame sat at x 210 on its way out, the next usable one at x 210 after the turn, and the tie read as "back". The clock now lives in controlClock.ts and takes the origin most frames agree with. On that run's own frames it reads 6.5 ms (bound 12), and its unit case for that layout fails on the old reading (150.7 ms astray). - The spring "moved on 61 of the 62 frames the control moved". At the block's end it was in its tail, under two of the cast's 1 px steps a frame, where a frame delivered early shows no step and the next catches up two. The harness now reads such a tail at its own pace (busyWindowProof), and the proof judges the whole block: on that run's own frames the earlier proof fails it and this one passes. No bound changes.
… (architecture:public-component-api INV23) Main's lint rule now keeps every private data attribute in its owner module. This change had moved `data-astryx-haptic-switch` from the haptics module to utils/hapticSwitch.ts, so the click guard could skip the hidden switch without importing haptics (architecture:react-component-runtime INV13). The name now lives in privateAttributes/hapticSwitch.stylex.ts, modelled on segmentDisabled.stylex.ts: it exports markHapticSwitch, which the haptics module uses on its hidden switch, and isHapticSwitch, which the click guard reads. utils/hapticSwitch.ts and the deprecated gestures/hapticSwitch.ts re-export go, the haptics module's baseline entry goes with its raw name, and the WebKit interaction job's scope covers the new module. No behavior changes.
|
Rebased onto main, with #9233 and #9007 merged, as a57e57e: the first five commits are an equal patch, the records' sentences byte-identical. A sixth commit moves the haptic switch's private attribute into its owner module, for main's new rule (
Under the required profiles every motion proof passes on both main and this change; where they fail, at five browsers a CPU and the start check at four workers on two cores, they fail alike on both trees, with both trees' numbers in the description. |
|
/approve-spec a57e57e |
There was a problem hiding this comment.
Changes requested: the record sentences are approved for this head, and last round's late-touch, changeset and coarse-pointer fixes hold, so what remains is code and evidence.
- Share the iOS WebKit check (
utils/selectionLock.ts,gestures/GestureHost.ts) throughutils/touchQueries.ts, and the flat-tree containment walk throughutils/flatTree.ts, instead of keeping two copies of each. - Reword the description's impact bullet: press feedback's pointer listeners were already on the window's capture phase; what moved were its scroll and page-hide listeners.
- Add a physical-iPhone take of the presented tap and drag proof with its correction-off control, as AST-094's Platform support asks, or have an engineering owner record a merge/release split for that evidence.
The pr-interaction-webkit failure is one animator timing proof this change does not edit, which passes repeatedly on both this branch and main; a re-run should clear it.
[Automated review]
| * same as utils/touchPresentation.ts's IOS_SUPPORTS_QUERY, kept here so the | ||
| * gesture modules import nothing from React. | ||
| */ | ||
| const IOS_SUPPORTS_QUERY = '(-webkit-touch-callout: none)'; |
There was a problem hiding this comment.
This re-spells IOS_SUPPORTS_QUERY (owned by utils/touchPresentation.ts:94) and GestureHost.ts:3065 tests the same engine a second way. utils/touchQueries.ts is the React-free half of touchPresentation.ts, already used here for TOUCH_QUERY: move the constant there with one isIosWebKit(view) reader and call it from both modules.
| } | ||
|
|
||
| /** Whether `node` is `ancestor` or inside it, across shadow roots. */ | ||
| function isWithin(node: Node, ancestor: Element): boolean { |
There was a problem hiding this comment.
Same walk as containsAlongFlatTree in GestureHost.ts:386. Exporting it from utils/flatTree.ts, beside parentAcrossShadow, gives both callers one copy.
| * workaround (`spec:AST-013` FR6) with its review condition and regression | ||
| * fixture in motion/presentation.ts. | ||
| */ | ||
| function isIosWebKit( |
There was a problem hiding this comment.
A second implementation of the iOS WebKit check utils/selectionLock.ts:176 also writes; utils/touchQueries.ts loads without React, so one reader there can serve both.
Behavior-module PR: click guard, input stream, gesture host touch ownership, page hold, finger-scroll watch, selection lock, presented hit testing
Section 3 of the merge plan: #8360 → #8098 → #8309 → #8763 → #8342. Six commits on main (a55352c), after #8071, #8076 and #9233 (the harness reads a slow tail at its own pace), holding only this change: the change itself, the late-touch fix with the spring proof, the record sentences, the last review round's fixes, the busy proof's control clock, and, sixth, the move of the haptic switch's private attribute into its owner module (
architecture:public-component-apiINV23). It carries #8074, #8270, #8293, #8315, #8320 and #8356, which closed as carried by it, and absorbs #8358 (the long press's tolerance comment and an unused import), which closes with a link here. It also takes the pieces of #8665's serving of every pointer that belong to its own features; #8665 keeps only the undecided rule for text selection during a mouse or pen drag.Primary intent
Make the gesture host the one touch owner on the page, and give the menu press model and the gesture host one guard for the click that follows a gesture that already acted. No narrower template fits: this is one section of the plan, internal infrastructure together with the four user-visible fixes those same changes make.
User or maintainer impact
stopPropagation, or a document-capturestopImmediatePropagationregistered before Core, hid touches from the press feedback and the recognizers. They now hear page-wide input through one stream on the window's capture phase.spec:AST-094FR29); it cancelled past 10 px.useFingerScroll; presented hit testing on iOS, so a touch on an element the animator is moving reaches what is drawn; and a hold's selection lock that each recognizer brings for its own contact.Problem and solution fit
spec:AST-094asks for one activation per gesture and for gesture work that costs nothing where it is not used (FR21, FR24), and every touch component builds on this host.Deltas:
utils/clickGuard.ts: one guard for both. It swallows the next click on the window's capture phase and disarms on that click, at the nextpointerdownor after 400 ms; a row's own activation passes throughwithoutClickGuard, and a keyboard's or assistive technology's click (detail0) always passes. Its click listener stays once the menu press model arms it (module:DropdownMenu/useMenuPressPR2) and lasts only while armed for the gesture host (FR24).utils/inputStream.ts: one native listener per event type on the window's capture phase, shared by every subscriber, passive unless a subscriber cancels the event.GestureHost:motion/presentation.ts).inputMode: 'pointer-events', or a window with noontouchstart), a claimed finger'stouchendcancelled from its element, as the Touch Events path cancels it (FR21): the lift is noted where its pointer landed, inside an open shadow root too, under a fine primary pointer wherever the browser sends Touch Events, and the note expires after 500 ms; at most eight notes wait, and none is kept where notouchendcan come (FR24).onIdle), a tap that waits on a double tap included, and in the microtask after its last detach, unless something attached again in that task, the host releases its notes, frames and timers (FR24).pageHold.ts,fingerScroll.tswithuseFingerScroll, andhostRegistry.ts: a scroll lock's page hold and the finger-scroll watch, which ask the host only about the touches it owns.claimsTouches: false) takes passive listeners only and follows a touch the browser scrolls with to the lift; a pan claims nothing before it begins at 10 pt; one velocity tracker for every gesture (spec:AST-100); the arena plumbing moves behindrecognizerInternals.utils/selectionLock.ts: the selection and callout lock a recognizer or a press tracker brings for its own contact; a long press brings one only when it has a hold, and an element attached withmanageStyle: falsekeeps its own styles (FR41).useGestureRecognizerandusePressTracker(now its own file) call no caller back from an Effect or after unmount, and what a sync unblocks resolves a microtask later, where its listener reports as usual;usePressTrackerbrakes with the host and keeps one tracker through an option change.useMenuPressuses the shared guard and FR29's drift allowance;useLongPresstakes its 500 ms and 10 px from the long-press recognizer's table, and its publicstarttakes the point only, so the per-press tolerance stays internal touseMenuPress(architecture:public-component-apiINV12).perEventPointeris accepted and does nothing.presentedHitTesting.interaction.browser.spec.ts, in Chromium and WebKit, and the README; the animator's busy proof is judged against the harness's compositor-only control (test(interaction-harness): a busy-window proof can judge a component against a compositor-only control #8906).Known gap: in iPadOS Safari, the stylus
touchendof a pen a recognizer claimed is not cancelled yet; that waits for evidence from that browser.Authority
spec:AST-094(current): FR11, FR19 (with its fourth exception: a gesture cancelled while its finger moves keeps that finger), FR20, FR21, FR24, FR29, FR41 and FR43;module:DropdownMenu/useMenuPress(current) FR5, FR9 and PR2;spec:AST-057FR6;spec:AST-100;architecture:react-component-runtimeINV2, INV3, INV5 and INV13. The hover recognizer and the host's hover routing land with hover's first user. This pull request adds the spec owner's approved sentences to FR11 (a tap on content that moved under the finger), FR19 (the document listener while that content moves) and INV13 (the shared input listener).Change and evidence
The click guard's unit cases failed before it; so did the case for the gesture host's arm, whose click listener now goes at the click, the next press or the end of the window, and the browser case where a claimed drag leaves no page listener once that window ends.
The held trigger's 12 and 14 px drifts are pinned under either primary pointer.
Each review fix the carried pull requests made has a case that failed before it.
In the browser proofs, a claimed drag leaves no page listener once the click window ends, and a touch on a moving sheet reaches the button drawn there; the rest of the gesture proof comes with feat(core): touch-action derivation, native scroll probe, haptics and the gesture host #8071.
The public
useLongPressstartkeeps its own tolerance when a caller passes one anyway, a case that failed before.With fingers from pointer events, a claimed pan's lift and a recognized tap's have their
touchendcancelled, under a fine primary pointer (with thattouchendcoming after the microtask that follows itspointerup, once the armed press's listeners have gone) and inside an open shadow root too, while an unclaimed lift keeps its own; those cases failed before.A
nothing left after the last detachblock runs 17 scenarios and checks that no listener, note, frame or timer remains after each; 16 of them failed before this round's fixes, and the double-tap wait's release failed before too.Claimed lifts that wait for their
touchendstop at eight, the oldest going first, and go once 500 ms pass, while a desktop with no Touch Events keeps none; those cases failed before.Fixed in this round, found by the load runs: a touch that a busy main thread handed the host late went to what was drawn when the host ran, not where the finger landed, so a tap on a moving sheet's Done could miss it. It now goes to what was drawn under the finger at touch-down (FR11): the host asks at the point that content has moved to since the touch's timestamp, and content that slid under the finger after it landed takes nothing, so the touch goes to what was beneath it. A unit case failed before each. In WebKit at 3 CPUs and 16 workers, 15 of 384 runs fail without it, all taps and drags on the moving sheet, and all 384 pass with it.
The presented browser proof drives the sheet with Core's animator and a critically damped spring, up into place and down off the screen over the scrim, with a tap on the drawn Done and a drag on the drawn sheet in each direction.
The WebKit interaction job's scope covers every module the gesture host now bundles:
hooks/isRtlElement, the haptic switch's owner moduleprivateAttributes/hapticSwitch.stylex.ts, and the utilsflatTree,inputStream,clickGuard,selectionLockandcoarsePointer. Its scope test failed on them before. That test's example of a util outside the scope moves fromclickGuard, which the gesture fixture now bundles, tofocusReturn.Sixth commit, for main's rule that a private data attribute is written only in its owner module (
@astryx/no-raw-private-attribute, INV23): the haptic switch's attribute now lives inprivateAttributes/hapticSwitch.stylex.ts, modelled onsegmentDisabled.stylex.ts. The haptics module marks its hidden switch throughmarkHapticSwitch, and the click guard reads it throughisHapticSwitch, still without importing haptics (INV13). This change's ownutils/hapticSwitch.tsand the deprecatedgestures/hapticSwitch.tsre-export go, and so does the haptics module's baseline entry, with its raw name. The rule failed on this change before. No behavior changes.Dropped: the browser check that compared the sheet's drawn box with the animator's offset. Under load, Linux WebKit draws the moving sheet ahead of the time it reports to script, so the two disagree: at 3 CPUs and 16 workers, 61 of 128 runs by more than 3 px, a median of 11 px and up to 27. The animator itself agrees with WebKit's animation every frame. On the iOS 26.2 simulator, with and without a busy main thread, the drawn box resolves where the animator records it (32 of 32 within 0.17 px), so the host's correction holds there. The tap and drag cases, which go through the host's hit path, still pass 384/384 at 3 CPUs. The spring itself is still checked: the animator's own curve speeds up and slows down. Follow-up: [Bug] Under CPU starvation, Linux WebKit draws a compositor-animated element ahead of its reported currentTime, so the animator's offset and the drawn box differ by 10–17 px #9186.
Fixed in this round, found by CI's WebKit lane profile (a 4-CPU container, one worker): "a spring keeps advancing every frame through a 1 s main-thread block" failed in two ways, each about once in a few hundred runs. A failing run's frames show each cause, and on those frames the fix passes.
controlClock, a test helper) now takes the origin most frames agree with; it reads 6.5 ms on that run's frames (bound 12), and its unit case for that frame layout failed before.Motion proofs under load, both trees run at the same time (the gesture, animated-layout and animator proofs, ten runs each). WebKit capped at 4 CPUs with
CI=1, and at 3 CPUs with one worker, renderer threads matched to the CPUs: every proof passes on main and with this change. At 3 CPUs and 16 workers, five workers a CPU, the real-time motion proofs fail on both alike: "a spring keeps advancing every frame…" main 10/10, this PR 10/10; "a finished animation leaves the element…" 9 and 9; "a spring that ends inside the block…" 8 and 8; the animated-layout flex case 4 and 5; this PR's own negative control, "a move driven from the main thread … fails it", 10/10. In Chromium pinned to 2 cores with one worker, where the busy-main-thread proofs are judged, every animator proof passes on both trees (main 140/140, this PR 150/150, ten runs each); at four workers on those cores, the animator's start check (|median| < 20ms) fails on both trees (main 13 of 30 runs, this PR 17 of 30) because it reads late screencast stamps; it is fixed in the stacked draft that reads the control's clock on this head.Re-measured in this round with each container's renderer threads matched to its CPUs, which an earlier setup left unmatched: the late-touch counts (15 of 384 without the fix), the drawn-ahead disagreement (a median of 11 px), and the motion proofs (all passing at the required profiles).
Records:
spec:AST-094FR11 and FR19 state what the host does with a touch on moving content, andarchitecture:react-component-runtimeINV13 states the shared input listener, in the spec owner's wording.Scope
Lifecycle audit
Every listener, record and timer the gesture host (and the arena, where it holds host state) creates, what creates and clears it, and what happens in each case (
spec:AST-094FR21, FR24). "Fixed" marks a row this round changed. Anothing left after the last detachblock runs 17 scenarios through one check: no listener added during the scenario is left on any target, every host collection is empty, no arena timer is armed, and no frame or timeout is pending. Before these fixes 16 of them failed.onIdle)'pointer-events'reads fingers from pointer eventstouchendthat cancels,contextmenu)touchend(a claimed lift)touchmovepointerdown(armed press)touchstarttouchmove(moving content)ResizeObservertouch-actionread and its framecancelledFingers)touchendortouchcancel; a later touch event without ittouchend; 500 ms; at most 8pointerup, before thetouchendthat comes later in the same taskownScrollrecords and their framesutils/clickGuard)pointerdown, or 400 msReal-browser evidence
simulator/emulator evidence gates merge; real iPhone + Android gate release.
Pages built from this PR's tree with esbuild, as the gesture browser fixture is: the real host, animator, recognizers and press tracker, nothing stubbed. iOS: iPhone 17 Pro simulator, iOS 26.2, Safari. WebKit: Playwright 1.61 with WebKit 26.5, in a touch-enabled phone context. Every take was recorded and checked frame by frame. The takes ran at d7e6d99; this round's changes touch neither path (they run after the last detach, or end an armed press under a fine pointer).
Hit testing on moving content (AST-013 FR10), iOS 26.2 Safari. A sheet slides on the compositor over a scrim that closes it: down 0→444 px, back up, down again, 4 s each. Taps land on the drawn Done button while it slides down (A) or up (B), or where Done started (C). The run was 18 takes with this PR, then 18 with the correction off (
presentedHitTesting: 'never') as a control. In every take the engine's own hit test was whatever sat under the finger where the slide started.Every click with this PR came from the host, with the engine's
touchendcancelled (18/18).Contact sheets of one loop: this PR, control.
Pointer-fed
touchendtiming (inputMode: 'pointer-events').pointerupcomes first andtouchendfollows in the same task, in every take: iOS 30/30, with equal timestamps; WebKit 30/30, 0–1 ms apart. Only microtasks ran between them; a message, asetTimeout(0)and a frame queued at thepointeruphad not run yet. Under a fine pointer the armed press's listeners go in that microtask, which is why a claimed lift's note outlives the uninstall.touchendcancelled, and no mousedown, mouseup or click reached the element. iOS 12/12, WebKit 12/12.A drag catching a moving sheet, iOS 26.2 Safari. While the sheet slides on the compositor (down 0→300 px or up 300→0 px over 4 s), a finger lands on it where it is drawn and drags 135 px down in about 0.4 s. The run was 14 takes with this PR and 14 with the correction off, on a page the sheet holds still.
With this PR the pan begins 31–51 ms after touch-down, and the sheet follows after 3–5 frames (the pan's 10 px threshold, while the slide goes on); from then on it stays within 2 px of the finger. Every move after the pan began is cancelled, and nothing scrolls or clicks.
Contact sheets: this PR, control.
A limit already on main, which this PR does not change: over a page that can scroll, with no page hold, the page's scroll takes about half the drags with or without the correction (this PR 8/14; the control 3/7 of its down drags). A one-way pan claims only when it begins at 10 px; at the simulator's 6.75 px per event it begins at 13.5 px, after Safari has committed to the scroll (GIF). An earlier claim for a non-modal sheet over a scrolling page is an owner decision under AST-094 FR19.
Writing rubric results
R1 narration: none; each added sentence states what the system does, in the present tense.
R2 research: none in the records; the measurements behind them are in this description and its evidence media.
R3 evidence: none; the added sentences cite no pull request, issue or commit.
R4 rejected alternatives: none added.
R5 change references: none in the added text (searched for
#followed by digits,/pull/and/issues/).R6 open questions: none touched.
Testing
What re-ran, and what carried
At this change's gate on the previous main (e71393ac9d), a test re-ran when what it loads (its imports and every page it bundles) includes a file that changed since this change's last full gate: 224 files, main's and the sixth commit's. Re-ran:
Carried from the last full gate: 1,262 test files whose loaded files are all unchanged.
On main 8a03d75, against e71393ac9d (379 differing files, most of them #9007's record moves), re-ran: interaction specs (5): harness, MonthScrollerSteps, TabPressPointer, presentedHitTesting, animator; accessibility specs (3): BottomSheet, CheckboxInput, lab Schedule; the performance spec; and 371 unit and node test files. Carried from e71393ac9d: 1,429 test files, the gesture host's proofs among them.
On main a55352c, against 44a5a82384 (88 differing files): the animatedLayout and animatedLayoutAdopters interaction specs and 210 unit and node test files re-ran; 1,603 carried.
Local gates at a57e57e, six commits on main a55352c holding only this change (107 files, +15,135/−1,808). A test re-ran when what it loads, its imports and the pages it bundles, includes a file that changed since the gate before: 540 test files at e71393ac9d (on main 3a2d3d9), 380 at 44a5a82384 (on main 8a03d75, after #9007) and 212 here. The rest load only identical files and carry those results:
pnpm test:ui(the UI tests of Core, Lab and Richtext): 710 files, 17,659 tests passed.pnpm exec vitest run --project node, after building Core: 831 files passed; the only failures are two cases inpackages/cli/foundation/release/cache.test.mjs, which fail on main as well (fix(cli): a crashed run's release install is recognized by the Node it ran on, whatever its name #8520). The WebKit job's scope test (.github/scripts/ci-interaction-webkit.test.mjs): 13/13.playwright.interaction.config.ts, a phone in Chromium and WebKit), all eleven with the gesture host's at e71393ac9d: Chromium 236 passed; WebKit 200 passed and 36 skipped. The five that re-ran at 44a5a82384: 110, and 91 with 19 skipped. The two that re-ran here: 74 and 74. WebKit skips input its driver does not provide: native scrolling or pinch-zoom from injected touches, a Chromium trace and a pen.presentedHitTesting.interaction.browser.spec.ts): 6/6 in Chromium and in WebKit. With the correction off, both taps and the drag on the sheet springing up fail in each browser; with the late-touch fix off, 15 of 384 fail at 3 CPUs and 16 workers, all taps and drags.CI=1, renderer threads matched, one worker, ten runs each): 2,000 of 2,000 at e71393ac9d, 910 of 910 at 44a5a82384, 740 of 740 here.TableRowGlide.a11y.browser.spec.tscases fail in WebKit on main as well, run at the same time. The sandbox's performance scene: 5/5.tsc --project packages/core/tsconfig.json --noEmit, strict ESLint and Prettier on every changed file: clean.pnpm check:knowledgeandpnpm check:repo: clean. The private-attribute baseline (no-raw-private-attribute.baseline.mjs --check): up to date. The harness's own tests: 104/104.