Skip to content

fix(AlertDialog): a finger that slides between the actions runs the one it lifts on - #8083

Draft
vjeux wants to merge 1696 commits into
meta:mainfrom
vjeux:uikit/c18-alerts-pr1
Draft

vjeux wants to merge 1696 commits into
meta:mainfrom
vjeux:uikit/c18-alerts-pr1

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8360, #8638 and #8637. Review only this PR's change: vjeux/astryx@27778e7...458e2d9

Why

On a phone, someone touches Delete in an alert and slides to Cancel before lifting: iOS Safari runs Delete, where the touch started, and the pressed look shows 150 ms late. An alert guards an irreversible action, so running the wrong button is its worst failure.

What led to it

The UIKit-parity work, which brings Astryx's touch behavior to iOS's, compared AlertDialog with the iOS 26.2 alert. Each button tracks its own press: the click target is fixed when the touch starts, and a 10 px move ends the press. iOS treats an alert's buttons as one group: the highlight follows the finger, and the button under it at the lift runs. family:action-groups (#8622; spec:AST-094 DEC-17) asks every action group to do the same, and AlertDialog's record makes it FR8 (#8594).

Options and tradeoffs

  1. Handlers in AlertDialog alone. Small, but ButtonGroup, ToggleButtonGroup and the action sheet need it too (feat(ButtonGroup): a held pointer is tracked across the buttons, and the one lifted on runs #8657, feat(ToggleButtonGroup): a held pointer is tracked across the toggles, and only the lifted one toggles #8660, DropdownMenu: on a phone the action sheet presents as iOS 26's does, an alert-style card #8104), and copies drift.
  2. CSS. Can't send a click to another element.
  3. The shared group tracker (feat(core): one tracker for a held finger across any group of targets #8346), adapted to a long press. One implementation, specified and tested. A press starting on the title or message no longer slides onto a button, so that text stays selectable; a mouse and a pen track the group too.

Why this choice

Option 3 fixes every group the same way, as the contract requires. AlertDialog attaches the tracker (hooks/useActionGroupTracking.ts, internal) to its button row while open; the row allows only pinch-zoom and selects no label. Open for the owner: UIKit fades the highlight in 100 ms (shared: 200 ms) and runs the handler once the alert is gone (~0.43 s); here onAction runs at the lift. Known public-API gaps (spec:AST-107 FR7): the tracker and the selection lock.

How to check it

On a phone, the iOS simulator or Chrome's touch emulation, Core/AlertDialog Delete story:

  1. Touch Delete, slide to Cancel, lift: Cancel runs. On main, Delete runs.
  2. Touch the message, slide onto Cancel, lift: nothing runs.
  3. Touch Delete, add a second finger, lift both: nothing runs.
  4. With a mouse, press Delete and release over Cancel: Cancel runs; release off both buttons: nothing runs. A click on Delete runs Delete once.
  5. Tab to Delete and press Enter or Space: Delete runs, as before; on Cancel, Space cancels.

Run on the iOS 26.2 simulator and the Android 15 emulator (steps 1–2), in Chromium (3–4; the simulator has one finger), and as unit and browser tests (all five): each behaved as written. On main, step 1 runs Delete and 7 of the browser tests fail.

Evidence

iOS 26.2 simulator, Mobile Safari, main first:

Delete to Cancel Cancel to Delete
Slow slide Message to Cancel
Off both actions Cancelled touch
Scroller flick Scroller rest

Chromium, a mouse and a second finger (storyboard; WebKit):

Mouse slide Second finger

Android 15 emulator, Chrome 124, main on the left:

Android: Delete to Cancel Android: message to Cancel

@vercel

vercel Bot commented Oct 24, 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 28, 2026 9:37pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 11:41 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 of this change waits on an engineering owner's spec decision, and the diff needs to shrink to the AlertDialog fix. The touch behavior for AlertDialog (Cancel and the action tracked as one group under a finger, with AlertDialog painting its buttons' touch press) and the draft spec:AST-094 it relies on have no approved spec yet, so an engineering owner needs to approve or amend them by commenting /approve-spec <full head SHA> on this pull request; the author may record that decision if they are an engineering owner. As written, AST-094's FR11 (two press profiles) and FR13 (no component-owned media query) contradict the AlertDialog implementation, so that decision has to settle which side changes. Independently of the spec decision:

  • Rebase onto main once the stacked pull requests land, so this one carries only its AlertDialog commit (today it is 23,663 authored lines).
  • Run the interaction proofs inside the existing core test lane, or land an AST-030 amendment that admits pr-interaction first (AST-030 FR10/FR11).
  • Add ## Writing rubric results for spec:AST-094 to the description, and drop or fix its citation of spec:AST-090, which is not in the repository.
  • Add a pen case for the action-group tracker, or limit it to touch.

[Automated review]

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself (its own scope step below) to run Playwright proofs for core components. spec:AST-030 FR11 says new coverage inside an existing surface "MUST join those owners and MUST NOT create a separately routed lane or required context", and FR10 says a new test type "is not a new surface by itself". Run these proofs inside the existing core test owner, or land an owner-approved AST-030 amendment that admits the lane first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This job comes from the interaction harness in the stack (#8043), not from this pull request's own commit (review the last commit, 4a23248). The lane question belongs to #8043 and waits in its reviewers' queue.

name: 'ActionGroupTracking',
minimumPressDuration: options.highlightOnsetMs,
allowableMovement: Number.POSITIVE_INFINITY,
allowedPointerTypes: options.pointerTypes ?? ['touch', 'pen'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pens are tracked by default here, so AlertDialog changes pen behavior too, while its docs and changeset only describe touch screens and no test drives a pen. architecture:interaction-modality asks pointer-dependent changes to verify "mouse, touch, and pen as applicable". Add a pen slide case to useActionGroupTracking.test.tsx, or pass pointerTypes: ['touch'] from AlertDialog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: pens stay tracked, as UIKit tracks Apple Pencil on an alert's actions like a finger, and two pen cases now drive the group: a slide from Delete to Cancel runs Cancel, and a lift off both actions runs nothing. The docs and changeset say a finger or a pen.

element.setAttribute(PRESSED_ATTRIBUTE, 'fading');
fadesRef.current.set(
element,
setTimeout(() => {

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 fade timer is never cleared when tracking is disabled or the component unmounts, so it can write to a closed or detached button. architecture:react-component-runtime INV5 wants component-owned timers released on disable and unmount; a cleanup that clears fadesRef and removes the attribute the timer would have removed covers it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: turning tracking off and unmounting now end every pending release fade (its timer, and the 'fading' paint it would remove). The new case 'leaves no fade behind when tracking turns off or the group unmounts' failed before the fix.

options: ActionGroupTrackingOptions,
): LongPressGestureRecognizer {
const optionsRef = useRef(options);
useIsomorphicLayoutEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3: "A new or changed Effect MUST name the external system it synchronizes." A one-line comment naming the recognizer delegate that reads optionsRef outside render would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: the effect now says it keeps the recognizer's delegate and listener, which run outside render, reading the latest options; the new fade effect says what it ends and when.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-090 is not in the repository at this head or on main (it is still an open proposal), so a reader following this amendment finds nothing. Drop the AST-090 amendment here (and in Current-state impact) until that record lands, or describe it in the pull request instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AST-094 comes from the spec pull request in the stack (#8052), not from this pull request's own commit. The citation and the rubric results belong to #8052 and wait in its reviewers' queue.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox are not both ready on the exact-head Vercel preview. CI concluded failure.

Modified Components

AlertDialog (@astryxdesign/core)
Metric Before After Delta
Entry File (ESM) 191B 191B 0B
Lines of Code 182 207 -
Complexity Medium Medium (15) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 74.8KB +17.7KB ⚠️ 12.4KB 12.5KB +34B
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 +671B 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.0KB +892B 19.9KB 19.9KB +18B
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 +727B 16.5KB 16.5KB +9B
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 234.3KB N/A N/A 2.4KB N/A
lab/Comments N/A 238.6KB 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 176.0KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 108.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 +17.7KB ⚠️ +34B

Bundle Size Summary

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

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

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 504 shot(s) changed.

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

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

and 35 more.


Generated by PR Enrichment workflow | View full report

@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 about the new direction here: AlertDialog tracking Cancel and its action as one group under touch and pen, taking its actions' touch press over from Button, and the draft spec:AST-094 requirements this branch adds, including where FR11 and FR13 meet the alert's behavior. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Independently of that decision, these need changes:

  • Land the dependency stack separately and rebase so this pull request carries only the AlertDialog commit (24,788 authored lines today).
  • Run the interaction proofs in the existing Core test lane, or admit the new pr-interaction lane through an AST-030 amendment first (FR10, FR11).
  • Add a pen test or drop pen from the hook's default, clear pending fade timers when tracking stops, and comment what each new effect synchronizes.
  • Report the Writing rubric results for the new record and fix its references to AST-090, utils/touchPresentation.ts and the missing motion files.

[Automated review]

name: 'ActionGroupTracking',
minimumPressDuration: options.highlightOnsetMs,
allowableMovement: Number.POSITIVE_INFINITY,
allowedPointerTypes: options.pointerTypes ?? ['touch', 'pen'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pen is tracked by default here, but no test drives a pen through the group tracking. architecture:interaction-modality asks pointer-dependent behavior to verify mouse, touch and pen as applicable: add a pen case (a slide and a lift off both), or default to ['touch'].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: pens stay tracked, as UIKit tracks Apple Pencil on an alert's actions like a finger, and two pen cases now drive the group: a slide from Delete to Cancel runs Cancel, and a lift off both actions runs nothing. The docs and changeset say a finger or a pen.

Comment on lines +122 to +134
const release = (element: HTMLElement) => {
clearFade(element);
element.setAttribute(PRESSED_ATTRIBUTE, 'fading');
fadesRef.current.set(
element,
setTimeout(() => {
fadesRef.current.delete(element);
if (element.getAttribute(PRESSED_ATTRIBUTE) === 'fading') {
element.removeAttribute(PRESSED_ATTRIBUTE);
}
}, PRESS_FADE_MS),
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Each release starts a fade timer that is cleared only when the same action is painted, cut or released again. The lift that runs an action usually closes the alert, so the timer outlives tracking; architecture:react-component-runtime INV5 wants node-owned timers released when disabled or unmounted. Clear every pending fade on disable and unmount.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: turning tracking off and unmounting now end every pending release fade (its timer, and the 'fading' paint it would remove). The new case 'leaves no fade behind when tracking turns off or the group unmounts' failed before the fix.

Comment on lines +101 to +103
useIsomorphicLayoutEffect(() => {
optionsRef.current = options;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3: a new Effect names the external system it synchronizes. One comment saying this keeps the recognizer's delegate reading the latest options would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: the effect now says it keeps the recognizer's delegate and listener, which run outside render, reading the latest options; the new fade effect says what it ends and when.

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-030 FR11: new coverage inside an existing surface joins that surface's lane and does not create a separately routed lane; FR10 admits a new surface only through an owner-approved amendment first. This job routes itself with its own scope check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This job comes from the interaction harness in the stack (#8043), not from this pull request's own commit (review the last commit, 4a23248). The lane question belongs to #8043 and waits in its reviewers' queue.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

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 spec:AST-090 exists in the repository, here or on main, so FR7's proposed departure points at nothing an owner can read. Cite the record that exists, or drop the reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AST-094 comes from the spec pull request in the stack (#8052), not from this pull request's own commit. The citation and the rubric results belong to #8052 and wait in its reviewers' queue.

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

This needs an owner decision before it can be approved: the AlertDialog touch group tracking has no current spec behind it, so approval needs an engineering owner to approve the governing spec, the proposed component:AlertDialog requirement and the draft spec:AST-094 (FR1–FR23, DEC-1/DEC-2), and to settle whether AST-094's FR11 and FR13 or the alert's own press profile and coarse-pointer media query should change. An engineering owner records that decision by commenting /approve-spec <full head SHA>, and the author may do so if they are an engineering owner. Independently of that decision, please:

  • land the gesture, motion and harness dependencies through their own pull requests and rebase this one onto them;
  • run the interaction proofs in the existing Core test lane instead of a separately routed pr-interaction job, or amend AST-030 first;
  • clear pending press-fade timers when tracking turns off or the dialog unmounts, and add a pen test or limit tracking to touch;
  • add AST-094's Writing rubric results to the description and fix the references to AST-090, utils/touchPresentation.ts, uikit/switch.ts, uikit/contextMenu.ts and sources.test.ts, none of which exist.

[Automated review]

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself with its own scope check, which spec:AST-030 FR11 rules out: new coverage inside an existing surface joins that surface's test lane and must not create a separately routed lane or required context. Run these proofs inside the Core test lane, or land an AST-030 amendment admitting a new surface first (FR10).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This job comes from the interaction harness in the stack (#8043), not from this pull request's own commit (review the last commit, 4a23248). The lane question belongs to #8043 and waits in its reviewers' queue.

element.setAttribute(PRESSED_ATTRIBUTE, 'fading');
fadesRef.current.set(
element,
setTimeout(() => {

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 fade timer is cleared only when the same action is painted, cut or released again. The lift that runs an action usually closes the alert, so the timer outlives tracking, while architecture:react-component-runtime INV5 asks for component-owned timers to be released when disabled or unmounted. Clearing every pending fade, and removing the attribute it would have removed, when tracking turns off and on unmount would fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: turning tracking off and unmounting now end every pending release fade (its timer, and the 'fading' paint it would remove). The new case 'leaves no fade behind when tracking turns off or the group unmounts' failed before the fix.

name: 'ActionGroupTracking',
minimumPressDuration: options.highlightOnsetMs,
allowableMovement: Number.POSITIVE_INFINITY,
allowedPointerTypes: options.pointerTypes ?? ['touch', 'pen'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pens are tracked by default here, but no test drives a pen and the docs and changeset mention only touch screens. architecture:interaction-modality asks pointer-dependent changes to verify mouse, touch and pen as applicable: a pen slide and a pen lift-off-both case would cover it, or AlertDialog could pass pointerTypes: ['touch'].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: pens stay tracked, as UIKit tracks Apple Pencil on an alert's actions like a finger, and two pen cases now drive the group: a slide from Delete to Cancel runs Cancel, and a lift off both actions runs nothing. The docs and changeset say a finger or a pen.

options: ActionGroupTrackingOptions,
): LongPressGestureRecognizer {
const optionsRef = useRef(options);
useIsomorphicLayoutEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV3 asks every new Effect to name the external system it synchronizes; a one-line comment saying what this keeps current would satisfy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4a23248: the effect now says it keeps the recognizer's delegate and listener, which run outside render, reading the latest options; the new fade effect says what it ends and when.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is no spec:AST-090 at this revision or on main, so this departure (and the amendment at line 235) points at nothing. Cite the record that holds these rules, or drop the reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AST-094 comes from the spec pull request in the stack (#8052), not from this pull request's own commit. The citation and the rubric results belong to #8052 and wait in its reviewers' queue.

## Touch presentation

Whether a component presents itself the touch (UIKit) way is answered once,
in `../utils/touchPresentation.ts`:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

../utils/touchPresentation.ts does not exist, and neither do TOUCH_QUERY, COMPACT_TOUCH_QUERY or useTouchPresentation; point component authors at what exists, or remove this section until it lands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At the current head (4a23248) this file exists: the base carries the touch presentation resolver (#8062, through #8076), with TOUCH_QUERY, COMPACT_TOUCH_QUERY and useTouchPresentation in packages/core/src/utils/touchPresentation.ts. The README itself is the gesture system's (#8074).

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

This needs an engineering owner's spec decision before it can be approved: the AlertDialog touch group tracking has no current spec behind it, so an owner has to approve or reject the proposed component:AlertDialog requirement, including whether callers can turn it off under the current touch-default rule, and the draft spec:AST-094 (FR1–FR23, DEC-1/DEC-2), and say which side changes where AST-094's FR4, FR11 and FR13 disagree with this branch. An engineering owner records that decision by commenting /approve-spec <full head SHA>, and the author may do so if they are an engineering owner. Independently of that decision, please:

  • land the gesture, motion, theme-role, resolver and harness dependencies through their own pull requests and rebase this one onto them, settling the theme roles and the pr-interaction job there against design:reduced-motion, spec:AST-012 FR2 and spec:AST-030 FR11;
  • keep the compact-touch line at width below md (768 px) or height below 500, as spec:AST-012 FR9 now requires, instead of 744 px;
  • load AlertDialog's touch tracking lazily or justify its 17 KB gzip cost (architecture:component-style-authoring INV6);
  • clear the press-fade timers on disable and unmount, add a pen test or limit tracking to touch, add AST-094's Writing rubric results, and fix the references to AST-090, uikit/switch.ts, uikit/contextMenu.ts and sources.test.ts.

[Automated review]

export const TOUCH_QUERY = '(pointer: coarse)';

/** The viewport is compact: layout behaviours. */
export const COMPACT_QUERY = '(width < 744px), (height < 500px)';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-012 FR9 (current on main) defines compact as width below the effective md point (768 by default) or height below 500, and says no resolver keeps size numbers of its own; DEC-6 rejects matching platform size classes. At 744 px a coarse-pointer screen 744–767 px wide gets the popover where the definition gives the sheet. Use (width < 768px), (height < 500px) until the theme exports the definition.

Comment on lines +125 to +133
fadesRef.current.set(
element,
setTimeout(() => {
fadesRef.current.delete(element);
if (element.getAttribute(PRESSED_ATTRIBUTE) === 'fading') {
element.removeAttribute(PRESSED_ATTRIBUTE);
}
}, PRESS_FADE_MS),
);

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 timer is cleared only when the same action is painted, cut or released again. The lift that runs an action usually closes the alert, so the timer outlives tracking; architecture:react-component-runtime INV5 asks component-owned timers to be released when disabled or unmounted. Clear fadesRef (and the attribute it would remove) when tracking turns off and on unmount.

name: 'ActionGroupTracking',
minimumPressDuration: options.highlightOnsetMs,
allowableMovement: Number.POSITIVE_INFINITY,
allowedPointerTypes: options.pointerTypes ?? ['touch', 'pen'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pens are tracked by default and AlertDialog passes no pointerTypes, but no test drives a pen and the docs and changeset mention only touch screens. architecture:interaction-modality asks pointer-dependent changes to verify mouse, touch and pen as applicable: add a pen slide and a pen lift-off-both case, or pass ['touch'].

import {themeProps} from '../utils/themeProps';
import {useTranslator} from '../i18n';
import {useMediaQuery} from '../hooks';
import {useActionGroupTracking} from '../hooks/useActionGroupTracking';

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 import brings the gesture host, the arena, the pan, tap and hover recognizers, the velocity tracker and the whole UIKIT_GESTURES table into every AlertDialog import: 30,347 to 47,413 bytes gzip (core modules, packages external). architecture:component-style-authoring INV6 asks touch-only JavaScript over 2 KB to load lazily on a coarse pointer or be justified, and shared tables to ship only the rows a component uses.

Comment thread .github/workflows/ci.yml Outdated
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself with its own scope check, which spec:AST-030 FR11 rules out: new coverage inside an existing surface joins that surface's test lane and must not create a separately routed lane or required context. Run these proofs inside the Core test lane, or land an FR10 amendment first.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-012 FR2 keeps when a closed vocabulary (width, height, pointer, contrast, motion) and says future environment fields require explicit additions to it; platform needs that amendment before it ships as a stable theme option.

* Cross-Fade Transitions on (opacity CABasicAnimation 0.5 s,
* (0.42, 0, 0.58, 1)), Oct 23 2026
*/
reducedMotionCrossFadeMs: 500,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

design:reduced-motion DR2 (current) caps a touch cross-fade at 200 ms, and DR4 says core theme values offer no way to a longer reduced-motion animation. This 500 ms default, and the theme role that can retune it (the docs set it to 300), would give touch presentations a longer fade.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is no spec:AST-090 at this revision or on main, so this departure (and the amendment at line 235) points at nothing an owner can read. Cite the record that holds these rules, or drop the reference.

@vjeux
vjeux force-pushed the uikit/c18-alerts-pr1 branch from a79b8e8 to 4a23248 Compare October 24, 2026 22:07
@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 4a23248:

  • Release fades end when tracking turns off and on unmount (new failing-before case).
  • Pen: two pen cases (a slide that runs Cancel, a lift off both that runs nothing); docs and changeset say a finger or a pen.
  • Each effect names what it synchronizes.
  • The gesture README's resolver link resolves: the base now carries feat(core): one touch presentation resolver #8062.

Waiting in the reviewers' queue: the spec decision (component:AlertDialog FR6 and the draft AST-094), the pr-interaction lane (#8043) and AST-094's references and rubric results (#8052). This pull request carries its stack until those land; review the last commit.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Touch gating. core/src/AlertDialog/AlertDialog.tsx:61 writes its own (pointer: coarse) check. This conflicts with AST-012 FR2 (touch is resolved once, through the closed adaptation vocabulary) and public-component-api INV16. Resolve through the shared touch resolver key so behavior matches the spec. If this behavior genuinely can't be expressed through the resolver, please raise it here as a spec question instead of shipping a separate check.

This is about where the behavior lives, not about the PR's intent.

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

This needs an engineering owner's spec decision before it can be approved: AlertDialog's touch and pen group tracking has no current spec behind it, so an owner has to approve or reject the proposed component:AlertDialog requirement, including whether callers can turn it off under the current touch-default rule, and the draft spec:AST-094 (FR1–FR23, DEC-1/DEC-2), and say which side changes where AST-094's FR4, FR11 and FR13 disagree with this branch. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Independently of that decision, please:

  • land the gesture, motion, theme-role, resolver and harness dependencies through their own pull requests and rebase this one onto them, settling the theme roles and the pr-interaction job there against design:reduced-motion, spec:AST-012 FR2 and spec:AST-030 FR11;
  • keep the compact-touch line at width below md (768 px) or height below 500, as spec:AST-012 FR9 requires, instead of 744 px;
  • report AlertDialog's size, load its touch tracking lazily or justify its 16 KB gzip cost (architecture:component-style-authoring INV6), and attach it only while the alert is open;
  • prove pens in the AlertDialog browser cases or track touch only, add AST-094's Writing rubric results, and fix the references to AST-090, uikit/switch.ts, uikit/contextMenu.ts and sources.test.ts.

[Automated review]

if (element == null) {
return undefined;
}
return getGestureHost(element.ownerDocument).attach(element, recognizer, {

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 recognizer attaches here whatever isEnabled says, and attaching installs the gesture host's twelve document and two window listeners (GestureHost.ts:463-490). Through useActionGroupTracking, every mounted AlertDialog keeps them while closed. architecture:react-component-runtime INV5 asks optional behavior to acquire local resources only while enabled; keying this attach on isEnabled (or attaching from AlertDialog only while open) would cover it.

name: 'ActionGroupTracking',
minimumPressDuration: options.highlightOnsetMs,
allowableMovement: Number.POSITIVE_INFINITY,
allowedPointerTypes: options.pointerTypes ?? ['touch', 'pen'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pens are tracked by default, and a pen lift relies on the host swallowing the browser's own trusted click after lifted.click() (GestureHost.ts:1018-1036, :1147-1163). The two pen cases run in jsdom, where no browser click follows, so they cannot catch a double activation. architecture:interaction-modality asks for real-browser evidence when behavior depends on pen synthesis and hit testing: a pen tap and a pen slide in AlertDialog.interaction.browser.spec.ts, or ['touch'] here, would settle it.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation. The local (pointer: coarse) user-select / touch-callout trio is gone; the action-group recognizer sets preventsTextSelection from #8270, so the gesture host's selection lock keeps a held finger or pen from starting a selection, and a mouse keeps selecting text.

@vjeux
vjeux force-pushed the uikit/c18-alerts-pr1 branch from 4a23248 to 65889f0 Compare October 24, 2026 23:30
vjeux added 10 commits October 27, 2026 07:08
… tree; a claimed touch finds its pointer finger wherever fingers come from pointer events

containsAlongFlatTree walks parentAcrossShadow, so a finger inside an open
shadow root, now that targets come from the composed path, counts as under
a scroller outside it: a scroll there takes it, its flick arms the scroll
brake, and a touch there brakes. cancelIfClaimed matches a touch to its
pointer finger whenever the host reads fingers from pointer events, not
only under inputMode 'pointer-events', so a claimed pan's moves are
cancelled where a browser that exposes no ontouchstart still sends Touch
Events.
… flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8320
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8293
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8356
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8358
…in, as documented; meta#8336: the host compares a finger with a scroller along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8360

# Conflicts:
#	packages/core/src/hooks/useLongPress.ts
…handlers type

UseLongPressHandlers.start takes a point only again, as released: the
per-press tolerance useMenuPress's trigger chooses as its press starts
lives on the internal useLongPressDetector and LongPressDetectorHandlers,
which the hooks entry does not export. useLongPress returns the same
detector, typed as before.
…7977, F2's meta#8360 5f6bc04) into meta#8083

AlertDialog.tsx takes main's useMediaQuery import from '../hooks/useMediaQuery' (meta#7977) beside this PR's own imports.

@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 is on hold until an engineering owner decides whether AlertDialog's actions become one tracked group under every pointer: no current spec on main gives an alert's buttons the menu's lift-decides model (spec:AST-094 FR26–FR28 cover menus, and FR11 keeps a control's activation with the browser's click), and the always-on text-selection lock on the action row and the 200 ms release fade need that decision too. An engineering owner records it by commenting /approve-spec b2c6bd383d54678a9ed5df38255ea21a9dba314b on this pull request, and the author may do so as an engineering owner. Whatever the decision, please address these:

  • Land the stacked pull requests and rebase, so this one carries only its AlertDialog files (it now changes 30,568 authored lines), and drop the unused useIsScrollable hook.
  • A mouse pressed on Delete and moved onto Cancel shows both buttons pressed: Button's :active plus the group's paint.
  • userEvent.click on an open alert's Delete runs Cancel in jsdom, so consumer tests that click alert actions this way break.
  • scripts/touch-support-bundle.test.mjs "is absent from the root entry" fails on this head, and the AlertDialog docs still say a mouse keeps each button's click.

[Automated review]

vjeux and others added 17 commits October 28, 2026 10:39
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts:
it takes main's merged meta#8046 text (the finger and button slots, the mouse
and key press) and keeps this pull request's own additions, the one
writer of the press attribute (writePress, paintPress, readPressPaint)
and the header line naming them.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
Main's button slot, merged with meta#8046, ends a press's release at once
when the next press starts, by clearing the press attribute directly. On
a row another painter holds, that clear erased the hold, which this pull
request's one writer refuses. A mouse press on a held row, released, then
a press elsewhere must leave the row held. Fails until the early end goes
through the one writer.
finish() writes none through writePress, as this pull request's
controller paints, so a held row stays its owner's when the next press
ends a release early, under the finger and the button slots alike.
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
…bute

From the review of meta#8076: the README said paintPress in
utils/pressFeedback.ts is the one writer of data-astryx-press, but the
action-group tracker on main (meta#8346) writes its groups' presses itself.
The sentence now says what holds at this head.
meta#8076's new head carries main 70ea859 with meta#8073's writer of the press
attribute on main's press controller, and its README names both writers.
utils/pressFeedback.ts keeps that controller with this line's own changes
on it: the pointer, drag, scroll and visibility listeners through the
shared input stream (meta#8356) and touchcancel only while the primary pointer
is coarse (meta#8270); the key path stays at the window's bubble phase, so a
Space press the page cancels paints nothing. usePressTracker stays in its
own module, one tracker for a component's life (meta#8315), so meta#8073's
rebuilt-tracker hook and its test are not taken. The harness files are
meta#8076's.
…s, and the action-group tracker writes its own

The header and writePress called it the one writer of the press attribute,
but main's action-group tracker writes its groups' presses itself. They now
say what holds, as meta#8076's gestures README does.
…he press attribute

The release note said every press painter writes data-astryx-press
through one internal writer, but the action-group tracker
(hooks/actionGroupTracker.ts) writes its groups' presses itself, as the
gestures README and utils/pressFeedback.ts already say. The note now says
paintPress writes every press it tracks and the action-group tracker
writes its own.
…arker

Takes meta#8360's press controller as it stands (its finger and button
slots, and the mouse-and-Space press from meta#8046) and re-applies only
this PR's change on it: the group item marker and the controller's
skip of a pointer press on a marked action.
…tion to its tracker

meta#8046 made the controller paint a mouse press itself where the primary
pointer is coarse (a tablet with a trackpad). On an action its group
tracks (family:action-groups), the group's tracker paints that press,
so the controller must paint nothing there, as it already does for a
finger; it paints the press, then turns the tracker's paint to
'fading' at the release. A Space press on the same action still paints:
the group tracks pointers only (guard).
… its tracker

Where the primary pointer is coarse the controller paints a mouse press
itself (meta#8046). It now skips an action a group's tracker paints for a
pointer's press, a mouse's included, as it already did for a finger, so
the two never write the press attribute over each other. A Space press
on the action still paints: the group tracks pointers only.
…he keyboard keeps the buttons

Her AlertDialog record (meta#8594) verifies FR8 with a press lifted outside
the actions under a mouse, a pen and a finger, and with Enter and Space on
each action. Add the two cases the tests lacked: a mouse press that slides
off both actions highlights nothing and runs nothing at the lift, and under
a coarse and a fine pointer Enter runs the action and Space cancels. A
mutant that stops tracking a mouse's moves fails the first; one that drops
the buttons' own click fails the second.

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

AlertDialog's slide-to-the-lifted-action fix works under a finger and a mouse, but approval waits on an engineering owner deciding whether the lifted action's highlight keeps the shared 200 ms press fade or takes the 100 ms measured for UIKit's alert actions (spec:AST-094 FR1/FR2). An engineering owner records that decision by commenting /approve-spec 458e2d9e88dc59e321db7703e20c831db74f056b on this pull request, and the author may do so as an engineering owner. Whatever that decision, these need changes:

  • Rebase onto the landed stack so this pull request carries only its 1,840-line AlertDialog change; it is 28,849 authored lines now.
  • In a jsdom test, userEvent.click on the action now runs Cancel instead of onAction, which would break consumers' tests.
  • Under a mouse, Delete keeps its :active press while the group lights Cancel, so two actions look pressed at once (family:action-groups FR2).
  • The actions row's always-on user-select: none goes past spec:AST-094 FR41's per-press suppression and a mouse's released behavior.
  • The root-entry case of scripts/touch-support-bundle.test.mjs fails, opening the alert on a desktop logs a false "stays inactive" warning, the docs say a mouse keeps each button's click, useIsScrollable is unused, and the changeset's iOS Safari claim needs real-device evidence.

[Automated review]

Comment on lines +63 to +67
trackingRow: {
touchAction: 'pinch-zoom',
userSelect: 'none',
WebkitUserSelect: 'none',
},

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 keeps the actions row unselectable for every pointer for as long as the alert is open. spec:AST-094 FR41 suppresses selection for the press only and keeps a mouse's released behavior, and FR43 rules out added stylesheet bytes. The recognizer already locks selection per press for a finger or a pen (useActionGroupTracking.ts:166), so userSelect and WebkitUserSelect can go, with touch-action left to the gesture host.

// actions keep their default styles otherwise.
manageStyle: false,
});
const actionTrackingProps = isTracking ? pressGroupItemProps : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With a mouse, a press on Delete slid onto Cancel keeps Delete's own :active press (Button.tsx:128-131, scale 0.98) while the group paints Cancel data-astryx-press="on", so both actions look pressed at once, against family:action-groups FR2 ("at most one at a time"). The group's paint should be the only press on a tracked item.

const DETACHED: RefObject<Element | null> = {current: null};

/** Every pointer a group tracks (`family:action-groups` FR5). */
const EVERY_POINTER: ReadonlyArray<PointerKind> = ['touch', 'pen', 'mouse'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tracking the mouse changes what a consumer's jsdom test sees: userEvent.click on Delete now runs Cancel (onOpenChange(false), no onAction), because jsdom lays nothing out and the tracker's hit test (actionGroupTracker.ts:309-324) matches the first member's empty box at the origin; on the base the same test calls onAction. Leaving the press to the browser's click when no member has a laid-out box would keep those tests working.

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

"A mouse, the keyboard and assistive technology keep the click of each button" (and line 199's "mouse/keyboard/AT keep each button click") no longer holds: the group tracks 'mouse' too (useActionGroupTracking.ts:85), as the changeset says.

Comment on lines +570 to +571
} else if (!this.isCoarse) {
this.warnFinePointer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An action-group attach takes this branch too, so opening an AlertDialog under a fine pointer logs "It stays inactive until the pointer is coarse" although the host serves the group there (family:action-groups FR5). Skip the warning for options.actionGroup, as for perEventPointer.

Comment on lines +10 to +12
* @position Internal hook for a touch-tracked surface whose content scroller
* keeps the browser's pan only where it overflows (AlertDialog, the action
* sheet card)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nothing imports this hook, though the header names AlertDialog and an action sheet as consumers. Please drop it and its test from this pull request.

'@astryxdesign/core': patch
---

[fix] AlertDialog: its two actions are tracked as one group under every pointer, a finger, a pen or a mouse, the way iOS tracks an alert's actions. A press that lands on one action and slides to the other now runs the action it lifts on. Before, iOS Safari ran the action the finger first touched (Delete, when the finger slid to Cancel), and other browsers ran nothing and left the alert open. The pressed action highlights as soon as it is pressed instead of 150 ms later, the highlight follows the pointer between the actions, and lifting off both still runs nothing; a second finger or the window losing focus ends the press with nothing run. Only the actions are tracked: a press on the title or the message is not, and their text stays selectable with a mouse. The keyboard and assistive technology keep each button's own activation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Before, iOS Safari ran the action the finger first touched" is a claim about iOS Safari behavior, and spec:AST-094 Platform support asks for real-device evidence for every such claim. Attach real-device evidence or drop the sentence.

This branch was successfully deployed

1 active deployment
Preview — 458e2d9e Deployed Oct 28, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants