Skip to content

fix(core): a BottomSheet opens and closes on the iOS sheet spring under a finger - #8203

Draft
vjeux wants to merge 2046 commits into
meta:mainfrom
vjeux:uikit/c17-sheet-present
Draft

vjeux wants to merge 2046 commits into
meta:mainfrom
vjeux:uikit/c17-sheet-present

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8128 and #8342. Review only this PR's change: https://github.com/vjeux/astryx/compare/3c55bcb86cfa1934942b035887fa53182f9d7e69..eeef50191a55dc4392ad224e15ac4e68bf297865

Primary intent

On a touch device, a BottomSheet opens and closes on iOS's sheet spring, with the dim following it, and ignores taps while it is still arriving.

User or maintainer impact

On a phone, a sheet opened and closed on the theme's fixed curve, so it looked unlike an iOS sheet and stuttered on a busy page, and unlike iOS it took taps while still arriving. Now, under a finger:

  • opening and closing follow UIKit's measured sheet spring, on the compositor, with the dim on its progress;
  • taps and grabs during the entrance are held (keys and assistive technology still work, and Escape still closes);
  • a sheet reopened while it closes springs back from where it is;
  • under Reduce Motion it cross-fades, as on main.

A mouse keeps today's transitions.

Problem and solution fit

spec:AST-094 FR7 asks for UIKit's presentation under a finger, and FR35 for the entrance hold. Options:

  1. Tune the theme's CSS curve: never matches a spring, still stalls on the main thread.
  2. Play UIKit's spring (CASpringAnimation mass 3, stiffness 1000, damping 500, critically damped) on the animator the release already uses (chosen): the same machinery as fix(core): BottomSheet releases a drag the way an iOS sheet does #8105's settle, so a release and a presentation look alike. It costs a layer-wide input hold while entering, which comes from feat(Layer): one input hold, first used by Dialog, Drawer and MobileNav (spec:AST-094 FR35) #8342.

Authority

spec:AST-094 FR6, FR7, FR35; architecture:interaction-modality INV8; design:reduced-motion; AST-090 FR14; spec:AST-107 FR7 with INV13's gaps (#8751). BottomSheet.spec.md names the hold's shield.

Change and evidence

iOS Safari (simulator), main then this PR: Before After

Through a 700 ms long task: open before, after. Android (emulator): Android

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

How to check, in the Snap points story on a phone: open the sheet (it rises on a spring, the dim with it); tap the content while it rises (nothing happens); close and reopen it mid-way (it turns back from where it is); with Reduce Motion on, it fades.

What ran: the whole core and lab packages pass. In Chromium and WebKit with real touch, the entrance and exit stay within 0.5 px of UIKit's spring and a tap during the entrance is ignored; on a touchscreen laptop a finger still pulls the sheet. touch-support-bundle fails here, not on main; #8671 replaces that check.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 30, 2026 3:15am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

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

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@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

MultiSelector (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 273B 273B 0B
Lines of Code 2642 2642 -
Complexity Very High Very High (344) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 57.3KB +292B 12.4KB 12.4KB 0B
core/AppShell 58.0KB 58.3KB +239B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 40.1KB 40.3KB +304B 6.4KB 6.4KB 0B
core/Banner 48.0KB 48.2KB +290B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.5KB 54.7KB +242B 9.9KB 9.9KB 0B
core/BottomSheet 33.9KB 64.4KB +30.5KB ⚠️ 7.9KB 8.0KB +98B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 66.5KB 66.8KB +297B 11.0KB 11.0KB 0B
core/Button 43.4KB 43.7KB +267B 9.2KB 9.2KB 0B
core/Calendar 55.1KB 55.3KB +283B 10.0KB 10.0KB 0B
core/Carousel 50.4KB 50.7KB +390B 10.2KB 10.2KB 0B
core/ChatComposer 60.8KB 61.0KB +276B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.9KB 56.2KB +266B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.9KB 56.2KB +265B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.6KB 46.9KB +266B 9.5KB 9.5KB 0B
core/ChatLayout 49.0KB 49.2KB +272B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.5KB 46.8KB +255B 9.6KB 9.6KB 0B
core/ChatSendButton 46.0KB 46.2KB +263B 9.2KB 9.2KB 0B
core/CheckboxInput 50.1KB 50.3KB +268B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.2KB 54.5KB +256B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.7KB 60.9KB +273B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 51.0KB 51.3KB +315B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 56.0KB 56.3KB +279B 10.6KB 10.6KB 0B
core/ContextMenu 74.7KB 75.1KB +320B 14.6KB 14.7KB +77B
core/ContextMenuItem 53.0KB 53.4KB +378B 9.6KB 9.6KB 0B
core/DateInput 99.7KB 130.3KB +30.6KB ⚠️ 14.8KB 14.9KB +85B
core/DateRangeInput 69.5KB 70.0KB +504B 11.2KB 11.2KB 0B
core/DateTimeInput 109.8KB 140.5KB +30.7KB ⚠️ 15.2KB 15.3KB +89B
core/DialogHeader 49.0KB 49.2KB +272B 9.8KB 9.8KB 0B
core/DrawerHeader 49.1KB 49.3KB +248B 11.2KB 11.2KB 0B
core/DropdownMenu 72.8KB 73.6KB +738B 14.7KB 14.8KB +78B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +284B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +287B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +275B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.2KB 58.5KB +267B 9.5KB 9.5KB 0B
core/FileInput 54.3KB 54.5KB +262B 10.3KB 10.3KB 0B
core/IconButton 43.5KB 43.7KB +272B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.4KB 63.7KB +285B 10.1KB 10.1KB 0B
core/Lightbox 51.3KB 51.6KB +254B 9.8KB 9.8KB 0B
core/Link 43.7KB 43.9KB +281B 7.6KB 7.6KB 0B
core/ListItem 53.7KB 53.9KB +261B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.6KB +98B 17.4KB 17.4KB +85B
core/MobileNav 51.2KB 51.4KB +261B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.4KB 46.6KB +259B 9.4KB 9.4KB 0B
core/MoreMenu 73.2KB 73.8KB +597B 14.7KB 14.8KB +78B
core/MultiSelector 73.1KB 73.3KB +272B 14.6KB 14.7KB +95B
core/NumberInput 55.9KB 56.1KB +290B 10.7KB 10.7KB 0B
core/Outline 28.8KB 29.0KB +285B 4.0KB 4.0KB 0B
core/Pagination 84.9KB 85.5KB +570B 15.3KB 15.4KB +90B
core/Popover 50.0KB 50.2KB +216B 12.1KB 12.2KB +87B
core/PowerSearch 158.2KB 188.0KB +29.8KB ⚠️ 19.9KB 20.0KB +101B
core/RadioListItem 52.4KB 52.6KB +264B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 76.5KB 76.5KB +28B 15.0KB 15.1KB +92B
core/SelectorOption 14.5KB 14.8KB +251B 5.1KB 5.1KB 0B
core/SideNav 58.1KB 58.4KB +277B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.8KB 47.0KB +248B 9.5KB 9.5KB 0B
core/SideNavHeading 54.9KB 55.1KB +248B 10.1KB 10.1KB 0B
core/SideNavItem 54.6KB 54.9KB +270B 10.1KB 10.1KB 0B
core/Step 32.6KB 32.8KB +279B 5.3KB 5.3KB 0B
core/Stepper 47.5KB 47.8KB +270B 9.6KB 9.6KB 0B
core/Switch 48.5KB 48.8KB +279B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.8KB 52.0KB +276B 10.0KB 10.0KB 0B
core/Table 83.9KB 84.1KB +214B 15.1KB 15.2KB +78B
core/TableCell 75.8KB 76.1KB +389B 14.8KB 14.9KB +78B
core/TableHeaderCell 75.5KB 75.9KB +402B 14.8KB 14.9KB +77B
core/TableSelectionToolbar 52.5KB 52.8KB +278B 11.2KB 11.2KB 0B
core/TextInput 53.7KB 54.0KB +274B 10.5KB 10.5KB 0B
core/Thumbnail 50.8KB 51.1KB +272B 11.0KB 11.0KB 0B
core/TimeInput 86.9KB 117.6KB +30.7KB ⚠️ 13.8KB 13.8KB +92B
core/Timestamp 27.7KB 27.7KB +1B 9.5KB 9.5KB 0B
core/Toast 60.0KB 60.2KB +258B 9.7KB 9.7KB 0B
core/ToggleButton 44.3KB 44.5KB +245B 9.3KB 9.3KB 0B
core/Token 29.7KB 30.0KB +289B 4.5KB 4.5KB 0B
core/Tokenizer 68.8KB 69.3KB +582B 16.5KB 16.6KB +71B
core/TopNav 52.0KB 52.3KB +274B 10.4KB 10.4KB 0B
core/TopNavHeading 54.3KB 54.5KB +223B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.3KB 54.6KB +273B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.7KB 54.0KB +258B 10.1KB 10.1KB 0B
core/TreeList 32.7KB 33.0KB +279B 4.7KB 4.7KB 0B
core/Typeahead 61.6KB 61.9KB +269B 11.3KB 11.3KB 0B
lab/ChatEmojiPicker N/A 65.2KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 65.7KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.4KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 47.8KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 216.3KB N/A N/A 2.4KB N/A
lab/Comments N/A 220.7KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.0KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.3KB N/A N/A 722B N/A
lab/ListInput N/A 75.3KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 188.0KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 122.2KB N/A N/A 775B N/A
lab/Schedule N/A 66.4KB N/A N/A 3.9KB N/A
lab/TourStep N/A 55.6KB N/A N/A 590B N/A
lab/TransferList N/A 67.1KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 72.8KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.1KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 212.9KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.6KB N/A N/A 2.3KB N/A

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

⚠️ Touch-only size budget

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

Component JavaScript CSS
core/BottomSheet +30.5KB ⚠️ +98B
core/DateInput +30.6KB ⚠️ +85B
core/DateTimeInput +30.7KB ⚠️ +89B
core/PowerSearch +29.8KB ⚠️ +101B
core/TimeInput +30.7KB ⚠️ +92B

Bundle Size Summary

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

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

Accessibility Audit

Status: No accessibility violations detected.

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 owner decision: the touch open and close springs, and the stacked release, tracking, dim and adaptive-breakpoint changes, rest on the draft spec AST-094 (FR4, FR5, FR7, FR8 and FR13), which no current spec states, so an engineering owner needs to adopt those requirements or identify the governing spec, recorded by commenting /approve-spec <full head SHA> on this pull request (the author may do this if they are an engineering owner). Independently of that decision, three changes are needed:

  • Land the predecessor pull requests first and rebase, so this one carries only its own 894-line commit instead of 27,971 authored lines.
  • Make pr-interaction part of the core test owner (the test join), as AST-030 FR11 requires for new coverage.
  • Plan the entrance in present() from the reopened rest instead of the previous settledOffset, so reopening after a close from a peek detent neither jumps nor keeps the peek's lighter scrim, and add a test for it.

[Automated review]

}
// Below the screen: the sheet's visible height at its rest.
const height = measureHeight();
const restOffset = settledOffset;

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 reads settledOffset from the render that reopened the sheet, while the reopen reset (setSettledOffset(0), line 812) renders after this microtask. After a touch close from a peek detent the entrance is planned for the peek, the reset render then drops the peek's resting transform mid-spring (a visible jump), and endScrimTracking(restingDim) at line 1440 leaves the peek's 0.3 scrim on a sheet resting at full height. Planning from the reopened rest (offset 0 and its dim), plus a close-from-peek-then-reopen test, would cover it.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself and is not in the test join's needs (line 888), so a failing BottomSheet interaction proof never fails test. AST-030 FR11 asks new coverage inside an existing surface to join that surface's owner rather than add a separately routed lane.

@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 owner decision: the touch open and close springs, and the stacked release, tracking, dim and adaptive-breakpoint changes, rest on the draft spec AST-094 (FR4, FR5, FR7, FR8 and FR13), which no current spec states, so an engineering owner needs to adopt or reject those requirements, recorded by commenting /approve-spec <full head SHA> on this pull request (the author may do this if they are an engineering owner). Independently of that decision, four changes are needed:

  • Land the predecessor pull requests first and rebase, so this one carries its own 986-line change instead of 28,063 authored lines.
  • Plan the entrance in present() from the reopened rest instead of the previous settledOffset, since after a close from a lower detent (the 96 px peek in the Snap Points With Peek story) the next open keeps a 0.3 scrim behind a full-height sheet, and add a test that closes from a peek and reopens.
  • Make pr-interaction part of the core test owner (the test join), as AST-030 FR11 requires for new coverage.
  • Name what the changed reopen reset effect in useSheetGestures.ts now synchronizes (the sheet's translate and the dialog's backdrop), as react-component-runtime INV3 requires.

[Automated review]

}
// Below the screen: the sheet's visible height at its rest.
const height = measureHeight();
const restOffset = settledOffset;

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 reads settledOffset from the render that reopened the sheet, while the reset to the tallest detent (setSettledOffset(0), line 812) renders only afterwards. After a close from a lower detent the entrance is planned for that detent and endScrimTracking(restingDim) (line 1440) leaves its dim on a full-height sheet: in Chromium with touch emulation, "Snap Points With Peek" reopens at backdrop opacity 0.3 instead of 1. Plan from the reopened rest (offset 0 and its dim), and add a test that closes from a peek and reopens.

setSettlingLayoutOffset(null);
gestureExitRef.current = null;
travelHandleRef.current = null;
animator.set({y: 0});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reset now also writes the animator, stops the scrim follow and reports scrim tracking off, but its comment names only the detent reset. architecture:react-component-runtime INV3 requires a new or changed Effect to name the external system it synchronizes: name the sheet's compositor translate and the dialog's ::backdrop dim here, or move the reset to where the open is planned.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself and is not in the test join's needs (lines 888–896), so a failing BottomSheet interaction proof never fails test. spec:AST-030 FR11: "New coverage inside an existing surface MUST join those owners and MUST NOT create a separately routed lane or required context." Run these proofs in the core test owner, or make this an internal job that test needs.

@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 decision: the touch open and close springs, the stacked release, tracking and dim changes, and the new adaptive size rule rest on the draft spec AST-094 (FR4, FR5, FR7, FR8 and FR13), which no current spec states, while the newest commit's entrance input hold contradicts that draft's FR6 and the release, tracking and dim changes also reach mouse drags, which the draft leaves out of scope. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request once the record matches it, and the author may do this as an engineering owner. Independently of that decision, four changes are needed:

  • Land the predecessor pull requests first and rebase, so this one carries its own 1,277-line change instead of 28,571 authored lines.
  • Plan the entrance in present() from the reopened rest instead of the previous settledOffset, since after a close from the 96 px peek in "Snap Points With Peek" the next open keeps a 0.3 dim behind a full-height sheet, and add a test that closes from a peek and reopens.
  • Make pr-interaction part of the core test owner (the test join), as AST-030 FR11 requires for new coverage.
  • Name what the changed reopen reset effect in useSheetGestures.ts now synchronizes (the sheet's translate and the dialog's backdrop dim), as react-component-runtime INV3 requires.

[Automated review]

}
// Below the screen: the sheet's visible height at its rest.
const height = measureHeight();
const restOffset = settledOffset;

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 reads settledOffset from the render that reopened the sheet, while the reset to the tallest detent (setSettledOffset(0), line 839) renders only afterwards. After a close from a lower detent, the entrance is planned for that detent, and endScrimTracking(restingDim) (line 1477) leaves its dim on a full-height sheet. In Chromium with touch emulation, "Snap Points With Peek" reopens at backdrop opacity 0.3 instead of 1. Plan from the reopened rest (offset 0 and its dim), and add a test that closes from a peek and reopens.

setSettlingLayoutOffset(null);
gestureExitRef.current = null;
travelHandleRef.current = null;
animator.set({y: 0});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reset effect now also writes the animator, stops the dim's animation and reports scrim tracking off to the host, but its comment (line 833) names only the detent reset. architecture:react-component-runtime INV3 requires a new or changed Effect to name the external system it synchronizes. Name the sheet's compositor translate and the dialog's ::backdrop dim here, or move the reset to where the open is planned.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction routes itself through its own scope step and is missing from the test join's needs (lines 888–896), so a failing BottomSheet interaction proof never fails test. spec:AST-030 FR11: "New coverage inside an existing surface MUST join those owners and MUST NOT create a separately routed lane or required context." Run these proofs in the core test owner, or make this an internal job that test needs.

// presentation, for its whole life (F4 b59): a hold covers the sheet and
// the scrim until the sheet lands, so such a touch reaches neither (its
// later moves and lift stay with the hold, even once it is gone).
const [isEntranceHeld, setIsEntranceHeld] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While the entrance hold is up, a tap on the dim and a grab are both ignored for about 0.6 s. The draft AST-094 states no such behavior, and its FR6 says "A finger MUST be able to catch any animation mid-flight at the value on screen." The cited source, "F4 b59", is not in the repository. The owner question on this pull request asks whether the hold stays, with FR6 amended, or goes.

const dimFor = (o: number) =>
scrimOpacityForOffset(o, offsets, dismissedOffset, peekOffset);
lastDimForRef.current = dimFor;
const release = resolveSheetRelease({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

settleFromDrag runs for mouse drags too, so a released mouse drag now takes the UIKit rule. At rest it dismisses only past halfway to gone, instead of 40% of the shortest detent's height below it. The move handler at line 1742 likewise drops the 40 px magnet and the 48 px stretch for every pointer. AST-094 lists pointer presentations as a non-goal, so these mouse changes have no authority even in the draft; they are part of the owner question.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…the full scrim

present() planned the entrance from `settledOffset`, which still held where
the last close left the sheet: the reopen reset renders after present's
microtask. After a close from a peek, the next open planned 96 px of travel
to the peek, then dropped the peek's transform mid-spring (a jump) and left
its 0.3 scrim behind a full-height sheet (astracat-bot on meta#8203). The
entrance now plans from the tallest detent, where every open rests.
@vjeux
vjeux force-pushed the uikit/c17-sheet-present branch from aa3b7de to 3398452 Compare October 25, 2026 00:33
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an engineering owner decision: the touch BottomSheet open and close springs, the entrance input hold and the stacked touch release, tracking and dim rest on the draft spec AST-094, which no current spec states, and that record itself conflicts with the hold (FR6), the stacked theme motion roles (FR4) and the current reduced-motion design spec (FR8). An engineering owner, the author included as one, records adoption by commenting /approve-spec <full head SHA> on this pull request once the record matches the decision. Independently of that decision, these changes are needed:

  • Land the stacked predecessors and rebase, so this pull request carries only its own eight commits (about 1,700 changed lines instead of 35,605).
  • Drop the adaptation platform condition, which the closed condition vocabulary of AST-012 FR2 does not admit.
  • Run the interaction proofs under the existing core test owner instead of the separately routed pr-interaction lane (AST-030 FR11).
  • Correct astryx docs motion, which says alerts, menus, navigation and scrolling play iOS motion, and the adaptive changeset's promise to follow a theme's own md point.

[Automated review]

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

platform is a sixth when field. spec:AST-012 FR2: "A when object may contain width, height, pointer, contrast, and motion … Future environment fields require explicit additions to this closed vocabulary." Drop it here, or have the record amended first.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr-interaction decides its own scope and is missing from the test join's needs (lines 888–896), so a failing BottomSheet interaction proof never fails test. spec:AST-030 FR11: "New coverage inside an existing surface MUST join those owners and MUST NOT create a separately routed lane or required context."

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Only the BottomSheet plays the motion table at this revision; no alert, menu or navigation component imports it, and AST-094 keeps scrolling native. Limit the sentence to what ships.

export const COMPACT_QUERY = DEFAULT_COMPACT_DEFINITION.query;

/** Both: presentations such as a sheet in place of a popover. */
export const COMPACT_TOUCH_QUERY =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the default definition (768 px). spec:AST-012 FR9 makes compact "below the effective md point", and FR1 lets a theme override md, while the changeset promises "the theme's md point". Resolve it from the active theme, or say 768 px in the changeset.

// with the sheet). The pin is lifted only after a style flush has applied
// the last value, so that value does not ease in again.
// Under a finger UIKit drops every touch that begins during a sheet's
// presentation, for its whole life (F4 b59): a hold covers the sheet and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"F4 b59" resolves nowhere in the repository (not in the UIKit table, MEASUREMENTS.md or AST-094), and AST-094 FR6 says "A finger MUST be able to catch any animation mid-flight". The entrance hold needs an owner decision and a recorded source.

// UIKit's sheet tracks (the release, not the drag, picks the detent).
// Up past fully open it holds: UIKit's sheet doesn't stretch beyond its
// largest detent.
const next = Math.max(0, raw);

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 move handler runs for every pointer, so mouse and pen drags also lose the 40 px detent magnet and the 48 px stretch above fully open, although AST-094 leaves pointer presentations out of scope and the release keeps today's rule under a mouse.

vjeux added 3 commits October 29, 2026 15:40
…ter a stalled dispatch, and a proof can check the pacing the page received

WebKit's touch command takes no timestamp, so the page times each event by its arrival. Under box contention a dispatch stalls 27 to 31 ms, and the driver, which aimed every event at the gesture's start, then sent the overdue moves together, 1 to 2 ms apart: C17 measured BottomSheet's flick at 2000 px/s reaching the page as 3,627 to 13,200 px/s.

playSteps now sends each event at its scripted time and never sooner after the one before it than the script spaces them, so a stall stretches one gap instead of bunching the rest. With `checkPacing`, WebKit's play reads the timeStamps the page received (a passive capture listener on touch events) and throws TouchPacingError, naming both gap lists, when an event is missing or a gap is more than TOUCH_PACING_TOLERANCE_MS off the script. retakeOnTouchPacing(testInfo, attempt) runs a proof's attempt once more on a TouchPacingError only: two attempts in all, a product failure is never re-taken, and each re-take is listed as a `touch-retake` annotation.

The tolerance is measured: on one worker of a loaded machine, 440 gaps over 40 WebKit flicks moved at most 4 ms (99th percentile 3 ms; the page's stamps are whole ms); under eight workers 4% of flicks had a gap further off, up to 21 ms.

Red first: the stalled-dispatch case fails on the previous scheduling; checkTouchPacing and retakeOnTouchPacing are new, with the three re-take cases (both attempts off pace stays red, a product failure after good input is not re-taken, off pace then good passes and is listed). In the browser a 40 ms driver stall makes WebKit's play throw TouchPacingError, and Chromium, whose page sees the scripted stamps, plays on.
…a click, and SCRIPTED_CLICK_SKIP names the skip where it does not

Playwright's WebKit synthesises a click only from its tap gesture (`driver.tap`), never from touches sent one by one, so a proof that needs the click of a scripted press and lift has to know. `driver.synthesizesClicks` says it (Chromium true, WebKit false), and such a proof skips with `test.skip(!driver.synthesizesClicks, SCRIPTED_CLICK_SKIP)`, as PARTIAL_LIFT_SKIP does for a partial lift.

The self-test plays a scripted tap and then driver.tap on one button and reads the order of touchstart, touchend and click: driver.tap's click comes last and a scripted tap's click, if any, before driver.tap's touchstart, so it waits on that order rather than on the clock. This came from the web-input drivers helper line, which no longer carries it.

@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: this pull request adds an Element jobs item for the entrance hold's shield to the current BottomSheet record, and an engineering owner needs to approve it, and settle whether its non-modal sentence should say pointer input as AST-094 FR35 does, by commenting /approve-spec <full head SHA> on this pull request (the author may do this as an engineering owner). Whatever that decision, these need changes:

  • Land or retarget the open predecessor pull requests and move the @astryxdesign/build treeshakeCompensation default to its own pull request, so this diff carries only its own change (37,948 authored lines now, about 2,556 of them this pull request's), then bring that under 2,000 lines or explain why it cannot be split.
  • On a non-modal sheet, keep key and assistive-technology activation live during the entrance and stop a held drag's expected click from swallowing the first tap after the sheet lands (sheetSelfHold.ts), with tests for both.
  • Play the sheet springs through AST-094 FR4 motion roles, keep the dim on the spring-back when a sheet is reopened during its exit (AST-108 FR4a), and state the touch exit in the BottomSheet record (AST-094 FR7).
  • Make the carried PressTracker a factory, as react-component-runtime INV13 asks.

[Automated review]

return;
}
expireClicks(hold);
if (hold.holders > 0 || hold.clicks.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While the hold is on, this swallows every click on a non-modal sheet, including the one Enter, Space or a screen reader dispatches with no pointer press, so Done does nothing during the entrance. AST-094 FR35: "Only pointer input is held: keys and assistive technology stay live." Swallowing only a click that consumes the expectation of a held pointer press (and not because holders > 0) keeps keys and assistive technology live.

hold.clicks = hold.clicks.filter(expiry => expiry > now);
};
const lifted = (hold: SelfHold) => {
hold.clicks.push(performance.now() + CLICK_WINDOW_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every lifted held press queues an expected click, but a drag never clicks, so its expectation outlives the entrance and swallows the next real tap for up to a second after the lift, once the sheet has landed. Queue it only for a press that can still click, or clear it at the next unheld press.

// start.
const handle = animator.animate(
{y: 0},
{spring: SHEET_PRESENT_SPRING},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SHEET_PRESENT_SPRING here (and SHEET_DISMISS_SPRING at line 1901, the reopen spring at line 1109) is a table constant, not a motion role: motion/roles.test.ts asserts springs.sheetPresent is no role. AST-094 FR4: "A component MUST read touch motion through these roles and MUST NOT play a literal." springs.viewChange carries the same measured value.

}
// And the dialog's ::backdrop dim: its animation stops, and the host
// gets its own easing back.
stopScrimFollow();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On a reopen during the touch exit the sheet springs back (lines 1107–1111), but this stops the exit's dim and hands it to the host, and nothing follows the spring-back, so the dim no longer moves with the sheet. AST-108 FR4a asks a sheet and its scrim to start and land together; following the scrim on the spring-back's curve, as present() does, would keep them in step.

shield inside the dialog: a transparent cover of the viewport that takes
a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is
removed when the sheet lands. A non-modal sheet adds no shield: it holds
only input on itself, and the page beside it stays live.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"holds only input on itself" reads as every input; AST-094 FR35 holds only pointer input and keeps keys and assistive technology live. AST-094 FR7 also asks this record to state the sheet's touch exit, which this edit could add.

return tap.numberOfTapsRequired === 1 && tap.numberOfTouchesRequired === 1;
}

export class PressTracker implements TouchConsumer {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

react-component-runtime INV13: "The recognizer family (its base, the arena, and the host) is the one place these modules use classes ... Every other module ... is built from create… factories." The press tracker is outside that family, so it would be a createPressTracker factory.

// compiled (the TypeScript transform drops it as unused), the module is
// never loaded, and its constants never reach the CSS: a media-query key
// fails the build and a value ships as an undefined custom property.
treeshakeCompensation: true,

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 flips the {stylexOptions} form's treeshakeCompensation default for every caller who leaves it unset. It has no relation to the BottomSheet change, so it would be easier to review and release on its own.

vjeux added 24 commits October 29, 2026 17:00
Once a component in the root entry holds its layer's input (a popover's
presentation hold, meta#8223), Layer/inputHold's static imports of
gestures/GestureHost and gestures/nativeScroll put eight gesture modules
(about 47 KB) in the root entry, against
architecture:react-component-runtime INV13. Bundle a component that imports
the input hold and require no gesture bytes. Fails while the input hold
imports from gestures/.
…e host asks it

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

In WebKit a touch carries its real dispatch time, so a dispatch that stalls
under load reaches the page as a burst of moves 1-2 ms apart, which the
sheet reads as a much faster flick and correctly dismisses. The three proofs
whose outcome follows a velocity (the downward flick, the moderate and fast
throws, and Android's flick) now play with `checkPacing`, and only on a
proven `TouchPacingError` re-take the gesture once, on a fresh page. A
product assertion is never re-taken, and a second pacing error fails with
its cause.
…se reads it, its last two move intervals and its lift

C17 adopted checkPacing on BottomSheet's flick detent proofs and every WebKit attempt failed, even on an idle worker: WebKit delivers a gesture's first moves with the page's own touchstart handling time (gaps of 4, 11, 3 ms against 8, 8, 8) and its lift about a frame late (8 to 11 ms against 2.1). Neither feeds a release velocity: Core's tracker reads the last three moved samples, two intervals, and a release reads as still only 40 ms after the last move. Before the check the same flicks reached the page at 2,000 px/s.

checkPacing now judges what decides a release: an event missing, either of the last two move intervals more than TOUCH_PACING_TOLERANCE_MS (4 ms) off the script, or the lift TOUCH_LIFT_STILLNESS_MS (40 ms, UIKit's measured stillness, spec:AST-100 FR2) or more after the last move. checkTouchPacing takes the played steps, so it knows which events are moves.

Red first: C17's received gaps fail the previous check and pass this one; a stall and its burst in the last two intervals still fails, and so does a lift 45 ms late, while the same stall earlier in the gesture passes. In the browser a 40 ms driver stall before the last move throws in WebKit, and Chromium plays on.
…il cancelled, and its compositor run is blocked only once on the compositor

C23 saw the control self-test fail under load at eight workers: "660 ms judged at 16.6 ms/frame: 28 frames presented, 0/28 moved". Its animations ran once, for 1,600 ms from before the screencast started, so on a loaded runner, where starting the cast takes long, the compositor's animation had ended before the block and nothing moved in the judged part.

Both animations now go back and forth until cancelled, as the pooled self-test's do, and the compositor's block uses `during`, so it starts only once that animation runs on the compositor.

Red first: with the setup stretched to 1.8 s, as a loaded runner takes, the previous test fails with exactly that read (0/39 moved) and this one passes. Normal runs at eight workers: 10 of 10 in Chromium and 10 of 10 in WebKit.
…control; the velocity proofs re-take an off-pace gesture) into meta#8109
# Conflicts:
#	packages/core/src/BottomSheet/__tests__/BottomSheetDetent.interaction.browser.spec.ts
… judges its first frame on that frame's time

'a spring started right before a block plays in full after it' waited a
fixed 300 ms of wall clock after the block, then read the screencast. In
the full interaction run on a loaded box no moving frame had reached the
screencast by then, and the proof failed on an empty read. It now waits
until the page shows the box a quarter of the way along and three more
frames reach the screencast. Its bound changes form, not strength: the
curve's own start (the handle's started) must come after the block, and
the first moving frame may be no further along than a curve started a
frame before the block ended, at that frame's own time, which is the old
30 px for a frame right after the block and still holds when a loaded
machine sends frames sparsely. A curve dated at the call is 107 px in.
…itor run against the control, since a loaded runner's compositor skips frames

At 16 workers on a loaded box (load 92), the self-test failed 6 of 32 Chromium runs on its compositor animation. Every presented frame moved (32/32 to 38/38), but the compositor presented none for 63 to 109 ms inside the block, past the display-rate bound. Its compositor run is now judged against startBusyControl's control in the same window, with the bound's own verdict recorded beside it. The rAF mutant stays the negative and fails both: it moved on 0 or 1 of the control's frames with a stall past the control's, and the bound alone fails its longest stall. The run follows the README's order: the cast, then the animation, then the block once the animation runs on the compositor.

At 16 workers (load 115 to 164) it passed 48 of 48 in Chromium, where the control's own longest stall in the rAF runs reached 33.5 ms against the rAF's 660 ms. WebKit at 8 workers: 8 of 8.
… animations wrap instead of turning

astracat found that the control self-test's back-and-forth animation turns at 1,600 ms. When a loaded runner starts the cast late enough that the block holds the turnaround, two cast frames straddling it evenly read one position, and the control verdict fails the correct compositor animation: "moved on 6 of the 7 frames the control moved". Both self-tests' compositor animations and their timed rAF loops now wrap from 560 px back to 0, a change on every frame. The pooled case's counted mutants still bounce: they are negatives, and a turn changes their position by a full step.

BusyProof.stepPx's note now describes the step it reads from the judged part's changes, and the file header names pooledBusyProof.

Red first: with the setup stretched so the block holds the turnaround, the previous test fails 7 of 12 Chromium runs ("moved on 5 of the 6" and "6 of the 7 frames the control moved"), and this one passes 12 of 12.
…rol inside an open dialog, leaving its backdrop on the compositor

C17 found that in WebKit the control's top-layer popover, shown above an open modal dialog, takes the dialog's `::backdrop` animation off the compositor. BottomSheet's dim held its value for 155 to 225 ms into the block, then jumped to rest, while the control moved on every frame. `startBusyControl(page, {within})` now mounts the box inside the given element as a plain fixed box, with no popover and no top-layer entry. Its frames still count presented frames, and `raise()` leaves such a box where it is.

The self-test opens a modal dialog whose backdrop dims through a block, and judges the dim's brightness at a point against the control.

Red first: in WebKit at 8 workers, with the top-layer control the dim stalled in 5 of 24 runs, from 43 to 78 ms at the block's start, while the control moved ("17/20 moved, longest stall 76.5 ms at +20 ms"); with `within` it passed 24 of 24. Chromium passed either way.
…hold asks the layer, not the gestures) into meta#8203

@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 on two record edits at this head: the new Element jobs item in component:BottomSheet (the entrance hold's viewport shield, and whether its non-modal sentence should say pointer input) and the spec:AST-094 FR41 clause on claimed mouse and pen drags carried from #8665. 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. Whatever that decision, these need changes:

  • Land or retarget the predecessors so the diff carries only this pull request's own change (41,248 authored lines today, 2,556 of them its own), and move the @astryxdesign/build default change to its own pull request.
  • Keep key and assistive-technology activation, and the first tap after a held drag, live on a non-modal sheet during its entrance (sheetSelfHold.ts, spec:AST-094 FR35).
  • Play the sheet springs through FR4 motion roles, keep the dim on the spring when a sheet is reopened during its exit (AST-108 FR4a), and state the touch exit in the BottomSheet record (FR7).

[Automated review]

return;
}
expireClicks(hold);
if (hold.holders > 0 || hold.clicks.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On a non-modal sheet this swallows every click while the hold is on, including the detail 0 click Enter, Space or a screen reader dispatches, so a keyboard user’s Done does nothing during the entrance. spec:AST-094 FR35: “Only pointer input is held: keys and assistive technology stay live”. Let a detail 0 click through, as utils/clickGuard.ts line 135 does, and swallow a pointer click only when it consumes the expectation of a press the hold took.

hold.clicks = hold.clicks.filter(expiry => expiry > now);
};
const lifted = (hold: SelfHold) => {
hold.clicks.push(performance.now() + CLICK_WINDOW_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every held pointerup queues an expected click, but a drag produces none, so this expectation outlives the entrance and swallows the next real tap for up to a second after the lift. Queue it only for a press that can still click, or clear it at the next unheld press, and add a test for a tap after a held drag.

// start.
const handle = animator.animate(
{y: 0},
{spring: SHEET_PRESENT_SPRING},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SHEET_PRESENT_SPRING (and SHEET_DISMISS_SPRING at line 1901) is played as a table literal; motion/roles.test.ts lines 169–170 assert it is no role. spec:AST-094 FR4: “A component MUST read touch motion through these roles and MUST NOT play a literal”. springs.viewChange carries the same measured value.

}
// And the dialog's ::backdrop dim: its animation stops, and the host
// gets its own easing back.
stopScrimFollow();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On a reopen during the touch exit the sheet springs back on SHEET_PRESENT_SPRING, but this stops the exit’s dim and hands it to the host’s stylesheet transition, so the sheet and its scrim run on two clocks (AST-108 FR4a). Follow the scrim on the spring-back’s curve from the dim on screen, as present() does.

shield inside the dialog: a transparent cover of the viewport that takes
a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is
removed when the sheet lands. A non-modal sheet adds no shield: it holds
only input on itself, and the page beside it stays live.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

“holds only input on itself” reads as every input; spec:AST-094 FR35 holds only pointer input and keeps keys and assistive technology live. The record also states no touch exit for the sheet, which FR7 asks each overlay record to state.

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants