From 61219dbca2f4a76b0b02c1f43e850ab09f08b073 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 10 Nov 2026 13:49:53 +0100 Subject: [PATCH] Phone: keyboard no longer leaves the page shifted Mobile browsers scroll the whole page up to show the focused input and do not always scroll it back when the keyboard closes; the app never scrolls the page itself, so the top bar (menu) stayed out of reach. - Viewport interactive-widget=resizes-content: on Android the keyboard shrinks the page instead of shifting it. - Elsewhere (iOS Safari) the page offset is undone when the visual viewport grows again or the input loses focus. - The chat keeps following the latest message when its pane shrinks. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 ++++++++ web/index.html | 4 +++- web/src/components/ChatView.tsx | 11 ++++++++++ web/src/main.tsx | 37 +++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7ea52..21243c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/); until 1.0, minor versions may contain breaking changes. +## [Unreleased] + +### Fixed + +- **Phone: page stayed shifted after the keyboard closed** – the top bar (menu) was out of + reach. On Android the keyboard now shrinks the page instead of shifting it + (`interactive-widget=resizes-content`; the chat stays at its latest message); browsers that + still shift the page (iOS Safari) get it scrolled back once the keyboard closes. + ## [v202611-00015] – 2026-11-10 ### Fixed diff --git a/web/index.html b/web/index.html index b0226d0..a2943c9 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,9 @@ - + + diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx index ad49a5a..0bda1f0 100644 --- a/web/src/components/ChatView.tsx +++ b/web/src/components/ChatView.tsx @@ -202,6 +202,17 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string const el = scrollRef.current; if (el && stick.current) el.scrollTop = el.scrollHeight; }); + // … also when the pane gets smaller (on-screen keyboard opens) without a render. + const hasSession = !!session; + useEffect(() => { + const el = scrollRef.current; + if (!el || typeof ResizeObserver === "undefined") return; + const ro = new ResizeObserver(() => { + if (stick.current) el.scrollTop = el.scrollHeight; + }); + ro.observe(el); + return () => ro.disconnect(); + }, [hasSession, sessionId]); if (loadError) return
{loadError}
; if (!session) return
{t("common.loading")}
; diff --git a/web/src/main.tsx b/web/src/main.tsx index 3ac17f7..8198779 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -10,6 +10,43 @@ import { captureShare } from "./share"; applyTheme(); captureShare(); registerSW({ immediate: true }); +undoKeyboardShift(); + +/** + * Browsers that do not resize the page for the on-screen keyboard (iOS Safari, older + * Android) scroll the whole page up to show the input – and do not always scroll it back + * when the keyboard closes, leaving the top bar (menu) out of reach. The app never + * scrolls the page itself (only its panes), so any page offset is undone once the + * visible area grows again. + */ +function undoKeyboardShift() { + const vv = window.visualViewport; + if (!vv) return; + let last = vv.height; + const reset = () => { + const el = document.scrollingElement ?? document.documentElement; + if (window.scrollY !== 0 || el.scrollTop !== 0 || document.body.scrollTop !== 0) { + window.scrollTo(0, 0); + el.scrollTop = 0; + document.body.scrollTop = 0; + } + }; + vv.addEventListener("resize", () => { + // Grown back (keyboard closed or closing): after the browser's own adjustment. + if (vv.height > last + 1) { + requestAnimationFrame(reset); + setTimeout(reset, 350); + } + last = vv.height; + }); + // Keyboard closed by leaving the input (no resize on some browsers). + document.addEventListener("focusout", () => + setTimeout(() => { + const a = document.activeElement; + if (!a || !(a instanceof HTMLInputElement || a instanceof HTMLTextAreaElement || (a as HTMLElement).isContentEditable)) reset(); + }, 300), + ); +} createRoot(document.getElementById("root")!).render(