This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/viewport.ts
T
naps62-yoloandClaude Opus 4.8 1006651fb4
ci / rust (push) Successful in 5m29s
fix(maestro): hide mobile key bar when keyboard is dismissed while focused
Focus alone was the wrong signal: dismissing the soft keyboard (e.g. Android
back button) keeps the terminal focused, so the key bar lingered. Detect the
keyboard's actual visibility from the visual-viewport shrink — while an editable
element is focused, a height past a threshold below the focus-free baseline
means the keyboard is up — and gate the bar on a new `html.kb-open` class in
addition to the per-terminal focus class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:04:15 +00:00

68 lines
2.9 KiB
TypeScript

/**
* Bind the app shell's height *and position* to the *visual* viewport.
*
* On mobile the layout viewport does not shrink when the on-screen keyboard
* opens — only the visual viewport does. A shell sized at `height: 100%` (or
* `100dvh`) therefore extends behind the keyboard, hiding anything pinned to the
* bottom: the attach view's special-key bar (NAP-342). Mirroring
* `visualViewport.height` onto a CSS custom property keeps the shell exactly as
* tall as the visible area, so the key bar lands just above the keyboard.
*
* Height alone isn't enough: when the keyboard opens the browser also scrolls
* the layout viewport to reveal the focused element, which drags the shell
* (anchored at layout-top) up and out of view — the key bar ends up at the top
* of the screen and the user has to scroll back down. `visualViewport.offsetTop`
* is how far the layout viewport has scrolled under the keyboard; translating
* the shell down by that amount re-pins it to the visible region. (Chrome's
* `interactive-widget=resizes-content`, set in the viewport meta, prevents the
* scroll entirely on Android; this covers iOS and older browsers that ignore
* it, where offsetTop stays non-zero.)
*
* No-ops where `visualViewport` is unavailable; the CSS falls back to `100%`.
*
* It also toggles an `html.kb-open` class that reflects whether the soft
* keyboard is actually up, used to gate the mobile key bar. Focus is the wrong
* signal: dismissing the keyboard (e.g. the Android back button) keeps the
* terminal focused, so the bar would linger. Instead we watch the visual
* viewport shrink: while an editable element is focused, a height noticeably
* below the tallest height seen with nothing focused means the keyboard is open.
* The baseline resets whenever nothing is being edited, so it self-corrects
* across orientation changes and URL-bar collapse.
*/
const KB_OPEN_THRESHOLD = 150;
export function installViewportSizing(): void {
const vv = window.visualViewport;
if (!vv) return;
const root = document.documentElement;
let raf = 0;
let baseline = vv.height;
const isEditing = () => {
const a = document.activeElement as HTMLElement | null;
if (!a) return false;
return a.tagName === "TEXTAREA" || a.tagName === "INPUT" || a.isContentEditable;
};
const sync = () => {
raf = 0;
root.style.setProperty("--app-vh", `${Math.round(vv.height)}px`);
root.style.setProperty("--app-top", `${Math.round(vv.offsetTop)}px`);
// Nothing focused ⇒ keyboard can't be open ⇒ this height is the baseline.
const editing = isEditing();
if (!editing) baseline = vv.height;
const kbOpen = editing && baseline - vv.height > KB_OPEN_THRESHOLD;
root.classList.toggle("kb-open", kbOpen);
};
const schedule = () => {
if (raf) return;
raf = requestAnimationFrame(sync);
};
vv.addEventListener("resize", schedule);
vv.addEventListener("scroll", schedule);
sync();
}