Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fa8cf7b
fix(ChatLayout): Safari reveals a document-scrolling composer
vjeux Nov 8, 2026
2c514fb
fix(ChatLayout): only the document's scroller leaves Safari the reveal
vjeux Nov 8, 2026
b143b86
docs(AST-099): name the exception by ChatLayout's scrollRef
vjeux Nov 8, 2026
37f7a9b
docs(ChatLayout): name every browser the scrollRef exception covers
vjeux Nov 8, 2026
386ebb1
Merge main into the double-lift fix
vjeux Nov 8, 2026
e1968f7
chore(changeset): name the CLI, whose browser-support guide changes
vjeux Nov 8, 2026
1419a71
fix(ChatLayout): lift the dock unless the browser pans to it
vjeux Nov 8, 2026
f26e420
Merge main into the double-lift fix
vjeux Nov 8, 2026
e391719
fix(ChatLayout): the keyboard module watches the dock's reveal
vjeux Nov 8, 2026
e18165a
fix(ChatLayout): a pan to the dock decides only its own show
vjeux Nov 8, 2026
df30dc5
fix(ChatLayout): the 3 s cap decides by where the dock rests
vjeux Nov 8, 2026
714141e
fix(ChatLayout): the dock follows the cover the pan leaves
vjeux Nov 8, 2026
05167c1
fix(ChatLayout): follow the pan on the dock's translate
vjeux Nov 8, 2026
7262c34
fix(ChatLayout): a pan's fling that moves no document rests too
vjeux Nov 8, 2026
7297840
fix(ChatLayout): a slow drag never counts as rest
vjeux Nov 9, 2026
ab43592
fix(ChatLayout): the Safari hold is tested, and a hide forgets touches
vjeux Nov 9, 2026
a64d445
docs(AST-099): FR3 states the dock's follow and a fling's two layouts
vjeux Nov 9, 2026
8e190c3
fix(ChatLayout): a learned cover survives a later pan to the dock
vjeux Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/chat-layout-document-scroll-safari-keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch
---

[fix] **ChatLayout**: In a page that scrolls the document (`scrollRef`), the composer stays right above the software keyboard in Safari on iOS, instead of jumping under the status bar. In Firefox for Android, the first keyboard show on each page that leaves the composer covered keeps it covered for about 0.75 s on a slow device before it rises.
@vjeux
57 changes: 52 additions & 5 deletions docs/specs/AST-099-mobile-keyboard/spec.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cli/assets/docs/browser-support.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const docs = {
content: [
{
type: 'prose',
text: "On a phone, the components that respond to the on-screen keyboard, a fully expanded `height='tall'` BottomSheet and ChatLayout's composer, measure it the same way: the layout viewport's height minus the visual viewport's height times its scale. Where the visual viewport shrinks for the keyboard, as in Safari on iOS in the browser and as a home-screen app, in Firefox for Android and in Chrome on Android, they keep the focused field and the surface around it above the keyboard and leave the page where it was. If Firefox for Android pans to the field, they put the page back within a couple of frames.",
text: "On a phone, the components that respond to the on-screen keyboard, a fully expanded `height='tall'` BottomSheet and ChatLayout's composer, measure it the same way: the layout viewport's height minus the visual viewport's height times its scale. Where the visual viewport shrinks for the keyboard, as in Safari on iOS in the browser and as a home-screen app, in Firefox for Android and in Chrome on Android, they keep the focused field and the surface around it above the keyboard and leave the page where it was. If Firefox for Android pans to the field, they put the page back within a couple of frames. One exception applies when ChatLayout's scrollRef names the document. Safari on iOS then reveals the composer itself and keeps its focus. In another browser without the VirtualKeyboard API, each show before that keeps the browser\u2019s pan to the composer. The first keyboard show that leaves the composer covered waits a short time before the composer rises.",
},
{
type: 'prose',
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/Chat/ChatLayout.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const docs = {
name: 'ChatLayout',
subComponentOf: 'Chat',
displayName: 'Chat Layout',
description: 'Layout shell for full chat interfaces. Messages flow in normal page flow, composer is fixed to the bottom with a frosted glass dock. Set density (compact/balanced/spacious) to control spacing, message max-width, and blur layer sizing. Includes built-in auto-scroll, a "New messages" scroll-to-bottom button, and a frosted glass blur layer behind the composer. By default the layout root is the scroll container; pass scrollRef to delegate scrolling to a parent element or the document body. Where the visual viewport shrinks for the software keyboard, the composer stays on top of the keyboard, and a conversation at its newest message stays there. In Chrome on Android the composer rides Gboard\u2019s slides in step with them. In Safari on iOS (in the browser and as a home-screen app) and in Firefox for Android, it moves above the keyboard once the browser reports it. On a touch screen, in a top-level page, every way into the composer\u2019s text (a tap, the keyboard\u2019s previous and next buttons, Tab, the input\u2019s focus()) focuses it without scrolling the page; under a mouse or inside an iframe the browser\u2019s own focus applies. Where your page sets interactive-widget=resizes-content, Chrome on Android shrinks the page for the keyboard itself and the composer stays above it without moving (see the browser-support guide).',
description: 'Layout shell for full chat interfaces. Messages flow in normal page flow, composer is fixed to the bottom with a frosted glass dock. Set density (compact/balanced/spacious) to control spacing, message max-width, and blur layer sizing. Includes built-in auto-scroll, a "New messages" scroll-to-bottom button, and a frosted glass blur layer behind the composer. By default the layout root is the scroll container; pass scrollRef to delegate scrolling to a parent element or the document body. Where the visual viewport shrinks for the software keyboard, the composer stays on top of the keyboard, and a conversation at its newest message stays there. In Chrome on Android the composer rides Gboard\u2019s slides in step with them. In Safari on iOS (in the browser and as a home-screen app) and in Firefox for Android, it moves above the keyboard once the browser reports it. On a touch screen, in a top-level page, every way into the composer\u2019s text (a tap, the keyboard\u2019s previous and next buttons, Tab, the input\u2019s focus()) focuses it without scrolling the page; under a mouse or inside an iframe the browser\u2019s own focus applies. When scrollRef names the document, Safari on iOS reveals the composer itself, in the browser and as a home-screen app. Done then rests focus on no composer group. In another browser without the VirtualKeyboard API, each keyboard show keeps the browser\u2019s focus until one leaves the composer covered. The composer then rises after a short wait, and later shows work as elsewhere. Where your page sets interactive-widget=resizes-content, Chrome on Android shrinks the page for the keyboard itself and the composer stays above it without moving (see the browser-support guide).',
props: [
{
name: 'children',
Expand Down Expand Up @@ -107,7 +107,7 @@ export const docs = {
{
name: 'Composer',
required: true,
description: 'The input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer. In a top-level page on a touch screen, its wrapper (without the scroll button) is a group with no name. When the keyboard\u2019s Done ends typing, focus rests on that group (it is no tab stop), so the next tap or Tab starts from the composer. It shows the focus ring after keyboard input only. Under a mouse or inside an iframe, the wrapper is a plain container.',
description: 'The input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer. In a top-level page on a touch screen, its wrapper (without the scroll button) is a group with no name. When the keyboard\u2019s Done ends typing, focus rests on that group (it is no tab stop), so the next tap or Tab starts from the composer. It shows the focus ring after keyboard input only. Under a mouse or inside an iframe, the wrapper is a plain container. So it is when scrollRef names the document in Safari on iOS, and in another browser without the VirtualKeyboard API until a keyboard show leaves the composer covered: the browser keeps focus there.',
},
{
name: 'Empty state',
Expand All @@ -121,7 +121,7 @@ export const docs = {
export const docsZh = {
name: 'ChatLayout',
displayName: 'Chat Layout',
description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过 density 属性控制密度。在可视视口会随软键盘缩小的浏览器中,编写器停在键盘上方,停留在最新消息的对话保持在最新消息。在 Android 上的 Chrome 中,编写器随 Gboard 的滑动同步移动。在 iOS 上的 Safari(浏览器中和添加到主屏幕的网页应用)和 Android 上的 Firefox 中,编写器会在浏览器报告键盘后移到键盘上方。在触摸屏上的顶层页面中,进入编写器文本的每种方式(轻点、键盘的上一项和下一项按钮、Tab、输入框的 focus())都会聚焦它而不滚动页面;使用鼠标或在 iframe 中时沿用浏览器自身的聚焦行为。如果页面设置了 interactive-widget=resizes-content,Android 上的 Chrome 会自行为键盘缩小页面,编写器停在键盘上方而不移动(参见浏览器支持指南)。',
description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过 density 属性控制密度。在可视视口会随软键盘缩小的浏览器中,编写器停在键盘上方,停留在最新消息的对话保持在最新消息。在 Android 上的 Chrome 中,编写器随 Gboard 的滑动同步移动。在 iOS 上的 Safari(浏览器中和添加到主屏幕的网页应用)和 Android 上的 Firefox 中,编写器会在浏览器报告键盘后移到键盘上方。在触摸屏上的顶层页面中,进入编写器文本的每种方式(轻点、键盘的上一项和下一项按钮、Tab、输入框的 focus())都会聚焦它而不滚动页面;使用鼠标或在 iframe 中时沿用浏览器自身的聚焦行为。当 scrollRef 指向文档时,iOS 上的 Safari(浏览器中和主屏幕网页应用)会自行将编写器显示在键盘上方,键盘的“完成”也不会把焦点停在编写器分组上。在其他没有 VirtualKeyboard API 的浏览器中,每次弹出键盘都沿用浏览器自身的聚焦,直到某次弹出让编写器被键盘遮住;编写器会在短暂等待后升到键盘上方,之后的弹出与其他情况相同。如果页面设置了 interactive-widget=resizes-content,Android 上的 Chrome 会自行为键盘缩小页面,编写器停在键盘上方而不移动(参见浏览器支持指南)。',
propDescriptions: {
children: '消息内容,通常是 ChatMessageList。在页面中自然流动。',
composer: '编写器元素,通常是 ChatComposer。固定在底部,带有毛玻璃底座。',
Expand All @@ -135,7 +135,7 @@ export const docsZh = {
export const docsDense = {
name: 'ChatLayout',
displayName: 'Chat Layout',
description: 'layout shell for full chat; msgs in page flow, composer fixed bottom w/ frosted glass dock; density prop (default balanced); scrollRef delegates to parent/body; in Chrome on Android (Gboard) the composer rides the software keyboard in step; in Safari on iOS and Firefox for Android it moves above the keyboard once reported (Chrome with interactive-widget=resizes-content: the page resizes, no move); transcript stays at newest msg, on a touch screen in a top-level page every focus route (tap, prev/next, Tab, focus()) w/o page scroll and Done rests focus on the composer group (no name, ring after keyboard input only, no tab stop); under a mouse or in an iframe: plain wrapper, browser focus',
description: 'layout shell for full chat; msgs in page flow, composer fixed bottom w/ frosted glass dock; density prop (default balanced); scrollRef delegates to parent/body; in Chrome on Android (Gboard) the composer rides the software keyboard in step; in Safari on iOS and Firefox for Android it moves above the keyboard once reported (Chrome with interactive-widget=resizes-content: the page resizes, no move); transcript stays at newest msg, on a touch screen in a top-level page every focus route (tap, prev/next, Tab, focus()) w/o page scroll and Done rests focus on the composer group (no name, ring after keyboard input only, no tab stop); under a mouse or in an iframe: plain wrapper, browser focus; scrollRef on the document in Safari on iOS: Safari reveals the composer, browser focus, no group; other browsers without the VirtualKeyboard API: browser focus until a show leaves the composer covered, which rises after a short wait',
usage: {
bestPractices: [
{guidance: true, description: 'Pass ChatMessageList as children and ChatComposer as composer prop for complete chat.'},
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Chat/ChatLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ export function ChatLayout({
{/* The composer's region. Under a finger in a top-level page,
useChatKeyboard makes it a group with no name, where focus rests
after the keyboard's Done. Elsewhere it is a plain container. */}
<KeyboardSurfaceContext value={true}>
<KeyboardSurfaceContext value={keyboard.isKeyboardSurface}>
<div
ref={keyboard.composerRef}
{...stylex.props(
Expand Down
Loading