Repository navigation
fix(Layer, Selector): the overlay slides above the software keyboard and keeps its top - #10169
Conversation
INV11 names the software keyboard's cap among the viewport caps, and the overflow each layer carries beside it. INV12 names the cap as the second change permitted after paint: once per keyboard movement, and after a press's click. It also names where the cap reads the layer's box. The change coupling, owning code and verification sections name the cap's checks, its modules and its tests.
The layer runtime now writes the software keyboard's block-end inset on each open anchor-mode layer, beside its keyboard cap: the keyboard's height and the 10 px clearance. It is a private custom property that only the runtime writes. It goes out in the commit that lifts the cap, before the cap reads the layer's box. Selector's selected-item overlay reads it as a CSS term of its block-start margin. With a keyboard up, the list ends above the keyboard instead of shrinking to a sliver or opening under it. Without one, the measured shift stands.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| Selector | 94.2 kB (+290 B) · 17.0 kB CSS (+18 B) | — | ✗ · ✓ · ✓ |
| LayerProvider | 4.1 kB (0) · 20 B CSS (0) | — | ✓ · ✓ · ✗ |
| SelectorOption | 19.5 kB (0) · 5.8 kB CSS (0) | — | ✗ · ✓ · ✓ |
113 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| CommandPalette | 🔴 67.1 kB (+7.4 kB) · 12.4 kB CSS (+20 B) | — | — · — · — |
| PowerSearch | 275.4 kB (+318 B) · 23.2 kB CSS (+13 B) | — | — · — · — |
| richtext/RichTextEditorToolbar | 245.4 kB (+307 B) · 454 B CSS (0) | — | — · — · — |
| Pagination | 104.3 kB (+274 B) · 17.3 kB CSS (+18 B) | — | — · — · — |
| AvatarGroupOverflow | 44.8 kB (+184 B) · 5.5 kB CSS (+18 B) | — | — · — · — |
| DateTimeInput | 199.5 kB (+172 B) · 18.2 kB CSS (+22 B) | — | — · — · — |
| SegmentedControl | 89.8 kB (+175 B) · 6.8 kB CSS (+19 B) | — | — · — · — |
| InfoTip | 43.6 kB (+166 B) · 5.3 kB CSS (+20 B) | — | — · — · — |
| StatusDot | 27.9 kB (+167 B) · 5.3 kB CSS (+19 B) | — | — · — · — |
| TableCell | 92.6 kB (+166 B) · 17.1 kB CSS (+19 B) | — | — · — · — |
| DropdownMenuSubMenu | 65.0 kB (+165 B) · 9.4 kB CSS (+19 B) | — | — · — · — |
| Typeahead | 74.8 kB (+162 B) · 12.2 kB CSS (+21 B) | — | — · — · — |
| TableHeaderCell | 92.2 kB (+162 B) · 16.8 kB CSS (+19 B) | — | — · — · — |
| BreadcrumbItem | 76.3 kB (+161 B) · 11.6 kB CSS (+18 B) | — | — · — · — |
| CheckboxListItem | 66.2 kB (+160 B) · 11.0 kB CSS (+19 B) | — | — · — · — |
| Carousel | 60.9 kB (+160 B) · 10.4 kB CSS (+18 B) | — | — · — · — |
| FileInput | 64.6 kB (+157 B) · 10.7 kB CSS (+18 B) | — | — · — · — |
| DateInput | 177.1 kB (+155 B) · 16.5 kB CSS (+19 B) | — | — · — · — |
| ListItem | 65.2 kB (+154 B) · 11.0 kB CSS (+19 B) | — | — · — · — |
| Switch | 96.7 kB (+153 B) · 9.9 kB CSS (+18 B) | — | — · — · — |
| Slider | 102.1 kB (+148 B) · 8.7 kB CSS (+22 B) | — | — · — · — |
| TopNav | 63.9 kB (+154 B) · 10.3 kB CSS (+16 B) | — | — · — · — |
| AvatarGroup | 42.5 kB (+150 B) · 5.6 kB CSS (+19 B) | — | — · — · — |
| ChatComposer | 68.7 kB (+151 B) · 11.3 kB CSS (+18 B) | — | — · — · — |
| lab/CommentPin | 53.0 kB (+168 B) · 1.2 kB CSS (0) | — | — · — · — |
| Tooltip | 26.2 kB (+147 B) · 4.3 kB CSS (+19 B) | — | — · — · — |
| ChatComposerInput | 66.8 kB (+145 B) · 10.7 kB CSS (+20 B) | — | — · — · — |
| ChatComposerTokenElement | 66.8 kB (+145 B) · 10.7 kB CSS (+20 B) | — | — · — · — |
| ContextMenu | 91.4 kB (+146 B) · 16.7 kB CSS (+19 B) | — | — · — · — |
| DropdownMenu | 87.6 kB (+148 B) · 16.8 kB CSS (+17 B) | — | — · — · — |
| TabMenu | 63.5 kB (+147 B) · 10.5 kB CSS (+17 B) | — | — · — · — |
| Table | 100.8 kB (+144 B) · 17.3 kB CSS (+19 B) | — | — · — · — |
| charts/Chart | 180.6 kB (+161 B) · 494 B CSS (0) | — | — · — · — |
| CheckboxList | 54.6 kB (+141 B) · 8.0 kB CSS (+20 B) | — | — · — · — |
| SideNavHeading | 67.2 kB (+140 B) · 10.7 kB CSS (+21 B) | — | — · — · — |
| Avatar | 39.8 kB (+140 B) · 5.5 kB CSS (+20 B) | — | — · — · — |
| ComplexSelector | 66.2 kB (+141 B) · 11.5 kB CSS (+19 B) | — | — · — · — |
| TabList | 40.5 kB (+140 B) · 6.4 kB CSS (+20 B) | — | — · — · — |
| TimeInput | 153.9 kB (+139 B) · 15.6 kB CSS (+21 B) | — | — · — · — |
| HoverCard | 24.7 kB (+139 B) · 4.3 kB CSS (+20 B) | — | — · — · — |
| MoreMenu | 87.9 kB (+141 B) · 16.8 kB CSS (+17 B) | — | — · — · — |
| MobileNav | 62.2 kB (+141 B) · 10.1 kB CSS (+16 B) | — | — · — · — |
| TopNavMenu | 66.3 kB (+138 B) · 10.6 kB CSS (+19 B) | — | — · — · — |
| AlertDialog | 96.3 kB (+138 B) · 13.8 kB CSS (+18 B) | — | — · — · — |
| SideNav | 81.8 kB (+140 B) · 10.8 kB CSS (+16 B) | — | — · — · — |
| TextArea | 58.2 kB (+135 B) · 9.2 kB CSS (+21 B) | — | — · — · — |
| FieldLabel | 47.4 kB (+134 B) · 5.6 kB CSS (+21 B) | — | — · — · — |
| TopNavHeading | 66.6 kB (+139 B) · 10.5 kB CSS (+16 B) | — | — · — · — |
| lab/CommentComposer | 139.3 kB (+155 B) · 1.9 kB CSS (0) | — | — · — · — |
| charts/ChartTooltip | 158.0 kB (+154 B) · 423 B CSS (0) | — | — · — · — |
| BaseTypeahead | 62.4 kB (+137 B) · 10.4 kB CSS (+17 B) | — | — · — · — |
| AppShell | 82.0 kB (+138 B) · 12.9 kB CSS (+14 B) | — | — · — · — |
| Calendar | 63.1 kB (+137 B) · 10.0 kB CSS (+15 B) | — | — · — · — |
| Lightbox | 103.4 kB (+136 B) · 9.9 kB CSS (+16 B) | — | — · — · — |
| SideNavCollapseButton | 52.9 kB (+136 B) · 9.5 kB CSS (+16 B) | — | — · — · — |
| Tokenizer | 86.2 kB (+131 B) · 18.2 kB CSS (+21 B) | — | — · — · — |
| DateRangeInput | 87.0 kB (+131 B) · 12.2 kB CSS (+20 B) | — | — · — · — |
| RadioList | 52.1 kB (+131 B) · 7.2 kB CSS (+20 B) | — | — · — · — |
| RadioListItem | 63.0 kB (+130 B) · 9.8 kB CSS (+20 B) | — | — · — · — |
| ToggleButton | 53.0 kB (+134 B) · 9.5 kB CSS (+16 B) | — | — · — · — |
| TopNavMegaMenu | 61.4 kB (+133 B) · 11.3 kB CSS (+16 B) | — | — · — · — |
| ChatSendButton | 52.5 kB (+131 B) · 9.4 kB CSS (+17 B) | — | — · — · — |
| InputGroup | 52.4 kB (+128 B) · 7.8 kB CSS (+20 B) | — | — · — · — |
| Markdown | 163.2 kB (+131 B) · 19.6 kB CSS (+17 B) | — | — · — · — |
| lab/TransferList | 91.4 kB (+148 B) · 1.7 kB CSS (0) | — | — · — · — |
| Button | 52.1 kB (+131 B) · 9.4 kB CSS (+16 B) | — | — · — · — |
| IconButton | 52.1 kB (+131 B) · 9.4 kB CSS (+16 B) | — | — · — · — |
| Thumbnail | 56.4 kB (+132 B) · 11.0 kB CSS (+15 B) | — | — · — · — |
| lab/Schedule | 100.1 kB (+147 B) · 3.9 kB CSS (0) | — | — · — · — |
| Banner | 54.2 kB (+129 B) · 9.9 kB CSS (+17 B) | — | — · — · — |
| ChatDictationButton | 52.7 kB (+130 B) · 9.5 kB CSS (+16 B) | — | — · — · — |
| TableSelectionToolbar | 61.6 kB (+130 B) · 12.5 kB CSS (+16 B) | — | — · — · — |
| ChatLayoutScrollButton | 52.6 kB (+128 B) · 9.6 kB CSS (+17 B) | — | — · — · — |
| CodeBlock | 66.8 kB (+130 B) · 10.7 kB CSS (+15 B) | — | — · — · — |
| DrawerHeader | 55.1 kB (+129 B) · 12.3 kB CSS (+16 B) | — | — · — · — |
| Field | 50.8 kB (+124 B) · 7.2 kB CSS (+21 B) | — | — · — · — |
| Popover | 80.0 kB (+127 B) · 13.5 kB CSS (+17 B) | — | — · — · — |
| ChatLayout | 55.4 kB (+126 B) · 9.9 kB CSS (+17 B) | — | — · — · — |
| MobileNavToggle | 52.4 kB (+127 B) · 9.4 kB CSS (+16 B) | — | — · — · — |
| SideNavItem | 62.1 kB (+127 B) · 10.5 kB CSS (+16 B) | — | — · — · — |
| Stepper | 53.8 kB (+128 B) · 9.6 kB CSS (+15 B) | — | — · — · — |
| Toolbar | 39.8 kB (+124 B) · 7.9 kB CSS (+19 B) | — | — · — · — |
| DialogHeader | 54.9 kB (+124 B) · 10.9 kB CSS (+17 B) | — | — · — · — |
| Toast | 89.7 kB (+125 B) · 9.7 kB CSS (+16 B) | — | — · — · — |
| lab/TourStep | 86.2 kB (+141 B) · 590 B CSS (0) | — | — · — · — |
| CheckboxInput | 60.1 kB (+119 B) · 9.1 kB CSS (+21 B) | — | — · — · — |
| Link | 49.0 kB (+120 B) · 7.0 kB CSS (+20 B) | — | — · — · — |
| NumberInput | 67.2 kB (+121 B) · 11.2 kB CSS (+19 B) | — | — · — · — |
| MultiSelector | 89.5 kB (+116 B) · 16.4 kB CSS (+22 B) | — | — · — · — |
| lab/DiffViewer | 78.5 kB (+137 B) · 2.1 kB CSS (0) | — | — · — · — |
| TextInput | 63.2 kB (+116 B) · 10.9 kB CSS (+20 B) | — | — · — · — |
| lab/ChatEmojiPicker | 97.9 kB (+136 B) · 773 B CSS (0) | — | — · — · — |
| lab/CommentThread | 302.8 kB (+136 B) · 2.4 kB CSS (0) | — | — · — · — |
| lab/Comments | 307.9 kB (+132 B) · 2.5 kB CSS (0) | — | — · — · — |
| lab/TransferListSelector | 98.4 kB (+132 B) · 1.9 kB CSS (0) | — | — · — · — |
| lab/EmojiPicker | 81.7 kB (+130 B) · 751 B CSS (0) | — | — · — · — |
| richtext/RichTextEditor | 291.3 kB (+130 B) · 2.6 kB CSS (0) | — | — · — · — |
| lab/ChatReactionBar | 99.1 kB (+129 B) · 1.1 kB CSS (0) | — | — · — · — |
| richtext/RichTextView | 271.5 kB (+127 B) · 2.3 kB CSS (0) | — | — · — · — |
| lab/ListInput | 96.6 kB (+126 B) · 1.6 kB CSS (0) | — | — · — · — |
| lab/SelectableCardGroup | 60.0 kB (+122 B) · 162 B CSS (0) | — | — · — · — |
| Heading | 8.2 kB (+1 B) · 5.4 kB CSS (+20 B) | — | — · — · — |
| ChatMessageList | 23.9 kB (0) · 7.0 kB CSS (+20 B) | — | — · — · — |
| Spinner | 20.5 kB (0) · 6.1 kB CSS (+20 B) | — | — · — · — |
| Text | 7.6 kB (0) · 5.4 kB CSS (+20 B) | — | — · — · — |
| ChatToolCalls | 27.8 kB (−1 B) · 8.3 kB CSS (+20 B) | — | — · — · — |
| CommandPaletteInput | 27.9 kB (0) · 7.5 kB CSS (+19 B) | — | — · — · — |
| Timer | 9.7 kB (0) · 5.9 kB CSS (+19 B) | — | — · — · — |
| ProgressBar | 11.5 kB (−1 B) · 6.2 kB CSS (+19 B) | — | — · — · — |
| Timestamp | 23.2 kB (−2 B) · 9.7 kB CSS (+19 B) | — | — · — · — |
| charts/ChartLegend | 35.8 kB (−3 B) · 224 B CSS (0) | — | — · — · — |
| layer | — | — | — · ✓ · — |
| Layer | — | — | ✓ · ✓ · — |
Visual changes: 8 snapshots in 1 component
A change is a question for the reviewer, not a failure.
Package size: @astryxdesign/core 6.82 MB packed (+3.2 kB); no new dependencies.
API changes: none in this pull request
Committed API report behind this build: cli 19 rows, core 24 rows; run pnpm api:report.
Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
The layer-runtime record's keyboard cap text comes with meta#10143.
The runtime now keeps the keyboard inset at its largest value until the layer closes. Selector's overlay reads it, so it keeps its top when the keyboard's report ends: a list the cap held grows downward in place. A keyboard that rises while it is open still moves it up above it, and a shorter keyboard then leaves it there. Every other anchored layer keeps its timing: its cap still lifts at once as the keyboard goes down. The layer-runtime record states this beside the keyboard cap.
There was a problem hiding this comment.
Approval waits on a spec decision. This pull request adds new requirement text to two current records, and an engineering owner needs to approve that text first.
What must be settled:
spec:AST-059FR9: the layer runtime writes a private keyboard block-end inset on each open anchored layer. A layer that places itself, such as Selector's overlay, moves its top edge to end 10 px above the keyboard, where FR9 today keeps that edge and only caps the layer.component:SelectorFR2: the viewport clamp now includes "above any software keyboard".architecture:react-component-runtimeINV11 lets the keyboard height reach styles only through a functional style on the consumer's own element. The decision should confirm that the private inset property fits that rule.
An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Beyond that decision the review found no defect. With a stand-in keyboard, the list ends above the keyboard with every option in view, and without a keyboard the overlay renders exactly as before. CI has not run on this head yet.
[Automated review]
The overlay margin property's comment keeps only its constraint, and the margin field drops a note that restated its name and type.
The INV11 line about layers that read the keyboard inset now says a layer placed while a keyboard is reported, which is what the runtime does: it cannot tell a lingering report from a keyboard that was up.
|
/approve-spec 48445f6 |
|
On the |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet; as a draft from a fork, it starts once the pull request is marked ready for review and a maintainer adds the ci:run label.
With a software keyboard up, the Selector's list now ends above the keyboard with every option in view and keeps its top when the keyboard goes, while without a keyboard it opens exactly where it did. The record text in spec:AST-059 FR9, component:Selector FR2 and the layer-runtime INV11 line has an engineering owner's approval for this head, no public API changes, and both earlier comment notes are addressed.
[Automated review]
Main brought the exit copy's INV13 row (meta#8186), the searchable Selector trigger's name (meta#10170) and AST-059's scroll placement (meta#10255, meta#10257, meta#10289). Their verification tables re-padded, so each conflicted as a whole table. Resolved row by row: the layer-runtime table keeps this branch's INV11, INV12 row, then main's INV13 row. AST-059's table keeps main's FR4 tooltip row and new scroll row, with this branch's FR9 row. Selector's table keeps main's AR1, AV2 row and this branch's FR2 row.
|
/approve-spec 48010db |
There was a problem hiding this comment.
Approved pending CI: the required test check is red at this head and needs a rerun before this can merge.
- The two failing jobs do not come from this change: one Popover touch-motion case (
PopoverTouchMotion.interaction.browser.spec.ts:590) passes when run again against this head's Storybook build, and the component accessibility suite stopped at its 8-minute budget with every test that ran passing, this pull request's own keyboard spec included.
With a software keyboard up, the Selector's list now ends above the keyboard with every option in view and keeps its top when the keyboard goes; without a keyboard it opens exactly where it did. The record text in spec:AST-059 FR9, component:Selector FR2 and the layer-runtime INV11 line has an engineering owner's approval for this head, and no public API changes.
[Automated review]
|
/approve-spec 990d94c |
There was a problem hiding this comment.
Approved: required CI passed on this head.
The default Selector overlay now slides above a software keyboard and keeps its top until it closes, and the record text it adds to spec:AST-059 FR9, component:Selector FR2 and architecture:layer-runtime INV11 carries an engineering owner's approval for this head. The merge from main leaves this pull request's own patch unchanged, and the stand-in keyboard checks in Chromium give the same geometry as before. There are no blocking findings.
[Automated review]
























Review only this PR's change: 010db3f...vjeux:astryx:48010db7cf54877e44d3f09656ca9fb83e5cd3eb (against main 010db3f). Its own commits: vjeux@4c9639d vjeux@6181c73 vjeux@46acf05 and vjeux@f21f5b9; the rest are merges from main.
User impact
On a phone, a Selector opened over its trigger while a software keyboard is up can run under the keyboard. When the list's viewport clamp places it below the keyboard's line, the layer runtime's keyboard cap shrinks it to a sliver, or leaves it under the keyboard, and its options cannot be reached. With this change the list slides up and ends 10 px above the keyboard with every option in view. Once open, it keeps its top until it closes: when the keyboard goes, it stays where a finger is already reaching. Without a keyboard nothing changes.
Problem and solution fit
The runtime now publishes the keyboard's block-end inset on each open anchor-mode layer, beside its cap. The overlay reads it as a CSS term of its block-start margin, so the list moves in the commit that publishes the inset, before the cap reads its box. The cap then shrinks only a list taller than the room above the keyboard.
In Safari on iOS, a tap on the trigger moves focus off the text field, and the keyboard goes, but Safari keeps reporting it for up to about a second. A person is already reaching for an option then, so a list that moved back down would put a different option under the finger. The inset therefore keeps its largest value until the layer closes: the list keeps its top, and a list the cap held grows downward in place. A keyboard that rises while the list is open still moves it up above it, and a shorter keyboard then leaves it there.
Expected behavior and authority
component:SelectorFR2: without explicit placement, a non-search popover aligns the selected row over the trigger and clamps it to the viewport. This change adds "above any software keyboard" and "a keyboard that goes down while it is open does not move it".spec:AST-059FR9: an anchor-mode layer ends 10 px above the software keyboard. The inset is recorded there, beside the cap, as private runtime state that keeps its largest value until the layer closes.architecture:layer-runtimeINV11 gains: "A layer that reads the keyboard inset and was placed while a keyboard is reported keeps its top until it closes: when the cap lifts it grows downward in place; when the keyboard rises it moves up above it and then holds."spec:AST-059FR10's timing.Smallest restoration
useLayerKeyboardClearance.ts,useLayer.tsx): a third private custom property beside the cap and the overflow,--_layer-keyboard-inset-block-end. Once a keyboard is up it holds the keyboard's height plus the 10 px clearance, and it keeps its largest value until the layer closes; before that it isinitial, so a nested layer does not inherit it. It is committed with the cap lifted, before the cap measures. The cap itself is unchanged: it still lifts at once as the keyboard goes down.hooks.ts,Selector.tsx): an internaluseSelectedItemOverlayMarginreturns the overlay's margin asmin(<measured shift>, max(<viewport top>, calc(<room> − var(<inset>, <room>)))). Without an inset the second term is 0, so the measured shift stands. The margin reaches the layer through a Selector-private custom property thatmargin-block-startreads. The publicuseSelectedItemOffsetis unchanged.pnpm api:reporton this head writes the same report as on main.Evidence
Reproduction before the change:
SelectorKeyboardInset.a11y.chromium.spec.tson main's build, with a stand-in software keyboard covering the lower 300 px of a 390 × 844 viewport (its line at 534 px), fails 13 of 13 (dc7fc55 is its first form alone on main, not for landing). Each case first checks that the list and its options are on screen.Result after the change: the same spec passes 13 of 13, twice, with every option above the keyboard.
The list keeps its top: without the hold (the earlier head, 4c9639d), 7 of the 13 cases fail and this head passes them.
iOS 26.2 simulator, Safari, the "Overlay beside keyboard" story, three takes per build. A tap in the Name field raises the keyboard; a tap on the Selector's trigger moves focus to it, and the keyboard goes. Safari keeps reporting the keyboard for 0.2–0.6 s after that tap.
Top: while Safari still reports the keyboard after the tap. Bottom: after the report ends. Left: main. Right: this change.
Representative unchanged path: an anchored layer that does not read the inset still loses its cap at once as the keyboard goes down (a unit case, where that moves it back over its trigger); Popover's keyboard proofs and the capped lists' keyboard proofs pass in Chromium, 14 of 14, and Popover's in WebKit, 6 passed and 1 skipped.
Regression tests: runtime unit cases check that the inset grows with a taller keyboard, holds against a shorter one or none, and clears when the layer closes, and that a layer that places itself by it keeps its top. Two of them fail when the hold is removed. A runtime unit case checks that a layer that places itself by the inset is not capped; it fails when the inset is published after the box is read. Selector's unit cases check the margin's expression with and without the keyboard term.
Scope
Testing
pnpm test:ui --maxWorkers=16: 851 files, 21,300 tests (before main was merged in).pnpm check:knowledge;SelectorKeyboardInset.a11y.chromium.spec.ts, 13 of 13, twice.pnpm -F @astryxdesign/core typecheck;CI=true pnpm exec eslinton the changed files: 0 problems;pnpm -F @astryxdesign/storybook typecheck.pnpm check:knowledge;pnpm api:report(no change against main's own report).SelectorKeyboardInset.a11y.chromium.spec.ts; Selector's accessibility specs;PopoverKeyboard.interaction.browser.spec.ts;keyboardCappedLists.interaction.browser.spec.ts. WebKit (container):PopoverKeyboard.interaction.browser.spec.ts.pnpm storybook:build(the story file changed).Writing rubric results
Run over the sentences this change adds to
spec:AST-059FR9 and its FR9 verification row,component:SelectorFR2 and its verification row, andarchitecture:layer-runtimeINV11 and its "INV11, INV12" verification row: