Skip to content

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

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c18-alerts-pr1
Nov 3, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c18-alerts-pr1

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User impact

In an AlertDialog, a press that slides from one action to the other runs nothing on lift. UIKit runs the action under the lift.

Problem and solution fit

  1. Why it occurs: the two buttons are separate pressables, with no group tracking.
  2. Why it harms the task: a person who presses the wrong action cannot slide to the right one.
  3. Why it matters: family:action-groups makes every group of sibling actions behave one way.

AlertDialog tracks a held pointer of any kind across its actions through a new internal hook over the shared tracker. The lift runs the action under the pointer through its own click; a lift outside both runs nothing. Focus then does what a click on the lifted action does (FR1), with the focus logic from #8657.

The press controller leaves a tracked action's pointer press to the tracker, its one painter (folded in from #8637, which this supersedes). A tracked action takes no :active paint, so a mouse slide lights one action; a key's press still paints.

Expected behavior and authority

component:AlertDialog FR8 (current); family:action-groups FR1–FR5; spec:AST-094 FR41. The record's FR8 gap note goes, and its verification row, verified_by and compatibility bullet now say FR8 ships: text-only edits that need an eng-owner /approve-spec on this head.

The lifted action fades on the shared 200 ms press fade, as the spec owners decided: a tap is not a gesture.

Smallest restoration

  • hooks/useActionGroupTracking.ts: the hook.
  • hooks/actionGroupTracker.ts: where several actions hold the point (no layout), the press resolves to the action it landed on.
  • utils/pressFeedback.ts, privateAttributes/pressGroupItem.stylex.ts: the marker and its skip.
  • hooks/pressGroupContext.ts, Button.tsx: the tracker alone paints a tracked press. A tracked Button sizes its own :active image to zero unless the tracker lights it. The overlay module, a public entry, gains no export; its SYNC header names Button.
  • scripts/touch-support-bundle.test.mjs: AlertDialog and its importers are gesture users.
  • The actions keep touch-action: pinch-zoom: Chrome on Android cancels a pointer it pans, which would end the press.

Evidence

  • Red-first, each failing before its change: a slide and a finger tap left focus off the lifted action; a user-event click on the action ran nothing in jsdom; with the tracked overlay off, a mouse press slid from Delete to Cancel left Delete's pressed image showing; with the host's style management off, a held finger refused no selection; with the controller's skip removed, three press-paint cases failed.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at 3576bcb:

  • Typecheck, pnpm check:repo, strict ESLint and Prettier on the changed files, the private-attribute baseline check, and the touched unit suites (1,492, with every tracker user) pass.
  • pnpm test:ui: 19,027 passed. Node project after a fresh core build: 16,055 passed, 2 expected failures; 7 CLI cases timed out at 30 s under machine load (load average 55). Rerun on their own, the 6 files' 761 cases pass (one file only when run alone).
  • The core build passes, and the API report check shows no change.
  • Storybook builds. AlertDialog's interaction spec (its FR2 case fails with the tracked overlay off): 12/12 in Chromium in two runs, 12/12 pinned to 2 cores with 4 workers, and 120/120 in WebKit in CI's 4-CPU container (×10, 1 worker).
  • AlertDialog's and Button's accessibility contracts: 25/25.
  • The comment-provenance baseline check fails on main itself, for files this PR does not touch.

Writing rubric results

component:AlertDialog: FR8's gap note, the readiness row, the Current-state impact bullet, the compatibility bullets, FR8's verification row and verified_by now say FR8 ships.

  • R1 narration, R2 research, R4 rejected alternatives, R5 change references: none.
  • R3 evidence: FR8's verification row names the suites that verify it.
  • 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 Nov 3, 2026 11:19am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 11:32 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

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
Contributor

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
Contributor

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
Contributor

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
Contributor

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

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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

ghost commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 3576bcb

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
lab/CommentThread 🔴 250.0 kB (+22.9 kB) · 2.4 kB CSS (0) — — · — · —
lab/Comments 🔴 255.3 kB (+22.8 kB) · 2.5 kB CSS (0) — — · — · —
AlertDialog · Storybook 🔴 79.0 kB (+22.6 kB) · 12.8 kB CSS (+42 B) 210 (+22) — · — · —
182 more components
Component Size LoC Visual · A11y · RTL
lab/MobilePowerSearch 214.5 kB (+859 B) · 1.2 kB CSS (0) — — · — · —
PowerSearch 184.6 kB (+504 B) · 20.7 kB CSS (+15 B) — — · — · —
Tokenizer 70.4 kB (+208 B) · 17.1 kB CSS (+28 B) — — · — · —
ContextMenuItem 52.8 kB (+188 B) · 9.9 kB CSS (+19 B) — — · — · —
Markdown 145.3 kB (+179 B) · 18.3 kB CSS (+20 B) — — · — · —
DateTimeInput 158.7 kB (+182 B) · 17.0 kB CSS (+15 B) — — · — · —
charts/Chart 63.1 kB (+185 B) · 20 B CSS (0) — — · — · —
CodeBlock 59.2 kB (+161 B) · 11.1 kB CSS (+17 B) — — · — · —
richtext/RichTextEditor 277.8 kB (+171 B) · 2.6 kB CSS (0) — — · — · —
DialogHeader 48.5 kB (+145 B) · 10.2 kB CSS (+25 B) — — · — · —
TableHeaderCell 76.0 kB (+145 B) · 15.6 kB CSS (+25 B) — — · — · —
Banner 47.4 kB (+148 B) · 10.3 kB CSS (+20 B) — — · — · —
ComplexSelector 55.7 kB (+150 B) · 11.1 kB CSS (+18 B) — — · — · —
Thumbnail 49.7 kB (+144 B) · 11.3 kB CSS (+21 B) — — · — · —
TableSelectionToolbar 52.4 kB (+142 B) · 11.5 kB CSS (+20 B) — — · — · —
AppShell 68.7 kB (+143 B) · 12.2 kB CSS (+18 B) — — · — · —
DateRangeInput 70.0 kB (+141 B) · 11.9 kB CSS (+20 B) — — · — · —
SideNavHeading 59.0 kB (+139 B) · 10.6 kB CSS (+21 B) — — · — · —
BaseTypeahead 55.3 kB (+140 B) · 10.3 kB CSS (+19 B) — — · — · —
Lightbox 50.6 kB (+139 B) · 10.2 kB CSS (+19 B) — — · — · —
ChatDictationButton 46.1 kB (+139 B) · 9.8 kB CSS (+18 B) — — · — · —
LayerProvider 62.9 kB (+139 B) · 10.5 kB CSS (+18 B) — — · — · —
BreadcrumbItem 67.3 kB (+135 B) · 11.5 kB CSS (+21 B) — — · — · —
DropdownMenu 73.2 kB (+139 B) · 15.5 kB CSS (+16 B) — — · — · —
NumberInput 55.1 kB (+135 B) · 11.2 kB CSS (+20 B) — — · — · —
Stepper 47.1 kB (+136 B) · 9.9 kB CSS (+19 B) — — · — · —
TopNavHeading 58.4 kB (+132 B) · 10.4 kB CSS (+21 B) — — · — · —
TopNavMegaMenu 54.5 kB (+134 B) · 11.2 kB CSS (+19 B) — — · — · —
Typeahead 63.0 kB (+130 B) · 11.8 kB CSS (+23 B) — — · — · —
ChatSendButton 45.3 kB (+134 B) · 9.5 kB CSS (+18 B) — — · — · —
TableCell 76.4 kB (+135 B) · 15.8 kB CSS (+16 B) — — · — · —
SideNavCollapseButton 46.2 kB (+132 B) · 9.8 kB CSS (+18 B) — — · — · —
ContextMenu 75.2 kB (+126 B) · 15.4 kB CSS (+23 B) — — · — · —
Pagination 86.1 kB (+127 B) · 16.0 kB CSS (+22 B) — — · — · —
Selector 77.1 kB (+127 B) · 15.7 kB CSS (+21 B) — — · — · —
MoreMenu 73.5 kB (+131 B) · 15.5 kB CSS (+16 B) — — · — · —
Popover 50.2 kB (+130 B) · 12.4 kB CSS (+17 B) — — · — · —
TextInput 53.0 kB (+126 B) · 11.0 kB CSS (+21 B) — — · — · —
TopNav 51.9 kB (+125 B) · 10.7 kB CSS (+21 B) — — · — · —
Toast 59.6 kB (+126 B) · 10.1 kB CSS (+19 B) — — · — · —
DateInput 103.4 kB (+126 B) · 15.3 kB CSS (+17 B) — — · — · —
MultiSelector 72.8 kB (+120 B) · 15.2 kB CSS (+23 B) — — · — · —
SideNav 65.8 kB (+123 B) · 10.9 kB CSS (+20 B) — — · — · —
TimeInput 90.2 kB (+122 B) · 14.3 kB CSS (+21 B) — — · — · —
ChatComposer 61.1 kB (+124 B) · 11.2 kB CSS (+18 B) — — · — · —
ChatLayoutScrollButton 45.9 kB (+123 B) · 9.9 kB CSS (+19 B) — — · — · —
MobileNavToggle 45.9 kB (+122 B) · 9.7 kB CSS (+19 B) — — · — · —
Button · Storybook 42.5 kB (+122 B) · 9.5 kB CSS (+18 B) 605 (+14) — · — · —
lab/CommentComposer 102.5 kB (+139 B) · 1.9 kB CSS (0) — — · — · —
ChatLayout 48.6 kB (+120 B) · 10.3 kB CSS (+18 B) — — · — · —
Table 84.5 kB (+114 B) · 16.0 kB CSS (+23 B) — — · — · —
lab/ListInput 81.6 kB (+137 B) · 1.6 kB CSS (0) — — · — · —
lab/Schedule 61.5 kB (+137 B) · 3.9 kB CSS (0) — — · — · —
Carousel 50.8 kB (+119 B) · 10.5 kB CSS (+17 B) — — · — · —
DrawerHeader 48.6 kB (+116 B) · 11.6 kB CSS (+19 B) — — · — · —
TopNavMenu 58.5 kB (+117 B) · 10.5 kB CSS (+18 B) — — · — · —
ChatComposerTokenElement 56.3 kB (+117 B) · 10.4 kB CSS (+17 B) — — · — · —
ToggleButton 43.4 kB (+115 B) · 9.6 kB CSS (+19 B) — — · — · —
ChatComposerInput 56.3 kB (+116 B) · 10.4 kB CSS (+17 B) — — · — · —
MobileNav 50.3 kB (+112 B) · 10.4 kB CSS (+21 B) — — · — · —
lab/EmojiPicker 70.3 kB (+133 B) · 722 B CSS (0) — — · — · —
lab/TourStep 55.6 kB (+133 B) · 590 B CSS (0) — — · — · —
TabMenu 55.5 kB (+118 B) · 10.4 kB CSS (+14 B) — — · — · —
lab/DiffViewer 70.9 kB (+132 B) · 2.1 kB CSS (0) — — · — · —
Calendar 54.4 kB (+114 B) · 10.3 kB CSS (+17 B) — — · — · —
lab/TransferList 74.5 kB (+131 B) · 1.7 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 222.1 kB (+130 B) · 454 B CSS (0) — — · — · —
IconButton 42.5 kB (+109 B) · 9.5 kB CSS (+18 B) — — · — · —
FileInput 53.8 kB (+104 B) · 10.7 kB CSS (+21 B) — — · — · —
lab/TransferListSelector 80.9 kB (+124 B) · 1.9 kB CSS (0) — — · — · —
lab/MobileTokenizer 101.2 kB (+123 B) · 880 B CSS (0) — — · — · —
SideNavItem 54.8 kB (+102 B) · 10.5 kB CSS (+20 B) — — · — · —
lab/ChatEmojiPicker 65.2 kB (+118 B) · 773 B CSS (0) — — · — · —
lab/ChatReactionBar 65.7 kB (+103 B) · 1.1 kB CSS (0) — — · — · —
richtext/RichTextView 258.3 kB (+93 B) · 2.3 kB CSS (0) — — · — · —
TreeList 38.0 kB (+83 B) · 5.0 kB CSS (0) — — · — · —
Switch 86.9 kB (+76 B) · 10.5 kB CSS (0) — — · — · —
Item 13.3 kB (+71 B) · 4.9 kB CSS (0) — — · — · —
lab/CommentPin 47.2 kB (+69 B) · 1.2 kB CSS (0) — — · — · —
CommandPalette 48.9 kB (+68 B) · 12.2 kB CSS (0) — — · — · —
CheckboxListItem 53.4 kB (+67 B) · 11.0 kB CSS (0) — — · — · —
SegmentedControlItem 10.4 kB (+67 B) · 4.0 kB CSS (0) — — · — · —
DropdownMenuItem 17.5 kB (+66 B) · 5.5 kB CSS (0) — — · — · —
DropdownMenuRadioItem 18.3 kB (+66 B) · 5.9 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 18.2 kB (+65 B) · 5.9 kB CSS (0) — — · — · —
Collapsible 22.5 kB (+63 B) · 4.5 kB CSS (0) — — · — · —
Outline 24.3 kB (+62 B) · 4.2 kB CSS (0) — — · — · —
CommandPaletteItem 10.0 kB (+59 B) · 3.6 kB CSS (0) — — · — · —
ClickableCard 14.6 kB (+58 B) · 7.1 kB CSS (0) — — · — · —
SelectorOption 16.6 kB (+57 B) · 5.4 kB CSS (0) — — · — · —
SelectableCard 14.6 kB (+56 B) · 7.3 kB CSS (0) — — · — · —
Step 27.9 kB (+56 B) · 5.6 kB CSS (0) — — · — · —
Tab 11.3 kB (+56 B) · 4.2 kB CSS (0) — — · — · —
Token 25.1 kB (+56 B) · 4.8 kB CSS (0) — — · — · —
CheckboxInput 48.8 kB (+55 B) · 9.7 kB CSS (0) — — · — · —
Link 42.9 kB (+54 B) · 7.9 kB CSS (0) — — · — · —
TopNavItem 10.6 kB (+54 B) · 4.0 kB CSS (0) — — · — · —
TopNavMegaMenuItem 10.7 kB (+54 B) · 4.2 kB CSS (0) — — · — · —
DropdownMenuSubMenu 56.1 kB (+53 B) · 9.9 kB CSS (0) — — · — · —
ListItem 52.8 kB (+50 B) · 11.0 kB CSS (0) — — · — · —
SegmentedControl 81.7 kB (+45 B) · 7.8 kB CSS (0) — — · — · —
AvatarGroupOverflow 39.0 kB (+39 B) · 6.5 kB CSS (0) — — · — · —
RadioListItem 51.3 kB (+38 B) · 10.1 kB CSS (0) — — · — · —
Timestamp 21.3 kB (−1 B) · 9.9 kB CSS (+19 B) — — · — · —
charts/ChartAxis 6.7 kB (0) · 112 B CSS (0) — — · — · —
AspectRatio 3.8 kB (0) · 3.6 kB CSS (0) — — · — · —
Avatar 35.0 kB (0) · 6.5 kB CSS (0) — — · — · —
AvatarGroup 37.7 kB (0) · 6.6 kB CSS (0) — — · — · —
AvatarStatusDot 7.2 kB (0) · 3.6 kB CSS (0) — — · — · —
Badge 4.0 kB (0) · 4.0 kB CSS (0) — — · — · —
Blockquote 3.6 kB (0) · 3.5 kB CSS (0) — — · — · —
BottomSheetSwitcher 15.0 kB (0) · 3.4 kB CSS (0) — — · — · —
Breadcrumbs 16.0 kB (0) · 3.4 kB CSS (0) — — · — · —
ButtonGroup 9.0 kB (0) · 3.4 kB CSS (0) — — · — · —
Card 6.5 kB (0) · 6.5 kB CSS (0) — — · — · —
Center 4.2 kB (0) · 4.7 kB CSS (0) — — · — · —
ChatComposerDrawer 17.5 kB (0) · 4.7 kB CSS (0) — — · — · —
ChatMessage 16.7 kB (0) · 3.7 kB CSS (0) — — · — · —
ChatMessageBubble 7.1 kB (0) · 3.7 kB CSS (0) — — · — · —
ChatMessageList 22.8 kB (0) · 7.9 kB CSS (0) — — · — · —
ChatMessageMetadata 19.6 kB (0) · 4.1 kB CSS (0) — — · — · —
ChatSystemMessage 6.8 kB (0) · 3.8 kB CSS (0) — — · — · —
ChatTokenizedText 7.1 kB (0) · 4.0 kB CSS (0) — — · — · —
ChatToolCalls 25.8 kB (0) · 9.3 kB CSS (0) — — · — · —
CheckboxList 43.3 kB (0) · 8.6 kB CSS (0) — — · — · —
Citation 19.9 kB (0) · 4.4 kB CSS (0) — — · — · —
Code 3.6 kB (0) · 3.5 kB CSS (0) — — · — · —
CollapsibleGroup 6.5 kB (0) · 3.3 kB CSS (0) — — · — · —
CommandPaletteEmpty 6.2 kB (0) · 3.5 kB CSS (0) — — · — · —
CommandPaletteFooter 17.0 kB (0) · 3.7 kB CSS (0) — — · — · —
CommandPaletteGroup 3.5 kB (0) · 3.5 kB CSS (0) — — · — · —
CommandPaletteInput 26.8 kB (0) · 8.5 kB CSS (0) — — · — · —
CommandPaletteList 16.0 kB (0) · 3.4 kB CSS (0) — — · — · —
Drawer 36.9 kB (0) · 8.2 kB CSS (0) — — · — · —
DropdownMenuGroup 6.2 kB (0) · 3.4 kB CSS (0) — — · — · —
DropdownMenuRadioGroup 6.1 kB (0) · 3.3 kB CSS (0) — — · — · —
Field 41.4 kB (0) · 8.3 kB CSS (0) — — · — · —
FormLayout 6.4 kB (0) · 3.5 kB CSS (0) — — · — · —
Grid 4.4 kB (0) · 3.8 kB CSS (0) — — · — · —
GridSpan 3.5 kB (0) · 3.4 kB CSS (0) — — · — · —
InputGroup 42.0 kB (0) · 8.4 kB CSS (0) — — · — · —
InputGroupText 6.4 kB (0) · 3.9 kB CSS (0) — — · — · —
Kbd 16.7 kB (0) · 3.6 kB CSS (0) — — · — · —
LayoutPanel 7.6 kB (0) · 5.0 kB CSS (0) — — · — · —
MetadataList 16.7 kB (0) · 3.8 kB CSS (0) — — · — · —
MetadataListItem 6.6 kB (0) · 3.5 kB CSS (0) — — · — · —
NavHeadingMenu 14.5 kB (0) · 3.4 kB CSS (0) — — · — · —
NavIcon 3.5 kB (0) · 3.4 kB CSS (0) — — · — · —
OverflowList 8.6 kB (0) · 3.6 kB CSS (0) — — · — · —
Overlay 19.8 kB (0) · 4.0 kB CSS (0) — — · — · —
ProgressBar 10.8 kB (0) · 7.3 kB CSS (0) — — · — · —
RadioList 42.9 kB (0) · 8.3 kB CSS (0) — — · — · —
ResizeHandle 18.7 kB (0) · 4.0 kB CSS (0) — — · — · —
Section 7.6 kB (0) · 6.3 kB CSS (0) — — · — · —
SideNavSection 6.6 kB (0) · 3.7 kB CSS (0) — — · — · —
Skeleton 3.8 kB (0) · 3.8 kB CSS (0) — — · — · —
Slider 88.6 kB (0) · 9.4 kB CSS (0) — — · — · —
StatusDot 23.2 kB (0) · 6.3 kB CSS (0) — — · — · —
TableBody 3.4 kB (0) · 3.3 kB CSS (0) — — · — · —
TableFooter 3.4 kB (0) · 3.3 kB CSS (0) — — · — · —
TableHeader 3.4 kB (0) · 3.3 kB CSS (0) — — · — · —
TableRow 6.5 kB (0) · 4.0 kB CSS (0) — — · — · —
TabList 35.4 kB (0) · 7.5 kB CSS (0) — — · — · —
TextArea 47.2 kB (0) · 9.8 kB CSS (0) — — · — · —
Timer 9.6 kB (0) · 7.0 kB CSS (0) — — · — · —
ToggleButtonGroup 6.4 kB (0) · 3.4 kB CSS (0) — — · — · —
Toolbar 34.7 kB (0) · 7.9 kB CSS (0) — — · — · —
TopNavMegaMenuFeaturedCard 6.9 kB (0) · 3.6 kB CSS (0) — — · — · —
TypeaheadItem 6.3 kB (0) · 3.6 kB CSS (0) — — · — · —
lab/ChartDotGL 7.4 kB (0) · 20 B CSS (0) — — · — · —
lab/ChartDotGLInteractive 8.3 kB (0) · 20 B CSS (0) — — · — · —
lab/ChartHeatmapGL 8.5 kB (0) · 20 B CSS (0) — — · — · —
lab/ChartStreamGL 7.5 kB (0) · 20 B CSS (0) — — · — · —
lab/ChatReasoning 7.0 kB (0) · 967 B CSS (0) — — · — · —
lab/ChatTypingIndicator 24.4 kB (0) · 612 B CSS (0) — — · — · —
lab/ChatUnreadDivider 3.6 kB (0) · 348 B CSS (0) — — · — · —
lab/CircularProgress 8.5 kB (0) · 1.1 kB CSS (0) — — · — · —
lab/CodeEditor 14.1 kB (0) · 743 B CSS (0) — — · — · —
lab/LogStream 8.0 kB (0) · 1.6 kB CSS (0) — — · — · —
lab/Stat 4.2 kB (0) · 646 B CSS (0) — — · — · —
lab/SVGIcon 7.6 kB (0) · 764 B CSS (0) — — · — · —
lab/ThreeDScatterGL 7.6 kB (0) · 20 B CSS (0) — — · — · —

Package size: @astryxdesign/core 6.10 MB packed (+10.2 kB); no new dependencies.

Wide change (shared styles: packages/core/src/utils/interactionOverlay.stylex.ts): add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

ghost left a comment

Copy link
Copy Markdown
Contributor

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'],

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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'].

ghost Oct 24, 2026

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),
);
};

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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;
});

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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:

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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.

ghost left a comment

Copy link
Copy Markdown
Contributor

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:

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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

ghost Oct 24, 2026

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(() => {

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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'],

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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'].

ghost Oct 24, 2026

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(() => {

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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`:

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Oct 24, 2026

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

ghost left a comment

Copy link
Copy Markdown
Contributor

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)';

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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),
);

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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'],

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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';

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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:

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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';

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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,

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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

ghost 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

ghost commented Oct 24, 2026

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.

ghost left a comment

Copy link
Copy Markdown
Contributor

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, {

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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'],

ghost Oct 24, 2026

Copy link
Copy Markdown
Contributor

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

ghost 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

ghost left a comment

Copy link
Copy Markdown
Contributor

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 spec decision: tracking AlertDialog's two actions as one group under every pointer departs from spec:AST-094 FR10, FR11, FR19 and FR41 and the buttons family's press-feedback owner as they stand on main, and the family:action-groups record it relies on is still an open spec pull request (#8622). An engineering owner settles that by approving, for this head, the fine-pointer listener, activation through the action's own click(), the tracker painting the press, the always-on label selection lock and the 200 ms release fade, by commenting /approve-spec <full head SHA> (the author may do so as an engineering owner), or by landing that amendment on main. Whatever the decision, these changes are still needed:

  • Land the stacked gesture, motion and harness pull requests separately and rebase, so this one carries only its AlertDialog change instead of about 29,600 authored lines, and drop the unused useIsScrollable.
  • With a mouse, sliding from Delete onto Cancel leaves Delete pressed by :active while Cancel is lit and about to run; let only the tracker paint a tracked action's press, and assert it in the mouse browser case.
  • AlertDialog.doc.mjs still tells builders a mouse keeps each button's click; describe tracking under every pointer.
  • Add real-device iOS Safari and Android Chrome evidence for the changeset's iOS Safari claim, or narrow the claim.

[Automated review]

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

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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 leaves Delete pressed by :active (its box stays at 0.98 scale, 66.12 × 31.36 against 67.47 × 32 at rest) while the tracker paints Cancel data-astryx-press="on", so both actions look pressed and the release runs Cancel. Under a coarse pointer :active is off, which is why the touch path shows one highlight. Letting only the tracker paint a tracked item's press (no :active press arm while data-astryx-press-group-item is set) would keep one highlight for every pointer; the mouse browser case could assert it.

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.',

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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 "On a touch screen … under a finger or a pen" and that "A mouse, the keyboard and assistive technology keep the click of each button", but the head tracks a mouse too (useActionGroupTracking.ts:82, :148), as the changeset says. The dense description at line 199 has the same wording.

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

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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; spec:AST-094 Platform support asks for real-device evidence for those (spec:AST-013 FR10), and the description's evidence is the iOS 26.2 simulator and an Android emulator.

} from './scrollGeometry';
import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';

export function useIsScrollable(

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing imports this hook at this head (AlertDialog does not use it, despite the header), so it and its test add 118 unused lines to the change; it could land with the component that needs it.

ghost left a comment

Copy link
Copy Markdown
Contributor

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 spec decision: tracking AlertDialog's two actions as one group under every pointer departs from spec:AST-094 FR10, FR11, FR19 and FR41 and the buttons family's press-feedback owner as they stand on main, and the family:action-groups record it relies on is still the open spec pull request #8622. An engineering owner can settle that by landing the amendment on main, or by approving this head's behavior (the fine-pointer listener, activation through the action's own click(), the tracker painting the press, the always-on label selection lock and the 200 ms release fade) with /approve-spec <full head SHA>; the author may do so as an engineering owner. Whatever the decision, these changes are still needed:

  • Land the stacked gesture, motion and harness pull requests separately and rebase, so this one carries only its AlertDialog change instead of 29,933 authored lines, and drop the unused useIsScrollable.
  • With a mouse, sliding from Delete onto Cancel leaves Delete pressed by :active while Cancel is lit and about to run; let only the tracker paint a tracked action's press, and assert it in the mouse browser case.
  • AlertDialog.doc.mjs still tells builders that a mouse keeps each button's click; describe tracking under every pointer.
  • Add real-device iOS Safari and Android Chrome evidence for the changeset's iOS Safari claim, or narrow it, and a browser case for the default pen tracking.

[Automated review]

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

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

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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, and a pen's single activation depends on the browser's click being swallowed after the group's click(), but only jsdom drives a pen (useActionGroupTracking.test.tsx:443, :471), where no browser click follows. architecture:interaction-modality asks pointer-dependent behavior to verify "mouse, touch, and pen as applicable" through browser paths (INV4); a pen slide and a pen tap in AlertDialog.interaction.browser.spec.ts, one activation each, would cover it.

ghost left a comment

Copy link
Copy Markdown
Contributor

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 spec decision: tracking AlertDialog's two actions as one group under every pointer departs from spec:AST-094 FR10, FR11, FR19 and FR41 and the buttons family's press-feedback owner as they stand on main, and the family:action-groups record it relies on is still the open spec pull request #8622. An engineering owner can settle that by landing that amendment on main, or by approving this behavior (the fine-pointer listener, activation through the action's own click(), the tracker painting the press, the always-on label selection lock and the 200 ms release fade) with /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Whatever the decision, these changes are still needed:

  • Land the stacked gesture, motion, harness and build pull requests separately and rebase, so this one carries only its AlertDialog change instead of 30,117 authored lines, and drop the unused useIsScrollable.
  • With a mouse, sliding from Delete onto Cancel leaves Delete pressed by :active while Cancel is lit and about to run; let only the tracker paint a tracked action's press, and assert it in the mouse browser case.
  • AlertDialog.doc.mjs still tells builders that a mouse keeps each button's click; describe tracking under every pointer.
  • Add real-device iOS Safari and Android Chrome evidence for the changeset's iOS Safari claim, or narrow it, and a browser case for the default pen tracking.

[Automated review]

ghost left a comment

Copy link
Copy Markdown
Contributor

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 spec decision: tracking AlertDialog's two actions as one group under every pointer departs from spec:AST-094 FR10, FR11, FR19 and FR41 and the buttons family's press-feedback owner as they stand on main, and the family:action-groups record it relies on is still the open spec pull request #8622. An engineering owner can settle that by landing that record on main, or by approving this behavior (the fine-pointer listener, activation through the action's own click(), the tracker painting the press, the always-on label selection lock and the 200 ms release fade) with /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Whatever the decision, these changes are still needed:

  • Land the stacked gesture, motion, harness and build pull requests separately, and #8671 before this one since main's bundle test fails on the root entry without it, then rebase so this pull request carries only its AlertDialog change instead of 30,268 authored lines, without the unused useIsScrollable.
  • With a mouse, sliding from Delete onto Cancel leaves Delete pressed by :active while Cancel is lit and about to run; let only the tracker paint a tracked action's press, and assert it in the mouse browser case.
  • AlertDialog.doc.mjs still tells builders that a mouse keeps each button's click; describe the behavior that is approved.
  • Add real-device iOS Safari and Android Chrome evidence for the changeset's iOS Safari claim, or narrow it, and a real-browser pen case.

[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\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.',

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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 "On a touch screen … under a finger or a pen" and that "A mouse, the keyboard and assistive technology keep the click of each button", but at this revision a mouse is tracked too: AlertDialog passes no pointerTypes, so useActionGroupTracking tracks ['touch', 'pen', 'mouse'] (useActionGroupTracking.ts:85), as the changeset and the component header say. Please describe the behavior an owner approves here and in the dense description (:199).

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

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With a mouse under a fine pointer, a press on Delete slid onto Cancel keeps Delete's :active press (Button.tsx:128-131, scale 0.98), because the group's fine-pointer pointerdown listener is passive (GestureHost.ts:916), while the tracker paints Cancel data-astryx-press="on" (Button.tsx:132). At the release both actions look pressed and Cancel runs. If mouse tracking stays, keep :active off a tracked group item (for example :active:where(:not([data-astryx-press-group-item]))) so the tracker is the one painter.

} from './scrollGeometry';
import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';

export function useIsScrollable(

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing imports this hook at this revision; only its own test does, although the header names AlertDialog as a consumer (:10-12). 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.

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

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 iOS Safari and Android Chrome claim (spec:AST-013 FR10); the description has simulator and emulator recordings and calls a real-device check the release gate. Please attach real-device recordings or narrow the wording to what the simulator shows.

test.describe('with a mouse', () => {
test.use({hasTouch: false, isMobile: false});

test('a press on Delete slid to Cancel runs Cancel once, and a click on Delete runs Delete once (family:action-groups FR5)', async ({

ghost Oct 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This mouse case asserts only which click ran, so the double press during the slide (Delete still :active while Cancel is lit) goes unobserved; reading both buttons' transform or press state over Cancel would cover it. Pens are tracked by default too but have no real-browser case here, only jsdom (architecture:interaction-modality asks for mouse, touch and pen where behavior depends on the pointer).

ghost left a comment

Copy link
Copy Markdown
Contributor

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]

ghost left a comment

Copy link
Copy Markdown
Contributor

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',
},

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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 at 8462850. The row's user-select is gone. The gesture host's per-press lock covers a finger or a pen, and a mouse keeps its selection; a browser case checks both. The row keeps touch-action: pinch-zoom, which the host keeps, because Chrome on Android cancels a pointer it pans. That atomic class already ships for six other components, so it adds no stylesheet bytes.

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

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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 at 8462850. A Button inside a tracked group takes no :active paint, so only the group paints the press. A mouse press slid from Delete onto Cancel now lights only Cancel. A browser case fails without the change and passes with it.

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

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

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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 at 8462850. Where several actions hold the point (no layout, as in jsdom), the press resolves to the action it landed on. userEvent.click on the action runs onAction again, and a unit case pins it.

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.',

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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 at 8462850. The docs now say every pointer is tracked, and only the keyboard and assistive technology keep each button's click.

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

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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 warning is not on main any more. A unit case opens the alert under a fine pointer and checks for no console warning.

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)

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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.

Removed at 8462850, with its test.

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

ghost Oct 29, 2026

Copy link
Copy Markdown
Contributor

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.

ghost Nov 3, 2026

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.

Removed at 8462850. The changeset makes no iOS Safari claim now.

@cixzhang

ghost commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 8462850

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AlertDialog's slide between the actions now runs the action the pointer lifts on for a finger, a pen and a mouse, and the spec side is approved for this head; three fixes remain before approval:

  • After a slide onto the action, focus stays on Cancel instead of moving to the lifted action as a click on it would (family:action-groups FR1), and under touch a plain tap no longer focuses the tapped action either.
  • useActionGroupTracking's resolveTarget re-implements actionGroupTracker's hit test to add the landed-on tie-break; move that rule into the tracker so it stays the one copy of the tracking (interaction-modality INV13).
  • The AlertDialog record now says FR8 ships, but its FR8 verification row still says no test exists and FR8 is not shipped, and verified_by and Compatibility were not updated.

Two comments in the new hook restate names and could go; details are in the inline notes.

[Automated review]

pointerRef.current === 'touch'
? optionsRef.current.highlightExpansion
: 0,
onActivate: optionsRef.current.onActivate,

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The lift runs the button's own click(), which moves no focus, and the browser's click is swallowed. In the Async story (Chromium), a mouse click on Revoke focuses Revoke, but a press on Cancel slid onto Revoke runs Revoke and leaves focus on Cancel; under touch even a tap now leaves focus on Cancel (the base focused Revoke). family:action-groups FR1: "After a slide, focus does what a click on the lifted action does." Giving the lifted action that focus at the lift (for example item.focus({preventScroll: true}) before its click when the press moved focus to the action it began on, or a finger lifted) plus a slide case and a tap case that check document.activeElement would close this.

ghost Nov 3, 2026

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 at 6aa706c. After a slide, focus does what a click on the lifted action does. A mouse or a pen moves it there when the press focused the action it began on. A finger does unless the engine is iOS WebKit. A lift outside the actions moves no focus. Unit cases cover the slide, the tap and the lift outside; two of them fail without the change.

Comment on lines +105 to +121
resolveTarget: (x, y, targets) => {
const margin =
pointerRef.current === 'touch'
? optionsRef.current.highlightExpansion
: 0;
const holders = targets.filter(
target =>
x >= target.left - margin &&
x <= target.right + margin &&
y >= target.top - margin &&
y <= target.bottom + margin,
);
const landed = landedRef.current;
const pressed = holders.find(
target => landed != null && target.element.contains(landed),
);
return (pressed ?? holders[0])?.element ?? null;

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This copies actionGroupTracker's default hit test (hooks/actionGroupTracker.ts:239-255, the rect grown by the margin) to add one rule: the action the press landed on wins where several hold the point. interaction-modality INV13 keeps the tracking in that one module, so a later fix to its hit test would miss AlertDialog. Moving the tie-break into the tracker's default (begin can take the press's target) would let this option go, with the pointer-dependent margin kept in highlightExpansion only.

ghost Nov 3, 2026

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 at 6aa706c. The tie-break now lives in actionGroupTracker: begin takes the node the press landed on, and the default hit test prefers the action holding it where several hold the point. The hook passes that node and sets no resolveTarget. A tracker case fails without the rule.

| End-user impact | A keyboard or screen-reader user is told a decision is needed (the title and description are announced), lands on Cancel, and cannot lose the dialog by clicking past it. On a touch screen the alert scales in and fades out as iOS's does and ignores taps while it enters (FR6, FR7), and on a phone it is 320 px wide (FR9); under every pointer, mouse included, a press that slides from Cancel to the action runs the action it lifts on (FR8). |
| Builder impact | None. |
| Compatibility/readiness | FR1–FR5 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6–FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. |
| Compatibility/readiness | FR1–FR5, FR8 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6, FR7 and FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. |

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With FR8 now listed as shipped, the FR8 verification row (line 228) still ends "No test exists yet; FR8 is not shipped", verified_by names none of the new suites, and Compatibility (lines 68-71) still says the released default holds until FR6–FR9 ship. Naming AlertDialog.test.tsx ("AlertDialog action tracking"), useActionGroupTracking.test.tsx and AlertDialog.interaction.browser.spec.ts there would keep the record consistent.

ghost Nov 3, 2026

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 at 6aa706c. FR8's verification row names AlertDialog.test.tsx ("AlertDialog action tracking"), useActionGroupTracking.test.tsx and AlertDialog.interaction.browser.spec.ts. verified_by lists the two new suites, and Compatibility says FR8 changes action activation under a slide.

/** The ref handed to the recognizer while tracking is off: no element. */
const DETACHED: RefObject<Element | null> = {current: null};

/** A group tracks every pointer. */

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): this restates EVERY_POINTER, as line 92's comment restates landedRef; both can go.

ghost Nov 3, 2026

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.

Both comments are gone at 6aa706c.

Comment on lines +193 to +195
// A held pointer of any kind is tracked across the two actions, and the lift
// runs the action under it. Each tracked action carries the press-group
// marker from its first render, so the tracker is its one painter.

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): the first sentence narrates the hook call. Suggested: "Each action carries the press-group marker from its first render, so the tracker is its only painter."

ghost Nov 3, 2026

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.

Taken at 6aa706c, with your wording.

@cixzhang

ghost commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 6aa706c

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes: a press that slides between AlertDialog's actions now runs the action it lifts on under a mouse, a pen and a finger, and the spec side is approved for this head, with one fix left:

  • Button.tsx (trackedOverlay, lines 156-173) re-declares the shared interaction overlay's hover and press arms without :active, so a later change to utils/interactionOverlay.stylex.ts would miss AlertDialog's two tracked actions; keep that arm set in the shared overlay module, or say in the description why it cannot live there and list Button in that file's SYNC header.

Two optional tidy-ups: the AlertDialog record's PR1 still says the tracking arrives "once FR8 ships", and the two useImperativeAlertDialog cases could return to user.click now that the tracker resolves a press to the action it landed on.

[Automated review]

Comment thread packages/core/src/Button/Button.tsx Outdated
Comment on lines +156 to +173
// A tracked action's overlay: the shared one without its `:active` arm, so
// the press attribute is its only press paint.
trackedOverlay: {
backgroundImage: {
default: null,
[ENABLED]: {
default: null,
[HOVER_HOVER]: {
default: null,
':hover:where(:not(:disabled,[aria-disabled="true"]))': HOVER_IMAGE,
[PRESSED_ON]: PRESS_IMAGE,
[PRESSED_FADING]: PRESS_IMAGE,
},
[PRESSED_ON]: PRESS_IMAGE,
[PRESSED_FADING]: PRESS_IMAGE,
},
},
},

ghost Nov 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This re-declares interactionOverlayStyles.backgroundImage (utils/interactionOverlay.stylex.ts:245-266) minus its :active arm: the enabled guard, the hover media query, the hover gradient and both press arms (lines 85-90 copy the module's constants). That module is the one place a change to the press is made, and architecture:react-component-runtime INV16 asks that a shared concern be extended in its owner. A later change to the overlay's hover or arm selectors would reach every Button but these two tracked actions. Keeping the tracked arm set in the overlay module (as the previous revision's backgroundImageTracked did) would fix it; if the module must not grow its public export, please say so in the description and add Button to its SYNC header.

ghost Nov 3, 2026 •

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 at 3576bcb. Button no longer copies the arms: a tracked Button keeps the shared interactionOverlayStyles.backgroundImage and only sizes its own :active image to zero unless the tracker lights it. The overlay module is a public entry (./utils/interactionOverlay.stylex in the package exports), so it gains no export. Its SYNC header names Button.

…e it lifts on

AlertDialog's two actions are an action group under every pointer, the way
UIKit tracks an alert's actions (component:AlertDialog FR8,
family:action-groups). A new internal hook, useActionGroupTracking, drives
the shared action-group tracker with a long press: the action under the
pointer highlights at once, the highlight follows the pointer between the
actions, and the lift runs the action under it through its own click. A lift
outside both, a cancelled pointer, a second finger or the window losing focus
runs nothing. After a slide, focus does what a click on the lifted action
does (family:action-groups FR1). Where several actions hold the point (no
layout, as in a test renderer), the shared tracker resolves the press to the
action it landed on, so a user-event click on an action runs that action.

One painter per press. The press controller leaves a tracked action's
pointer press to the group's tracker, through the private press-group marker
(privateAttributes/pressGroupItem.stylex.ts); a key's press there still
paints. A Button inside a tracked group (an internal context) takes no
:active paint, so a mouse press slid from one action to the other lights one
action at a time (family:action-groups FR2).

The gesture host writes the actions' touch-action (pinch-zoom only) and
refuses a text selection only while a finger or a pen holds them
(spec:AST-094 FR41). AlertDialog, and the public imports that reach it
(Tokenizer, PowerSearch, Table), are declared gesture users in the bundle
test. AlertDialog's record now says FR8 ships.

Supersedes meta#8637 (the press controller's skip for group-tracked actions), whose
change is folded in here, with the focus-after-lift from meta#8657.
@cixzhang

ghost commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 3576bcb

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommending approval. AlertDialog's Cancel and action now act as one group for a finger, a pen and a mouse: a press that slides between them runs the action it lifts on, and a lift off both runs nothing, as the AlertDialog record's FR8 and the action-groups family require. Button now keeps the shared interaction overlay and only hides its own :active image inside a tracked group, which resolves the earlier duplication, and a Space-key press on the action still looks the same. Base and head checks in Chromium, the unit suites, typecheck and lint back this up.

[Automated review]

This branch was successfully deployed

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

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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