Skip to content

feat(List): a row tapped to push stays selected until the pop deselects it - #8082

Closed
vjeux wants to merge 891 commits into
meta:mainfrom
vjeux:uikit/c11-selection-hold
Closed

vjeux wants to merge 891 commits into
meta:mainfrom
vjeux:uikit/c11-selection-hold

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: vjeux/astryx@3ad772a...7e0020c (depends on #8079; the base adds #8077's release fix, which this head carries).

User need

On iOS, a row tapped to open a detail stays highlighted while covered and fades as the detail slides away, so coming back you see where you were. In Astryx it faded at the lift.

Problem and solution fit

What led to it: this is part of a program to make Astryx's list components behave like iOS tables on phones. Lab's NavigationStack (#8114) pushes a detail over a list, and nothing held the row.

A navigation can now hold the tapped row selected and drive its fade with its own transition: the highlight stays while the detail is open, follows a back swipe, returns on a cancel, and ends with focus on the row. Touch screens only (spec:AST-094 FR13); Reduce Motion fades within 200 ms.

Size (gzip JS/CSS, base → head): Item +230/+51 B, ListItem +85/+122 B, TreeList +270/+51 B.

Current specification

Pending the owner's decision (component:List), following UIKit on iOS 26.2.

Public delta and caller burden

None: holdSelection is internal, and a test pins the public exports. Interactive rows carry data-astryx-pressable="cell" (was "").

Design and alternatives

Known gap (spec:AST-107): the hold's paint stays on internal press feedback.

Evidence

iOS 26.2 simulator, Mobile Safari, touch input; a real device is the release gate.

Path Clip
Push, hold, pop Push
Reduce Motion Reduce
600 ms block into the pop: still fading Busy

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. On an iPhone (or the iOS simulator), open Storybook's List "Selection hold" story and tap "Sent". A "Sent (pushed)" panel stands in for the pushed screen, and the "Sent" row stays highlighted.
  2. Tap "Back": the highlight fades over about 0.35 s, and focus lands on the row with no keyboard ring.
  3. Tap "Sent", drag "Pop progress" halfway (half faded), then tap "Cancel pop": the highlight comes back.
  4. With Reduce Motion, steps 1–2 fade within 0.2 s.
  5. With a mouse, no highlight is held.

What was run: these steps on the iOS 26.2 simulator (the clips above), and unit and browser tests of each in Chromium and WebKit, busy main thread included. All pass; the new ones failed first. CI's pr-rtl "Verify D7 native target regression" fails only because this head predates main's new RTL touch-target test (0472c4e); it clears when the branch next takes main.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 27, 2026 10:13pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

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

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision first: the draft spec:AST-094 and the new holdSelection selection hold for List, Item and TreeList rows have no approved spec yet, so an engineering owner (the author included, if they are one) needs to approve or amend them by commenting /approve-spec followed by the full head commit SHA on this pull request. These changes are needed whatever that decision is:

  • Retarget or rebase this pull request so its diff is its own commit; against main it carries 13,883 authored lines from the whole stack.
  • Let the dividers beside a held row return with the pop's deselect instead of reappearing in one frame at end(true).
  • Make focus() on a TreeList row focus its treeitem, not the row's tabindex="-1" button.
  • Fix the spec:AST-094 amendment of spec:AST-090, which has no record on main.

[Automated review]

Comment thread packages/core/src/List/ListItem.tsx Outdated
borderBlockEndColor: {
default: colorVars['--color-border'],
'[data-astryx-press="on"]': 'transparent',
'[data-astryx-press="held"]': 'transparent',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While a row is held this keeps both dividers transparent until end() removes the attribute, so they never take part in the deselect; uikit/list.ts:52-61 measures UIKit fading the separators with the background over the same 350 ms. Could the dividers follow the hold's progress, or come back when the pop starts?

}
return (
row.querySelector<HTMLElement>(
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inside a TreeList this matches the row's tabIndex={-1} label button (or the chevron toggle), not the role="treeitem" that holds the roving Tab stop, so focus() leaves focus outside the tree's focus model. Focusing the closest treeitem would match the TreeList contract.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is no AST-090 record on main or in this branch, so this amendment has no target an owner can rule on.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Item (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 284B 284B 0B
Lines of Code 1372 1377 -
Complexity Very High Very High (122) -
TreeList (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 648B 648B 0B
Lines of Code 872 878 -
Complexity Very High Very High (100) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.4KB +515B 12.4KB 12.5KB +24B
core/AppShell 57.9KB 58.3KB +424B 11.6KB 11.7KB +22B
core/AspectRatio 3.7KB 3.8KB +66B 3.5KB 3.5KB +40B
core/Avatar 37.5KB 37.5KB +54B 6.3KB 6.3KB +23B
core/AvatarGroup 39.8KB 39.8KB +50B 6.4KB 6.5KB +22B
core/AvatarGroupOverflow 39.9KB 40.4KB +522B 6.4KB 6.4KB +23B
core/AvatarStatusDot 7.1KB 7.1KB +61B 3.4KB 3.4KB +41B
core/Badge 3.9KB 4.0KB +64B 3.8KB 3.9KB +39B
core/Banner 47.8KB 48.3KB +494B 9.9KB 10.0KB +22B
core/BaseTypeahead 54.3KB 54.8KB +448B 9.9KB 9.9KB +22B
core/Blockquote 3.5KB 3.6KB +66B 3.4KB 3.4KB +39B
core/BottomSheet 34.0KB 34.0KB +69B 7.9KB 7.9KB +24B
core/BottomSheetSwitcher 14.8KB 14.8KB +67B 3.3KB 3.3KB +41B
core/BreadcrumbItem 66.4KB 67.3KB +960B 11.0KB 11.4KB +412B
core/Breadcrumbs 22.4KB 22.5KB +71B 3.3KB 3.3KB +40B
core/Button 43.3KB 43.8KB +504B 9.2KB 9.2KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 54.9KB 55.4KB +509B 10.0KB 10.0KB +22B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.2KB 50.7KB +495B 10.2KB 10.2KB +21B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 60.6KB 61.1KB +502B 10.8KB 10.8KB +19B
core/ChatComposerDrawer 23.9KB 23.9KB +66B 4.5KB 4.6KB +39B
core/ChatComposerInput 55.7KB 56.2KB +494B 10.1KB 10.1KB +21B
core/ChatComposerTokenElement 55.7KB 56.2KB +493B 10.1KB 10.1KB +21B
core/ChatDictationButton 46.5KB 47.0KB +490B 9.5KB 9.5KB +20B
core/ChatLayout 48.8KB 49.3KB +478B 9.9KB 10.0KB +20B
core/ChatLayoutScrollButton 46.4KB 46.8KB +471B 9.6KB 9.6KB +20B
core/ChatMessage 23.1KB 23.1KB +59B 3.5KB 3.6KB +39B
core/ChatMessageBubble 7.0KB 7.1KB +67B 3.6KB 3.6KB +42B
core/ChatMessageList 28.9KB 29.0KB +71B 7.8KB 7.8KB +21B
core/ChatMessageMetadata 25.8KB 25.9KB +60B 3.9KB 3.9KB +23B
core/ChatSendButton 45.8KB 46.3KB +483B 9.2KB 9.2KB +21B
core/ChatSystemMessage 6.7KB 6.8KB +71B 3.7KB 3.7KB +39B
core/ChatTokenizedText 6.9KB 7.0KB +63B 3.9KB 3.9KB +40B
core/ChatToolCalls 31.9KB 31.9KB +65B 8.9KB 8.9KB +22B
core/CheckIndicator 9.0KB 9.1KB +68B 3.6KB 3.6KB +39B
core/CheckboxIndicator 4.1KB 4.1KB +65B 3.8KB 3.8KB +22B
core/CheckboxInput 49.9KB 50.4KB +476B 9.3KB 9.3KB +22B
core/CheckboxList 46.0KB 46.1KB +63B 8.1KB 8.2KB +22B
core/CheckboxListItem 54.0KB 54.9KB +933B 10.5KB 11.0KB +541B
core/Citation 25.9KB 26.0KB +69B 4.1KB 4.2KB +41B
core/ClickableCard 12.8KB 13.3KB +548B 6.9KB 7.0KB +46B
core/Code 3.5KB 3.6KB +63B 3.4KB 3.4KB +41B
core/CodeBlock 60.5KB 61.0KB +483B 10.7KB 10.7KB +27B
core/Collapsible 11.7KB 12.2KB +516B 4.2KB 4.3KB +39B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 50.9KB 51.4KB +487B 11.8KB 11.8KB +23B
core/CommandPaletteEmpty 6.1KB 6.2KB +64B 3.3KB 3.4KB +42B
core/CommandPaletteFooter 23.3KB 23.3KB +68B 3.5KB 3.6KB +39B
core/CommandPaletteGroup 3.5KB 3.5KB +64B 3.3KB 3.4KB +39B
core/CommandPaletteInput 32.3KB 32.4KB +60B 8.1KB 8.1KB +22B
core/CommandPaletteItem 8.4KB 8.9KB +497B 3.5KB 3.5KB +40B
core/CommandPaletteList 22.4KB 22.5KB +65B 3.3KB 3.3KB +41B
core/ComplexSelector 55.8KB 56.3KB +522B 10.6KB 10.7KB +20B
core/ContextMenu 77.7KB 78.7KB +968B 14.6KB 15.2KB +550B
core/ContextMenuItem 16.6KB 17.4KB +889B 5.2KB 5.6KB +420B
core/DateInput 99.9KB 100.3KB +410B 14.8KB 14.8KB +20B
core/DateRangeInput 69.4KB 69.8KB +483B 11.2KB 11.2KB +21B
core/DateTimeInput 110.1KB 110.5KB +415B 15.2KB 15.2KB +20B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 48.8KB 49.3KB +503B 9.8KB 9.8KB +20B
core/Divider 6.5KB 6.5KB +64B 3.6KB 3.6KB +43B
core/Drawer 31.6KB 31.7KB +85B 8.0KB 8.1KB +23B
core/DrawerHeader 48.9KB 49.4KB +474B 11.2KB 11.2KB +22B
core/DropdownMenu 72.7KB 73.6KB +930B 14.7KB 15.3KB +545B
core/DropdownMenuCheckboxItem 16.1KB 17.1KB +968B 5.6KB 6.0KB +404B
core/DropdownMenuDivider 6.6KB 6.6KB +61B 3.6KB 3.6KB +42B
core/DropdownMenuGroup 6.1KB 6.1KB +69B 3.3KB 3.3KB +39B
core/DropdownMenuItem 15.4KB 16.4KB +974B 5.2KB 5.6KB +421B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.2KB 17.1KB +965B 5.6KB 6.0KB +404B
core/DropdownMenuSubMenu 58.1KB 59.0KB +934B 9.5KB 9.9KB +408B
core/EmptyState 7.7KB 7.8KB +64B 3.5KB 3.6KB +42B
core/Field 44.2KB 44.3KB +71B 8.0KB 8.0KB +22B
core/FieldLabel 39.8KB 39.8KB +58B 6.5KB 6.5KB +23B
core/FieldStatus 10.4KB 10.5KB +74B 4.2KB 4.3KB +23B
core/FileInput 54.1KB 54.5KB +453B 10.3KB 10.4KB +23B
core/FormLayout 6.3KB 6.3KB +66B 3.4KB 3.4KB +40B
core/Grid 4.3KB 4.4KB +67B 3.7KB 3.7KB +40B
core/GridSpan 3.4KB 3.5KB +63B 3.2KB 3.3KB +40B
core/HStack 7.3KB 7.3KB +62B 4.9KB 4.9KB +44B
core/Heading 9.1KB 9.2KB +64B 6.8KB 6.8KB +23B
core/Icon 8.8KB 8.9KB +60B 3.6KB 3.6KB +39B
core/IconButton 43.3KB 43.8KB +491B 9.2KB 9.2KB +21B
core/InputGroup 44.8KB 44.9KB +61B 8.0KB 8.1KB +21B
core/InputGroupText 6.3KB 6.4KB +66B 3.7KB 3.8KB +40B
core/Item 11.4KB 12.4KB +1.1KB 4.7KB 5.1KB +423B
core/Kbd 6.7KB 6.7KB +67B 3.5KB 3.5KB +40B
core/LayerProvider 63.3KB 63.8KB +527B 10.1KB 10.1KB +23B
core/Layout 8.1KB 8.2KB +63B 5.5KB 5.5KB +43B
core/LayoutContent 7.4KB 7.5KB +64B 4.9KB 5.0KB +43B
core/LayoutFooter 7.3KB 7.4KB +68B 4.8KB 4.9KB +42B
core/LayoutHeader 7.4KB 7.4KB +65B 4.8KB 4.9KB +42B
core/LayoutPanel 7.6KB 7.6KB +65B 4.8KB 4.9KB +43B
core/Lightbox 51.2KB 51.6KB +456B 9.8KB 9.8KB +21B
core/Link 43.5KB 44.0KB +500B 7.6KB 7.6KB +25B
core/List 6.7KB 6.8KB +59B 3.4KB 3.5KB +40B
core/ListItem 53.5KB 54.4KB +907B 10.5KB 11.0KB +541B
core/Markdown 144.5KB 145.4KB +921B 17.4KB 17.9KB +545B
core/MetadataList 23.1KB 23.2KB +67B 3.6KB 3.6KB +42B
core/MetadataListItem 6.5KB 6.5KB +64B 3.4KB 3.4KB +43B
core/MobileNav 51.0KB 51.4KB +443B 10.1KB 10.1KB +22B
core/MobileNavToggle 46.2KB 46.7KB +489B 9.4KB 9.4KB +20B
core/MoreMenu 72.9KB 73.8KB +908B 14.7KB 15.3KB +545B
core/MultiSelector 76.2KB 76.6KB +452B 14.6KB 14.7KB +24B
core/NavHeadingMenu 8.9KB 9.0KB +65B 3.2KB 3.3KB +42B
core/NavIcon 3.4KB 3.5KB +63B 3.3KB 3.3KB +42B
core/NumberInput 55.7KB 56.2KB +486B 10.7KB 10.7KB +31B
core/Outline 28.6KB 29.1KB +482B 4.0KB 4.1KB +40B
core/OverflowList 8.5KB 8.6KB +67B 3.5KB 3.5KB +40B
core/Overlay 19.5KB 19.6KB +43B 3.8KB 3.9KB +17B
core/Pagination 87.7KB 88.6KB +900B 15.3KB 15.7KB +395B
core/Popover 49.8KB 50.3KB +419B 12.1KB 12.1KB +20B
core/PowerSearch 155.1KB 155.9KB +909B 18.6KB 19.0KB +406B
core/ProgressBar 7.7KB 7.8KB +66B 6.6KB 6.6KB +20B
core/RadioIndicator 3.8KB 3.9KB +61B 3.7KB 3.7KB +20B
core/RadioList 45.6KB 45.6KB +61B 8.0KB 8.0KB +22B
core/RadioListItem 52.1KB 53.0KB +894B 9.7KB 10.1KB +401B
core/ResizeHandle 25.1KB 25.2KB +73B 3.9KB 3.9KB +41B
core/Section 7.5KB 7.6KB +67B 6.2KB 6.2KB +42B
core/SegmentedControl 38.8KB 38.9KB +73B 6.3KB 6.4KB +22B
core/SegmentedControlItem 8.6KB 9.1KB +512B 3.9KB 3.9KB +41B
core/SelectableCard 12.8KB 13.3KB +530B 7.1KB 7.2KB +46B
core/Selector 79.1KB 80.0KB +944B 15.0KB 15.4KB +403B
core/SelectorOption 14.5KB 15.6KB +1.1KB 5.1KB 5.5KB +421B
core/SideNav 58.0KB 58.4KB +447B 10.6KB 10.6KB +21B
core/SideNavCollapseButton 46.6KB 47.1KB +473B 9.5KB 9.5KB +20B
core/SideNavHeading 54.7KB 55.2KB +462B 10.1KB 10.2KB +22B
core/SideNavItem 54.5KB 55.0KB +474B 10.1KB 10.1KB +22B
core/SideNavSection 6.5KB 6.6KB +59B 3.6KB 3.6KB +38B
core/Skeleton 3.8KB 3.8KB +60B 3.6KB 3.6KB +22B
core/Slider 48.5KB 48.5KB +58B 8.5KB 8.5KB +21B
core/Spinner 26.3KB 26.4KB +58B 7.5KB 7.5KB +19B
core/Stack 7.1KB 7.1KB +62B 4.9KB 4.9KB +44B
core/StackItem 6.0KB 6.1KB +63B 3.3KB 3.3KB +38B
core/StatusDot 19.0KB 19.0KB +59B 6.2KB 6.2KB +19B
core/Step 32.4KB 32.9KB +500B 5.3KB 5.3KB +22B
core/Stepper 47.4KB 47.9KB +469B 9.6KB 9.6KB +20B
core/Switch 48.4KB 48.9KB +492B 9.3KB 9.4KB +21B
core/Tab 9.7KB 10.2KB +519B 4.1KB 4.1KB +41B
core/TabList 40.2KB 40.3KB +63B 7.2KB 7.2KB +23B
core/TabMenu 51.6KB 52.1KB +458B 10.0KB 10.0KB +21B
core/Table 84.0KB 84.9KB +952B 15.1KB 15.7KB +537B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 78.8KB 79.7KB +981B 14.8KB 15.4KB +534B
core/TableFooter 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableHeader 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableHeaderCell 78.6KB 79.5KB +961B 14.8KB 15.3KB +547B
core/TableRow 6.2KB 6.3KB +66B 3.5KB 3.6KB +40B
core/TableSelectionToolbar 52.4KB 52.8KB +482B 11.2KB 11.2KB +21B
core/Text 8.6KB 8.6KB +66B 6.8KB 6.8KB +23B
core/TextArea 49.4KB 49.5KB +68B 9.4KB 9.4KB +21B
core/TextInput 53.5KB 54.0KB +490B 10.5KB 10.5KB +22B
core/Thumbnail 50.7KB 51.2KB +512B 11.0KB 11.0KB +25B
core/TimeInput 86.8KB 87.3KB +464B 13.8KB 13.8KB +21B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.5KB 27.6KB +76B 9.5KB 9.6KB +21B
core/Toast 59.8KB 60.3KB +480B 9.7KB 9.7KB +22B
core/ToggleButton 44.1KB 44.6KB +464B 9.3KB 9.3KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 29.6KB 30.1KB +519B 4.5KB 4.5KB +42B
core/Tokenizer 64.8KB 65.3KB +521B 11.7KB 11.7KB +21B
core/Toolbar 39.8KB 39.8KB +63B 7.7KB 7.8KB +46B
core/TopNav 51.9KB 52.3KB +445B 10.4KB 10.4KB +23B
core/TopNavHeading 54.2KB 54.6KB +446B 9.9KB 10.0KB +20B
core/TopNavItem 9.0KB 9.5KB +515B 3.8KB 3.9KB +39B
core/TopNavMegaMenu 54.2KB 54.7KB +484B 10.8KB 10.9KB +21B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.8KB +66B 3.4KB 3.4KB +38B
core/TopNavMegaMenuItem 9.1KB 9.6KB +519B 4.1KB 4.1KB +38B
core/TopNavMenu 53.6KB 54.1KB +483B 10.1KB 10.1KB +23B
core/TreeList 32.5KB 33.6KB +1.0KB 4.7KB 5.1KB +423B
core/Typeahead 61.5KB 61.9KB +473B 11.3KB 11.3KB +21B
core/TypeaheadItem 6.2KB 6.3KB +60B 3.4KB 3.5KB +39B
core/VStack 7.3KB 7.3KB +61B 4.9KB 4.9KB +44B

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

Bundle Size Summary

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

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

Accessibility Audit

Status: 2 accessibility violation(s) found — 2 serious.

Radio group pattern - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 3/11 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
RadioList - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 3/14 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

55 of 502 shot(s) changed.

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

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

and 35 more.


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

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's decision: the draft spec:AST-094 and the new holdSelection selection hold for List, Item and TreeList rows have no approved spec yet, so an engineering owner needs to approve or amend AST-094 (FR1–FR23, DEC-1, DEC-2) and provide or identify the record that governs the held row and its @astryxdesign/core/utils export, recording the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner). Independent of that decision, these need changes:

  • Retarget the pull request onto its predecessor's branch, or rebase it once the stack lands, so the diff is this commit's ~1,600 lines rather than the stack's ~14,800.
  • In a TreeList, SelectionHold.focus() lands on the row's tabindex="-1" button instead of the treeitem that holds the tree's roving Tab stop (selectionHold.ts:163-172).
  • In a divided List, the dividers beside a held row stay hidden through the whole pop, although motion/uikit/list.ts measures UIKit fading them with the row.
  • AST-094 FR7 amends an AST-090 record that does not exist.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
@vjeux
vjeux force-pushed the uikit/c11-selection-hold branch from e7922b8 to bf924aa Compare October 24, 2026 15:57
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Cindy's decision: our own components use the public gesture and motion APIs wherever they cover the job. That means createPanGesture/usePanGesture, createDragToSnapPoints/useDragToSnapPoints, createAnimator/useAnimator, createReorderable and useAnimatedLayout, rather than the internals underneath. Internals stay only for jobs the public layer doesn't expose, such as press feedback, the action-group tracker and the layer holds. Each of those uses is listed as a known gap.

The public factories and hooks are being specified now (spec:AST-107). Once they land, this PR moves its pan, snap-point and animator code onto them. Nothing needs to change before then.

An attach reads the element's own will-change in a task of its own. A
shared animator (animatorFor) is never detached, so its node can leave the
document with that read still waiting, and the read then forces a style
pass on a node with no style: a test run that mounts and unmounts many
pagers piled up such reads until a later wait timed out. The read now
skips a node out of the document; a promotion that needs it reads it then,
as one on a node attached in the same task does. A detach and dispose
already cancel it.

The new test fails on the previous code (one read of a removed node); the
in-task read test now puts its element in the document, where the read
happens.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner to approve, or to provide or identify the governing spec for, the List and TreeList row behavior added here, which no current spec covers: a row held after a tap until the pop deselects it, the dividers hidden beside a pressed or held row, and the press layer replacing a caller's own ::before. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so, being an engineering owner) or by amending component:List, component:Item and component:TreeList; the 350 ms deselect played as an internal constant also needs spec:AST-094 FR4 updated to allow it. Independently, please fix:

  • focus() after a pop lands on a row's first start-content control instead of the row's own control, and in a TreeList it leaves the Tab stop on a different row.
  • The changeset bumps @astryxdesign/cli although no CLI file changes.
  • The diff still carries #8076, #8077 and #8079 (19,965 authored lines), so land or retarget them until it shows only the hold.

[Automated review]

return row;
}
return (
row.querySelector<HTMLElement>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This takes the first focusable in DOM order, and Item renders startContent before its own button or anchor, so a row with a control in startContent gets that control focused after the pop instead of the row’s own control. Could it target Item’s own control, with a startContent case in selectionHold.test.ts?

function focusTarget(row: HTMLElement): HTMLElement {
const item = row.closest<HTMLElement>('[role="treeitem"]');
if (item != null) {
return item;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Focusing the treeitem does not move the roving Tab stop: useTreeFocus’s handleFocus keeps whichever item already has tabindex="0", so Shift+Tab back into the tree lands on that other row (component:TreeList AR1). Could the hold move the stop to this row, with a tabIndex assertion in the tree test?

@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No CLI file changes in this pull request, so this line only adds a List note to the CLI’s changelog. Drop it?

it off owns that end (no consumer callback from an Effect). A handler that
wants UIKit's `.cancelled` calls `recognizer.setEnabled(false)` itself,
which reports it at once.
- `usePressTracker` rebuilds its tracker when `trackingExpansion`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

usePressTracker builds its PressTracker once in useState with the first option values (useGestureRecognizer.ts), so changing these options later has no effect. Either rebuild on change or correct this line.

vjeux added 10 commits October 26, 2026 23:57
…(spec:AST-094 FR18, DEC-13)

A screen-edge pan attached to a stack container, as wide as the screen,
with a touch 4 px from its edge: inside the 40.2 px band the pan decides at
UIKit's 15 px whatever the element's width, and then tracks the travel past
it. From several edges too. Both fail while a wider element decides at
FR19's 10 px.
…'s width (spec:AST-094 FR18, DEC-13)

FR18: inside the 40.2 px band a screen-edge pan decides at UIKit's 15 px
screen-edge hysteresis; outside it the author's scrolling is untouched and
FR19's 10 px governs every other claim. The pan decided at 10 px on any
element wider than the band, so NavigationStack's edge pan, attached to the
stack container, began 5 px early. The edge zone still decides only the
touch-action; a touch outside every band still fails at once. The two
cases that pinned the 10 px decision go.
… 15 px whatever the element's width) into the experimental touch exports
…er's move keeps that finger until it lifts) into the experimental touch exports
…than the band (spec:AST-094 FR18, DEC-22)

FR18 as amended in meta#8631 (DEC-22): the band's pan-x claim and 15 px start
hold only where the screen-edge pan is attached to an element within the
band; on an element that reaches beyond it, whose touch-action cannot
claim per touch, a touch in the band decides at FR19's 10 px like every
other claim. This restores the 10 px decision 3d85c30 removed and keeps
15 px on an element within the band. The cases pin DEC-22 both ways (a wide
element with a touch at x=4 begins at 10, an in-band strip at 15, and from
several edges at 10 on a wide element); the 10 px cases fail on 3d85c30.
…an the edge band decides at 10 px again, DEC-22) into the experimental touch exports
…xperimental touch exports

The motion README's Files table is the union: main's rows, with meta#8663's
resolveSnapPoint.ts and selectPage.ts, plus this branch's, in main's
order.

# Conflicts:
#	packages/core/src/motion/README.md
… rows united) into the row press release change

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner to approve, or to provide or identify the governing spec for, the List and TreeList row behavior added here, which no current spec covers: a row held after a tap until the pop deselects it, the dividers hidden beside a pressed or held row, and the press layer replacing a caller's own ::before. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner) or by amending component:List, component:Item and component:TreeList; the 350 ms deselect played as an internal constant also needs spec:AST-094 FR4 updated to allow it. Independently, please fix:

  • focus() after a pop lands on a row's first start-content control instead of the row's own control, and in a TreeList it leaves the Tab stop on a different row.
  • The changeset bumps @astryxdesign/cli although no CLI file changes.
  • The diff still carries #8076, #8077 and #8079 (20,137 authored lines), so land or retarget them until it shows only the hold.

[Automated review]

Comment on lines +203 to +210
if (row.matches('a[href], button, [tabindex]:not([tabindex="-1"])')) {
return row;
}
return (
row.querySelector<HTMLElement>(
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
) ?? row
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outside a tree this takes the first link, enabled button or tabbable element in DOM order, and Item renders startContent (Item.tsx:752-760) before its own anchor (:775) or button (:789), so a row with a control in startContent gets that control focused after the pop instead of the row's own control. Target the row's own control and add a startContent case to selectionHold.test.ts.

Comment on lines +199 to +202
const item = row.closest<HTMLElement>('[role="treeitem"]');
if (item != null) {
return item;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Focusing the treeitem does not move TreeList's roving Tab stop: handleFocus calls syncTabStops, which keeps whichever item already has tabindex="0" (useTreeFocus.ts:364-372). After a pop to another row, Shift+Tab back into the tree lands on that other row (component:TreeList AR1). Move the stop to this row and assert tabIndex in the tree case of the test.

@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No file under packages/cli changes in this pull request, so this line only adds a List note to the CLI's changelog; dropping it leaves the core entry.

Comment on lines +52 to +54
- `usePressTracker` rebuilds its tracker when `trackingExpansion`,
`highlightDelay`, `cancelTravel` or `quickTapFlash` change, from the next
touch, and reads its callbacks (`isBraking` included) on every touch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

usePressTracker builds its PressTracker once in useState with the first option values (useGestureRecognizer.ts:136-156), so changing these options later has no effect. Rebuild on change or correct this line.

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Offer, following the owner's ruling in #8687's review (no new press treatment, so the dividers stay drawn): one commit on this head that takes the dividers part out — vjeux/astryx@3e73461...1d9c7ec. It removes the divider arms beside a pressed or held row, the press layer's reach over a hidden divider, and the deselecting mark that only those arms read, with their tests, docs and changeset; the selection hold is unchanged. Red first: build-css asserts no touch arm hides a divider. Green: List, Item and selection-hold units, and the hold and press-path browser specs in Chromium and WebKit. Merge it or leave it, as you prefer.

vjeux added 8 commits October 27, 2026 13:14
WebKit starts the row's release at the commit after a 150 ms block, as
Chromium does (first frame 0.958; the bar is 0.7), so the proof no longer
skips WebKit.
…ns in WebKit too) into the selection hold change
…tart instead of skipping it

WebKit may pin a motion created just before a block to the last frame
before it (some runs do, some start at the commit), so the first frame
after a 150 ms block can be up to about 150 ms into the 350 ms deselect;
Chromium starts it at the commit after the block. The proof runs in both
engines: it bounds each engine's first frame, and in both every later frame
moves on toward rest with none skipped, ending at rest.
The animator settles a motion on its own timer at the curve's end, by wall
time, and holding the animations does not hold that timer, so a seek loop
that outlasted the pop ended the hold mid-loop and read the row at rest.
The proof gates the page's timers before the pop and freezes them while it
seeks, as the row press release's curve proof does.

Red first: with more wall time than the pop passing during the hold, every
sample read 0 in both engines without the gate.
…elease

After 150 ms of script at the release's write, the release starts after the
block; the press attribute must stay until the layer has reached rest on
the curve, not go while the layer is still painted at about half strength
(the release's clock started at the write).
…busy first frame outran

pressFeedback.ts removes data-astryx-press="fading" on a timer of the
release's duration started at the write, for the controller's release and
for paintPress's. These fail on that code: with a release animation still
running, or still waiting for its first frame, when the clock runs out,
the release must stay fading until the animation finishes. Guards: a
cancelled animation ends it; no animation ends it on the clock (reduced
motion's instant release, a surface without a press layer); a
descendant's, an endless or a finished animation is not waited for; a new
press keeps its paint when the old release ends later.
…y first frame started it

endAfterRelease replaces the clock that removed data-astryx-press="fading"
at the release's duration after the write, for the controller's release
(apply's fade) and for every other painter's (paintPress). The clock now
only looks: at the release's duration, a release animation still running
on the surface (one of its own CSS animations, on it or its ::before or
::after, finite and unfinished) is waited for until it finishes or is
cancelled, so a busy main thread that starts the animation late ends it
late too, at strength 0. With none (reduced motion's instant release, a
surface without a press layer, an engine without Web Animations) the
release ends on the clock, as before. Nothing at the write reads the
document timeline: WebKit starts an animation created after such a read
at the time read, which would skip a busy first frame into the release.
Any new write, or a new press, cancels the wait; the controller's pending
clock is now a cancel function, so the release's wait and every timer
clear the same way.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner to approve, or to provide or identify the governing spec for, the List and TreeList row behavior that no current spec covers: a row tapped to push a view stays painted until the pop deselects it, its 350 ms deselect is played as a constant although spec:AST-094 FR4 asks for a motion role the List record measures, and the press layer replaces a caller's own ::before, which component:List OQ1 leaves open. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner) or by amending component:List and component:TreeList on main. Whatever the decision, please:

  • Drop the divider hiding beside a pressed or held row, which the owner ruling relayed here rules out (the offered commit does this).
  • Return focus after a pop to the row's own control instead of a startContent control, and make the returned TreeList row the Tab stop.
  • Remove the @astryxdesign/cli line from the hold's changeset and correct the usePressTracker README note.
  • Land or retarget #8076, #8077 and #8079 so the diff (20,443 authored lines) shows only the hold.

[Automated review]

return row;
}
return (
row.querySelector<HTMLElement>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outside a tree this takes the row's first link, enabled button or tabbable element, and Item renders startContent (Item.tsx:752-758) before its own anchor (:775) or button (:789). A probe with startContent={<button>Star</button>} puts focus on Star after the pop. Could the hold remember the control the activation went through and focus that, with a startContent case in selectionHold.test.ts?

function focusTarget(row: HTMLElement): HTMLElement {
const item = row.closest<HTMLElement>('[role="treeitem"]');
if (item != null) {
return item;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Focusing the treeitem does not move TreeList's roving Tab stop: handleFocus only re-syncs and keeps the item that already has tabindex="0" (useTreeFocus.ts:364-372), so Shift+Tab back into the tree lands on another row (component:TreeList AR1). Move the stop to this row and assert tabIndex in the tree test?

@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No file under packages/cli changes here, so this line only adds a List note to the CLI's changelog; dropping it leaves the core entry.

// fading. A selection hold (the row kept selected through a navigation
// push) hides them the same way until its deselect begins, and again
// after a cancel. With a mouse the dividers stay drawn.
borderBlockEndColor: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The owner ruling relayed on this pull request keeps today's pressed overlay with no new press treatment, so the dividers stay drawn; these coarse-pointer arms, the ::before reach below and the hasDividers docs would go with the offered removal commit.

it off owns that end (no consumer callback from an Effect). A handler that
wants UIKit's `.cancelled` calls `recognizer.setEnabled(false)` itself,
which reports it at once.
- `usePressTracker` rebuilds its tracker when `trackingExpansion`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

usePressTracker builds its PressTracker once in useState from the first option values (useGestureRecognizer.ts:136-156), so changing these options later has no effect. Rebuild on change or correct this line.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Thanks for the careful UIKit work on this. Cindy decided not to change anything here. A product that wants a row to stay selected while its page is open sets ListItem's existing isSelected. So holdSelection won't go into Core's List, and this PR shouldn't land as written. If Lab's NavigationStack wants the back-swipe fade, it should build it there on Core's public APIs (Core and Lab use the public layer). Could you drop this PR, or move it and #8114 to the top of the stack, so the List line (#8085, #8106, #8359, #8139 and the reorder PRs) no longer depends on it? If this stays idle for about 30 minutes, we'll restack the line off it, keeping your authorship.

@vjeux

vjeux commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Closing per Cindy's decision on #8082/#8114: a row's selection through navigation stays the product's call through isSelected. Thanks for the review; the branch stays.

This branch was successfully deployed

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

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants