Skip to content

Commit 3fa1b85

Browse files
Stvadclaude
andcommitted
fix(mobile): place editing toolbar above the keyboard via interactive-widget=resizes-content
The mobile keyboard toolbar's placement leaned on a sentinel + monotonic maxVvHeight heuristic that misfired on layout-anchored browsers (Edge/Samsung): the URL-bar-polluted baseline floated the bar too high, and it drifted as the URL bar collapsed/expanded on scroll. Set `interactive-widget=resizes-content` so Chromium (Chrome/Edge/Samsung) and Firefox shrink the LAYOUT viewport when the on-screen keyboard opens — a `position:fixed; bottom:0` toolbar then rides above the keyboard with inset 0. Verified on a real Android device via CDP + on-screen checks (Chrome, Edge Canary, Firefox); iOS Safari is unaffected (it ignores the key and pins fixed elements to the visual viewport, which already lands the toolbar correctly). useKeyboardInset stays only as a fallback for a browser that neither honors the meta key nor visual-viewport-pins fixed elements; its now-stale comments (which claimed Edge needs a nonzero inset) are corrected to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cbcb77f commit 3fa1b85

2 files changed

Lines changed: 31 additions & 33 deletions

File tree

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<link rel="icon" type="image/svg+xml" href="./icon.svg" />
66
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
77
<link rel="manifest" href="./manifest.webmanifest" />
8-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
8+
<!-- interactive-widget=resizes-content shrinks the LAYOUT viewport when the on-screen keyboard opens, so the mobile editing toolbar (position:fixed; bottom:0) rides above the keyboard on Chromium (Chrome/Edge/Samsung) and Firefox. Safari ignores it and pins fixed elements to the visual viewport, which already lands the toolbar correctly. Load-bearing for src/plugins/mobile-keyboard-toolbar. -->
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
910
<meta name="theme-color" content="#1e1b4b" />
1011
<meta name="apple-mobile-web-app-capable" content="yes" />
1112
<meta name="mobile-web-app-capable" content="yes" />

src/plugins/mobile-keyboard-toolbar/MobileKeyboardToolbar.tsx

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,31 @@ import { EXIT_EDIT_ACTION_ID, mobileKeyboardToolbarItemsFacet } from './facet.ts
1010

1111
/** Computes the on-screen keyboard's CSS-px inset for the toolbar.
1212
*
13-
* Three browser shapes have to be handled and earlier attempts each
14-
* broke at least one:
15-
* - Chrome on Android (resizes-content default): both layout and
16-
* visual viewports shrink with the IME. `bottom: 0` already lands
17-
* above the keyboard; we just want inset = 0.
18-
* - iOS Safari: visual viewport shrinks, layout stays full, but
19-
* position:fixed is *pinned to the visual viewport*. `bottom: 0`
20-
* again lands above the keyboard; inset must be 0 or we open a gap.
21-
* - Edge / Samsung Internet on Android: visual viewport shrinks,
22-
* layout stays full, AND position:fixed is anchored to the layout
23-
* viewport. `bottom: 0` lands under the keyboard; inset must be the
24-
* keyboard height — and *just* the keyboard height, not the URL
25-
* bar (which is what the naive `innerHeight - vv.height` formula
26-
* accidentally added in earlier attempts, producing the gap the
27-
* user reported).
13+
* The PRIMARY mechanism is CSS, not this hook: `index.html` sets
14+
* `interactive-widget=resizes-content`, so when the keyboard opens the
15+
* browser shrinks the *layout* viewport and a `position: fixed; bottom: 0`
16+
* toolbar already rides above the keyboard — inset 0 — on every browser in
17+
* our fleet (verified on device: Chrome, Edge, Firefox; iOS Safari):
18+
* - Chromium (Chrome / Edge / Samsung Internet) and Firefox honor the meta
19+
* key and shrink the layout viewport. (Edge earlier — before the meta key
20+
* — kept the layout viewport full and needed a nonzero inset; with
21+
* resizes-content it shrinks like the rest and no longer does.)
22+
* - iOS Safari ignores the meta key but pins position:fixed to the *visual*
23+
* viewport, so `bottom: 0` again lands above the keyboard — inset 0.
2824
*
29-
* The fix has two pieces:
30-
* - Track a *baseline* maximum visualViewport.height across the
31-
* component lifetime. The URL bar height is constant — present in
32-
* both the baseline and the current measurement — so it cancels
33-
* out. The keyboard height is the only delta:
34-
* `keyboardHeight = baseline - current`.
35-
* - Use a hidden 1×1 sentinel at `position: fixed; bottom: 0` to
36-
* detect which anchoring mode the browser is using. If the
37-
* sentinel's bottom (in CSS-px) sits below the visual viewport's
38-
* bottom, the browser is layout-anchoring fixed elements (Edge
39-
* case) and we apply the inset. Otherwise (Chrome / iOS) we keep
40-
* inset = 0 because `bottom: 0` is already correct. */
25+
* So this hook is now only a FALLBACK for a browser that neither honors
26+
* `interactive-widget` nor visual-viewport-pins fixed elements — i.e. one
27+
* that layout-anchors them against a full-height viewport (older Chromium /
28+
* some WebViews). There `bottom: 0` lands under the keyboard and we reserve
29+
* the keyboard height, measured URL-bar-invariantly:
30+
* - Track a *baseline* maximum visualViewport.height across the component
31+
* lifetime; the URL bar is present in both the baseline and the current
32+
* measurement, so it cancels — the keyboard height is the only delta
33+
* (`baseline - current`).
34+
* - A hidden 1×1 sentinel at `position: fixed; bottom: 0` detects the
35+
* anchoring mode: if its bottom (CSS-px) sits below the visual viewport's
36+
* bottom the browser is layout-anchoring, so apply the inset; otherwise
37+
* (the whole fleet, post-meta-key) inset stays 0. */
4138
const useKeyboardInset = (active: boolean): number => {
4239
const sentinelRef = useRef<HTMLDivElement | null>(null)
4340
const [inset, setInset] = useState(0)
@@ -226,11 +223,11 @@ export function MobileKeyboardToolbar() {
226223
return (
227224
<div
228225
ref={toolbarRef}
229-
// `keyboardInset` is 0 on browsers where bottom:0 already lands
230-
// above the keyboard (Chrome on Android, iOS Safari) and equals
231-
// the keyboard's CSS-px height on browsers that anchor
232-
// position:fixed to a full-height layout viewport (Edge,
233-
// Samsung Internet) — see useKeyboardInset.
226+
// `keyboardInset` is 0 on every browser in our fleet now that
227+
// index.html sets interactive-widget=resizes-content (bottom:0
228+
// rides above the keyboard). It's nonzero only as a fallback on a
229+
// browser that ignores the meta key AND layout-anchors fixed
230+
// elements against a full-height viewport — see useKeyboardInset.
234231
className="mobile-keyboard-toolbar fixed left-0 right-0 z-50 flex items-center justify-around gap-1 border-t border-border bg-background/95 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/80"
235232
style={{bottom: keyboardInset}}
236233
data-block-interaction="ignore"

0 commit comments

Comments
 (0)