Skip to content

spec(AST-099): one shared on-screen keyboard module owns measurement and keyboard riding - #8304

Merged
cixzhang merged 18 commits into
mainfrom
spec/keyboard-module
Oct 25, 2026
Merged

cixzhang merged 18 commits into
mainfrom
spec/keyboard-module

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Builds on the BottomSheet keyboard work by @imdreamrunner, which this record makes the shared authority.

Exact decision needed

Which module owns on-screen keyboard measurement and keyboard riding in Astryx, and what every keyboard-aware surface must do.

Existing authority searched

Decision

New system spec spec:AST-099, decided by Cindy (DEC-1–DEC-3):

  • FR1: every keyboard-aware Core and Lab component responds to the same keyboard height; architecture:react-component-runtime INV11 owns the single source, Core's keyboard module (packages/core/src/keyboard/).
  • FR2: keyboard height = layout viewport height − visual viewport height × scale, so a pan or a pinch zoom never reads as a keyboard.
  • FR3: no public CSS custom property, export, or prop exposes the height; components apply it on their own elements so layout is correct with no animation; riding surfaces (composer, scroll view, header) move in sync with no per-frame layout work and commit the resting value once. INV11 records the mechanism: one published transition, compositor translates, at most one layout read.
  • FR4: every focus route avoids panning the page; Done parks focus; a slipped scroll while scroll-locked is put back.
  • FR5: moving a surface dismisses the keyboard without a first-frame jump.
  • FR6: capability detection, never the user agent; a browser that reports the keyboard itself goes to FR8; a non-secure page follows FR2.
  • FR7: top-level documents only; inside an iframe the height is 0.
  • FR8 (DEC-2): in a browser that reports the keyboard itself, such as Chrome on Android, Core surfaces ride the keyboard's slide on show and hide within one frame, with one layout commit and no per-frame JavaScript; reduced motion follows design:reduced-motion DR2; Lab surfaces commit at rest; the app's viewport setting is never overridden outside keyboard-aware fields. Firefox for Android gets FR2–FR5.
  • DEC-3: iOS Simulator and Gboard on the Android emulator gate merge; physical iPhone and Android phones (Samsung Keyboard, real refresh timing) gate release.

component:BottomSheet gains one line: its on-screen keyboard behavior is owned by spec:AST-099.

Rejected alternatives

  • Each component measuring the keyboard by its own rule and predicting its motion.
  • A public CSS custom property for the height.
  • A one-step relayout on Chrome for Android.
  • Physical devices as a merge gate, or as optional follow-up.

Claim scope and non-goals

On-screen keyboard measurement, page-pan prevention, keyboard riding, and how components apply the height. Layer geometry (spec:AST-059), hardware keyboard access, the app's viewport meta, and native platforms are outside.

Evidence and implementation relationship

The keyboard module in #8217 implements this record: main's BottomSheet hook moved into packages/core/src/keyboard/, capability detection, the zoom-safe height, one listener set per window, the published transition, and drag to dismiss. One known way to meet FR8 is #8287's: opt into the VirtualKeyboard API's overlays-content mode while a keyboard-aware field has focus, read the keyboard's final rect once, and ride the Android IME curve (4.4 px RMS against seven recorded keyboard slides); the record leaves the mechanism to the implementation. Reference: Chrome's VirtualKeyboard API documentation (developer.chrome.com/docs/web-platform/virtual-keyboard).

  • Compatibility: no breaking change. Today's hook gates only its focus claim on an iOS user agent; under FR8 a fully expanded Tall sheet in Chrome on Android keeps its accommodation and rides the keyboard. BottomSheet.doc.mjs states each platform's behavior.

Writing rubric results

  • R1 narration: none.
  • R2 research in the record: none; the mechanism comparison stays out of the record.
  • R3 evidence naming a change: none; verification names test states.
  • R4 rejected alternatives over one line or outside a decision: none; each sits on one line inside DEC-1, DEC-2, or DEC-3.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none; the record has no open questions.

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge (CI lint)
  • Prettier
  • git diff --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 25, 2026 5:37am 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
🟢 Reviewed (for maintainers only) Oct 25, 2026, 6:14 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 24, 2026
@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

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8, DEC-1 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. Approval alone will not clear the review, because these edits are still needed:

  • FR3's Popover height cap conflicts with architecture:layer-runtime INV11 and component:Popover FR3; route the keyboard height through the layer runtime or drop Popover from FR3.
  • The browser-evidence line, the physical-device row and OQ1 conflict with spec:AST-013 FR10, which requires Safari on iOS evidence for the iOS claims; spec:AST-009 does not cover browser evidence.
  • FR1 and FR3 prescribe an internal module and StyleX variable, which architecture:knowledge-contracts INV18 keeps out of product contracts.
  • FR2's formula reports a keyboard while the page is pinch-zoomed; define the height so zoom alone gives 0 and add that state to the verification table.

[Automated review]

StyleX functional style on its own element, such as a bottom offset, a
padding, or a maximum height, so layout is correct with no animation running:
the chat composer, a search field placed at the bottom, and tab bar search
position from it, Popover caps its height with it, and a fully expanded sheet

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Popover capping its own height here conflicts with architecture:layer-runtime INV11 (one definition of the viewport caps; no consumer defines its own) and component:Popover FR3 (viewport capping is the layer runtime's). It also leaves placement blind to the keyboard, so the runtime can still open the Popover below its trigger and under the keyboard. Consider carrying the keyboard height into the layer runtime as a block-end inset, or dropping Popover from FR1 and FR3.

Comment on lines +119 to +121
- Browser evidence: DOM and visual-viewport tests prove FR1–FR7 in a top-level
document. Physical-device passes are recommended `spec:AST-009` follow-up
evidence, not a merge or release requirement.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-013 FR10 requires evidence from the actual browser for a named-browser claim (Chromium and Playwright WebKit do not stand in for Safari on iOS), and FR2–FR5 are stated for Safari on iOS. spec:AST-009 covers real assistive-technology observation and says it does not own browser support, so it cannot make device passes optional. The FR2–FR5 row and OQ1 need the same change.

Comment on lines +70 to +73
layout. Inside Core the module holds it in an internal StyleX variable that
Core does not export, and each consumer applies the hook's value through a
StyleX functional style on its own element, such as a bottom offset, a
padding, or a maximum height, so layout is correct with no animation running:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An internal StyleX variable and per-consumer StyleX functional styles are internal mechanism, which architecture:knowledge-contracts INV18 keeps out of product contracts unless the mechanism is itself public, and this one is explicitly internal. It also conflicts with line 76 (“No consumer reads a global CSS variable”) and with the Non-goals line on equivalent implementations. Consider stating the observable contract here and moving the mechanism to an architecture record.

Comment on lines +64 to +67
- **FR2 — The height a pan cannot corrupt.** The keyboard height is the layout
viewport height minus the visual viewport height, in CSS pixels, and 0 when
no keyboard is shown. A page pan or a visual-viewport offset is a pan to put
back (FR4), never a smaller keyboard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A pinch-zoom shrinks the visual viewport's height in CSS pixels by the zoom factor while the layout viewport keeps its height, so this formula reports a keyboard (400px at 2x on an 800px layout viewport) when none is shown. Consider defining the height so zoom alone yields 0 and adding a zoomed-without-keyboard state to the FR2 and FR6 verification rows.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Whether ChatLayout's composer moves with Gboard or jumps on Android depends on FR8, so here are measurements of FR8's setting next to the ride in #8287.

Chrome 124 with Gboard, Android 15 emulator, recorded at 540×1200, ChatLayout's full chat story on main with interactive-widget=resizes-content:

Question: on Android, should ChatLayout keep riding Gboard's slide (a measured difference under FR6/FR8), or is FR8's one-step relayout the intended behavior?

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1, the new architecture:react-component-runtime INV11, the architecture:layer-runtime INV11 amendment and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. Approval alone will not clear the review, because these edits are still needed:

  • The keyboard-aware cap changes spec:AST-059 FR2–FR4 and FR8 and layer-runtime INV12 without amending them, and a cap alone still lets a Popover open under the keyboard, so amend AST-059 here or drop the layer clause.
  • architecture:layer-runtime describes only the shipped runtime, so its keyboard-aware cap belongs in AST-099's Current-state impact until the change that ships it.
  • FR1 covers Lab components, but FR3 and INV11 keep the hook unexported, so say how packages outside Core conform.
  • FR8's Chrome on Android claim needs evidence from Chrome on Android under spec:AST-013 FR10.

[Automated review]

Comment on lines +75 to +77
and the layer runtime's viewport cap accounts for it, so Popover and other
anchor-mode layers stay above the keyboard (`architecture:layer-runtime`
INV11). How the value reaches styles is

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A cap alone does not keep a layer above the keyboard: spec:AST-059 FR4 still decides fit against the layout viewport, so a Popover opened below its trigger can land under the keyboard. spec:AST-059 FR2, FR3 and FR8 also still define the caps and the one runtime observation without it; consider amending AST-059 here (the keyboard as a block-end bound like a declared bar, and the measurement admitted in FR8) or dropping the layer clause.

Comment thread docs/architecture/layer-runtime.md Outdated
Comment on lines +371 to +373
app-declared inset have one definition in the runtime, and the caps account
for the on-screen keyboard height (`spec:AST-099` FR3); every anchor-mode
layer inherits them, the layer box never exceeds the caps, the toast viewport reads

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 record describes the runtime shipped on main (line 64), and AST-099's Current-state impact says the cap does not account for the keyboard today. Consider keeping this delta under AST-099's Current-state impact, as AST-059 did, and amending INV11 and INV12 in the change that ships it.

Comment on lines +56 to +63
- **FR1 — One keyboard height for every component.** Every Core and Lab
component that positions, sizes, or dismisses relative to the on-screen
keyboard responds to the same FR2 height at the same moment, including
BottomSheet, the chat composer, a search field placed at the bottom, tab bar
search, Popover and other anchor-mode layers, drag to dismiss, and any
keyboard layout guide, so no two components disagree about where the
keyboard is. `architecture:react-component-runtime` INV11 owns the single
source of that height.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR1 binds Lab components (the bottom search field and tab bar search in open work are Lab), but FR3 and react-component-runtime INV11 keep the hook unexported and forbid a component's own keyboard listener, and Lab reaches Core only through package exports, which architecture:public-component-api INV12 makes public. Consider scoping FR1 to Core and saying how packages outside Core conform, as INV10 does, or admitting a Lab-reachable export.

| FR5 | Surface move tests | sheet drag between heights, drag to dismiss, close, with a focused field | The field stays focused after a move starts, or content jumps on the first frame |
| FR6 | Capability tests with the user agent varied | visual viewport shrinking with a fixed layout viewport; platform keyboard geometry API present; neither | Behavior that changes with the user agent alone, or measurement where the platform geometry API applies |
| FR7 | Tests in an iframe and in a top-level document | the same keyboard event sequence in both | A non-zero value or a claimed focus inside an iframe |
| FR8 | Consumer-doc check | the documented viewport setting and its effect | Astryx setting the viewport meta, or the setting missing from the docs |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 makes a claim about Chrome on Android, and spec:AST-013 FR10 requires evidence from that browser; a consumer-doc check proves only the documentation. Consider making a Chrome on Android pass with the setting (emulator or device) the FR8 gate, or narrowing FR8 to documenting the option.

Comment on lines +144 to +146
hook's value: Core holds it in an internal StyleX variable, and each
consumer applies it through a StyleX functional style on its own element.
No component reads a global CSS variable for it or keeps its own

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A stylex.defineVars variable is emitted on :root (spec:AST-066 DEC-5), so "an internal StyleX variable" and "No component reads a global CSS variable for it" cannot both hold for that implementation. Consider saying consumers receive the height only as the hook's return value, applied on their own element through a functional style, with no custom property on the document root.


## Current-state impact

- BottomSheet's internal keyboard hook implements FR2, FR4, and FR5 for a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Today's hook compares visualViewport.height with innerHeight without the scale term FR2 now requires (packages/core/src/BottomSheet/useMobileKeyboard.ts:58-65), so it implements only the pan-safe part of FR2.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1, the new architecture:react-component-runtime INV11 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. That decision also settles the open Android question, since FR6 and FR8 rule out riding the keyboard's slide and leave a one-step relayout as the keyboard hides. Approval alone will not clear the review, because these edits are still needed:

  • INV11 gives each Lab component its own FR2 listener, but AST-099's FR1 verification row, DEC-1 and Intent reject separate measurements, so make them agree and say how Lab meets FR4 and FR5.
  • INV11's "internal StyleX variable" would be a :root custom property under spec:AST-066 DEC-5, which INV11 itself forbids, so say the height lives only in the hook.
  • Current-state impact should say today's hook lacks FR2's zoom term and that Android Tall sheets lose their keyboard inset under FR6 and FR8.
  • The FR8 Chrome on Android row needs a browser failure signal, and the metadata should name the consumer doc, list AST-099 in BottomSheet's system_specs and record this head's approver.

[Automated review]


| Contract | Verification | Representative states | Mutation or failure expectation |
| -------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FR1 | Source boundary check over Core and Lab plus a shared test that drives one keyboard change | each listed consumer; a new keyboard-aware component | A component with its own keyboard measurement or visual-viewport keyboard listener, or two components reading different heights for one keyboard |

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 row runs over Core and Lab and fails any component with its own keyboard listener, but architecture:react-component-runtime INV11 tells a Lab component to keep exactly one listener that measures by FR2, so a Lab surface built that way fails here. Consider checking the observable outcome (no two components report different heights for one keyboard) and leaving the listener check to INV11, scoped to Core; DEC-1 and the Intent need the same alignment.

one shared internal hook measures the on-screen keyboard, keeps the page
from panning on focus, and dismisses the keyboard when a surface moves
(`spec:AST-099`); it is not exported. Its height reaches styles only as the
hook's value: Core holds it in an internal StyleX variable, and each

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

stylex.defineVars emits its group on :root (spec:AST-066 DEC-5), so an internal StyleX variable is the global CSS variable that line 146 and this invariant's verification row forbid. Consider saying the height lives only in the hook and reaches CSS only through each consumer's functional style.


## Current-state impact

- BottomSheet's internal keyboard hook implements FR2, FR4, and FR5 for a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Today's hook never reads the visual viewport's scale (useMobileKeyboard.ts:58-65), so it implements only FR2's pan-safe part. Its inset is also not limited to iOS: only the focus claim checks the user agent (line 179), so FR6 and FR8 take away the keyboard room Android Tall sheets get today. Worth recording both here.

| FR5 | Surface move tests | sheet drag between heights, drag to dismiss, close, with a focused field | The field stays focused after a move starts, or content jumps on the first frame |
| FR6 | Capability tests with the user agent varied | visual viewport shrinking with a fixed layout viewport; platform keyboard geometry API present; neither | Behavior that changes with the user agent alone, or measurement where the platform geometry API applies |
| FR7 | Tests in an iframe and in a top-level document | the same keyboard event sequence in both | A non-zero value or a claimed focus inside an iframe |
| FR8 | Consumer documentation check plus Chrome on Android, emulator or device, with and without the viewport setting | the documented viewport setting and its effect | Astryx setting the viewport meta, or the setting missing from the docs |

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 Chrome on Android pass has no failure of its own in this row. Consider naming what fails in the browser, for example: with the setting a field ends under the keyboard or Astryx measures or claims focus; without it Astryx changes layout.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1, the new architecture:react-component-runtime INV11 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. That decision also settles the Android question raised here, since FR6 and FR8 rule out riding the keyboard's slide and leave a one-step relayout as the keyboard hides. Approval alone will not clear the review, because these edits are still needed:

  • INV11 says no CSS custom property carries the height, but the StyleX functional style it prescribes compiles to an inline custom property on the consumer's element, so limit the ban to the document root and shared ancestors and list the retirement of today's private --_sheet-keyboard-inset in Current-state impact.
  • INV11's verification row still fails "a second keyboard listener" with no Core scope, which the one listener per component outside Core that INV11 prescribes adds, and it names a variable that no longer exists.
  • FR6 exempts only browsers that expose their own keyboard geometry API while FR8 promises no Android handling at all, so on Firefox for Android, a Baseline browser without that API, Astryx would still act whenever that browser shrinks only its visual viewport; state FR8 in FR6's terms or name Firefox for Android's outcome and give it a verification row.

[Automated review]

Comment on lines +143 to +146
(`spec:AST-099`); it is not exported. Its height reaches styles only as the
hook's value, which lives only in the hook: each consumer applies it
through a StyleX functional style on its own element, and no CSS custom
property carries it. No Core component keeps its own visual-viewport

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

StyleX 0.19 compiles a functional style such as (keyboardHeight) => ({insetBlockEnd: keyboardHeight}) into an inline --x-insetBlockEnd on the element plus @property --x-insetBlockEnd { syntax: "*"; inherits: false; }, so every consumer built this way carries the height in a CSS custom property and fails "no CSS custom property carries it". Consider limiting the ban to custom properties on the document root or a shared ancestor, which is what the verification row's "global CSS variable" checks, and listing the retirement of today's private --_sheet-keyboard-inset in AST-099's Current-state impact.

| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced |
| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement, a consumer reading a global CSS variable for the height, an export of the hook or variable, or a listener left after unmount |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

INV11 gives each keyboard-aware component outside Core one listener of its own, and this record applies to packages/lab/src/, so two such components fail "A second keyboard listener or measurement" here. Consider "a Core component with its own keyboard listener or measurement, or a component outside Core with more than one", and "an export of the hook", since the height now lives only in the hook and there is no variable.

Comment on lines +94 to +96
from the user agent (`spec:AST-013` FR6). Where the platform exposes its own
keyboard geometry API (the Android path, FR8), Astryx neither measures nor
claims focus, and the keyboard height stays 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 exempts only a browser that exposes its own keyboard geometry API, while FR8 says Astryx adds no Android keyboard handling at all. Firefox for Android is a Baseline browser and has no VirtualKeyboard API, so under this test Astryx measures and claims focus there whenever that browser shrinks only its visual viewport for the keyboard, which FR8 and Current-state impact rule out. Consider stating FR8's outcome in FR6's capability terms, or naming Firefox for Android's intended outcome, and adding it to the verification table.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1, the new architecture:react-component-runtime INV11 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. That decision also settles the Android question raised here, since FR6 and FR8 keep the keyboard height at 0 on Chrome for Android and leave a one-step relayout as the keyboard hides. Approval alone will not clear the review, because these edits are still needed:

  • FR3's new ride names the hook's transition (start time, resting value, curve) and requires compositor-only motion with one layout read, which architecture:knowledge-contracts INV18 keeps in an architecture record, and it calls the ride optional while requiring every consumer to ride it, though INV11 gives components outside Core no transition.
  • Current-state impact says every Android Tall sheet loses its keyboard inset, but FR8 now gives Firefox for Android FR2–FR5, and Platform support names no Firefox for Android evidence, which spec:AST-013 FR10 requires.
  • The Chrome for Android change removes the Tall-sheet keyboard accommodation BottomSheet.doc.mjs documents for every platform, so name that doc in affects_consumer_docs and Current-state impact, and record the change as breaking under spec:AST-017 FR1 with the viewport setting as its migration.

[Automated review]

Comment on lines +76 to +84
keyboard is optional and layers on top of that layout. Each keyboard change is published, as
the hook's value, as one transition: a start time, a resting value, and a
curve. Every consumer, such as the composer, a scroll view, and a header,
rides that same transition with compositor animations, a translate to the
resting value, never JavaScript per frame, so they move in sync; each commits
the resting value once to layout, as an offset, a padding, or a spacer, so
nothing is hidden or unscrollable, with at most one layout read per keyboard
change. The transition is internal like the height: no public API or CSS
custom property exposes it.

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 half of FR3 names the hook's value and its shape (start time, resting value, curve) and prescribes compositor-only motion with at most one layout read, which architecture:knowledge-contracts INV18 keeps out of a system spec ("Internal ownership, seams, and mechanisms belong in an architecture record"). It also makes the ride required ("Every consumer … rides that same transition") right after calling it optional, as the Non-goals and DEC-1 also do, and INV11 gives components outside Core no transition to ride. Consider keeping only the observable outcome here, making the ride conditional or required in all three places, and moving the transition's shape and the compositor and layout-read rules into INV11.

Comment on lines +126 to +129
- Browser evidence: Safari on iOS, in the iOS Simulator or on a device and in a
top-level document, proves the iOS claims of FR2–FR5, and Chrome on Android,
in an emulator or on a device, proves FR8 (`spec:AST-013` FR10); DOM and
visual-viewport tests prove the rest of FR1–FR7. A pass on a physical

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Chrome on Android cannot prove FR8's Firefox for Android clause, and FR2–FR5 now also apply to Firefox for Android (lines 114-116). Consider naming Firefox for Android here and in the floor line above, as the verification row at line 166 already does under spec:AST-013 FR10.

Comment on lines +149 to +150
- On Android, a fully expanded Tall sheet loses its keyboard inset under FR6
and FR8: Android follows the app's viewport setting instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 now gives Firefox for Android FR2–FR5, so only Chrome for Android, or any browser exposing its own keyboard geometry API, loses the inset. That loss also removes behavior packages/core/src/BottomSheet/BottomSheet.doc.mjs documents for every platform ("Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation"), and an app keeps it only by adding interactive-widget=resizes-content, which spec:AST-017 FR1 treats as breaking. Consider naming that doc in affects_consumer_docs and here, and recording the change as breaking with the viewport setting as its migration.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1, the new architecture:react-component-runtime INV11 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. That decision also settles the Android question raised here, since FR6 and FR8 keep the keyboard height at 0 on Chrome for Android and leave a one-step relayout as the keyboard hides, and it accepts the Tall-sheet change on Chrome for Android that the record now classifies as breaking. Two small edits are still needed either way, and making them first lets an owner approve the final head once:

  • Platform support's floor line, OQ1 and the physical-device row still describe the Android split as Chrome only, so name Firefox for Android where FR8 gives it FR2–FR5 and label the Android Chrome device state as FR8 evidence.
  • INV11's verification row has no failure for a component outside Core that rides, and its list of failure signals runs two items together.

[Automated review]

Comment on lines +122 to +124
- Supported feature/engine floor: the package's supported browsers
(`spec:AST-013` FR3). FR2–FR5 apply to Safari on iOS, in the browser and as a
home-screen app; FR8 applies to Chrome on Android.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 also gives Firefox for Android FR2–FR5 (lines 116-118), and the evidence line below already names it, but this floor line still reads as if FR8 were Chrome only. OQ1 (lines 199-201) has the same gap, and the physical-device row (line 175) labels its Android Chrome state FR2–FR5, where FR6 and FR8 switch those off. Consider naming Firefox for Android here and in OQ1's gate sentence, and labelling that row FR2–FR5 and FR8.

| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced |
| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core, a consumer reading a CSS custom property on the root or a shared ancestor for the height, an export of the hook, a component outside Core measuring by a rule other than AST-099 FR2, or a listener left after unmount, a Core rider driven by JavaScript per frame or by its own transition, or more than one layout read per keyboard change |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

INV11 says a component outside Core does not ride (lines 153-154), but this row fails only a Core rider, so a Lab surface riding on its own timing passes. Consider adding "a component outside Core that rides", and fixing the list where "or a listener left after unmount, a Core rider driven by …" runs two failure signals together.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1–DEC-3, the new architecture:react-component-runtime INV11 and the new BottomSheet line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. That decision also covers this head's Chrome on Android ride (DEC-2) and its emulator merge gate and physical-phone release gate (DEC-3). These record edits are needed either way, and making them first lets an owner approve the final head once; the failing typecheck is in an untouched lab test that main has fixed, so a rebase clears it.

  • FR8 requires every keyboard-aware surface in Chrome on Android to ride, including Lab's bottom search field and tab bar search, while INV11 says a component outside Core does not ride, so the two records need to agree.
  • FR6 switches FR2 off in Chrome on Android, but FR8 names no height there and the FR1 and FR6 rows still fail any other measurement, and FR6's API test also catches desktop Chrome and misses pages that are not a secure context.
  • FR8's required, timed ride contradicts the Non-goals, FR3 and DEC-1 "optional polish" lines and needs a reduced-motion exception.
  • "Today's hook acts on iOS only" is false because only the focus claim checks the user agent, DEC-2's last sentence narrates a draft, DEC-3 should not cite spec:AST-009, and the BottomSheet line should state ownership rather than name a module.

[Automated review]

Comment on lines +111 to +115
- **FR8 — Android rides the keyboard smoothly.** In Chrome on Android,
keyboard-aware surfaces ride the keyboard on show and on hide: they move with
the keyboard's slide, within one frame of it on the measured keyboards, with
no one-step jump and no surface left under the keyboard, no JavaScript per
frame, and one layout commit at rest (FR3). Outside keyboard-aware fields,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 requires every keyboard-aware surface in Chrome on Android to ride with no one-step jump, and its verification row tests a bottom search field, but architecture:react-component-runtime INV11 says a component outside Core does not ride and fails it if it does. The bottom SearchField and TabBar search in open work are Lab components, so they cannot satisfy both. Consider scoping the ride to Core surfaces and stating the Lab outcome, or giving components outside Core a conforming ride in INV11.

Comment on lines +154 to +155
keyboard change. A component outside Core does not ride; it commits the
resting value once. Packages outside Core cannot import the hook; a

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 no-ride rule now conflicts with spec:AST-099 FR8, which requires every keyboard-aware surface in Chrome on Android to ride; the FR2 measurement prescribed a few lines below is also switched off there by FR6. Whichever way the owner decides, the two records need to agree.

Comment on lines +104 to +106
from the user agent (`spec:AST-013` FR6). Where the platform exposes its own
keyboard geometry API, as Chrome on Android does, the visual-viewport
measurement does not apply and FR8 governs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With FR2 switched off here, the record never says which height Chrome on Android surfaces share, and the FR1 row (line 171) and FR6 row (line 176) still fail any other measurement, including the geometry read FR8's ride needs. The test also reaches past Chrome on Android: Chromium exposes navigator.virtualKeyboard on desktop too, and only in a secure context, so desktop Chrome lands in FR8 and a plain-http page in Chrome on Android would fall back to FR2–FR5.

Comment on lines +49 to +51
- The curve or timing of an animation that moves a surface with the keyboard.
Such an animation is optional polish; under reduced motion it follows
`design:reduced-motion`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR8 makes the ride required in Chrome on Android with a timing bound ("within one frame"), which this line and the "optional polish" wording in FR3 and DEC-1 contradict. FR8 also has no reduced-motion exception, so with Reduce Motion on it would require the travel the reduced-motion design record forbids. Consider scoping these lines to exclude FR8, and adding the exception and a row state to FR8.

Comment on lines +159 to +162
- Today's hook acts on iOS only. Under FR8, a fully expanded Tall sheet in
Chrome on Android keeps the keyboard accommodation `BottomSheet.doc.mjs`
documents and moves with the keyboard, so no consumer loses behavior and no
breaking change applies; `BottomSheet.doc.mjs` states each platform's

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 hook's focus claim checks the user agent (useMobileKeyboard.ts:179, 449-454); the inset (:253, :339), the scroll put-back and the blur on travel and close run on every platform, so Android Tall sheets get keyboard room today. Since FR6 switches that measurement off in Chrome on Android, "no consumer loses behavior" holds only if FR8's ride ships with that switch, which is worth stating here.

Comment on lines +213 to +215
this record states only the motion and its limits. This replaces the earlier
choice of a one-step relayout on Chrome for Android, and with it the breaking
change that choice caused for Tall sheets.

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 sentence describes an earlier draft of this pull request rather than the system; architecture:knowledge-contracts INV10 keeps change history out of records. Consider deleting it.

Comment on lines +229 to +230
real devices. This narrowly clarifies `spec:AST-009` for this record: physical
devices are a release requirement here, not optional follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-009 covers real assistive-technology evidence and says it does not own browser support; its FR17 already lets an owning record require real-browser evidence for merge and release, and named-browser evidence belongs to spec:AST-013 FR10. Consider dropping this sentence or citing those clauses instead.

and records that sheets retain local scrim and swipe behavior.
- `spec:AST-025` owns effective scroll measurement, overflow and containment,
automatic keyboard-owner selection, and focus continuity.
- BottomSheet consumes the shared on-screen keyboard module (`spec:AST-099`).

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 an internal module in a component contract, which architecture:knowledge-contracts INV18 keeps in architecture records. Consider stating ownership like the neighbouring lines: "spec:AST-099 owns on-screen keyboard measurement, focus without a page pan, and keyboard dismissal when the sheet moves."

…oard and keeps the page still

New system spec: BottomSheet's keyboard hook becomes the single core keyboard
module. The height is the layout viewport minus the visual viewport, so a pan
cannot shrink it, exposed as --astryx-keyboard-inset; every focus route avoids
panning the page; moving a surface dismisses the keyboard; capability
detection, top-level documents only; Android through the app's
interactive-widget setting. component:BottomSheet consumes the module.
@vjeux

vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

A measured question on FR8's mechanism, before #8287's Android ride is rebuilt on the keyboard module.

FR8 takes the height from Chrome's keyboard geometry (navigator.virtualKeyboard, which needs overlaysContent). On the Android emulator (Chromium 158, Gboard, 12 show/hide cycles per mode), a page marker is painted on each report and compared with Gboard's slide in the recording:

overlaysContent + geometrychange default visual-viewport resize
keyboard shown 23 of 24 with the overlay set at touchstart (8 of 12 when set at focus) 12 of 12
first frame painted after the report, vs Gboard's first moved frame 0–67 ms after (median 40) 14–35 ms after (median 33)
side effects Chromium hides its bottom toolbar on that frame (+56 px of layout) none

Neither report comes before the slide, so a ride joins the slide in progress. The viewport report is earlier and tighter (a 21 ms spread against 67 ms), and its height (innerHeight − visualViewport.height × scale) matches the geometry's 311–312 px. Proposal: FR8 reads the visual-viewport report on Chrome, and the show joins Gboard's slide where it is.

Gap: with the app's interactive-widget=resizes-content, the layout viewport shrinks instead, so that report reads 0. Proposed cover: a FLIP.

Question: is the visual-viewport report acceptable as FR8's keyboard report, or should FR8 keep the geometry API?

Edit: the timing row is corrected; a frame-clock mapping error had put the viewport report before the slide.

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

This spec needs an engineering-owner decision before it can be approved: no current record covers on-screen keyboard behaviour, so spec:AST-099 FR1–FR8 and DEC-1–DEC-3, the new architecture:react-component-runtime INV11 and the new BottomSheet ownership line wait until an engineering owner comments /approve-spec <full head SHA> on this pull request, which the author can do as an engineering owner. These record edits are needed either way, and making them first lets an owner approve the final head once:

  • INV11 still tells components outside Core to measure by FR2, but FR6 and FR8 replace FR2 with the browser's keyboard report in Chrome on Android, so Lab's bottom search field and tab bar search have no conforming measurement there.
  • FR8's "the resting value commits at once" under reduced motion conflicts with design:reduced-motion DR2, which asks for an opacity-only cross-fade on a touch screen, and the FR8 rows have no reduced-motion state.
  • The FR8 verification row fails a bottom search field that does not ride, which is what FR8 asks of a Lab surface.
  • FR6 hands any browser that reports keyboard geometry to FR8, but FR8 covers only Chrome on Android, leaving desktop Chrome with an on-screen keyboard and pages that are not a secure context without an outcome; the description's Decision list, evidence paragraph and rubric R6 also still describe an earlier draft.

[Automated review]

Comment on lines +156 to +157
component there that responds to the keyboard keeps one listener that
measures as `spec:AST-099` FR2 defines, keeps focus from panning as FR4

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 outside-Core rule still says FR2 everywhere, but spec:AST-099 FR6 and FR8 replace FR2 with the browser's own keyboard report in Chrome on Android, and the FR1 row now accepts FR8 there. A Lab surface such as the bottom search field then fails this invariant's row (line 304) if it follows FR8, or measures by a rule FR6 switches off if it follows this line. Consider "measures as spec:AST-099 FR2 defines, or FR8 in Chrome on Android" here and in the row.

Comment on lines +119 to +121
frame, and one layout commit at rest (FR3). Under reduced motion nothing
travels with the keyboard (`design:reduced-motion`); the resting value
commits at once. A Lab surface commits the resting value without riding

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Chrome on Android runs on phones with a coarse primary pointer, where design:reduced-motion DR2 asks for an opacity-only cross-fade of 200 ms or less; an immediate change is DR3's fine-pointer rule. The Non-goals line already sends iOS rides to that record, and neither FR8 row has a reduced-motion state. Consider "under reduced motion the change follows design:reduced-motion" and a reduced-motion state in the FR8 row.

| FR5 | Surface move tests | sheet drag between heights, drag to dismiss, close, with a focused field | The field stays focused after a move starts, or content jumps on the first frame |
| FR6 | Capability tests with the user agent varied | visual viewport shrinking with a fixed layout viewport; Chrome on Android reporting keyboard geometry; a desktop browser with the API and no on-screen keyboard; a page that is not a secure context; neither | Behavior that changes with the user agent alone, or measurement where the platform geometry API applies |
| FR7 | Tests in an iframe and in a top-level document | the same keyboard event sequence in both | A non-zero value or a claimed focus inside an iframe |
| FR8 | Chrome on Android in the emulator with Gboard, top-level document, plus a consumer documentation check | keyboard show and hide with the composer, a bottom search field, and a Tall sheet; an app with `interactive-widget=resizes-content`, focus outside keyboard-aware fields | A surface jumping after the keyboard settles, lagging the keyboard by more than a frame, left under the keyboard, moved by JavaScript per frame, or committing layout more than once; the app's viewport setting overridden outside keyboard-aware fields; or the setting missing from the docs |

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 row drives a bottom search field and fails a surface that jumps after the keyboard settles or is left under it, but FR8 now says a Lab surface commits the resting value without riding, and the only bottom search field is the Lab one in #8216. Consider limiting the ride failures to Core surfaces and giving the Lab field its own one-commit-at-rest state.

Comment on lines +105 to +108
from the user agent (`spec:AST-013` FR6). Where the browser itself reports
the on-screen keyboard's geometry, as Chrome on Android does, the
visual-viewport measurement does not apply and FR8 governs; a desktop browser
that offers the same API but shows no on-screen keyboard reads 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.

FR6 hands any browser that reports keyboard geometry to FR8, but FR8 covers only Chrome on Android. Desktop Chrome exposes the same API in a secure context, so one that does show an on-screen keyboard (a touchscreen laptop or tablet) gets no outcome, and on a page that is not a secure context Chrome exposes no navigator.virtualKeyboard, so FR8's "browser's own report" does not exist there (the FR6 row lists that state). Consider keying FR8 on this capability and saying that a page without the report gets FR2–FR5.

…ced motion follows DR2; Lab measurement under FR8; verification states
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 3c9ad90

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval 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.

Thanks for tightening this: an engineering owner's approval on this head settles the direction for AST-099, INV11 and the BottomSheet line, and the knowledge check passes. Two small record edits remain, both in the measurement rule, and because a new commit clears the approval, an engineering owner will need to re-approve the updated head.

  • FR1 (line 62) and INV11's verification row in react-component-runtime.md still require FR2, while FR8 and INV11's own text let a component take the browser's keyboard report, so a Lab search field that follows INV11 would fail that row.
  • FR1's verification row accepts FR8 only "in Chrome on Android", but FR6 and FR8 now cover every browser that reports the keyboard; saying "where FR8 applies" there, in FR3 and in Platform support would line them up.

[Automated review]

| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced |
| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |

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 row still fails "a component outside Core measuring by a rule other than AST-099 FR2", but INV11's text above (lines 156-158) now lets that component take the browser's keyboard report where FR8 applies, and AST-099's FR1 row counts FR8 as a measurement rule. A Lab bottom search field or tab bar search that follows the text in Chrome on Android would fail this row. Suggest: "measuring by a rule other than AST-099 FR2 or, where FR8 applies, the browser's keyboard report".


- **FR1 — One keyboard height for every component.** Every Core and Lab
component that positions, sizes, or dismisses relative to the on-screen
keyboard responds to the same FR2 height at the same moment, including

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 same FR2 height": in a browser that reports the keyboard, FR6 and FR8 replace FR2 with that report (lines 105-107, 115-118), so this sentence contradicts them. Suggest "the same keyboard height (FR2, or the browser's report where FR8 applies)".


| Contract | Verification | Representative states | Mutation or failure expectation |
| ------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| FR1 | Source boundary check over Core and Lab plus a shared test that drives one keyboard change | each listed consumer; a new keyboard-aware component | Two components reading different heights for one keyboard, or a component measuring by a rule other than FR2 or, in Chrome on Android, FR8 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"in Chrome on Android, FR8" is narrower than FR6, which hands FR8 every browser that reports the keyboard "on any device that shows an on-screen keyboard" (lines 105-108); FR6's own row lists a desktop browser with the API. A surface that follows FR8 in desktop Chrome with a touch keyboard would fail this row. Suggest "where FR8 applies, FR8", with the same wording in FR3 (lines 80-81) and Platform support (lines 136-137).

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 1926fd9

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

`placement="bottom"` is the search field iOS 26 puts in a phone's bottom
toolbar (UIKit's `.integrated`): 38 px tall, 33 px from each side, in a bar
the app keeps at the bottom of the viewport.

Keyboard (spec:AST-099 at meta#8304's head 3c9ad90): layout only. The
field's own listener reads the keyboard's height and a functional style
sets the bar's `bottom` from it: while active the field's centre stays
36 px above the keyboard's top, otherwise the bar keeps its resting gap. A
Lab surface commits the resting value and never rides the keyboard (FR8,
react-component-runtime INV11), so each report moves the bar at once, with
no animation and no prediction, under reduced motion too.
- Where the keyboard covers the page (Mobile Safari), the height is FR2's
  visual-viewport measurement.
- Where the browser reports the keyboard itself (Chrome on Android, FR8),
  the field reads that report: while it has focus the keyboard overlays
  the page (`overlaysContent`) and each `geometrychange` gives the height;
  after the blur the overlay goes back once Chrome reports the keyboard
  down, so a tap that blurred the field (the close button) still lands
  where it began, and the app's own viewport setting holds everywhere else.

No pan (FR4): where the keyboard covers the page, a tap on the field's
text is claimed and focuses it with `preventScroll`, putting the caret
where the finger lifted, and that tap's late mouse events are swallowed
(from the delivered touchend until its click, the next touch, pointer or
key, or unmount). In Chrome on Android the tap stays the browser's: only
its own tap brings the keyboard up (a focus from script in the tap does
not, on the Android emulator), and with the keyboard overlaying the page
that focus pans nothing.

Motion under a finger: activating swells the field, narrows it past its
place and grows a 38 px glass close button in from inside the field's end;
cancelling narrows the field 16 px at each side, then springs it out while
the button shrinks and fades. SwiftUI draws these frame by frame with no
animation object, so the field plays the measured frames
(`uikit.search.bottom`) from the frame that presents it at rest, and a
change of mind adds an eased turn to the curves on screen, on the whole
clock. Under a mouse or a trackpad the parts take their places at once.

Stories: Bottom and BottomRightToLeft. Proofs: jsdom geometry and lift,
no prediction on blur, Chrome's report read while it has focus and until
the keyboard is down; Chromium and WebKit replay the measured frames
within 0.5 px, move the bar at once on each report (Mobile Safari's and
Chrome's) with no animation, keep it up until the hide is reported, claim
the tap where Safari's keyboard covers the page and swallow its late mouse
events, and keep Reduce Motion to opacity fades.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…d (AST-099 FR8)

Measured on the Android 15 emulator (Chromium 158, Gboard) by recording
the screen and reading Gboard's top in every frame, with a marker the page
paints on each report: Gboard slides up over the page in about 267 ms
(cubic-bézier(0.1, 0, 0, 1), within 2.4 px rms over 20 shows) and down in
about 167 ms. Chrome reports each slide once, through the visual viewport,
just after it starts: the report's first painted frame trails Gboard's
first moved frame by 14 to 35 ms (median 33, twelve shows). Until now the
composer sat under the rising keyboard until that report and then jumped,
the one-step relayout spec:AST-099's DEC-2 rejects.

The keyboard module now measures Chrome's visual viewport under a finger
(FR8, proposed; pending owner review on meta#8304), and the keyboard layout
guide reads Chrome's schedule in a browser with the VirtualKeyboard API (a
capability, keyboardProfileOf): a show rides Gboard's show curve from the
report, dated back by androidKeyboard.showReportLagMs so it joins the slide
where it is; a hide rides Gboard's hide curve from the blur (predicted) or
from its report (Back); focus moving between fields holds Chrome's
momentary hide. rideKeyboard plays the keyboard's own motion: UIKit's
spring on iOS, Gboard's slides in Chrome, theme roles all of them
(androidKeyboard.showMs, showCurve, hideMs, hideCurve), registered by the
touch module before its first ride. Under Reduce Motion nothing travels
(design:reduced-motion).

navigator.virtualKeyboard with overlaysContent (the keyboard's own
geometry) was measured and not used: its geometrychange was painted 0 to
67 ms after Gboard started (against 14 to 35 ms), it failed to show the
keyboard in 4 of 12 shows when set on focus, and it hides Chromium's
bottom toolbar on the keyboard's frame (+56 px of layout). The full chat
stories fit the visible height (100dvh, less the preview's padding), so
the composer rests on screen in Chrome and Safari.

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

Thanks for the quick turnaround: FR1, FR3, Platform support and INV11 now accept FR8 wherever it applies, and an engineering owner's approval on this head settles the direction for AST-099, INV11 and the BottomSheet line. One record edit remains, and because a new commit clears the approval, an engineering owner (the author included) re-approves the updated head by commenting /approve-spec <full head SHA>.

  • The Firefox for Android row in AST-099's verification table (line 191) covers focus routes only, while Platform support (lines 143-144) says Firefox for Android proves FR2–FR5; adding the Safari row's pan, pinch-zoom and moved-sheet states and its "height is wrong after a pan or zoom" failure, or narrowing that Platform support line, would line them up.

[Automated review]

| FR6 | Capability tests with the user agent varied | visual viewport shrinking with a fixed layout viewport; Chrome on Android reporting keyboard geometry; a desktop browser with the API and no on-screen keyboard; a page that is not a secure context; neither | Behavior that changes with the user agent alone, or measurement where the platform geometry API applies |
| FR7 | Tests in an iframe and in a top-level document | the same keyboard event sequence in both | A non-zero value or a claimed focus inside an iframe |
| FR8 | Chrome on Android in the emulator with Gboard, top-level document, plus a consumer documentation check | keyboard show and hide with the composer and a Tall sheet; a Lab bottom search field; reduced motion on; an app with `interactive-widget=resizes-content`, focus outside keyboard-aware fields | A Core surface jumping after the keyboard settles, lagging the keyboard by more than a frame, left under the keyboard, moved by JavaScript per frame, or committing layout more than once; a Lab surface riding instead of committing at rest; anything travelling with the keyboard under reduced motion instead of DR2's cross-fade; the app's viewport setting overridden outside keyboard-aware fields; or the setting missing from the docs |
| FR8 | Firefox for Android, emulator or device, top-level document | focus routes with the keyboard shown and hidden | The page lurches or the field ends under the keyboard |

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 row is the only Firefox for Android merge-gate evidence, but Platform support (lines 143-144) says Firefox for Android proves FR2–FR5, and the Safari row below proves the same claims with "page panned and pinch-zoomed, a sheet moved" and fails "the height is wrong after a pan or zoom". Suggest giving this row those states and that failure and labelling it FR2–FR5, or narrowing lines 143-144 to the focus routes this row proves.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 986dad0

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

This spec is ready to merge: an engineering owner approved spec:AST-099, react-component-runtime INV11 and the BottomSheet ownership line at this exact head, and the Firefox for Android row now proves the FR2–FR5 states that Platform support claims. Two optional fixes if you touch the record again: cite AST-013 FR1–FR3 on Platform support's floor line, since FR3 alone names only desktop Chrome and Safari on iOS, and say where focus goes when FR5 blurs a field during a drag between heights. For the implementing PRs, the FR8 row's per-frame JavaScript and layout-commit failures only show in a performance trace beside the emulator recording, and a short reply to the open question about FR8's keyboard report on this thread would help.

[Automated review]

Comment on lines +135 to +136
- Supported feature/engine floor: the package's supported browsers
(`spec:AST-013` FR3). FR2–FR5 apply to Safari on iOS, in the browser and as a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: spec:AST-013 FR3 names only the two explicit rows (latest desktop Chrome and Safari on iOS). Chrome and Firefox on Android, which this record names, come from FR1's Baseline tier, so citing AST-013 FR1–FR3 here would match the browsers below.

Comment on lines +97 to +102
- **FR5 — Moving a surface dismisses the keyboard.** When a surface that holds
the focused control starts to move, such as a sheet dragged between heights,
a drag to dismiss, or a close, the control is blurred so the keyboard
dismisses, and the surface keeps its keyboard layout until the visual
viewport has recovered, so content does not jump on the first frame of the
move.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: FR5 blurs the field when a surface starts to move but does not say where focus goes. On a close the sheet's dialog returns focus; on a drag between heights nothing defines it, and a plain blur leaves a screen-reader user's focus on the page body. Today's hook parks focus on the sheet, as FR4 does after Done; stating that here would close the gap.

@cixzhang
cixzhang enabled auto-merge (squash) October 25, 2026 06:14
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…d (AST-099 FR8)

Measured on the Android 15 emulator (Chromium 158, Gboard) by recording
the screen and reading Gboard's top in every frame, with a marker the page
paints on each report: Gboard slides up over the page in about 267 ms
(cubic-bézier(0.1, 0, 0, 1), within 2.4 px rms over 20 shows) and down in
about 167 ms. Chrome reports each slide once, through the visual viewport,
just after it starts: the report's first painted frame trails Gboard's
first moved frame by 14 to 35 ms (median 33, twelve shows). Until now the
composer sat under the rising keyboard until that report and then jumped,
the one-step relayout spec:AST-099's DEC-2 rejects.

The keyboard module now measures Chrome's visual viewport under a finger
(FR8, proposed; pending owner review on meta#8304), and the keyboard layout
guide reads Chrome's schedule in a browser with the VirtualKeyboard API (a
capability, keyboardProfileOf): a show rides Gboard's show curve from the
report, dated back by androidKeyboard.showReportLagMs so it joins the slide
where it is; a hide rides Gboard's hide curve from the blur (predicted) or
from its report (Back); focus moving between fields holds Chrome's
momentary hide. rideKeyboard plays the keyboard's own motion: UIKit's
spring on iOS, Gboard's slides in Chrome, theme roles all of them
(androidKeyboard.showMs, showCurve, hideMs, hideCurve), registered by the
touch module before its first ride. Under Reduce Motion nothing travels
(design:reduced-motion).

navigator.virtualKeyboard with overlaysContent (the keyboard's own
geometry) was measured and not used: its geometrychange was painted 0 to
67 ms after Gboard started (against 14 to 35 ms), it failed to show the
keyboard in 4 of 12 shows when set on focus, and it hides Chromium's
bottom toolbar on the keyboard's frame (+56 px of layout). The full chat
stories fit the visible height (100dvh, less the preview's padding), so
the composer rests on screen in Chrome and Safari.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…ve the keyboard

`placement="bottom"` is the search field iOS 26 puts in a phone's bottom
toolbar (UIKit's `.integrated`): 38 px tall, 33 px from each side, in a bar
the app keeps at the bottom of the viewport.

Keyboard (spec:AST-099 at meta#8304's head 3c9ad90): layout only. The
field's own listener reads the keyboard's height and a functional style
sets the bar's `bottom` from it: while active the field's centre stays
36 px above the keyboard's top, otherwise the bar keeps its resting gap. A
Lab surface commits the resting value and never rides the keyboard (FR8,
react-component-runtime INV11), so each report moves the bar at once, with
no animation and no prediction, under reduced motion too.
- Where the keyboard covers the page (Mobile Safari), the height is FR2's
  visual-viewport measurement.
- Where the browser reports the keyboard itself (Chrome on Android, FR8),
  the field reads that report: while it has focus the keyboard overlays
  the page (`overlaysContent`) and each `geometrychange` gives the height;
  after the blur the overlay goes back once Chrome reports the keyboard
  down, so a tap that blurred the field (the close button) still lands
  where it began, and the app's own viewport setting holds everywhere else.

No pan (FR4): where the keyboard covers the page, a tap on the field's
text is claimed and focuses it with `preventScroll`, putting the caret
where the finger lifted, and that tap's late mouse events are swallowed
(from the delivered touchend until its click, the next touch, pointer or
key, or unmount). In Chrome on Android the tap stays the browser's: only
its own tap brings the keyboard up (a focus from script in the tap does
not, on the Android emulator), and with the keyboard overlaying the page
that focus pans nothing.

Motion under a finger: activating swells the field, narrows it past its
place and grows a 38 px glass close button in from inside the field's end;
cancelling narrows the field 16 px at each side, then springs it out while
the button shrinks and fades. SwiftUI draws these frame by frame with no
animation object, so the field plays the measured frames
(`uikit.search.bottom`) from the frame that presents it at rest, and a
change of mind adds an eased turn to the curves on screen, on the whole
clock. Under a mouse or a trackpad the parts take their places at once.

Stories: Bottom and BottomRightToLeft. Proofs: jsdom geometry and lift,
no prediction on blur, Chrome's report read while it has focus and until
the keyboard is down; Chromium and WebKit replay the measured frames
within 0.5 px, move the bar at once on each report (Mobile Safari's and
Chrome's) with no animation, keep it up until the hide is reported, claim
the tap where Safari's keyboard covers the page and swallow its late mouse
events, and keep Reduce Motion to opacity fades.
@cixzhang
cixzhang merged commit c899f0b into main Oct 25, 2026
67 checks passed
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…ve the keyboard

`placement="bottom"` is the search field iOS 26 puts in a phone's bottom
toolbar (UIKit's `.integrated`): 38 px tall, 33 px from each side, in a bar
the app keeps at the bottom of the viewport.

Keyboard (spec:AST-099 at meta#8304's head 3c9ad90): layout only. The
field's own listener reads the keyboard's height and a functional style
sets the bar's `bottom` from it: while active the field's centre stays
36 px above the keyboard's top, otherwise the bar keeps its resting gap. A
Lab surface commits the resting value and never rides the keyboard (FR8,
react-component-runtime INV11), so each report moves the bar at once, with
no animation and no prediction, under reduced motion too.
- Where the keyboard covers the page (Mobile Safari), the height is FR2's
  visual-viewport measurement.
- Where the browser reports the keyboard itself (Chrome on Android, FR8),
  the field reads that report: while it has focus the keyboard overlays
  the page (`overlaysContent`) and each `geometrychange` gives the height;
  after the blur the overlay goes back once Chrome reports the keyboard
  down, so a tap that blurred the field (the close button) still lands
  where it began, and the app's own viewport setting holds everywhere else.

No pan (FR4): where the keyboard covers the page, a tap on the field's
text is claimed and focuses it with `preventScroll`, putting the caret
where the finger lifted, and that tap's late mouse events are swallowed
(from the delivered touchend until its click, the next touch, pointer or
key, or unmount). In Chrome on Android the tap stays the browser's: only
its own tap brings the keyboard up (a focus from script in the tap does
not, on the Android emulator), and with the keyboard overlaying the page
that focus pans nothing.

Motion under a finger: activating swells the field, narrows it past its
place and grows a 38 px glass close button in from inside the field's end;
cancelling narrows the field 16 px at each side, then springs it out while
the button shrinks and fades. SwiftUI draws these frame by frame with no
animation object, so the field plays the measured frames
(`uikit.search.bottom`) from the frame that presents it at rest, and a
change of mind adds an eased turn to the curves on screen, on the whole
clock. Under a mouse or a trackpad the parts take their places at once.

Stories: Bottom and BottomRightToLeft. Proofs: jsdom geometry and lift,
no prediction on blur, Chrome's report read while it has focus and until
the keyboard is down; Chromium and WebKit replay the measured frames
within 0.5 px, move the bar at once on each report (Mobile Safari's and
Chrome's) with no animation, keep it up until the hide is reported, claim
the tap where Safari's keyboard covers the page and swallow its late mouse
events, and keep Reduce Motion to opacity fades.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…d (AST-099 FR8)

Measured on the Android 15 emulator (Chromium 158, Gboard) by recording
the screen and reading Gboard's top in every frame, with a marker the page
paints on each report: Gboard slides up over the page in about 267 ms
(cubic-bézier(0.1, 0, 0, 1), within 2.4 px rms over 20 shows) and down in
about 167 ms. Chrome reports each slide once, through the visual viewport,
just after it starts: the report's first painted frame trails Gboard's
first moved frame by 14 to 35 ms (median 33, twelve shows). Until now the
composer sat under the rising keyboard until that report and then jumped,
the one-step relayout spec:AST-099's DEC-2 rejects.

The keyboard module now measures Chrome's visual viewport under a finger
(FR8, proposed; pending owner review on meta#8304), and the keyboard layout
guide reads Chrome's schedule in a browser with the VirtualKeyboard API (a
capability, keyboardProfileOf): a show rides Gboard's show curve from the
report, dated back by androidKeyboard.showReportLagMs so it joins the slide
where it is; a hide rides Gboard's hide curve from the blur (predicted) or
from its report (Back); focus moving between fields holds Chrome's
momentary hide. rideKeyboard plays the keyboard's own motion: UIKit's
spring on iOS, Gboard's slides in Chrome, theme roles all of them
(androidKeyboard.showMs, showCurve, hideMs, hideCurve), registered by the
touch module before its first ride. Under Reduce Motion nothing travels
(design:reduced-motion).

navigator.virtualKeyboard with overlaysContent (the keyboard's own
geometry) was measured and not used: its geometrychange was painted 0 to
67 ms after Gboard started (against 14 to 35 ms), it failed to show the
keyboard in 4 of 12 shows when set on focus, and it hides Chromium's
bottom toolbar on the keyboard's frame (+56 px of layout). The full chat
stories fit the visible height (100dvh, less the preview's padding), so
the composer rests on screen in Chrome and Safari.
@github-actions
github-actions Bot deleted the spec/keyboard-module branch October 26, 2026 06:31

This branch was successfully deployed

1 active deployment
Preview — 986dad05 Deployed Oct 25, 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants