Skip to content

feat(AlertDialog): under a finger the alert appears and leaves the way iOS's does - #8084

Draft
vjeux wants to merge 1737 commits into
meta:mainfrom
vjeux:uikit/c18-alerts-pr2
Draft

vjeux wants to merge 1737 commits into
meta:mainfrom
vjeux:uikit/c18-alerts-pr2

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8083. Review only this PR's change: vjeux/astryx@458e2d9...19af775

Why

On a phone, AlertDialog plays the theme's desktop entrance: it rises 16 px and grows from 95% as the dimming pops on, and it vanishes in one frame when closed. A tap during the entrance already runs a button. The iOS alert shrinks into place, ignores touches until it settles, and fades out.

What led to it

The UIKit-parity work, which brings Astryx's touch behavior to iOS's, measured the iOS 26.2 alert (motion/uikit/alert.ts), and spec:AST-094 asks every touch device to present it that way (FR4, FR13, FR35). Cindy's AlertDialog record (#8594) now specifies it: FR6 (the motion), FR7 (input held while it enters) and FR9 (320 px on a phone). This PR implements FR6 and FR9; #8373 completes FR7 on the shared input hold (#8342).

Options and tradeoffs

  1. CSS @starting-style. Simple, but no spring (no UIKit curve, no turning around mid-way), and Safari can't animate the exit without overlay.
  2. A per-frame script animation. Exact curves, but on the main thread: it stalls on a busy page and lays out every frame.
  3. The shared animator, playing the measured springs as compositor scale and opacity keyframes (the ::backdrop's too). Smooth on a busy page and interruptible, at the cost of a narrower alert on phones (320 vs 400).

Why this choice

Option 3 is the only one that matches UIKit and stays smooth. Under a finger the alert follows the springs.alertPresent and springs.alertDismiss roles, ignores touches on its actions for 435 ms from the start, runs nothing while it leaves, and cross-fades under Reduce Motion. A mouse keeps today's behavior. As her record says, the public API doesn't change: there is no opt-out prop, and a width the caller sets still wins on a phone.

How to check it

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

  1. Open the alert: it shrinks into place from slightly larger as it and the dimming fade in. Close it: it fades out. On main, it rises, grows, and vanishes at once.
  2. Tap Cancel the moment it opens: nothing happens until it settles; then Cancel works.
  3. Close it and quickly reopen it: it turns around without a jump.
  4. Turn on Reduce Motion: a cross-fade, with no scaling.
  5. With a mouse: today's entrance and close, 400 wide. On a tablet-sized touch screen: the iOS motion, 400 wide.

Steps 1 to 4 were run on the iOS 26.2 simulator (step 1 also on the Android 15 emulator), and step 5 in the browser and unit tests: each behaved as written. This head's browser tests passed in Chromium (24) and WebKit (20, plus 4 Chromium-only cases skipped by design).

Evidence

iOS 26.2 simulator, Mobile Safari, main on the left (present and dismiss storyboards):

Present, then Cancel Tap mid-entrance
Busy thread Reduce Motion
Early close Tap mid-exit Reopened mid-exit

Android 15 emulator, Chrome 124 (blur off for capture), main on the left:

Android: present, then Cancel Blur on

Writing rubric results

spec:AST-094 FR4's role table gains the alert's two springs, springs.alertPresent and springs.alertDismiss. FR4's table renamed to match AlertDialog FR6.

  • R1 narration: none; each row states its value and source.
  • R2 research: none.
  • R3 evidence: each row's source is the measurement (UIAlertController, iOS 26.2 simulator, 2026-10-23), not a pull request.
  • R4 rejected alternatives: none.
  • R5 change references: none added.
  • R6 open questions: none touched.

@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 30, 2026 2:01am 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 30, 2026, 2:55 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.

This pull request is on hold for an owner decision: AlertDialog's touch motion rests only on draft spec:AST-094 (FR4, FR5, FR7, FR8 and DEC-1), and FR4 narrows the current architecture:theme-tokens INV3, so an engineering owner has to approve or amend those requirements. An engineering owner records the decision by commenting /approve-spec <full head SHA> on the pull request, and the author may do so if they are an engineering owner. Separately from that decision:

  • Land the stacked pull requests first and rebase, so this diff is only the AlertDialog motion commit instead of 25,379 authored lines.
  • While the alert fades out under touch, keyboard and assistive-technology activations still run onAction, and Escape calls onOpenChange(false) again; make the leaving alert inert.
  • Under React StrictMode, an AlertDialog that mounts open never starts its presentation and stays invisible and modal; the status effect should own and clean up its animation frame.
  • The adaptive threshold change contradicts component:Selector DEC-1; split it out or keep the current query.
  • The lint job failed on this head with an ESLint out-of-memory error and needs a rerun.

[Automated review]

ref={ref}
isOpen={isOpen}
ref={mergedRef}
isOpen={motion.isPresented}

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 a coarse pointer this keeps the native dialog open for the 404 ms fade (504 ms backstop), but only touch and pen are inert (useActionGroupTracking.ts:186). The still-focused Delete takes a second Enter or AT activation and runs onAction again (:271), and Escape calls onOpenChange(false) again. Consider making the content inert while isLeaving, or dropping onAction, cancel and dismissal once isOpen is false.

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 6dca4bf: while the alert leaves (closed by the caller, still presented during its fade), AlertDialog drops a click, Enter or assistive-technology activation on an action, Cancel, and Escape, as UIKit delivers no input during a dismissal. Focus stays where it is, so Dialog returns it to the opener. The new case 'on iOS, while the alert leaves, runs nothing' failed before the fix (onAction ran).

const was = statusRef.current;
const now: Status = !isPresented ? 'hidden' : isOpen ? 'open' : 'leaving';
statusRef.current = now;
if (!isEnabled || was === now) {

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 replay of a mount-open alert (for example useImperativeAlertDialog's first show()), statusRef is already 'open', so this returns. Meanwhile the cleanup at :211 has cancelled the presentation frame, so the panel stays at opacity 0 and scale 1.2 and touches stay inert. Giving this effect its own cleanup (cancel the frame, restore statusRef) would make the replay re-run the opening (architecture:react-component-runtime INV3).

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.

The presentation no longer waits for an animation frame (it starts in the effect that opens the dialog), so a StrictMode replay has nothing to cancel, as the later review notes. 6dca4bf adds the StrictMode mount-open case: the alert presents and takes touches once the presentation completes.

// and large touch displays stay anchored, phones in either orientation get
// the sheet. The query is the shared touch presentation's
// (utils/touchPresentation.ts).
export const COMPACT_TOUCH_PRESENTATION_QUERY = COMPACT_TOUCH_QUERY;

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 moves adaptive from (max-width: 768px) and (pointer: coarse) to (width < 744px) or (height < 500px). Current component:Selector DEC-1 records that "adaptive keeps its width-and-pointer query". The change belongs in its own pull request with an owner decision, or this should keep the current query.

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 change comes from the touch presentation resolver (#8062), which this pull request reaches through #8076; it is not part of this pull request's own commit. The Selector DEC-1 question belongs to #8062 and waits in its reviewers' queue.

@github-actions

github-actions Bot commented Oct 24, 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 460 -
Complexity Medium High (24) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.3KB 88.0KB +30.7KB ⚠️ 12.5KB 12.6KB +73B
core/AppShell 66.8KB 67.4KB +623B 11.8KB 11.8KB 0B
core/AvatarGroupOverflow 40.3KB 40.9KB +657B 6.4KB 6.4KB 0B
core/Banner 48.5KB 49.1KB +611B 10.1KB 10.1KB 0B
core/BaseTypeahead 55.0KB 55.6KB +601B 10.1KB 10.1KB 0B
core/BreadcrumbItem 67.3KB 68.1KB +783B 11.2KB 11.2KB 0B
core/Button 43.7KB 44.3KB +640B 9.3KB 9.3KB 0B
core/Calendar 55.6KB 56.2KB +598B 10.2KB 10.2KB 0B
core/Carousel 50.9KB 51.5KB +611B 10.3KB 10.3KB 0B
core/ChatComposer 61.3KB 61.9KB +612B 11.0KB 11.0KB 0B
core/ChatComposerInput 56.2KB 56.7KB +607B 10.2KB 10.2KB 0B
core/ChatComposerTokenElement 56.2KB 56.7KB +604B 10.2KB 10.2KB 0B
core/ChatDictationButton 47.2KB 47.8KB +627B 9.6KB 9.6KB 0B
core/ChatLayout 49.5KB 50.2KB +669B 10.1KB 10.1KB 0B
core/ChatLayoutScrollButton 47.0KB 47.7KB +630B 9.7KB 9.7KB 0B
core/ChatSendButton 46.4KB 47.0KB +610B 9.3KB 9.3KB 0B
core/CheckboxInput 50.5KB 51.1KB +593B 9.5KB 9.5KB 0B
core/CheckboxListItem 54.7KB 55.2KB +593B 10.6KB 10.6KB 0B
core/ClickableCard 12.8KB 13.4KB +663B 6.9KB 6.9KB 0B
core/CodeBlock 61.2KB 61.8KB +625B 10.8KB 10.8KB 0B
core/Collapsible 18.7KB 19.4KB +652B 4.4KB 4.4KB 0B
core/CommandPalette 51.5KB 52.1KB +629B 12.0KB 12.0KB 0B
core/CommandPaletteItem 8.4KB 9.0KB +630B 3.5KB 3.5KB 0B
core/ComplexSelector 56.5KB 57.1KB +655B 10.8KB 10.8KB 0B
core/ContextMenu 75.3KB 76.1KB +851B 14.8KB 14.8KB 0B
core/ContextMenuItem 53.5KB 54.2KB +709B 9.8KB 9.8KB 0B
core/DateInput 100.2KB 100.9KB +651B 14.9KB 14.9KB 0B
core/DateRangeInput 70.1KB 70.7KB +630B 11.4KB 11.4KB 0B
core/DateTimeInput 110.4KB 111.1KB +630B 15.4KB 15.4KB 0B
core/DialogHeader 49.5KB 50.1KB +642B 10.0KB 10.0KB 0B
core/DrawerHeader 49.6KB 50.2KB +622B 11.4KB 11.4KB 0B
core/DropdownMenu 73.4KB 74.2KB +796B 15.0KB 15.0KB 0B
core/DropdownMenuCheckboxItem 16.4KB 17.0KB +637B 5.7KB 5.7KB 0B
core/DropdownMenuItem 15.6KB 16.2KB +616B 5.3KB 5.3KB 0B
core/DropdownMenuRadioItem 16.4KB 17.0KB +626B 5.7KB 5.7KB 0B
core/DropdownMenuSubMenu 58.7KB 59.4KB +772B 9.7KB 9.7KB 0B
core/FileInput 54.8KB 55.4KB +581B 10.5KB 10.5KB 0B
core/IconButton 43.7KB 44.3KB +638B 9.3KB 9.3KB 0B
core/Item 11.4KB 12.0KB +638B 4.7KB 4.7KB 0B
core/LayerProvider 64.0KB 64.6KB +622B 10.3KB 10.3KB 0B
core/Lightbox 51.9KB 52.4KB +569B 10.0KB 10.0KB 0B
core/Link 44.1KB 44.7KB +621B 7.7KB 7.7KB 0B
core/ListItem 54.1KB 54.7KB +576B 10.6KB 10.6KB 0B
core/Markdown 145.2KB 146.0KB +795B 17.8KB 17.8KB 0B
core/MobileNav 51.7KB 52.2KB +564B 10.2KB 10.2KB 0B
core/MobileNavToggle 46.9KB 47.5KB +593B 9.6KB 9.6KB 0B
core/MoreMenu 73.7KB 74.5KB +792B 15.0KB 15.0KB 0B
core/MultiSelector 73.6KB 74.3KB +675B 14.8KB 14.8KB 0B
core/NavHeadingMenu 13.0KB 13.5KB +532B 3.3KB 3.3KB 0B
core/NumberInput 56.4KB 57.0KB +660B 10.9KB 10.9KB 0B
core/Outline 29.0KB 29.6KB +624B 4.0KB 4.0KB 0B
core/Pagination 85.5KB 86.3KB +811B 15.5KB 15.5KB 0B
core/Popover 50.3KB 50.9KB +631B 12.2KB 12.2KB 0B
core/PowerSearch 166.6KB 167.8KB +1.2KB 20.1KB 20.1KB +62B
core/RadioListItem 52.8KB 53.4KB +614B 9.8KB 9.8KB 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 77.0KB 77.8KB +780B 15.2KB 15.2KB 0B
core/SelectorOption 14.7KB 15.3KB +623B 5.2KB 5.2KB 0B
core/SideNav 64.6KB 65.3KB +636B 10.7KB 10.7KB 0B
core/SideNavCollapseButton 47.3KB 47.9KB +608B 9.6KB 9.6KB 0B
core/SideNavHeading 59.2KB 59.9KB +773B 10.4KB 10.4KB 0B
core/SideNavItem 55.3KB 55.8KB +598B 10.3KB 10.3KB 0B
core/Step 33.0KB 33.6KB +622B 5.5KB 5.5KB 0B
core/Stepper 48.1KB 48.7KB +618B 9.7KB 9.7KB 0B
core/Switch 49.0KB 49.6KB +601B 9.5KB 9.5KB 0B
core/Tab 9.7KB 10.3KB +642B 4.1KB 4.1KB 0B
core/TabMenu 55.9KB 56.7KB +759B 10.2KB 10.2KB 0B
core/Table 84.7KB 85.5KB +809B 15.6KB 15.6KB 0B
core/TableCell 76.5KB 77.3KB +840B 15.3KB 15.3KB 0B
core/TableHeaderCell 76.1KB 76.9KB +827B 15.0KB 15.0KB 0B
core/TableSelectionToolbar 52.8KB 53.4KB +636B 11.3KB 11.3KB 0B
core/TextInput 54.2KB 54.8KB +578B 10.6KB 10.6KB 0B
core/Thumbnail 51.4KB 51.9KB +597B 11.1KB 11.1KB 0B
core/TimeInput 87.5KB 88.0KB +573B 13.9KB 13.9KB 0B
core/Timestamp 27.9KB 27.9KB -1B 9.7KB 9.7KB 0B
core/Toast 60.6KB 61.2KB +612B 9.9KB 9.9KB 0B
core/ToggleButton 44.5KB 45.1KB +595B 9.4KB 9.4KB 0B
core/Token 30.2KB 30.8KB +608B 4.6KB 4.6KB 0B
core/Tokenizer 69.3KB 70.0KB +734B 16.7KB 16.7KB +41B
core/TopNav 52.5KB 53.1KB +572B 10.5KB 10.5KB 0B
core/TopNavHeading 58.6KB 59.4KB +783B 10.2KB 10.2KB 0B
core/TopNavItem 9.0KB 9.7KB +637B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 55.0KB 55.6KB +609B 11.0KB 11.0KB 0B
core/TopNavMegaMenuItem 9.1KB 9.7KB +635B 4.1KB 4.1KB 0B
core/TopNavMenu 58.3KB 59.0KB +767B 10.3KB 10.3KB 0B
core/TreeList 40.4KB 41.0KB +627B 4.8KB 4.8KB 0B
core/Typeahead 62.2KB 62.8KB +625B 11.4KB 11.4KB 0B
lab/ChatEmojiPicker N/A 66.4KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.9KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 95.3KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.5KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 247.7KB N/A N/A 2.4KB N/A
lab/Comments N/A 251.9KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.8KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 64.2KB N/A N/A 722B N/A
lab/ListInput N/A 76.4KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 178.0KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 123.3KB N/A N/A 809B N/A
lab/Schedule N/A 61.1KB N/A N/A 3.9KB N/A
lab/TourStep N/A 56.2KB N/A N/A 590B N/A
lab/TransferList N/A 68.1KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.6KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 272.0KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.8KB N/A N/A 454B N/A
richtext/RichTextView N/A 252.4KB 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 +30.7KB ⚠️ +73B

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 35.4KB 35.4KB +44B

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

@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 pull request is on hold for an owner decision: AlertDialog's touch motion rests only on draft spec:AST-094 (FR4, FR5, FR7, FR8 and DEC-1), and FR4 narrows the current architecture:theme-tokens INV3, so an engineering owner needs to approve or amend those requirements. 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. Separately from that decision:

  • Land the stacked pull requests first and rebase, so this diff is only the AlertDialog motion commit instead of 26,547 authored lines.
  • While the alert fades out under touch, keyboard and assistive-technology activations still run onAction and Escape calls onOpenChange(false) again; make the leaving alert inert.
  • Under React StrictMode, an AlertDialog that mounts open never starts its presentation and stays invisible and unresponsive to touch; let the status effect own and clean up its animation frame.
  • The adaptive threshold change contradicts component:Selector DEC-1; split it out or keep the current query.
  • The lint job runs out of memory on this head while the base passes.

[Automated review]

@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 pull request is on hold for an owner decision: AlertDialog's touch motion rests only on draft spec:AST-094 (FR4, FR5, FR7, FR8 and DEC-1), and FR4 narrows the current architecture:theme-tokens INV3, so an engineering owner needs to approve or amend those requirements. 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. Separately from that decision:

  • Land the stacked pull requests first and rebase, so this diff is only the AlertDialog motion commit instead of 27,525 authored lines.
  • While the alert fades out under touch, keyboard, click and assistive-technology activations still run onAction, and Escape calls onOpenChange(false) again; make the leaving alert inert.
  • The adaptive threshold change contradicts component:Selector DEC-1; split it out or keep the current query.
  • The lint job runs out of memory on this head while main passes.
  • Add a StrictMode mount-open test for the new motion effect; the StrictMode presentation issue raised on the earlier head no longer applies.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
@vjeux
vjeux force-pushed the uikit/c18-alerts-pr2 branch from b635c03 to 1c55503 Compare October 24, 2026 21:29
@vjeux vjeux changed the title feat(AlertDialog): on a touch screen the alert appears and leaves the way iOS's does feat(AlertDialog): on iPhone and iPad the alert appears and leaves the way iOS's does Oct 24, 2026

@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 pull request is on hold for an owner decision: AlertDialog's iPhone and iPad motion rests only on draft spec:AST-094 (FR5, FR7, DEC-1), and that record's FR4 and DEC-1 forbid the theme-tunable touch motion this head adds through defineTheme({motion: {springs, durations, curves}}), so an engineering owner needs to settle which holds, and whether the two non-component experimental subpaths are an acceptable boundary under spec:AST-017 FR21. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request once the record says so, and the author may do so if they are an engineering owner. Separately from that decision:

  • While the alert fades out on iOS, keyboard and assistive-technology activations still run onAction and Escape calls onOpenChange(false) again; make the leaving alert inert.
  • Under Reduce Motion the iOS fades last 404 ms on a spring, but design:reduced-motion DR2 allows an opacity-only change of 200 ms or less, and DR4 bars theme values that lengthen it.
  • The compact test's 744 px width contradicts spec:AST-012 FR9 (the theme's md point or a height under 500), and the platform adaptation condition extends the vocabulary FR2 closes.
  • Land the stacked pull requests first and rebase, so this diff is only the AlertDialog motion commit instead of 31,379 authored lines.
  • Add a StrictMode mount-open test for the new motion effect.

[Automated review]

ref={ref}
isOpen={isOpen}
ref={mergedRef}
isOpen={motion.isPresented}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While the alert fades out on iOS, Dialog stays an open modal here, but only the touch tracker consults the inert window (:250). A second Enter or assistive-technology activation of the still-focused Delete runs onAction again (:285), and Escape calls onOpenChange(false) on an alert the caller already closed. Making the content inert while isPresented && !isOpen, or dropping the handlers once isOpen is false, would close that window.

const handle = panel.animate(
{opacity: 1, scale: 1},
{spring: springs.alertPresent},
{crossfade: springs.alertPresent},

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 Reduce Motion this fades on springs.alertPresent, a 404 ms curve, and the exit at :252 does the same on springs.alertDismiss. design:reduced-motion DR2 (current) requires an opacity-only change that finishes in 200 ms or less on a coarse pointer, and DR1 rules out springs.

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) defines compact as width below the theme's effective md point or height below 500, used by this resolver and by Selector's adaptive, with no size numbers of their own. 744px keeps a number of its own and gives the popover at 744–767 px where FR9 gives the sheet.

* 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 (current) closes the when vocabulary to width, height, pointer, contrast and motion: "Future environment fields require explicit additions to this closed vocabulary." platform needs that amendment first, or should come out of this pull request; AlertDialog's motion does not use it.

@vjeux
vjeux force-pushed the uikit/c18-alerts-pr2 branch from 1c55503 to 6dca4bf Compare October 24, 2026 22:07
@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 6dca4bf:

  • While the alert leaves, a click, Enter or assistive-technology activation on an action, Cancel and Escape do nothing (new failing-before case).
  • StrictMode mount-open case added; the presentation no longer waits for an animation frame.
  • lint passes on this head (the out-of-memory run was on b635c03).

Waiting in the reviewers' queue: the spec decision (draft AST-094 FR4, FR5, FR7, FR8, DEC-1 and theme-tokens INV3; the alert's springs and durations are now theme motion roles) and the adaptive query, which belongs to #8062. This pull request carries its stack until it lands; review the last commit.

@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 pull request is on hold for an owner decision: AlertDialog's iPhone and iPad motion rests only on draft spec:AST-094 (FR5, FR7, DEC-1), and that record's FR4 and DEC-1 forbid the theme-tunable touch motion this head adds through defineTheme({motion: {springs, durations, curves}}), so an engineering owner needs to settle which holds, and whether the two non-component experimental subpaths are an acceptable boundary under spec:AST-017 FR21. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request once the record says so, and the author may do so as an engineering owner. Separately from that decision:

  • Under Reduce Motion the iOS fades last 404 ms on a spring, but design:reduced-motion DR2 allows an opacity-only change of 200 ms or less, and DR4 bars theme values that lengthen it.
  • The changeset and docs say no click, key or assistive-technology activation reaches the alert during either transition, but during the entrance they still run onAction and onOpenChange; gate them until the presentation completes or narrow the text.
  • The compact test's 744 px width contradicts spec:AST-012 FR9 and the compact definition main now exports, and the platform adaptation condition extends the vocabulary FR2 closes.
  • Land the stacked pull requests first and rebase, so this diff is only the AlertDialog motion commit instead of 31,567 authored lines.

[Automated review]

// or an assistive-technology activation on an action, and Escape, do
// nothing until it is hidden. Focus stays where it is, so Dialog returns it
// to the opener when the fade ends.
const isLeaving = motion.isDriving && motion.isPresented && !isOpen;

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 drops clicks, keys and Escape only while the alert leaves. During the entrance only the touch tracker consults the 435 ms window (:270), so a keyboard or assistive-technology activation of Delete still runs onAction, and Cancel or Escape still calls onOpenChange(false), while .changeset/alertdialog-touch-motion.md and the usage docs say nothing reaches the alert during either transition. Either gate these handlers on the same window as acceptsTouches() (with a keyboard-during-entrance test), or narrow the changeset, docs and file header to touches.

@vjeux
vjeux force-pushed the uikit/c18-alerts-pr2 branch from 6dca4bf to 3b4bd41 Compare October 24, 2026 23:31

@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 pull request is on hold for an engineering-owner decision: AlertDialog's touch entrance and exit, dropping keys and assistive-technology activation while the alert leaves, and the alert.present and alert.dismiss rows added to AST-094's FR4 table have no current spec, because AST-094 leaves each overlay's presentation and exit to its own record and component:AlertDialog is still a draft. An engineering owner records the decision by stating it in the AlertDialog record and commenting /approve-spec <full head SHA>, or by approving a spec pull request that states it; the author may do this as an engineering owner. Independent of that decision, please:

  • get scripts/touch-support-bundle.test.mjs passing, since its root-entry case fails on this head (the description points to #8671 for that);
  • render the alert's opening state in the commit instead of repairing it in a passive effect (useAlertMotion.ts:273);
  • add the WebKit busy-thread proof and the real-device evidence AST-094 asks for, or narrow the changeset's claim that the animation keeps running when the page is busy;
  • land the stacked layers first so this diff carries only the AlertDialog change (33,172 changed lines today), and align the usage text, FR6 and the DEC-11 test with the code.

[Automated review]

// dialog can stay on the main thread (motion/animator.ts, known limits);
// that case is not yet proven here.
transitionEndsAtRef.current = Number.POSITIVE_INFINITY;
panel.set({opacity: 0, scale: presentScaleFrom(elementRef.current)});

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 opening commit has already rendered the dialog at opacity: 1 with the theme keyframes off (AlertDialog.tsx:85-86 plus Dialog's open style), and these start values arrive only in this passive Effect, which architecture:react-component-runtime INV1 forbids. Rendering the start state in the commit, or writing it in a layout Effect, closes the window that an open from a timer or a network response leaves before paint.

<div
data-astryx-theme="slow"
style={{
['--motion-springs-alert-present' as string]: '0.8s 0',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since the rename the roles read --motion-alert-present and --motion-alert-dismiss (motion/roleValue.ts:29-36), so setting --motion-springs-alert-* here cannot make this test fail. Setting the roles' own variables makes it test DEC-11.

| FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | verify |
| FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | verify |
| FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | verify |
| FR6 | On phones (compact width with a touch pointer), alerts are 320 px wide in every theme, matching iOS, with no new theme role; elsewhere the default width is 400. | owner decision, 2026-10-26 | settled |

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 code keeps 400 under a finger with presentation="dialog" and honours an explicit width (AlertDialog.tsx:255-256); FR6 could say it sets the default and name that exception. The FR6 verification row below fills 2 of the table's 5 columns.

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 still says a mouse keeps the click of each button, but the head tracks a mouse across the actions (the "tracks a mouse too" test), and the presentation description says the actions are tracked under every pointer.

vjeux added 5 commits October 28, 2026 06:04
Her merged record (meta#8594) keeps AlertDialog's public contract unchanged:
the touch presentation is a behavior change, and on a phone a caller's
width is its way back (FR9). Pin AlertDialogProps so it has no
presentation prop and keeps width. Fails typecheck while the prop exists.
…keeps the public contract

Her merged AlertDialog record (meta#8594) changes behavior under a touch
presentation without changing the public API: a caller's width is the
way back on a phone (FR9), and no other prop is added. Remove the
presentation prop, its AlertDialogPresentation type, the
DialogPresentation story, its doc entry and the tests that used it. The
width test also covers a caller's width on a phone.
…lertDismiss

Her merged AlertDialog FR6 (meta#8594) names the alert's motion roles
springs.alertPresent and springs.alertDismiss, the springs table's rows.
Expect those role ids, the alert's two springs among the registered
springs roles, and AlertDialog registering those two rows alone. Fails
while the roles are registered as alert.present and alert.dismiss.
…d springs.alertDismiss

Her merged AlertDialog FR6 (meta#8594) names the alert's motion roles by
their springs table rows. Register the alert's two springs among the
springs roles under those names, rename AST-094 FR4's two rows to match,
and let AlertDialog register those two rows alone (registerAlertSprings),
so its bundle still carries no other spring. The alert table keeps only
its measured values. Same springs and timings; only the role ids change.

@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 pull request is on hold for an engineering-owner decision. AlertDialog's touch entrance and exit, and its dropping of keys and assistive-technology activation while it fades out, rest only on the draft component:AlertDialog record, because current AST-094 leaves each overlay's presentation and exit to that record. The two alert rows added to AST-094's FR4 table also go against its DEC-5. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request for the FR4 rows, and by approving the AlertDialog behavior here or making that record current; the author may do this as an engineering owner. Independent of that decision, please:

  • get scripts/touch-support-bundle.test.mjs passing: its root-entry case fails on this head and passes on main, and #8671 is still open;
  • land the parent stack first, so this diff carries only the AlertDialog change (33,046 authored lines today);
  • add the WebKit busy-main-thread proof and the device evidence AST-094 FR5 asks for, or start the entrance in the next animation frame as animator.ts advises;
  • make the usage docs say the actions are tracked for a mouse too.

[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 the actions are tracked "under a finger or a pen" and that "A mouse, the keyboard and assistive technology keep the click of each button", but useActionGroupTracking defaults to every pointer and AlertDialog passes no pointerTypes. The test "tracks a mouse too (family:action-groups FR5)" confirms it, and family:action-groups FR5 applies the tracking to every web pointer. A mouse press on Delete released over Cancel runs Cancel. Suggest: "Under every pointer, mouse included, the two actions are tracked as one group …; the keyboard and assistive technology keep each button's own activation." The dense text at line 199 needs the same change.

Comment on lines +264 to +281
if (now === 'open' && was === 'hidden') {
// The Dialog opened its modal in this same flush (its effect runs
// first), and nothing has painted it yet. The alert starts at its
// starting values and moves from rest with no start time: the browser
// starts it at the commit that first shows it, as Core Animation starts
// an animation at its transaction's commit, and the animator promotes
// it in this task. Chromium runs it on the compositor from that first
// frame through a busy main thread (the busy-thread browser proof). In
// iOS Safari an animation created in the task that first displays a
// dialog can stay on the main thread (motion/animator.ts, known limits);
// that case is not yet proven here.
transitionEndsAtRef.current = Number.POSITIVE_INFINITY;
panel.set({opacity: 0, scale: presentScaleFrom(elementRef.current)});
backdrop.set({opacity: 0});
const handle = present(panel, backdrop);
presentRef.current = handle;
armTouchWindow(handle);
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.

The presentation is created in the same passive-effect flush that runs Dialog's showModal(). motion/animator.ts records that in iOS Safari "an animation created in the task that first displays a dialog stays on the main thread: start it in the next animation frame", and the comment here says that case "is not yet proven". AST-094 FR5 requires the motion to keep advancing through a main-thread block, and its Platform support asks for a WebKit busy proof and device evidence. The alert's busy test skips WebKit, while the changeset promises the animation keeps running on a busy page. Could the presentation start in the next frame, with the StrictMode-safe cancel, plus a WebKit busy proof or a device clip?

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 rows put the alert's measured springs into AST-094's own FR4 table. FR4 and DEC-5 list only the roles this record names, and send roles a component's own record measures through the generated motion role table. This needs an engineering owner's decision (/approve-spec <full head SHA>), or the rows move to the AlertDialog record's measurement.

vjeux and others added 18 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.

This pull request is on hold for an engineering-owner decision: AlertDialog's touch entrance and exit, and its dropping of keys and assistive-technology activation while it fades out, rest only on the draft AlertDialog record, because current AST-094 leaves each overlay's presentation and exit to that record, and the two alert rows added to AST-094's FR4 table go against its DEC-5. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request for the FR4 rows, and by approving the AlertDialog behavior here or making that record current; the author may do this as an engineering owner. Independent of that decision, please:

  • get scripts/touch-support-bundle.test.mjs passing: its root-entry case fails on this head and passes at the base, and #8671 is still an open draft;
  • land the parent stack first, so this diff carries only the AlertDialog change (31,326 authored lines today);
  • add the WebKit busy-main-thread proof and device evidence AST-094 FR5 asks for, or start the entrance in the next animation frame as animator.ts advises;
  • make the usage docs say the actions are tracked for a mouse too.

[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 the actions are tracked under a finger or a pen and that "A mouse, the keyboard and assistive technology keep the click of each button", but useActionGroupTracking tracks every pointer by default (EVERY_POINTER) and AlertDialog passes no pointerTypes; the test "tracks a mouse too (family:action-groups FR5)" passes, and both changesets say "every pointer". A mouse press on Delete released over Cancel runs Cancel. Suggest: the two actions are tracked as one group under every pointer, mouse included; the keyboard and assistive technology keep each button's own activation. The dense description at line 199 says the same thing.

transitionEndsAtRef.current = Number.POSITIVE_INFINITY;
panel.set({opacity: 0, scale: presentScaleFrom(elementRef.current)});
backdrop.set({opacity: 0});
const handle = present(panel, backdrop);

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 entrance is created here, in the same effect flush as Dialog's showModal(). animator.ts:100-101 records that iOS Safari keeps "an animation created in the task that first displays a dialog" on the main thread and says to start it in the next animation frame, and the comment above (line 274) says this case "is not yet proven here". AST-094 FR5 and its Platform support ask for the busy-main-thread proof in WebKit and real-device evidence, and the busy test skips WebKit, while the changeset promises the animation keeps running on a busy page. Either start the presentation in the next frame (keeping the StrictMode-safe start) and add the WebKit proof and a device clip, or narrow the changeset's promise.

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 rows put a component's measured pair into AST-094's own FR4 table. FR4 says "further roles a component's own record measures join through the generated motion role table (DEC-5)", and DEC-5 says FR4 lists the roles this record names and cites the generated table for the roles a component's record measures. This is a normative change to a current record, so it needs an engineering owner's decision (an exact-head /approve-spec) or the rows should move to the alert's own record and the generated table.

This branch was successfully deployed

1 active deployment
Preview — 19af7753 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