Skip to content

feat(ChatLayout): dragging the conversation to the composer dismisses the keyboard - #8217

Draft
vjeux wants to merge 1911 commits into
meta:mainfrom
vjeux:uikit/c08-keyboard-dismiss
Draft

vjeux wants to merge 1911 commits into
meta:mainfrom
vjeux:uikit/c08-keyboard-dismiss

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: vjeux/astryx@7373545...04727e8 (plus #8671's test file, which this stacks on), and the tap fix vjeux@927c9bb1fc.

Stacked on #8176 and #8671.

Why

In Messages on iOS, dragging the conversation down to the text field puts the keyboard away. ChatLayout kept it up, covering about 40% of an iPhone's screen, so the only way to read more of the conversation was to tap outside the field.

What led to it

Part of bringing Astryx's touch behavior to parity with iOS's UIKit, text entry first. #8176 puts ChatLayout's composer on top of the keyboard, which made the missing gesture stand out. The engineering owner decided it is built-in behavior with no prop (component:ChatLayout DEC-1), and spec:AST-094 FR36–FR37 say a drag decides once, when it lifts.

Options and tradeoffs

  • Make the keyboard follow the finger, as UIKit does: a web page can't move the system keyboard.
  • Dismiss as soon as the finger reaches the composer: someone who is only scrolling would lose the keyboard mid-gesture.
  • Count a fast flick as reaching it: UIKit doesn't; on the iOS 26.2 simulator a flick that lifts above the keyboard keeps it.

Why this choice

Decide once, when the drag lifts: at or past the top of the composer the keyboard goes, moving back up first keeps it, a drag that starts in the text field (moving the caret) never dismisses, and a press counts as a drag only after it travels UIKit's 10 px pan threshold, so a tap that drifts onto the composer keeps the keyboard. The keyboard stays up during the drag. On desktop a mouse press already moves focus off the field, and wheel scrolling doesn't dismiss, which stays an open question.

How to check it

  1. On an iPhone or the iOS simulator, open ChatLayout's "Full AI Chat" story and tap the composer.
  2. Drag the conversation down to the composer and lift. Expected: the keyboard goes away as you lift.
  3. Drag again, but move back up before lifting. Expected: the keyboard stays.
  4. Start a drag in the text field. Expected: the keyboard stays.
  5. Tap just above the composer and let the finger drift a few pixels onto it. Expected: the keyboard stays.

Evidence

What the finger does Recording
Stays above the composer above
Reaches the composer reach
Same drag, before before
Starts in the text field from text
Android, Chrome 124: reach, then back chrome

The recordings come from an earlier version with the same finger handling; they'll be redone after the rebase. Unit and browser tests (Chromium, WebKit) replay finger, mouse and pen drags, including a touch-screen laptop. Known failures not from this change: slow first loads from main (#8353, #8471), and pr-rtl's new touch-target check, whose test file reached main after this branch's last merge.

Size (INV6, gzip, main → head): ChatLayout JS 50,010 → 67,822 B, CSS 10,173 → 10,212 B.

Writing rubric results

component:ChatLayout (FR10 and its FR43 scope, intro, summary rows, verification): R1, R2, R3, R5 and R6 none; R4: DEC-1's one rejected line.

@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 29, 2026 9:52pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 5:41 PM 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

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

Modified Components

MultiSelector (@astryxdesign/core)
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.3KB 57.4KB +134B 12.5KB 12.5KB 0B
core/AppShell 66.8KB 66.9KB +101B 11.8KB 11.8KB 0B
core/AvatarGroupOverflow 40.3KB 40.4KB +139B 6.4KB 6.4KB 0B
core/Banner 48.5KB 48.6KB +125B 10.1KB 10.1KB 0B
core/BaseTypeahead 55.0KB 55.1KB +118B 10.1KB 10.1KB 0B
core/BottomSheet 33.9KB 49.1KB +15.2KB ⚠️ 7.9KB 7.9KB +2B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 67.3KB 67.4KB +116B 11.2KB 11.2KB 0B
core/Button 43.7KB 43.8KB +112B 9.3KB 9.3KB 0B
core/Calendar 55.6KB 55.7KB +114B 10.2KB 10.2KB 0B
core/Carousel 50.9KB 50.8KB -66B 10.3KB 10.3KB 0B
core/ChatComposer 61.3KB 61.9KB +586B 11.0KB 11.0KB 0B
core/ChatComposerDrawer 24.3KB 24.3KB +15B 4.5KB 4.5KB 0B
core/ChatComposerInput 56.2KB 56.7KB +548B 10.2KB 10.2KB 0B
core/ChatComposerTokenElement 56.2KB 56.7KB +546B 10.2KB 10.2KB 0B
core/ChatDictationButton 47.2KB 47.3KB +128B 9.6KB 9.6KB 0B
core/ChatLayout 49.5KB 66.9KB +17.4KB ⚠️ 10.1KB 10.1KB +39B
core/ChatLayoutScrollButton 47.0KB 47.1KB +106B 9.7KB 9.7KB 0B
core/ChatMessage 23.4KB 23.4KB +14B 3.5KB 3.5KB 0B
core/ChatMessageBubble 7.0KB 7.0KB 0B 3.6KB 3.6KB 0B
core/ChatMessageList 29.3KB 29.3KB +13B 7.8KB 7.8KB 0B
core/ChatMessageMetadata 26.4KB 26.4KB +13B 4.0KB 4.0KB 0B
core/ChatSendButton 46.4KB 46.5KB +109B 9.3KB 9.3KB 0B
core/ChatSystemMessage 6.7KB 6.7KB 0B 3.7KB 3.7KB 0B
core/ChatTokenizedText 6.9KB 6.9KB 0B 3.9KB 3.9KB 0B
core/ChatToolCalls 32.4KB 32.5KB +14B 9.1KB 9.1KB 0B
core/CheckboxInput 50.5KB 50.6KB +107B 9.5KB 9.5KB 0B
core/CheckboxListItem 54.7KB 54.8KB +104B 10.6KB 10.6KB 0B
core/ClickableCard 12.8KB 12.9KB +117B 6.9KB 6.9KB 0B
core/CodeBlock 61.2KB 61.3KB +121B 10.8KB 10.8KB 0B
core/Collapsible 18.7KB 18.8KB +102B 4.4KB 4.4KB 0B
core/CommandPalette 51.5KB 51.6KB +107B 12.0KB 12.0KB 0B
core/CommandPaletteItem 8.4KB 8.5KB +110B 3.5KB 3.5KB 0B
core/ComplexSelector 56.5KB 56.6KB +120B 10.8KB 10.8KB 0B
core/ContextMenu 75.3KB 75.2KB -110B 14.8KB 14.8KB +3B
core/ContextMenuItem 53.5KB 53.5KB -33B 9.8KB 9.8KB 0B
core/DateInput 100.2KB 114.2KB +14.0KB ⚠️ 14.9KB 14.9KB +4B
core/DateRangeInput 70.1KB 69.8KB -259B 11.4KB 11.4KB 0B
core/DateTimeInput 110.4KB 124.3KB +13.8KB ⚠️ 15.4KB 15.4KB +3B
core/DialogHeader 49.5KB 49.6KB +118B 10.0KB 10.0KB 0B
core/DrawerHeader 49.6KB 49.7KB +123B 11.4KB 11.4KB 0B
core/DropdownMenu 73.4KB 73.3KB -82B 15.0KB 15.0KB +5B
core/DropdownMenuCheckboxItem 16.4KB 16.5KB +98B 5.7KB 5.7KB 0B
core/DropdownMenuItem 15.6KB 15.7KB +100B 5.3KB 5.3KB 0B
core/DropdownMenuRadioItem 16.4KB 16.5KB +102B 5.7KB 5.7KB 0B
core/DropdownMenuSubMenu 58.7KB 58.8KB +104B 9.7KB 9.7KB 0B
core/FileInput 54.8KB 54.9KB +92B 10.5KB 10.5KB 0B
core/IconButton 43.7KB 43.8KB +131B 9.3KB 9.3KB 0B
core/Item 11.4KB 11.5KB +98B 4.7KB 4.7KB 0B
core/LayerProvider 64.0KB 64.1KB +108B 10.3KB 10.3KB 0B
core/Lightbox 51.9KB 52.0KB +96B 10.0KB 10.0KB 0B
core/Link 44.1KB 44.2KB +111B 7.7KB 7.7KB 0B
core/ListItem 54.1KB 54.2KB +100B 10.6KB 10.6KB 0B
core/Markdown 145.2KB 145.0KB -184B 17.8KB 17.8KB +3B
core/MobileNav 51.7KB 51.8KB +103B 10.2KB 10.2KB 0B
core/MobileNavToggle 46.9KB 47.0KB +121B 9.6KB 9.6KB 0B
core/MoreMenu 73.7KB 73.6KB -139B 15.0KB 15.0KB +5B
core/MultiSelector 73.6KB 73.3KB -321B 14.8KB 14.8KB +3B
core/NumberInput 56.4KB 56.5KB +131B 10.9KB 10.9KB 0B
core/Outline 29.0KB 29.1KB +126B 4.0KB 4.0KB 0B
core/Pagination 85.5KB 85.3KB -221B 15.5KB 15.5KB +4B
core/Popover 50.3KB 50.3KB +2B 12.2KB 12.2KB +3B
core/PowerSearch 166.6KB 178.7KB +12.1KB ⚠️ 20.1KB 20.1KB +4B
core/RadioListItem 52.8KB 52.9KB +106B 9.8KB 9.8KB 0B
core/SegmentedControlItem 8.6KB 8.7KB +116B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 12.9KB +114B 7.1KB 7.1KB 0B
core/Selector 77.0KB 76.7KB -305B 15.2KB 15.2KB +4B
core/SelectorOption 14.7KB 14.8KB +99B 5.2KB 5.2KB 0B
core/SideNav 64.6KB 64.7KB +94B 10.7KB 10.7KB 0B
core/SideNavCollapseButton 47.3KB 47.4KB +116B 9.6KB 9.6KB 0B
core/SideNavHeading 59.2KB 59.3KB +126B 10.4KB 10.4KB 0B
core/SideNavItem 55.3KB 55.4KB +115B 10.3KB 10.3KB 0B
core/Step 33.0KB 33.1KB +121B 5.5KB 5.5KB 0B
core/Stepper 48.1KB 48.2KB +106B 9.7KB 9.7KB 0B
core/Switch 49.0KB 49.1KB +110B 9.5KB 9.5KB 0B
core/Tab 9.7KB 9.8KB +114B 4.1KB 4.1KB 0B
core/TabMenu 55.9KB 56.0KB +113B 10.2KB 10.2KB 0B
core/Table 84.7KB 84.4KB -295B 15.6KB 15.6KB +4B
core/TableCell 76.5KB 76.4KB -102B 15.3KB 15.3KB +6B
core/TableHeaderCell 76.1KB 76.0KB -128B 15.0KB 15.1KB +4B
core/TableSelectionToolbar 52.8KB 52.9KB +116B 11.3KB 11.3KB 0B
core/TextInput 54.2KB 54.3KB +98B 10.6KB 10.6KB 0B
core/Thumbnail 51.4KB 51.5KB +117B 11.1KB 11.1KB 0B
core/TimeInput 87.5KB 101.8KB +14.3KB ⚠️ 13.9KB 13.9KB +4B
core/Timestamp 27.9KB 28.0KB +12B 9.7KB 9.7KB 0B
core/Toast 60.6KB 60.7KB +126B 9.9KB 9.9KB 0B
core/ToggleButton 44.5KB 44.6KB +104B 9.4KB 9.4KB 0B
core/Token 30.2KB 30.3KB +118B 4.6KB 4.6KB 0B
core/Tokenizer 69.3KB 69.4KB +113B 16.7KB 16.7KB +4B
core/TopNav 52.5KB 52.6KB +105B 10.5KB 10.5KB 0B
core/TopNavHeading 58.6KB 58.7KB +123B 10.2KB 10.2KB 0B
core/TopNavItem 9.0KB 9.2KB +116B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 55.0KB 55.1KB +134B 11.0KB 11.0KB 0B
core/TopNavMegaMenuItem 9.1KB 9.2KB +119B 4.1KB 4.1KB 0B
core/TopNavMenu 58.3KB 58.4KB +114B 10.3KB 10.3KB 0B
core/TreeList 40.4KB 40.5KB +108B 4.8KB 4.8KB 0B
core/Typeahead 62.2KB 62.3KB +125B 11.4KB 11.4KB 0B
lab/ChatEmojiPicker N/A 65.7KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.2KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 96.2KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.1KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 218.2KB N/A N/A 2.4KB N/A
lab/Comments N/A 240.7KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.3KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.7KB N/A N/A 722B N/A
lab/ListInput N/A 75.2KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 161.3KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 107.1KB N/A N/A 809B N/A
lab/Schedule N/A 60.3KB N/A N/A 3.9KB N/A
lab/TourStep N/A 55.5KB N/A N/A 590B N/A
lab/TransferList N/A 67.2KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 72.7KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.6KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 212.7KB N/A N/A 454B N/A
richtext/RichTextView N/A 252.0KB 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 +15.2KB ⚠️ +2B
core/ChatLayout +17.4KB ⚠️ +39B
core/DateInput +14.0KB ⚠️ +4B
core/DateTimeInput +13.8KB ⚠️ +3B
core/PowerSearch +12.1KB ⚠️ +4B
core/TimeInput +14.3KB ⚠️ +4B

Bundle Size Summary

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

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

Accessibility Audit

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

ChatComposer - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/19 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
ChatComposerInput - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/18 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

55 of 504 shot(s) changed.

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

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 440,175
Chat Full AI Chat probe dark 438,105
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Chat Empty State probe light 122,185
Chat Empty State probe dark 120,076
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
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an owner decision: no current spec covers ChatLayout's new keyboardDismissMode prop or its default-on 'interactive' dismissal. An engineering owner needs to provide or identify the governing spec, meaning whether ChatLayout ships the prop and which default it uses, and record it by commenting /approve-spec <full head SHA> on this pull request; the author can do this if they are an engineering owner. Independently of that decision, these changes are needed:

  • The diff includes four open predecessor pull requests (#8045, #8067, #8074, #8176), about 27,000 authored lines, so please land them through their own reviews and rebase this branch down to its ChatLayout commit.
  • 'onDrag' blurs the composer even when no software keyboard is shown, unlike 'interactive', so please gate it on the keyboard being up and add a test.
  • The new draft spec:AST-094 needs its own owner ruling in the pull request that introduces it, and its FR7 cites spec:AST-090, which is not in the repository.

[Automated review]

};

// The browser took the touch to scroll: a drag began.
const onPointerCancel = (event: PointerEvent) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'onDrag' dismisses here without checking that a software keyboard is up, while 'interactive' returns early when guide.inset <= 0 (line 127). On a touch screen used with a hardware keyboard, a ChatLayout set to onDrag loses the composer's focus on every touch scroll of the transcript. Consider requiring guide.inset > 0 for onDrag too (at the touch start or here), and adding the "no keyboard" case to the onDrag tests.

name: 'ChatLayout',
displayName: 'Chat Layout',
description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过 density 属性控制密度。',
description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过 density 属性控制密度。在手机上,编写器随软键盘同步上移并停在键盘上方,停留在最新消息的对话保持在最新消息;轻点编写器会聚焦它而不滚动页面。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

docsZh was not updated for this change: its description omits the drag-to-dismiss behavior that is now the default, and propDescriptions has no keyboardDismissMode entry, so Chinese readers see the English text for the new prop.

@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: no current spec covers ChatLayout's new keyboardDismissMode prop or its default-on 'interactive' dismissal. An engineering owner needs to provide or identify the governing spec, meaning whether ChatLayout ships the prop and which default it uses, and record it by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Independently of that decision, these changes are needed:

  • The diff includes four open predecessor pull requests (#8045, #8067, #8074, #8176), about 27,000 authored lines, so please land them through their own reviews and rebase this branch down to its ChatLayout commit.
  • 'onDrag' blurs the composer even when no software keyboard is shown, unlike 'interactive', so please gate it on the keyboard being up and add a test for that case.
  • The new draft spec:AST-094 needs its own owner ruling in the pull request that introduces it, and its FR7 cites spec:AST-090, which is not in the repository.
  • docsZh in ChatLayout.doc.mjs has no keyboardDismissMode entry and does not mention the new default.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an owner decision: no current spec covers ChatLayout's new keyboardDismissMode prop or its default-on 'interactive' dismissal. An engineering owner needs to provide or identify the governing spec, meaning whether ChatLayout ships the prop and which default it uses, and record it by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Independently of that decision, these changes are needed:

  • The diff includes six open predecessor pull requests (#8045, #8067, #8074, #8176, #8258, #8287), about 29,800 authored lines, so please land them through their own reviews and rebase this branch down to its ChatLayout commit.
  • 'onDrag' blurs the composer even when no software keyboard is shown, unlike 'interactive', so please gate it on the keyboard being up and add a test for that case.
  • The draft spec:AST-094 needs its own owner ruling in the pull request that introduces it: its FR7 cites spec:AST-090, which is not in the repository, and its FR4 rules out the theme-tunable touch motion that #8258 adds.
  • docsZh in ChatLayout.doc.mjs has no keyboardDismissMode entry and does not mention the new default.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

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

  • Commit or spring back on release. core/src/keyboard/keyboardDismiss.ts:72 decides on its own whether a release finishes or springs back. A shared release rule is being specified (AST-057 FR5 covers row swipe today); avoid a component-specific threshold model.

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

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

  • Commit or spring back on release: the dismissal will take its decision from the shared release rule once that rule is specified, with no threshold model of its own. In the next push it reads the keyboard height from the shared keyboard module (AST-099).

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
@vjeux
vjeux force-pushed the uikit/c08-keyboard-dismiss branch from edc7843 to cd49b1b Compare October 25, 2026 01:51
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in cd49b1b: the dismissal reads the shared height from the keyboard module, stacked on #8176.

vjeux added 27 commits October 28, 2026 17:42
…lowance under every primary pointer

The useMenuPress record's FR9 governs the trigger's held finger by
spec:AST-094 FR29 under every primary pointer (FR43). The trigger now
hands the long press FR29's 13 px drift allowance whatever the primary
pointer, so it no longer reads (pointer: coarse) as its press starts, and
the menus drop their coarsePointer import. ContextMenu keeps the long
press's own allowable movement. The changeset says the same.
Strict ESLint reported six unused bindings in files this pull request
carries: the host's HAPTIC_SWITCH_ATTRIBUTE import, two unused imports
in the pan's tests (scrollProbe, ScreenEdgePanGestureRecognizer), the
swipe test's drift, the presented-hit-testing test's gestures, and a
GestureHost test's before, which now asserts what it was read for: a
second finger's moves leave the pan where it was.
…eal angular speed

The release velocity of a rotation whose listener sets rotation = 0 on
each change, at 2 degrees every 16 ms, read 4.363 rad/s against the
2.182 rad/s of the same stream without a re-base: the test asserted only
more than 1.5. It now asserts the 2.18 rad/s (spec:AST-100 FR2), and
fails.
The rotation's velocity tracker samples the raw unwrapped angle, which
setting rotation does not move, so the setter's shift of the samples read
each re-base as extra turn and doubled the release velocity of a
listener that re-bases on every change. The setter no longer touches the
tracker.
…mates while it runs

The proof read the element's animations 230 ms in, after the dip had
ended, and asserted only that every animated key is opacity, which an
empty list passes: with the cross-fade removed from the plan (the move
still landing), the proof still passed. It now reads the keyframe keys
right after the call starts and asserts they include opacity and nothing
else; the same mutant fails it. Chromium and WebKit pass. (F1's in-flight
read from c648034.)
…roller, and its claimed moves are cancelled under pointer events

Two comparisons stop at the shadow boundary, each failing a case:
- once the host's listener on a recognizer's inner node has seen the
  finger, isFingerDownIn reads it as outside a light-DOM scroller around
  that root, so a scroll under it is not the finger's (FR20);
- under inputMode 'pointer-events', a claimed pan on a shadow host
  cancels none of its finger's moves over the inner content, since the
  finger's target is the inner node and its touches reach the host
  retargeted (FR19).
…the flat tree

isFingerDownIn and the pointer-events finger match compare with
containsAlongFlatTree, as the scroll watch already does: a finger on
content inside an open shadow root is down in the scroller around that
root, and its touches, retargeted to the shadow host, match its pointer
finger, so a claimed pan cancels its moves there (spec:AST-094 FR19,
FR20).
…achments asks, and only then

The host let the last attachment decide whether an element is an action
group (useGestureRecognizer always passes the option), and never
recomputed it at a detach: a plain attachment after the group's turned
the group off, and a group's attachment released left a recognizer that
never asked taking presses under a fine pointer. Fails until the host
counts the attachments that ask.
…ents asks

The host counts the attachments that pass actionGroup: true and keeps the
element a group while one remains, recomputed at each detach. An
attachment that passes false, as useGestureRecognizer does by default,
no longer turns a group off, and a group's last asking attachment taking
its leave ends the group.
… that content

The presented hit test takes the document's elementFromPoint, which
answers a shadow host for anything inside its open root, and compares
with Node.contains, which stops at the boundary: a recognizer on a
control inside a moving sheet's shadow root gets no finger, and the tap's
click goes to the host. Fails until the hit test descends into open roots
and compares along the flat tree.
Where a moving element is drawn away from its layout box, the host
hit-tests what is drawn under the finger with the document's
elementFromPoint, which answers a shadow host for anything inside its
open root. The hit test now descends through each open root's own
elementFromPoint and compares with the movers along the flat tree, so a
recognizer on a control inside a moving sheet's shadow root gets its
finger and the tap clicks that control.
…nt lets go

The host's touch-action rules sit in an adopted sheet or a style element
per document or shadow root, and were never removed, so a page whose
last recognizer detached kept them, against spec:AST-094 FR24 (nothing
touch-related remains after the last instance unmounts). Fails until a
root's rules go with its last marked element.
…th its last marked element

The touch-action writer records which elements it marked in each
document or shadow root, and when the last one is unmarked (its last
recognizer detached) it removes that root's adopted sheet or style
element, so nothing touch-related remains after the last instance
unmounts (spec:AST-094 FR24). A later mark writes the rules again; an
element marked in another root first lets go of the old one.
… the inline style it reads

- The motion registry, the host's engine gate and the README name the
  presented hit test as an engine workaround (spec:AST-013 FR6) with its
  review condition (re-measure on each iOS release; remove it once iOS
  WebKit hit-tests a compositor-moved element where it is drawn) and its
  regression fixture.
- While a curve runs the animator holds the value it starts from inline,
  so presentation.ts and the README no longer say the inline style holds
  the end.
…core/internal/touch

Core ships the internal entry @astryxdesign/core/internal/touch, which
re-exports the gesture primitives for Astryx's own packages, and the
Lab-import guard fails a Lab import of core outside core's exports
(architecture:react-component-runtime INV13). The README and the barrel's
header named internal workspace imports and said nothing here is
exported; they now name that entry.
…ill needs the element, and the listeners that cancel

The release note, the stream's header, the host's comment and the README
said the stream is one passive listener per event type and that a
document-capture stopPropagation no longer starves the recognizers. Such a
stop never hid a listener on the same node, and some subscriptions cancel.
They now say what changed: an app's window-capture stopPropagation, or a
document-capture stopImmediatePropagation registered before core, no
longer hides a touch; a recognizer that must claim still needs the touch to
reach its element; the gesture host's touchend, click and contextmenu
cancel while a claimer is attached and the page hold's touchmove while it
holds; and the stream stays ahead of an app's window-capture listener only
until a type's listener is re-added. The README's list of blocking
touchmove listeners gains the claimed touch's target and the document
while a claiming element moves.
…default it moves

The {stylexOptions} form of astryxStylex now passes StyleX
treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3
asks the release note to name the old and new default and the option
that restores the old one; it now does.
…er and for the two stops the input stream fixes

- A still finger with sub-pixel jitter over a scroll view keeps its tap
  and scrolls nothing (spec:AST-094 FR18–FR22).
- A tap is still recognized under an app's window-capture
  stopPropagation, and under a document-capture
  stopImmediatePropagation registered before core's: the recognizers hear
  touches on the window's capture phase.
Chromium and WebKit pass.
…s once

Where nothing that claims is attached, the host's touchend listener is
passive and cannot cancel the engine's own click after a retargeted tap
clicks what is drawn. The shared click guard swallows that late click
whatever is attached; this case pins it, as asked on meta#8293.
…esture host's subscriptions

The gesture host subscribes no click: the one cancelling click subscriber
is the click guard's, added at a window's first arm and kept. The release
note, the stream's header and the gestures README listed click with the
host's touchend and contextmenu; they now name the guard's own.
A press on the transcript decided at its release whether it lifted past the
keyboard's line, however little it moved, so a tap that landed just above the
composer and drifted a few pixels onto it dismissed the keyboard. In UIKit a
scroll view's drag begins only once its pan travels its 10 px hysteresis, so
that tap never drags. A press is now followed as a drag only once it travels
UIKit's pan hysteresis on either axis; until then its lift decides nothing.
New cases for a finger and a mouse drifting 8 px across the line fail before
this change, and a 10 px drag still dismisses.
BottomSheet.test.tsx's imports from both sides are kept; the search goldens
are regenerated after the remaining merges.
meta#8360 carries the gesture foundation this branch took from meta#8076. The only
conflict was the shared motion tables list in tableImports.test.ts, where
this branch adds the keyboard table and meta#8360 the long-press table; both
entries are kept, meta#8360's text unchanged.
… guide

The keyboard layout guide (this pull request) reads where focus moves to tell
a keyboard that comes up from one that stays: meta#8559 removed the store's focus
reports because nothing there read them, so they return here with their first
reader. One pair of capture listeners on the document, attached while
anything subscribes, reports each focusin and focusout with the current
height; BottomSheet's reveal still acts on viewport reports only.
Conflicts were in this stack's own keyboard files: the browser-support
sentence keeps ChatLayout's composer and takes meta#8559's page wording,
BottomSheet's test imports follow the sheetKeyboard split, and the keyboard
notes keep both the sheet's and ChatLayout's Chrome 124 findings. The search
goldens are regenerated after the merge.
…op-level page

The composer's tap route, its scroll-free focus and Done's park attach only
where the keyboard is measurable, which excludes a page inside an iframe
(spec:AST-099 FR7); there the browser's own focus applies. The docs and
changeset now say so.
ChatLayout.doc.mjs conflicted where both pull requests edit the same
description lines: meta#8176's touch-screen and top-level-page focus wording is
kept and this pull request's drag-to-dismiss sentences are added to it in
English, Chinese and the dense line. The search goldens pass unchanged.

@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: component:ChatLayout FR10 and DEC-1 add drag-to-dismiss to a draft record as built-in behavior with no caller control, and FR10 now attaches for every pointer and no longer relies on the INV16 carve-out for touch-only behavior that the earlier owner decision asked for. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or names the FR10 text they want. Independently of that decision:

  • Land #8176, the work under it and #8671 through their own reviews, then rebase so this pull request carries only its dismissal change; today it changes about 39,900 lines around a change of roughly 1,560.
  • Re-record the device checks at the final head on a real iPhone in Safari and real Android devices in Chrome and Firefox, since the changeset names those browsers and the current recordings come from an earlier revision.

[Automated review]

'@astryxdesign/core': patch
---

[feat] ChatLayout: dragging the conversation down to the composer and lifting there now dismisses the software keyboard, as in Messages, wherever ChatLayout keeps its composer on the keyboard (Safari on iOS, Firefox and Chrome for Android); a release moving back up keeps it. A finger drags, and so does a pen the browser reports as touches; in a desktop browser a mouse press, or a press of a pen reported as a pointer, already takes focus off the composer, so the keyboard leaves at the press. Where a page sets `interactive-widget=resizes-content`, Chrome resizes the page for the keyboard, ChatLayout measures no keyboard, and no drag dismisses it. A web page cannot move the system keyboard, so the keyboard leaves on its own at the lift instead of following the finger. Drags that start in the composer never dismiss it, and dismissing moves focus nowhere. Drag-to-dismiss has no caller control. Typing with a keyboard is unchanged.

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 names Safari on iOS, Firefox and Chrome for Android. spec:AST-013 FR10 asks that evidence for a named browser come from that browser, and spec:AST-094's Platform support asks for real-device evidence for iOS Safari and Android Chrome claims, while the description's recordings come from an earlier revision. Please re-record the checks at the final head on a real iPhone in Safari and real Android devices in Chrome and Firefox, and link them in the description.

| FR7 | `density` MUST reflect on the root as the `chat-layout` target's `data-density`, and MUST select the dock, message-area, dock-inner, and blur-layer geometry together. | shipped implementation; pinned tests; family doc `visualProps` | verify |
| FR8 | The layout MUST publish its scroll container and message content refs through `ChatLayoutContext` so the message list can register the observed content box. | shipped implementation | verify |
| FR9 | Accepted `ref`, DOM/data/ARIA props, `className`, `style`, and `xstyle` MUST reach and compose on the layout root. | `architecture:public-component-api/INV5–INV6, INV8` | settled |
| FR10 | While the keyboard module measures a software keyboard (`spec:AST-099` FR2; none where a page sets `interactive-widget=resizes-content`, FR8), a drag down the transcript by a finger, or by a pen the browser reports as touches, MUST dismiss the keyboard, as in Messages, when it lifts at or past the composer's top, with no projection (`spec:AST-094` FR36; UIKit's interactive dismissal, measured on the iOS 26.2 simulator, in `keyboard/keyboardDismiss.ts`), and a release moving back up MUST keep it (FR37). The dismissal attaches for every pointer (`spec:AST-094` FR19, FR43); in a desktop browser a mouse press, or a press of a pen the browser reports as a pointer, takes focus off the composer itself, so the keyboard leaves at the press. Dismissing only blurs the focused field; focus moves nowhere else. Drag-to-dismiss is part of ChatLayout's behavior, with no caller control: DEC-1 rejects an off-switch prop. Under a fine pointer it listens on the layout's own element, and on the document only while a button is down (`spec:AST-094` FR24); the keyboard is unchanged. | `spec:AST-094` DEC-1, FR19, FR24, FR36–FR37, FR43; `spec:AST-099`; DEC-1 | settled |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The code now follows a press as a drag only after 10 px of travel on either axis (keyboardDismiss.ts:134-142), so a tap that drifts onto the composer keeps the keyboard. FR10 does not say so; consider stating it here, with spec:AST-094 FR17 as its basis, so the row the owners approve protects that case.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:34

This branch was successfully deployed

1 active deployment
Preview — 19749b7e Deployed Oct 29, 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