diff --git a/web/src/styles.css b/web/src/styles.css index 3ef4d52..775e5b6 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1192,10 +1192,13 @@ textarea { background: var(--surface); } -/* Shown on touch only while the terminal is focused (soft keyboard up) — the - `keybar-active` class is toggled on textarea focus/blur in attach.ts. */ +/* Shown on touch only while *this* terminal is focused (`keybar-active`, toggled + on textarea focus/blur in attach.ts) AND the soft keyboard is actually up + (`html.kb-open`, detected from the visual-viewport shrink in viewport.ts). + Both are needed: dismissing the keyboard doesn't blur the terminal, so focus + alone would leave the bar lingering. */ @media (pointer: coarse) { - .attach-keybar.keybar-active { + html.kb-open .attach-keybar.keybar-active { display: grid; } } diff --git a/web/src/viewport.ts b/web/src/viewport.ts index f5f400b..45f98bd 100644 --- a/web/src/viewport.ts +++ b/web/src/viewport.ts @@ -19,18 +19,42 @@ * 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;