Skip to content

spec(AST-099): Core text inputs stay above the on-screen keyboard - #9750

Merged
cixzhang merged 12 commits into
mainfrom
spec/inputs-work-with-the-keyboard
Nov 4, 2026
Merged

cixzhang merged 12 commits into
mainfrom
spec/inputs-work-with-the-keyboard

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

On a phone, a focused field never hides behind the keyboard in the page, in a Core sheet or dock, in a scroller, or in an app's bar docked to the bottom edge. That bar moves up and back with the keyboard.

Exact decision needed

None open. On 2026-11-03 the owner decided:

  • Core inputs work with the keyboard. Keeping a focused field above the on-screen keyboard is the expected default for every Core field that brings up a keyboard on a touch device. It is internal to the field, with no prop and no public API.
  • Riding the keyboard is possible for every Core field. A field docked at the bottom of the viewport moves its docked container with the keyboard.
  • Scope: an app-owned container moves only when it is fixed and docked to the viewport's bottom edge. A fixed container that fills the viewport never moves. Page content Core reveals or moves is never panned or translated. A field in scrolling content is scrolled into place by its nearest scroller instead, and a field near a scroller's end gets the missing scroll range while the keyboard shows.

This PR records these decisions and amends architecture:react-component-runtime INV11 to admit the one write on an app element. The branch includes current main (PowerSearch's DEC-3).

Existing authority searched

  • spec:AST-099 (on-screen keyboard): owns the keyboard height, focus without page pan, dismissal, and the ride (FR8, DEC-2), but only for keyboard-aware surfaces.
  • spec:AST-059 FR9: the 10 px keyboard clearance for layers, reused here for page fields and Core hosts.
  • architecture:react-component-runtime INV2 and INV11. INV11 is amended here and holds every mechanism.
  • architecture:knowledge-contracts INV18: product records state observable behavior only.
  • family:input-fields: no keyboard-position rule. FR12 covers keyboard hints only.
  • component:PowerSearch DEC-3: the sheet flow is the adaptive default on compact coarse-pointer screens.
  • Code on main: each covered field and host listed below, BottomSheet/useMobileKeyboard.ts, and the absence of packages/core/src/keyboard/.
  • Overlapping open work (implementation, not authority): #8559 (keyboard module), #8176 (chat composer rides the keyboard), #8208 (anchored layers above the keyboard). None records this rule.

Decision

spec:AST-099 FR9 and DEC-5, in observable terms. When the on-screen keyboard opens, a focused Core text-entry field stays visible above it, and the app does nothing, in two cases:

  • (a) A field in scrolling content, or in a Core host (a sheet, the chat composer's dock). The field sits 10 CSS px above the keyboard. Its nearest scroller (the document included) scrolls it into place in the frame the keyboard is reported. If the scroller cannot scroll far enough, it gains scroll range at its end while the keyboard shows. A Core host moves itself; the field is not moved twice. This holds for a scroller inside any fixed container, including one FR9 leaves to the app.
  • (b) A field in an app's fixed bar docked to the bottom edge, with no translate of its own.
    • Docked means the bar's layout box, before any transform, sits at the viewport's bottom edge.
    • Some docked containers would not fit above the keyboard once moved, such as a full-height side drawer or a pane below a fixed header. If a scroller holds the field, the scroller reveals it and the container does not move. If not, the container moves only as far as the focused field needs to clear the keyboard, never more than its own overlap. The field stays in view even if the container's top leaves the screen.
    • The bar moves by the distance from its bottom as drawn (after the app's own lift) to the keyboard's top. That distance is measured after each keyboard report and focus move, once the page's own listeners have run. The bar's bottom meets the keyboard's top: no 10 px gap.
    • Core leaves a bar the app is moving with a CSS transition or animation that is running when Core measures. A CSS lift the app starts after Core has measured can be lifted twice.
    • Core cannot see a bar moved from script each frame. An app that does this positions the bar with its own translate, which Core never moves.
    • The move adds to the app's transform. It is gone once the keyboard hides after the field loses focus, or once the field unmounts. Core changes nothing else on app elements.

FR9 leaves some fixed containers to the app: one floating above the bottom edge, one with its own translate, or a full-viewport one.

  • A field inside such a container that a scroller holds follows rule (a).
  • Only a field with no scroller between it and the container keeps the browser's own focus reveal, which in Safari may pan the page.
  • Bars that position with translate don't ride yet; use transform or no translate.

No prop, option, context, or other public API. Mechanism (the keyboard module, its offset constant, the translate write, the too-tall and measuring rules, the own-element scroll-range offset, the focus-handler start) lives only in INV11.

Covered fields, each checked on main: TextInput, TextArea, NumberInput; the typed fields of DateInput, DateTimeInput, and TimeInput; the Typeahead and Tokenizer inputs (BaseTypeahead) and Tokenizer's sheet search field; the Selector and MultiSelector search row; PowerSearch's query input, popover value-editor fields, and its sheet flow's search and value fields; CommandPalette's search input; ChatComposer's input. Outside the rule (no keyboard): read-only touch picker fields, native date/time pickers, DateRangeInput's trigger, FileInput, Slider, and Selector and ComplexSelector triggers.

Rejected alternatives

  • An opt-in prop or per-component keyboard handling. An app would have to remember to turn it on, fields would drift apart, and the public API would grow.
  • Leaving an app's docked bar to the browser's reveal. The bar would jump or hide under the keyboard.

Claim scope and non-goals

  • Records the rule, the ride, its scope, the covered fields, and the gaps only.
  • Popover and anchored-layer geometry stays with spec:AST-059.
  • Lab fields follow INV11's rule for components outside Core.
  • Follow-up, outside FR9: riding floating bars and composing with an app's own translate.

Changed records

  • docs/specs/AST-099-mobile-keyboard/spec.md:

    • Intent sentence and FR9 narrowed to cases (a) and (b).
    • The height bound for docking, the scroller rule inside app-left containers, and the CSS- and script-moved bar outcomes.
    • The FR3 exception sentence, the FR4 row's scope, and current-state impact. translate bars and floating bars do not ride; a field a scroller holds in them is still revealed. Too-tall docked containers move only as far as a scroller-less field needs.
    • Implementation gaps (11) plus a Follow-up note, four FR9 verification rows (TBD), DEC-5, affects_families, and path applicability. Both FR9 test rows fail a scroller-held field left under the keyboard; the ride row adds a full-height drawer with a scroller (no move), and a tall pane with a field near its top (no move) or bottom (just enough).
  • docs/architecture/react-component-runtime.md: INV11 admits a translate on an app's docked fixed container under these conditions:

    • its own translate is none;
    • no CSS motion of the app's own is running on its position.

    The translate equals the drawn distance to the keyboard's top, measured after each keyboard report and focus move, after the page's listeners. It rides the hook's one transition and is removed once the keyboard hides after focus leaves, or on unmount. When the moved box would not fit, it writes no translate if a scroller holds the field, otherwise only as far as the field needs. Its verification row adds the too-tall layouts. spec:AST-099/DEC-5 is added to its deciding specs.

  • docs/families/input-fields.md: FR13 narrowed the same way, citing spec:AST-099 FR9. Its row fails a field under the keyboard in any scroller, a Core host, or a docked bar Core moves. Also an implementation-gap bullet and spec:AST-099/DEC-5 in deciding_specs.

Gap list (implementation gaps on main)

No covered field meets FR9 today. 11 gaps, all open:

  1. Keyboard module: packages/core/src/keyboard/ does not exist. Only BottomSheet's hook handles the keyboard. It acts only in a fully expanded Tall sheet and gates focus on an iOS user agent.
  2. TextInput, TextArea, NumberInput: no keyboard handling. In the page, only the browser's own reveal scroll helps.
  3. DateInput, DateTimeInput, TimeInput typed fields: no keyboard handling.
  4. Typeahead and Tokenizer inputs: no keyboard handling. BaseTypeahead reads the visual viewport itself to size its list.
  5. Tokenizer's sheet search field: partly met. Covered only in a fully expanded Tall sheet on an iOS user agent.
  6. Selector and MultiSelector search row: on touch it sits in a hug sheet, which the hook does not cover.
  7. PowerSearch: on compact coarse-pointer screens its default is the sheet flow (DEC-3), in Tall sheets partly covered as in Gap 5. The query input and popover value editor render there only with presentation="popover", and have no handling.
  8. CommandPalette search input: in a Dialog, no handling.
  9. ChatComposer: ChatLayout docks it fixed or sticky at the bottom, and nothing applies the keyboard height. On iOS it stays under the keyboard.
  10. App-built fixed hosts: a covered field in an app's fixed bottom bar, a fixed Toolbar, or a non-Tall or partly expanded sheet has no handling.
  11. Docked ride and page-content reveal: no covered field rides the keyboard. Nothing moves an app's docked bar to the keyboard's top, and no field scrolls into place 10 px above the keyboard in the keyboard's frame. The missing scroll range is not added yet. Until it is, an element that has nothing to scroll does not count as a scroller, and the field keeps the browser's own reveal.

Follow-up (not a gap, outside FR9): riding floating bars and composing with an app's own translate.

Evidence and implementation relationship

Spec only. Evidence is current source on main. Implementation follows in separate PRs against FR9, starting with the keyboard module.

Writing rubric results

  • R1 narration: none. One current-state line names the change FR9 needs, as that section asks. FR9, DEC-5, FR13, INV11, and the gap list state the system in the present tense.
  • R2 research in the record: none.
  • R3 evidence naming a change: none. Gaps cite current source facts.
  • R4 rejected alternatives over one line or outside a decision: two, both on DEC-5's single Rejected: line.
  • R5 pull request or issue references remaining: none in the new text of any touched record.
  • R6 ruled open questions still listing options: none. Open questions stay "None."

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. (Mechanism lives only in INV11.)
  • 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.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope.
  • Public text and artifacts contain no internal Meta context.

Validation

  • Install (frozen lockfile): pass
  • pnpm check:knowledge: "Knowledge templates and records are aligned."
  • Prettier: pass
  • git diff --check: clean

@vercel

vercel Bot commented Nov 3, 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 Nov 4, 2026 2:54am UTC

Request Review

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

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 2:58 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 13ee3ee

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes: the keyboard rule itself is owner-approved, but one requirement needs fixing before it can guide implementation.

Blocking

  • For a field in the page, FR9 requires the field to stay visible "without moving the document", and its first verification row fails on "a document scroll" (docs/specs/AST-099-mobile-keyboard/spec.md lines 162–164 and 287); this contradicts FR4, where the surface reveals the control with its own scroller, so an in-page field (Gap 2) has no conforming way above the keyboard; please state how an in-page field is revealed, and narrow the row's failure to a scroll the browser makes or one left in place.

Smaller suggestions: Gap 7 could also cover PowerSearch's experimental bottom-sheet presentation. FR9's "already applies" wording for the chat dock contradicts Gap 9. Adding the Selector and MultiSelector directories to the path list would route Gap 6's work to this record.

[Automated review]

internal path, the keyboard module (`architecture:react-component-runtime`
INV11): the height comes from FR2, focus does not pan the page (FR4), and no
field measures the keyboard or listens to the visual viewport itself. The
field keeps itself visible without moving the document. A Core host that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a field in normal page flow, the document is the only scroller that can reveal it. FR4 lets the surface bring the control into view "with its own scroller". This sentence, and the "a document scroll" failure at line 287, rule that scroller out. Could FR9 say how an in-page field is revealed? For example: its nearest scroller, the document included, scrolls it into view after a focus that does not scroll, and that scroller's range grows by the keyboard height when the field sits at its end.

| FR8 | Firefox for Android, emulator or device, top-level document | focus routes, keyboard shown and hidden, page panned and pinch-zoomed, a sheet moved | The page lurches, the field ends under the keyboard, or the height is wrong after a pan or zoom |
| FR2–FR5 | Safari on iOS in the iOS Simulator or on a device, top-level document | focus routes, keyboard shown and hidden, page panned and pinch-zoomed, a sheet moved, in Safari and as a home-screen app | The page lurches, the field ends under the keyboard, or the height is wrong after a pan or zoom |
| FR2–FR5, FR8 | Release gate: physical devices in a top-level document (DEC-3) | iPhone Safari with the toolbar expanded and collapsed; iPhone home-screen app; Firefox for Android (FR2–FR5); Android Chrome with Gboard and Samsung Keyboard at the phone's refresh rate | The page lurches, the field ends under the keyboard, or the surface does not follow the keyboard |
| FR9 | TBD (Gap 1–10): a focus test per covered field, `<Field>.keyboard.test.tsx` beside each one, driving one keyboard change through the keyboard module's test double | each covered field in the page, a Tall and a `hug` BottomSheet, ChatLayout's fixed and sticky dock, an app's fixed bottom bar, and a fixed Toolbar; focus by tap, Tab, and `focus()` | A focused field left under the keyboard, a document scroll, a second offset inside a host that already applies the height, or a field reading the visual viewport itself |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"a document scroll" fails every way to reveal a covered field "in the page". Suggest FR4's wording instead: a scroll the browser makes, or a slipped scroll left in place.

pointer the row sits in a `hug` BottomSheet, which BottomSheet's hook does
not cover, and it has no keyboard focus handling of its own.
- **Gap 7 — PowerSearch.** Its query input (Gap 4) and the text and number
fields of its value editor, which open in a popover, have no keyboard focus

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: with experimental.presentation="bottom-sheet", PowerSearch's content search and value editor sit in pinned tall sheets, not a popover. There the sheet hook partly covers them, as Gap 5 says for Tokenizer.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 2526d84

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes: the keyboard rule and the docked ride are owner-approved, but two requirements need fixing before they can guide implementation.

Blocking

  • FR9 says "The document never moves" and its first verification row fails "a document scroll" for a field in the page (docs/specs/AST-099-mobile-keyboard/spec.md lines 170 and 298), so under FR4 and FR2 a field in page flow, or in an app's sticky bottom bar, has no conforming way above the keyboard; please say how such a field is revealed and narrow the row's failure to a scroll the browser makes or one left in place.
  • The ride that moves an app-owned "nearest fixed-position ancestor" (lines 159-161, and docs/families/input-fields.md lines 261-263) contradicts architecture:react-component-runtime INV11, which lets a component apply the keyboard height only through a StyleX style on its own element; please amend INV11 in this pull request to cover the ride and who puts the element back, or drop the app-owned case.

Smaller points: FR9 calls the chat dock a host that "already applies" the height while Gaps 9 and 11 say it does not, the app-owned container needs a bound so a full-viewport fixed app shell does not ride as a whole, "a small gap" needs a value, and Gap 7 could also cover PowerSearch's bottom-sheet presentation.

[Automated review]

internal path, the keyboard module (`architecture:react-component-runtime`
INV11): the height comes from FR2, focus does not pan the page (FR4), and no
field measures the keyboard or listens to the visual viewport itself. The
document never moves; only the docked container does. A Core host that already

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a field in normal page flow, the document is the scroller that can reveal it: current FR4 has the surface bring the control into view "with its own scroller", and FR2 puts back a visual-viewport pan. With the document never moving, a TextInput low in a form (Gap 2), or a field in an app's sticky bottom bar (Gap 10), has no conforming way above the keyboard. Could FR9 say how such a field is revealed? For example: after a focus that does not scroll, its nearest scroller, the document included, brings it into view.

| FR8 | Firefox for Android, emulator or device, top-level document | focus routes, keyboard shown and hidden, page panned and pinch-zoomed, a sheet moved | The page lurches, the field ends under the keyboard, or the height is wrong after a pan or zoom |
| FR2–FR5 | Safari on iOS in the iOS Simulator or on a device, top-level document | focus routes, keyboard shown and hidden, page panned and pinch-zoomed, a sheet moved, in Safari and as a home-screen app | The page lurches, the field ends under the keyboard, or the height is wrong after a pan or zoom |
| FR2–FR5, FR8 | Release gate: physical devices in a top-level document (DEC-3) | iPhone Safari with the toolbar expanded and collapsed; iPhone home-screen app; Firefox for Android (FR2–FR5); Android Chrome with Gboard and Samsung Keyboard at the phone's refresh rate | The page lurches, the field ends under the keyboard, or the surface does not follow the keyboard |
| FR9 | TBD (Gap 1–11): a focus test per covered field, `<Field>.keyboard.test.tsx` beside each one, driving one keyboard change through the keyboard module's test double | each covered field in the page, a Tall and a `hug` BottomSheet, ChatLayout's fixed and sticky dock, an app's fixed bottom bar, and a fixed Toolbar; focus by tap, Tab, and `focus()` | A focused field left under the keyboard, a document scroll, a second offset inside a host that already applies the height, or a field reading the visual viewport itself |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"a document scroll" fails every way to reveal a covered field "in the page". Suggest FR4's wording instead: a scroll the browser makes, or a slipped scroll left in place.

Comment on lines +160 to +161
container the app owns as well: the field moves its nearest fixed-position
ancestor. It is the one place Core moves layout it does not own, an exception

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moving the app's nearest fixed-position ancestor means styling an element the field does not render. architecture:react-component-runtime INV11 allows the keyboard height only "through a StyleX functional style on its own element", and this pull request leaves INV11 unchanged. Could INV11 be amended here to say how the keyboard module moves that element and puts it back on keyboard hide, blur and unmount, or the app-owned case be dropped?

INV11): the height comes from FR2, focus does not pan the page (FR4), and no
field measures the keyboard or listens to the visual viewport itself. The
document never moves; only the docked container does. A Core host that already
applies the keyboard height, such as a fully expanded Tall BottomSheet or the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: Gaps 9 and 11 say ChatLayout's dock does not apply the keyboard height. Suggest "a Core host that applies the keyboard height under FR3", so the composer input never adds its own offset or rides while Gap 9 is open.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec e6e335f

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes: the keyboard rule, the docked ride and their scope are owner-approved at this head, and the new nearest-scroller reveal settles the earlier document-scroll conflict, but one requirement still contradicts a current record.

Blocking

  • The app-owned ride (docs/specs/AST-099-mobile-keyboard/spec.md lines 159–162, and FR13 in docs/families/input-fields.md) contradicts architecture:react-component-runtime INV11, which allows the keyboard height only through a StyleX style on a component's own element; amend INV11 in this pull request to cover writing and removing the translate on an app element, or drop the app-owned case from FR9, FR13 and DEC-5.

Smaller wording notes are optional: name the own-element offset that gives a field near a scroller's end its room, reconcile the chat dock's "already applies" with Gaps 9 and 11, give the gap a size, say how the ride works on iOS, scope the FR4 row, and update Gap 7, the stale current-state line and the path list.

[Automated review]

Comment on lines +159 to +163
ride (DEC-2), keeping a small gap above the keyboard. This holds for a docked
container the app owns as well: the field moves its nearest fixed-position
ancestor that is docked to the viewport's bottom edge (its bottom sits at the
viewport's bottom); a fixed container that fills the viewport is never moved.
It is the one place Core moves layout it does not own, an exception to FR3 the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: moving an app-owned ancestor contradicts architecture:react-component-runtime INV11 ("each consumer applies them through a StyleX functional style on its own element"), which this pull request leaves unchanged and which FR3 and FR9 still defer to (lines 97–98, 168–170). Amend INV11 here to say how the keyboard module writes and removes the translate on an element the field does not render (INV5), or drop the app-owned case from FR9, FR13 and DEC-5.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 06f6ec0

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes: the keyboard rule, the docked ride, its narrowed scope and the INV11 amendment are owner-approved at this head, and the earlier promise-versus-exclusion, eased-lift and FR3 problems are fixed, but three record defects remain. Line numbers are in docs/specs/AST-099-mobile-keyboard/spec.md unless another file is named.

Blocking

  • A bar the app lifts from script each frame (a JavaScript spring or tween) runs no CSS transition or animation, so at Core's one measurement it looks like a bar that stays put or one lifted part of the way and Core adds the rest, lifting it twice against "never lifted twice" (lines 190–192 and 457) and "behaves as before" (lines 257–258); please either narrow those promises to CSS transitions and animations and say what a script-moved bar gets, or have Core leave a bar whose drawn position is still changing, and add that state to the ride row (line 359).
  • Three verification rows fail what the narrowed text now allows: the ride row (line 359) and INV11's row (docs/architecture/react-component-runtime.md line 508) fail a move left after the field blurs although FR9 keeps it until the keyboard hides after focus leaves (lines 195–197), INV11's row fails the docked bar's extra layout read that INV11 allows (lines 193–194), and FR13's row (docs/families/input-fields.md line 441) fails a field left under the keyboard in any fixed host although FR13 leaves floating and own-translate hosts to the app, so please align all three with the text.
  • Gap 7 (lines 313–315) calls PowerSearch's sheet flow its "experimental touch presentation", but on main the PowerSearch record's DEC-3 makes it a stable presentation and the adaptive default on compact coarse-pointer screens, so please rebase and describe it as the default sheet flow, with the query input reached there only through presentation="popover".

One non-blocking note, on the eligibility tests reading Core's own translate, is inline.

[Automated review]

lifts part of the way moves the rest, and a bar that stays put moves by the
full overlap. Core does not move a bar the app is already moving (a running
transition or animation on its position at the time Core measures); it
leaves that bar to the app, so it is never lifted twice. The move adds to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A lift the app drives from script each frame (a JavaScript spring or tween) runs no CSS transition or animation, so at Core's one measurement the bar looks like one that stays put or one lifted part of the way, and Core adds the rest: once the app's motion finishes, the bar is lifted twice. Either narrow "never lifted twice" (here, DEC-5 line 457 and the impact bullet at lines 257–258) to CSS transitions and animations and say what a script-moved bar gets, or have Core leave a bar whose drawn position is still changing when it measures; then add that state to the ride row (line 359).

- **Gap 7 — PowerSearch.** Its query input (Gap 4) and the text and number
fields of its value editor, which open in a popover, have no keyboard
handling. Keeping the popover itself above the keyboard is `spec:AST-059`'s.
In its experimental touch presentation the filter work sits in Tall bottom

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On main, the PowerSearch record's DEC-3 makes this sheet flow a stable presentation and the adaptive default on compact coarse-pointer screens, so it is no longer experimental, and on a phone the query input does not render unless presentation="popover" is set. Suggest describing the sheet flow as PowerSearch's default there.

| FR9 | TBD (Gap 1–11): a focus test per covered field, driving one keyboard change | each covered field in the page, a Tall and a `hug` BottomSheet, ChatLayout's fixed and sticky dock, an app's fixed bar docked to the bottom edge with no `translate` of its own, and a fixed Toolbar docked there; focus by tap, Tab, and `focus()`; a field in scrolling content in the page and in a nested scroller; a field near its scroller's end | A focused page field or Core-host field left under the keyboard or less than 10 CSS px above it, a field in an app's docked bar whose bar's bottom does not meet the keyboard's top, scroll range left at the scroller's end after the keyboard hides, a page pan or translate, a reveal later than the frame the keyboard is reported in, or a second offset inside a host that already applies the height |
| FR9 | TBD (Gap 1): a source boundary check over the covered fields | every covered field's props type and source | A public prop, option, or context for the behavior |
| FR9 | TBD (Gap 1–11): Safari on iOS in the iOS Simulator and Chrome on Android in the emulator, top-level document | TextInput in the page, ChatComposer in its dock, a Selector search row in its sheet, and a TextInput in a fixed Toolbar | The field ends under the keyboard, or the page lurches |
| FR9 | TBD (Gap 11): a ride test per covered field, plus physical-device evidence under DEC-3 | each covered field in a fixed bottom bar, a sheet, an app-owned bar whose layout box sits at the viewport's bottom edge; an app bar the app lifts to the keyboard's top on the same visual-viewport resize, with its listener running before and after Core's; an app bar the app lifts part of the way with its own transform; an app bar the app lifts behind a CSS transition or animation still running when Core measures; an app bar centred with its own `translate`; an app bar floating above the bottom edge; an app-owned fixed container that fills the viewport; keyboard show and hide; the field blurs; the field unmounts; reduced motion on | The docked container stays put, lags or jumps after the keyboard settles, a Core host or content field less than 10 CSS px above the keyboard, an app bar left under the keyboard or above its top edge once the keyboard settles, a move left on an app element after the keyboard hides, the field blurs, or it unmounts, any other change on an app element, a bar the app lifts moved a second time, a bar the app is still moving moved by Core, a floating bar moved, the app's transform replaced, a bar with its own `translate` moved or its `translate` changed, the page is panned or translated, a fixed container that fills the viewport, or whose layout box is not at the bottom edge, moves, or a Core host that already rides is moved twice |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FR9 now keeps the move until the keyboard hides after the field loses focus (lines 195–197), so "a move left on an app element after the keyboard hides, the field blurs, or it unmounts" fails a conforming implementation when focus moves to another field with the keyboard still up. Suggest: "a move left after the keyboard hides once focus has left the field, or after the field unmounts".

| 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 |
| 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 translate left on an app element after the keyboard hides, the field blurs, or it unmounts; any other write on an app element; a translate on a container whose layout box is not at the bottom edge, that fills the viewport, or whose own `translate` is not `none`; a translate on a container whose position the app is animating when the module measures; a translate measured before the page's own keyboard listeners run; a covered field deciding its keyboard behavior in an Effect that reacts to focus; 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
Contributor

Choose a reason for hiding this comment

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

Two failures here lag INV11's text: "a translate left on an app element after the keyboard hides, the field blurs, or it unmounts" (INV11 now removes it once the keyboard hides after focus leaves the field, lines 199–200), and "more than one layout read per keyboard change" (INV11 now exempts the docked bar's read, lines 193–194).

gets none, and rides the hook's one transition to that value like every Core
rider. The read of that bar is the one exception to the hook's single layout
read per keyboard change, stated below. The `translate` property composes with
a `transform` the app sets. The module writes nothing on an ancestor whose own

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: after Core's first move the bar's computed translate is Core's own and Core's ride runs on it, so read literally these tests (here and lines 186–188, FR9 spec.md lines 193–194) stop Core from updating the bar on a later keyboard report, such as a taller emoji or numeric keyboard with focus still in the bar. Consider saying they read only what the app sets, and adding a keyboard-height change with the field still focused to the ride row.

Comment thread docs/families/input-fields.md Outdated
| FR10 | A type test over every Field-composed component's props that fails if one accepts root `xstyle`, `className`, or `style`; per-component render tests for every base prop, `container*`, and `control*` prop and for both development warnings | every Field-composed component standalone, in horizontal-label FormLayout, and inside InputGroup where admitted | A component type-checks with root styling, a base prop lands off the primary semantic control, `container*` lands off the Field box or on the control box, `control*` lands off the control box, or a warning is missing |
| FR11 | `InputClearButton.test.tsx` tooltip and accessible-name assertions, plus real-Chromium hover captures | a long field label; a locale override; light and dark | The tooltip repeats the field label, or the accessible name loses the field label |
| FR12 | `Typeahead.test.tsx`, `BaseTypeahead.test.tsx`, `Tokenizer.test.tsx`, `Selector.test.tsx`, and `MultiSelector.test.tsx` cases for both attributes, with Enter keeping its member-owned effect; `SearchKeyboard.interaction.browser.spec.ts` on a touch-emulated phone, where the search input a tap focuses carries both | Typeahead; Tokenizer; Selector and MultiSelector search row; a phone viewport with touch emulation | A missing attribute, or an Enter whose effect changes with the label, fails |
| FR13 | TBD (gap): the per-field keyboard focus tests and simulator evidence `spec:AST-099` FR9 names | each member FR13 covers, in the page, a sheet, and a fixed host | A focused field left under the keyboard, or a public prop for the behavior |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FR13 now leaves a member in a floating or own-translate fixed container unmoved (lines 263–265), so "a fixed host" here fails a conforming implementation; suggest "a fixed bar spec:AST-099 FR9 moves".

…te; browser reveal for fields Core does not move; PowerSearch sheet default
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 4a6f8c8

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes. The keyboard rule, the docked ride, its scope and the INV11 amendment are owner-approved at this head, and the earlier script-lift, stale-row and Gap 7 problems are fixed. Two record defects need fixing before these records can guide implementation.

Blocking

  • The FR9 ride row (spec.md:369) fails a floating or own-translate bar left under the keyboard, and any page pan, although FR9 leaves those bars to the app and now lets Safari's own reveal pan the page for their fields (spec.md:169-171); also, "a field Core does not move" (:169-170, :366, :457-458) reads as covering page fields that Core reveals by scrolling, so name the fixed containers FR9 leaves to the app and give the ride row the exemption FR13's row already has.
  • DEC-5 (spec.md:467-469) and the impact bullet (:262-265) now say Core leaves any bar the app moves with a CSS transition, which drops FR9's "running when Core measures" condition, so a bar whose CSS lift starts after the app's listener returns is lifted twice; restore the condition and say that a later CSS lift can be lifted twice.

Also worth fixing: INV11's "no CSS transition or animation on its position is running" (react-component-runtime.md:187-188) should limit this to the app's motion, as its row and FR9 do, so that Core's own ride does not block its next update.

[Automated review]

| FR9 | TBD (Gap 1–11): a focus test per covered field, driving one keyboard change | each covered field in the page, a Tall and a `hug` BottomSheet, ChatLayout's fixed and sticky dock, an app's fixed bar docked to the bottom edge with no `translate` of its own, and a fixed Toolbar docked there; focus by tap, Tab, and `focus()`; a field in scrolling content in the page and in a nested scroller; a field near its scroller's end | A focused page field or Core-host field left under the keyboard or less than 10 CSS px above it (a field in a container Core does not move keeps the browser's own reveal and is not a failure), a field in an app's docked bar whose bar's bottom does not meet the keyboard's top, scroll range left at the scroller's end after the keyboard hides, a page pan or translate, a reveal later than the frame the keyboard is reported in, or a second offset inside a host that already applies the height |
| FR9 | TBD (Gap 1): a source boundary check over the covered fields | every covered field's props type and source | A public prop, option, or context for the behavior |
| FR9 | TBD (Gap 1–11): Safari on iOS in the iOS Simulator and Chrome on Android in the emulator, top-level document | TextInput in the page, ChatComposer in its dock, a Selector search row in its sheet, and a TextInput in a fixed Toolbar | The field ends under the keyboard, or the page lurches |
| FR9 | TBD (Gap 11): a ride test per covered field, plus physical-device evidence under DEC-3 | each covered field in a fixed bottom bar, a sheet, an app-owned bar whose layout box sits at the viewport's bottom edge; an app bar the app lifts to the keyboard's top on the same visual-viewport resize, with its listener running before and after Core's; an app bar the app lifts part of the way with its own transform; an app bar the app lifts behind a CSS transition or animation still running when Core measures; an app bar the app lifts from script with its own `translate`; a keyboard that changes height while focus stays in a docked bar; an app bar centred with its own `translate`; an app bar floating above the bottom edge; an app-owned fixed container that fills the viewport; keyboard show and hide; the field blurs; the field unmounts; reduced motion on | The docked container stays put, lags or jumps after the keyboard settles, a Core host or content field less than 10 CSS px above the keyboard, an app bar left under the keyboard or above its top edge once the keyboard settles, a move left on an app element after the keyboard hides once focus has left the field, or after the field unmounts, any other change on an app element, a bar the app lifts moved a second time, a bar the app is moving with CSS motion moved by Core, a bar with its own `translate` moved, a docked bar not updated when the keyboard changes height, a floating bar moved, the app's transform replaced, a bar with its own `translate` moved or its `translate` changed, the page is panned or translated, a fixed container that fills the viewport, or whose layout box is not at the bottom edge, moves, or a Core host that already rides is moved twice |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This row lists a floating bar and a bar with its own translate as states, but it fails "an app bar left under the keyboard" and "the page is panned or translated" with no exception. FR9 leaves those bars to the app (:271-273) and lets Safari's own reveal pan the page for their fields (:169-171). Add the exemption that FR13's row has: such a bar left under the keyboard, or the browser's reveal panning the page for a field in it, is not a failure.

layout box, before any transform, sits at the viewport's bottom edge, and it
moves by the distance from its bottom as drawn, after the app's own lift, to
the keyboard's top, so a bar the app already lifts is not lifted twice and no
opt-out is needed. Core leaves a bar the app moves with a CSS transition or

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FR9 leaves a bar alone only while its CSS transition or animation is "running on its position when Core measures" (:193-195). This sentence, and the impact bullet at :262-265, drop that condition. A bar whose CSS lift starts after the app's listener returns is at rest when Core measures, so Core moves it, and then the app lifts it again. Put the condition back, and say that a later CSS lift can be lifted twice.

…running when Core measures; app-own motion in INV11
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 6b80579

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes. The keyboard rule, the docked ride, its scope and the INV11 amendment are owner-approved at this head, and the earlier ride-row, browser-reveal and measuring-condition problems are fixed. Two record contradictions remain in how FR9 treats a field inside an app's tall fixed container.

Blocking

  • FR9 now gives a field inside a full-viewport fixed container the browser's own reveal and both FR9 rows pass it left under the keyboard (spec.md:169-172, :368, :371, DEC-5 :459-462), while FR9's scrolling-content rule (:173-177) has that field's nearest scroller reveal it, so an app whose page scrolls inside a position: fixed; inset: 0 shell gets two conflicting rules; limit the reveal clause to a field with no scroller between it and the container, or treat a field in a scroller inside a full-viewport container as scrolling content.
  • The docked test (spec.md:185-187, DEC-5 :466-467, INV11 react-component-runtime.md:184-186) has no height bound, so a full-height side drawer or a pane below a fixed header moves up by the whole keyboard height and a field near its top leaves the screen, against FR9's "stays visible above it" (:159) and "Nothing else moves" (:177); let a container move only when its drawn box stays inside the room above the keyboard, send a field in a taller container to the scrolling-content rule, and add those layouts to the FR9 ride row and INV11's row.

[Automated review]

Comment on lines +169 to +172
is never panned or translated for a field Core reveals or moves. A field
inside a fixed container that FR9 leaves to the app (one floating above the
bottom edge, one that sets its own `translate`, or a full-viewport one) keeps
the browser's own focus reveal, which in Safari may pan the page.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This list now includes "a full-viewport one", so a field in the scroller of an app's position: fixed; inset: 0 shell keeps the browser's reveal and passes both FR9 rows left under the keyboard (:368, :371). The Scrolling content rule below (:173-177) has that field's nearest scroller reveal it instead. Suggest limiting this clause to a field with no scroller between it and the container, here, in DEC-5 (:459-462) and in both rows.

Comment on lines +185 to +187
- **App-owned docked containers.** A container the app owns rides too when it
is fixed-position, does not fill the viewport, and is docked: its layout
box, before any transform, sits at the viewport's bottom edge. It moves up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing bounds the container's height, so a full-height side drawer (top: 0; bottom: 0; width: 85%) or a pane below a fixed header (top: 56px; bottom: 0) is docked and moves up by the whole keyboard height. With a 300 px keyboard, a search field 16 px from the drawer's top ends 284 px above the viewport, against "stays visible above it" (:159) and "Nothing else moves" (:177). Suggest moving a container only when its drawn box stays inside the room above the keyboard, sending a field in a taller container to the Scrolling content rule, and adding both layouts to the ride row (:371) and INV11's row.

…d; too-tall docked containers move only as far as the field needs
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 5ed56a2

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving. This records-only change writes the on-screen-keyboard rule for Core fields (spec:AST-099 FR9 and DEC-5), amends architecture:react-component-runtime INV11 to admit the one translate Core writes on an app's docked bottom bar, and points family:input-fields FR13 at FR9. The keyboard rule, the docked ride, its scope and the INV11 amendment are owner-approved at this head, and the earlier full-viewport-reveal and too-tall-container contradictions are now fixed: a field with a scroller inside an app-left fixed container follows the scrolling-content rule, and a too-tall docked container moves only as far as the field needs (or is revealed by its scroller) instead of by the whole keyboard height.

Two small, non-blocking wording nits you may want to tighten in a follow-up:

  • FR3's exception sentence states the ride as "the distance from the bar's drawn bottom to the keyboard's top"; that is the fitting case, so it reads as slightly more than a too-tall container actually moves (FR9 has the exact rule).
  • FR13's prose lists the app-left containers as floating or own-translate and omits the full-viewport case that its own verification row and FR9 include.

Nothing ships until the implementation PRs follow against FR9.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 29d1927

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving. This records-only change writes the on-screen-keyboard rule for Core fields (spec:AST-099 FR9 and DEC-5), amends architecture:react-component-runtime INV11 to admit the one translate Core writes on an app's docked bottom bar, and points family:input-fields FR13 at FR9. The keyboard rule, the docked ride, its scope and the INV11 amendment are owner-approved at this head, and the earlier full-viewport-reveal and too-tall-container contradictions are fixed: a field with a scroller inside an app-left fixed container follows the scrolling-content rule, and a too-tall docked container moves only as far as the field needs (or is revealed by its scroller) instead of by the whole keyboard height.

Two small, non-blocking wording nits you may want to tighten in a follow-up:

  • FR3's exception sentence states the ride as "the distance from the bar's drawn bottom to the keyboard's top"; that is the fitting case, so it reads as slightly more than a too-tall container actually moves (FR9 has the exact rule).
  • FR13's prose lists the app-left containers as floating or own-translate and omits the full-viewport case that its own verification row and FR9 include.

Nothing ships until the implementation PRs follow against FR9.

[Automated review]

@cixzhang
cixzhang merged commit 65516e6 into main Nov 4, 2026
90 of 93 checks passed
@github-actions
github-actions Bot deleted the spec/inputs-work-with-the-keyboard branch November 4, 2026 06:04

This branch was successfully deployed

1 active deployment
Preview — 29d19274 Deployed Nov 4, 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.

1 participant