Skip to content

refactor(AlertDialog): hold input through the Layer input hold - #8373

Draft
vjeux wants to merge 1915 commits into
meta:mainfrom
vjeux:uikit/c18-alert-hold
Draft

vjeux wants to merge 1915 commits into
meta:mainfrom
vjeux:uikit/c18-alert-hold

Conversation

@vjeux

@vjeux vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8084 and #8342. Review only this PR's change: vjeux/astryx@a0a8843...fd8cec9 (the base merges #8084 and #8342; the compared head has this PR's tree, c3a4cdc)

Why

Under a finger, #8084 makes AlertDialog ignore input while it animates with code of its own: the button tracker drops touches through an isInert check, and render guards drop a click, Enter or Escape while it leaves. #8342 adds one shared input hold to the Layer module for every touch presentation (ContextMenu and MobileNav use it). Two mechanisms for one rule drift apart.

What led to it

The UIKit-parity work, which brings Astryx's touch behavior to iOS's, gave AlertDialog its touch presentation in #8084 before the shared hold existed. Cindy's decision extends the alert's entrance rule to every touch presentation through #8342's hold, recorded by #8618 (spec:AST-094 FR35, DEC-16): during an entrance a tap or a drag reaches nothing, keys and assistive technology work, and a pinch still zooms. #8595 (INV16) makes the runtime own that one hold.

Options and tradeoffs

  1. Keep AlertDialog's own guards. No change, but two implementations of one rule.
  2. Move AlertDialog onto holdInput. One implementation. One detail moves: while the alert fades out its content is inert and focus leaves it; Dialog returns focus to the opener at the end.

Why this choice

Option 2. useAlertMotion holds the whole viewport from the entrance's start until UIKit's 435 ms window ends (or the reduced-motion cross-fade does), and the alert alone until the fade-out ends; its unused acceptsTouches goes. AlertDialog drops the tracker's isInert check and its click guards, and keeps its Escape guard, because inert content doesn't stop the dialog's own cancel. A mouse is unchanged. Known public-API gap (spec:AST-107 FR7): the input hold.

How to check it

On a phone or the iOS simulator, in the Core/AlertDialog Delete story:

  1. Open the alert and tap Cancel at once: nothing happens; once it settles, Cancel works.
  2. Open it and drag on it right away: nothing scrolls or runs. Pinch: the page zooms and the alert stays.
  3. Open it and press Escape at once on a hardware keyboard, or activate Cancel with VoiceOver: it closes.
  4. Close it and tap the opener while it fades out: nothing happens.
  5. On a desktop with a mouse: everything works at once, as before.

All five were run as browser tests in Chromium (the VoiceOver step as the click VoiceOver sends), and all but the drag and the pinch in WebKit, which can't inject them: each behaved as written.

Evidence

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 30, 2026 2:03am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 2:57 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's decision: no current spec covers AlertDialog's touch input rule as implemented here (every tap, press and click is held for 435 ms after the alert starts presenting, and while it fades out its content is inert and focus leaves it), so an engineering owner needs to approve or reject that rule and say which record states it, by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Independently of that decision, these need changes:

  • The branch carries the unmerged #8084, #8342 and touch-foundation commits (about 37,000 authored lines in 214 files); once they land, rebase so this pull request is only the 7-file AlertDialog commit.
  • gestures/VelocityTracker.ts offers four selectable release filters and no stillness reset, which contradicts architecture:interaction-modality INV10 and spec:AST-100 FR2.
  • AlertDialog's touch action tracking has no presentation opt-out, which architecture:public-component-api INV16 requires unless a current AlertDialog record defines it.
  • Reopening the alert during its fade leaves focus on the document body, because the dismissal hold blurs the focused action and nothing restores it.
  • Under React StrictMode, an alert mounted open loses its presentation hold: the unmount-only effect's cleanup releases it during replay and the transition effect never places it again.
  • The description should give element counts for the hold's shield, as architecture:component-style-authoring INV7 asks for added elements.

[Automated review]

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

AlertDialog (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 191B 191B 0B
Lines of Code 182 505 -
Complexity Medium High (23) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 88.7KB +31.7KB ⚠️ 12.4KB 12.5KB +74B
core/AppShell 58.0KB 58.6KB +577B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 40.1KB 40.7KB +658B 6.4KB 6.4KB 0B
core/Banner 48.0KB 48.6KB +619B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.5KB 55.0KB +586B 9.9KB 9.9KB 0B
core/BreadcrumbItem 66.5KB 67.3KB +813B 11.0KB 11.0KB 0B
core/Button 43.4KB 44.1KB +639B 9.2KB 9.2KB 0B
core/Calendar 55.1KB 55.7KB +623B 10.0KB 10.0KB 0B
core/Carousel 50.4KB 51.0KB +610B 10.2KB 10.2KB 0B
core/ChatComposer 60.8KB 61.4KB +617B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.9KB 56.5KB +616B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.9KB 56.5KB +612B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.6KB 47.2KB +621B 9.5KB 9.5KB 0B
core/ChatLayout 49.0KB 49.6KB +642B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.5KB 47.1KB +623B 9.6KB 9.6KB 0B
core/ChatSendButton 46.0KB 46.6KB +609B 9.2KB 9.2KB 0B
core/CheckboxInput 50.1KB 50.6KB +593B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.2KB 54.8KB +582B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.4KB +663B 6.9KB 6.9KB 0B
core/CodeBlock 60.7KB 61.3KB +611B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.3KB +638B 4.2KB 4.2KB 0B
core/CommandPalette 51.0KB 51.7KB +655B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 9.0KB +630B 3.5KB 3.5KB 0B
core/ComplexSelector 56.0KB 56.6KB +627B 10.6KB 10.6KB 0B
core/ContextMenu 74.7KB 75.6KB +846B 14.6KB 14.6KB 0B
core/ContextMenuItem 53.0KB 53.7KB +712B 9.6KB 9.6KB 0B
core/DateInput 99.7KB 100.3KB +642B 14.8KB 14.8KB 0B
core/DateRangeInput 69.5KB 70.2KB +646B 11.2KB 11.2KB 0B
core/DateTimeInput 109.8KB 110.4KB +656B 15.2KB 15.2KB 0B
core/DialogHeader 49.0KB 49.6KB +610B 9.8KB 9.8KB 0B
core/DrawerHeader 49.1KB 49.6KB +585B 11.2KB 11.2KB 0B
core/DropdownMenu 72.8KB 73.6KB +791B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.8KB +646B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 16.0KB +643B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.8KB +652B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.2KB 59.0KB +756B 9.5KB 9.5KB 0B
core/FileInput 54.3KB 54.8KB +575B 10.3KB 10.3KB 0B
core/IconButton 43.5KB 44.1KB +637B 9.2KB 9.2KB 0B
core/Item 11.4KB 12.0KB +638B 4.7KB 4.7KB 0B
core/LayerProvider 63.4KB 64.1KB +636B 10.1KB 10.1KB 0B
core/Lightbox 51.3KB 51.9KB +585B 9.8KB 9.8KB 0B
core/Link 43.7KB 44.3KB +625B 7.6KB 7.6KB 0B
core/ListItem 53.7KB 54.2KB +593B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 145.3KB +807B 17.4KB 17.4KB 0B
core/MobileNav 51.2KB 51.7KB +584B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.4KB 47.0KB +597B 9.4KB 9.4KB 0B
core/MoreMenu 73.2KB 73.9KB +778B 14.7KB 14.7KB 0B
core/MultiSelector 73.0KB 73.6KB +638B 14.6KB 14.6KB 0B
core/NumberInput 55.8KB 56.5KB +642B 10.7KB 10.7KB 0B
core/Outline 28.8KB 29.4KB +629B 4.0KB 4.0KB 0B
core/Pagination 84.9KB 85.6KB +806B 15.3KB 15.3KB 0B
core/Popover 50.0KB 50.6KB +641B 12.1KB 12.1KB 0B
core/PowerSearch 158.1KB 159.1KB +960B 19.9KB 19.9KB +65B
core/RadioListItem 52.4KB 53.0KB +614B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 9.2KB +628B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.4KB +664B 7.1KB 7.1KB 0B
core/Selector 76.4KB 77.2KB +799B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 15.1KB +637B 5.1KB 5.1KB 0B
core/SideNav 58.1KB 58.7KB +616B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.8KB 47.4KB +610B 9.5KB 9.5KB 0B
core/SideNavHeading 54.9KB 55.5KB +599B 10.1KB 10.1KB 0B
core/SideNavItem 54.6KB 55.2KB +581B 10.1KB 10.1KB 0B
core/Step 32.6KB 33.2KB +619B 5.3KB 5.3KB 0B
core/Stepper 47.5KB 48.1KB +611B 9.6KB 9.6KB 0B
core/Switch 48.5KB 49.1KB +611B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.3KB +644B 4.1KB 4.1KB 0B
core/TabMenu 51.8KB 52.4KB +623B 10.0KB 10.0KB 0B
core/Table 83.9KB 84.7KB +826B 15.1KB 15.1KB 0B
core/TableCell 75.8KB 76.6KB +845B 14.8KB 14.8KB 0B
core/TableHeaderCell 75.5KB 76.4KB +863B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.5KB 53.1KB +628B 11.2KB 11.2KB 0B
core/TextInput 53.7KB 54.3KB +608B 10.5KB 10.5KB 0B
core/Thumbnail 50.8KB 51.4KB +624B 11.0KB 11.0KB 0B
core/TimeInput 86.9KB 87.5KB +560B 13.8KB 13.8KB 0B
core/Timestamp 27.7KB 27.7KB +1B 9.5KB 9.5KB 0B
core/Toast 60.0KB 60.6KB +602B 9.7KB 9.7KB 0B
core/ToggleButton 44.3KB 44.9KB +590B 9.3KB 9.3KB 0B
core/Token 29.7KB 30.4KB +637B 4.5KB 4.5KB 0B
core/Tokenizer 68.8KB 69.5KB +765B 16.5KB 16.6KB +42B
core/TopNav 52.0KB 52.6KB +579B 10.4KB 10.4KB 0B
core/TopNavHeading 54.3KB 54.9KB +589B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.7KB +637B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.3KB 54.9KB +624B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.7KB +635B 4.1KB 4.1KB 0B
core/TopNavMenu 53.7KB 54.3KB +611B 10.1KB 10.1KB 0B
core/TreeList 32.7KB 33.3KB +618B 4.7KB 4.7KB 0B
core/Typeahead 61.6KB 62.2KB +626B 11.3KB 11.3KB 0B
lab/ChatEmojiPicker N/A 65.9KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.4KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.8KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.1KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 247.8KB N/A N/A 2.4KB N/A
lab/Comments N/A 252.2KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.3KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.7KB N/A N/A 722B N/A
lab/ListInput N/A 75.9KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 189.7KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 122.7KB N/A N/A 775B N/A
lab/Schedule N/A 60.6KB N/A N/A 3.9KB N/A
lab/TourStep N/A 56.0KB N/A N/A 590B N/A
lab/TransferList N/A 67.5KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.0KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.4KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.3KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.9KB N/A N/A 2.3KB N/A

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

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

Component JavaScript CSS
core/AlertDialog +31.7KB ⚠️ +74B

Bundle Size Summary

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

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

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 504 shot(s) changed.

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

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

and 35 more.


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

@vercel

vercel Bot commented Oct 25, 2026

Copy link
Copy Markdown

Deployment failed for project astryx with the following error:

The provided GitHub repository does not contain the requested branch or commit reference. Please ensure the repository is not empty.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8342 (64a5a7f) into meta#8373

Brings in the alert stack's new head (on meta#8270 a44569c) and the
presentation hold on meta#8336 af532ad. The gesture files are meta#8336's own
merged versions (meta#8336 carries meta#8270), and AlertDialog keeps this pull
request's migration onto the hold.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's decision: no current spec covers AlertDialog's touch input rule as implemented here (every tap, press and click is held for 435 ms after the alert starts presenting, and while it fades out its content is inert and focus leaves it), so an engineering owner needs to approve or reject that rule and say which record states it, by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Independently of that decision, these need changes:

  • The branch carries the unmerged #8084, #8342 and touch-foundation commits (about 37,600 authored lines in 198 files); once they land, rebase so this pull request is only its 7-file AlertDialog commit.
  • AlertDialog's touch action tracking has no presentation opt-out, which architecture:public-component-api INV16 requires unless a current AlertDialog record defines it.
  • AlertDialog's import now carries the whole touch layer and the complete spring table, while architecture:component-style-authoring INV6 asks for a lazy load under a coarse pointer or a stated justification, and only the table rows the alert uses.
  • Reopening the alert during its fade leaves focus on the document, because the dismissal hold blurs the focused action and nothing restores it.
  • Under React StrictMode an alert mounted open loses its presentation hold, since the unmount-only effect's cleanup releases it during replay and the transition effect never places it again.
  • The description should give element counts for the hold's shield, as architecture:component-style-authoring INV7 asks for added elements.

[Automated review]

Comment on lines +206 to +212
useEffect(
() => () => {
releaseHoldRef.current?.();
releaseHoldRef.current = null;
},
[],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Under StrictMode this cleanup runs during the replay and releases the hold the transition effect below placed; the replayed transition effect then returns early (was === now), so an alert mounted open has no hold or shield during its presentation in development. architecture:react-component-runtime INV3 asks the effect that acquires a resource to release it in its own cleanup: let the transition effect own the hold and re-place it when a replayed setup finds the alert still presenting, and have the StrictMode test touch during the presentation.

Comment on lines +267 to +275
if (now === 'open' && was === 'leaving') {
// Reopened mid-exit: fade back from where it is, with its velocity.
// Touches come back a presentation's time after that fade starts.
exitRef.current = null;
holdWhilePresenting(
fadeFromHere(panel, backdrop, 1, springs.alertPresent),
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Releasing the 'layer' hold here removes inert but leaves focus where the dismissal hold put it, on the document, and Dialog does not re-run its [data-autofocus] focus because isOpen stayed true. The reopened alert has nothing focused, so Enter does nothing until Tab. Refocus the element the hold blurred, or Cancel, when a reopen releases a 'layer' hold.

import {useMergedRefs} from '../hooks/useMergedRefs';
import {alertController} from '../motion/uikit/alert';
import {useTouchPresentation} from '../utils/touchPresentation';
import {useAlertDialogTouch} from './AlertDialogTouch';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This static import brings the animator, the Layer hold, the gesture host and the long-press recognizer into every AlertDialog import, though they run only under a coarse pointer (the size report for the previous revision measured +30.1 KB gzip). architecture:component-style-authoring INV6 asks for a lazy load under a coarse pointer or a stated justification.

Comment on lines +94 to +104
useActionGroupTracking(surfaceRef, {
itemSelector,
highlightOnsetMs: alertController.actions.highlightOnsetMs,
highlightExpansion: alertController.actions.highlightExpansion,
shouldTrack: target => target !== surfaceRef.current,
isEnabled: isTracking,
// A mouse keeps selecting the alert's text: the host's permanent
// long-press styles stay off. A held finger selects nothing, since the
// group's hold does something (`spec:AST-094` FR41).
manageStyle: false,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This group tracking switches how the open alert follows a finger, and AlertDialog has no presentation value that keeps each button's own click: architecture:public-component-api INV16 requires one unless a current AlertDialog record defines the tracking.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…ng) into meta#8373

The hold's presentation window, a cancellable timer here, is timed from the spring the alert plays, as meta#8084's window is; the CLI docs golden and the motion roles are regenerated, not hand-merged.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…lus UIKit's lag) into meta#8373

The changed-springs test runs on the hold's window timer here.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
F2's line at 04:52Z reaches the migration through the hold; it carries meta#8270 d25ba89, a fast-forward of the meta#8270 head under meta#8084. No conflicts; the motion roles (45 in 4 tables) and the CLI docs golden are unchanged.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
F2's line at 06:14Z reaches the migration through the hold: meta#8076 07aa9c0 and meta#8270 14db8b5. No conflicts; the base retires the theme motion surface (the role generator and its generated kinds), which this branch never edited.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…into meta#8373

The hold's window timer takes the same spring-derived window, now read from the table; the no-theme-variable test runs on that timer here.
vjeux added 17 commits October 29, 2026 10:17
…ill needs the element, and the listeners that cancel

The release note, the stream's header, the host's comment and the README
said the stream is one passive listener per event type and that a
document-capture stopPropagation no longer starves the recognizers. Such a
stop never hid a listener on the same node, and some subscriptions cancel.
They now say what changed: an app's window-capture stopPropagation, or a
document-capture stopImmediatePropagation registered before core, no
longer hides a touch; a recognizer that must claim still needs the touch to
reach its element; the gesture host's touchend, click and contextmenu
cancel while a claimer is attached and the page hold's touchmove while it
holds; and the stream stays ahead of an app's window-capture listener only
until a type's listener is re-added. The README's list of blocking
touchmove listeners gains the claimed touch's target and the document
while a claiming element moves.
…default it moves

The {stylexOptions} form of astryxStylex now passes StyleX
treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3
asks the release note to name the old and new default and the option
that restores the old one; it now does.
…er and for the two stops the input stream fixes

- A still finger with sub-pixel jitter over a scroll view keeps its tap
  and scrolls nothing (spec:AST-094 FR18–FR22).
- A tap is still recognized under an app's window-capture
  stopPropagation, and under a document-capture
  stopImmediatePropagation registered before core's: the recognizers hear
  touches on the window's capture phase.
Chromium and WebKit pass.
…s once

Where nothing that claims is attached, the host's touchend listener is
passive and cannot cancel the engine's own click after a retargeted tap
clicks what is drawn. The shared click guard swallows that late click
whatever is attached; this case pins it, as asked on meta#8293.
meta#8360 now carries the review fixes of the foundation it holds. The two
conflicts are this pull request's own files:
- GestureHost.ts: meta#8360 counts the attachments that ask for an action
  group; here the fine-pointer group listener and the per-event recognizer
  set are gone (every pointer is served, FR43), so the merge keeps the
  count and drops those two.
- GestureHost.test.ts: both sides added the same assertion to the
  second-finger case; the merge keeps this side's comment. meta#8360's
  action-group lifetime case counts a group's own pointerdown under a fine
  pointer, which this pull request replaces with one passive pointerdown
  on any element that uses a gesture (its own idle-listener case), so the
  merge leaves it out.
… attached, in this host's terms

meta#8360's action-group lifetime case pins that an element stays a group
while any attachment asks and stops with the last. Here every element
that uses a gesture is served as a group under a fine pointer, so the
same invariant reads as the element's one passive pointerdown: with two
gestures attached, detaching either keeps it, and detaching the last
removes it. It passes on this head.
meta#8360 carries this pull request's predecessors and the review fixes of
the foundation it holds; this pull request's own diff is now its six
commits (a second finger fails a one-finger pan wherever fingers come
from, and the check reruns once that finger is delivered; a press's
selection lock gets what it landed on; a pinch's second finger on another
pan claims neither; the wider-band edge pan's real-touch proof; text).
Two conflicts:
- GestureHost.ts: the selection lock's target takes meta#8360's presented
  target, which is what the press landed on unless an element under it
  is moving, so it keeps this pull request's fix and meta#8293's correction.
- .changeset/gesture-press-tracking.md: meta#8360's text, which names both
  writers of the press attribute.
…esture host's subscriptions

The gesture host subscribes no click: the one cancelling click subscriber
is the click guard's, added at a window's first arm and kept. The release
note, the stream's header and the gestures README listed click with the
host's touchend and contextmenu; they now name the guard's own.
meta#8360's input-stream notes no longer list click among the gesture host's
subscriptions. The merge is clean.
… lift and the browser's menu, keeps a kept press's finger, and goes when the page hides

From the review of this pull request, three cases fail on its head:
- an armed press installed the page listeners without the claiming
  ones, so a claimed finger's touchend stayed passive and the browser's
  menu opened during a claimed long press (spec:AST-094 FR21);
- a kept press got its blocking touchmove during an armed press, but its
  moves were cancelled only where the primary pointer is coarse, so a
  held stepper on a touchscreen laptop lost its finger (FR11, DEC-21);
- a page hidden mid-press cancelled the press but kept the page's
  listeners until the next lift anywhere (FR24).
…lift and the browser's menu, keeps a kept press's finger, and goes when the page hides

- A finger's or a pen's armed press now brings the claiming listeners
  (the cancelable touchend and the contextmenu canceller) with the page
  listeners, as wherever the host installs, so a claimed lift clicks
  nothing and a claimed hold opens no browser menu (spec:AST-094 FR21). A
  mouse's press needs neither and adds neither.
- A kept press keeps its finger under any pointer: its target blocks and
  its moves are cancelled once its press is delivered, not only where the
  primary pointer is coarse (FR11, DEC-21).
- A page hidden mid-press releases the armed press as a blur does, so the
  page's listeners go with the cancelled press (FR24).
…'s selection

An action group's press is a long press with no minimum duration, so it
claims at the press's pointerdown, before any movement. The drag
selection hold then cleared the page's selection, so a plain click on a
group's button left its Copy or Cut with nothing to act on. The case
fails on this head: the selection is gone after the pointerdown.
…age's selection

The drag selection hold cleared the page's selection when any mouse or
pen contact was claimed, but an action group's press claims at its
pointerdown, before it has moved or selected anything, so a plain click
on a group's button left its Copy or Cut with nothing to act on. A claim
before any movement now keeps the page's selection: the hold still
refuses a new one, and puts the page's back over a selection that
appears while it runs. A claim after movement clears what its drag
selected, as before (spec:AST-094 FR41).

The case before the new one now waits for its hold to end a task after
the lift, so no hold outlives that test.
…t every pointer does

- GestureHost's header, the gestures README and coarsePointer.ts no
  longer describe the coarse-only host: a finger or a pen takes the
  touch-down lock under any primary pointer, a kept press's finger is
  kept during a fine pointer's press too, and recognizers attach for
  every pointer while the page listeners last a press (spec:AST-094 FR19,
  FR24, FR41, FR43).
- The fine-pointer selection case asserted the replaced rule and passed
  only because it read iOS WebKit's inline lock without iOS WebKit: it
  now asserts the lock a fine pointer's finger and pen take, and that a
  mouse takes none.
- FR41's sentence and its verification row say that a press claimed at
  its own pointerdown, before it moves, keeps the page's selection, while
  a drag that moved before its claim loses what it selected.
…ion, in the present tense

The sentence said a drag that moved before its claim loses the selection
it started, but the hold clears the page's selection then, whatever made
it; and an unclaimed mouse drag kept "today's" behavior, a phrase that
ages. It now says a claim after movement clears the page's selection, a
press claimed at its own pointerdown keeps it, and an unclaimed mouse
drag keeps its released behavior.
This pull request now sits on meta#8665, so a stack carrying it and meta#8665's
other dependants (meta#8676) needs no second GestureHost resolution. Two
hunks in GestureHost.ts's pointerdown, both this pull request's:
- the second-finger check where fingers come from pointer events stays,
  followed by meta#8665's rule that a right click, a pen's barrel button or
  a macOS ctrl-click starts no press;
- the check that runs again once the new finger is delivered stays,
  followed by meta#8665's selection lock call, which no longer takes the
  window.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:36
vjeux added 6 commits October 29, 2026 15:55
Once a component in the root entry holds its layer's input (a popover's
presentation hold, meta#8223), Layer/inputHold's static imports of
gestures/GestureHost and gestures/nativeScroll put eight gesture modules
(about 47 KB) in the root entry, against
architecture:react-component-runtime INV13. Bundle a component that imports
the input hold and require no gesture bytes. Fails while the input hold
imports from gestures/.
…e host asks it

Layer/inputHold registered a touch gate with the gesture host
(getGestureHost(doc).addTouchGate(isInHeldLayer)) and walked the flat
tree through gestures/nativeScroll, so any component that holds its
layer statically carried the gesture system into its bundle and the root
entry (architecture:react-component-runtime INV13). Invert it: the host
asks isInHeldLayer for every touch itself (gestures may import Layer),
the hold registers nothing, and it walks the flat tree through
utils/flatTree, where parentAcrossShadow already lives. addTouchGate stays
for other gates. A host test holds a held layer's touches with no gate
(it fails if the host stops asking the hold).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on engineering-owner decisions: this head amends spec:AST-094 (two alert spring rows in FR4's table and a claimed mouse or pen drag selection rule in FR41), and AlertDialog's exit hold, UIKit motion default and 320 px phone width rest only on the draft component:AlertDialog, with no presentation value that keeps today's dialog. An engineering owner records a decision on the spec text by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), and provides or identifies the current spec that governs those AlertDialog touch defaults, such as an approved component:AlertDialog. Independently of those decisions, these need changes:

  • Land #8342 and #8084 and rebase, so this pull request is its own change rather than 34,664 authored lines that also carry an unrelated build-package fix.
  • Correct AlertDialog.doc.mjs lines 24 and 199, which say a mouse keeps each button's click while the actions are tracked under every pointer.
  • Let a mouse through the entrance hold on a coarse-primary device, as FR35 keeps a mouse's behavior, or have an owner narrow that sentence, and test that case.
  • Add the before-and-after mouse evidence spec:AST-017 FR59 asks for, land #8671 before this, and reword the three changesets that say no component uses the hold, the group-item marker or the tracker.

[Automated review]

usage: {
description:
'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nAlertDialog passes its requested width through to Dialog, which clamps the surface to the container and dynamic viewport with token gutters. Generic Dialog footers should wrap, but Dialog does not own action semantics or order; consumer composition controls that. AlertDialog owns its confirmation semantics: above 640px, actions render horizontally and may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width. Button labels retain their standard single-line behavior. The breakpoint follows available width, not pointer or hover capability. The body scrolls when block space is constrained.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',
'AlertDialog asks the user to confirm a destructive or irreversible action before it happens. Use it for things like deleting content, revoking access, or discarding unsaved changes.\n\nIt implements the WAI-ARIA APG [Alert Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/): `role="alertdialog"`, a title linked by `aria-labelledby`, a consequence description linked by `aria-describedby`, focus moved into the dialog on open and returned to the trigger on close, and no dismissal by clicking outside. Escape cancels.\n\nAlertDialog passes its requested width through to Dialog, which clamps the surface to the container and dynamic viewport with token gutters. Generic Dialog footers should wrap, but Dialog does not own action semantics or order; consumer composition controls that. AlertDialog owns its confirmation semantics: above 640px, actions render horizontally and may move onto another row; at 640px and below, the destructive action appears above Cancel and both buttons fill the footer width. Button labels retain their standard single-line behavior. The breakpoint follows available width, not pointer or hover capability. The body scrolls when block space is constrained.\n\nOn a touch screen the two actions are tracked as one group under a finger or a pen, the way iOS tracks the actions of an alert: the action under the finger highlights as soon as it is touched, the highlight follows the finger from one action to the other, and lifting the finger runs the action under it. Lifting it off both actions runs nothing. A mouse, the keyboard and assistive technology keep the click of each button.\n\nUnder a finger, on any touch device, the alert also appears and leaves the way an iOS alert does: it scales down slightly into place while it and the dimming fade in, and it fades out with the dimming when it closes. While it appears a touch does nothing (keys and assistive technology still act), and while it leaves nothing reaches it: a touch, a click, a key or an assistive-technology activation does nothing. With reduced motion it only cross-fades. With a mouse it keeps the theme entrance and closes at once.\n\nFor cases where you want to show an alert without managing open state, use the `useImperativeAlertDialog` hook: call `alert.show(options)` and render `alert.element` in your tree.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This says a mouse keeps each button's click, but the actions are tracked as one group under every pointer: AlertDialog.tsx passes no pointerTypes (the recognizer takes every pointer, useActionGroupTracking.ts:167), and AlertDialog.test.tsx:754 pins a mouse press slid from the action to Cancel cancelling. Please describe the every-pointer tracking here and in the dense docs at line 199.

Comment on lines +120 to +140
const SWALLOWED = [
'pointerdown',
'pointerup',
'touchstart',
'touchend',
'mousedown',
'mouseup',
'click',
'contextmenu',
] as const;

const swallow = (event: Event) => {
// A touch's start stays uncancelled: cancelling it would cancel the page's
// pinch-zoom too. The shield's `touch-action: pinch-zoom` keeps a finger
// from panning, and a tap's mouse events and click land on the shield,
// which swallows them.
if (event.cancelable && event.type !== 'touchstart') {
event.preventDefault();
}
event.stopPropagation();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The shield swallows these events whatever the pointer type, and the alert places a 'viewport' hold whenever (pointer: coarse) matches. On a tablet whose primary pointer stays coarse with a mouse or trackpad attached, a click in the first 435 ms does nothing, while spec:AST-094 FR35 says "A mouse keeps today's behavior (DEC-16)". Either let a mouse's input through during the entrance or have an engineering owner narrow FR35's sentence to a fine primary pointer, and add that case to the tests (both mouse cases today run under a fine pointer).

'@astryxdesign/core': patch
---

[chore] Internal: one input hold for every layer (`Layer/inputHold`). While a layer presents, a touch, a press or a click anywhere reaches nothing (keys and assistive technology still act, and a pinch still zooms the page); while it dismisses, the same, and the layer's content is inert. The gesture host holds the layer's touches through its touch gates, and `isInputShield` lets a hit test see past a hold's shield. Not exported from the package entry point; no component uses it yet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"no component uses it yet" is not true at this head: useAlertMotion.ts calls holdInput. Please reword so the release notes match what ships.

'@astryxdesign/core': patch
---

[chore] Internal: the touch press controller leaves the press of an action marked as tracked by its group (`data-astryx-press-group-item`) to that group's own tracker, so one writer paints each press. No component carries the marker yet, so nothing visible or behavioral changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"No component carries the marker yet" is not true at this head: AlertDialog.tsx:295 spreads pressGroupItemProps on both actions.

'@astryxdesign/core': patch
---

[chore] The internal action-group tracker paints its highlight through the one press writer in `utils/pressFeedback.ts`, as the document's press controller does, so neither cuts the other's fade short and a `held` paint is left alone. No component uses the tracker yet, and no package entry point exports it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"No component uses the tracker yet" is not true at this head: AlertDialog drives it through useActionGroupTracking.

Comment on lines +147 to +148
| `springs.alertPresent` | mass 1, stiffness 522.35, damping 45.71 | measured: UIAlertController alert appearing, iOS 26.2 simulator, 2026-10-23 |
| `springs.alertDismiss` | mass 1, stiffness 522.35, damping 45.71 | measured: UIAlertController alert dismissing, iOS 26.2 simulator, 2026-10-23 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These two rows add the alert's own roles to a current record, where FR4 says further roles a component's own record measures join through the generated motion role table (DEC-5). They need an engineering owner's /approve-spec on this exact head, or a move into component:AlertDialog.

Comment on lines +473 to +477
(`spec:AST-017` FR59; DEC-9). A mouse or pen drag that a recognizer claims
takes no new text selection for that drag only. A claim made after the
drag has moved clears the page's selection; a press claimed at its own
pointerdown, before it moves, keeps it. An unclaimed mouse drag keeps its
released behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This changes FR41's mouse rule (a claimed mouse or pen drag now takes no selection and can clear the page's), where FR41 today keeps a mouse on its released behavior (spec:AST-017 FR59). It needs an engineering owner's /approve-spec on this exact head, or its own spec pull request.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
… into 19ee3fa

menuTriggerPress.ts takes the new side: meta#8078's port of meta#8360's trigger
changes replaces the older port this base carried.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
… as a dependency [skip ci]

meta#8220 no longer carries the withdrawn meta#7839. This PR's action-sheet
card used two things only meta#7839 provided: its opensSubMenu helper, and
header and footer on a data row. isPlainAction now checks for a nested
level itself, by main's rule (a non-empty items), and drops the per-row
header and footer checks, since rows have neither. The level's own
header and footer still keep the sheet (hasRegions).

The DropdownMenu bottom sheet takes main's regions again, inside this
PR's card-or-sheet branch. DropdownMenuTouch.ts names both register
functions it calls, and says the card plays the alert's two springs
through useAlertMotion (meta#8373). The presentation description keeps this
PR's card sentence and meta#8080's wording.

This branch was successfully deployed

1 active deployment
Preview — c3a4cdc5 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants