diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 013bb0a027..d8c8f8b992 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -65,6 +65,7 @@ import { getWorkspaceSidebarKey } from "./utils/workspace"; import { WindowsToolchainBanner } from "./components/WindowsToolchainBanner"; import { RosettaBanner } from "./components/RosettaBanner"; import { isDesktopMode } from "./hooks/useDesktopTitlebar"; +import { useMobileKeyboardFix } from "./hooks/useMobileKeyboardFix"; import { cn } from "@/common/lib/utils"; function AppInner() { @@ -102,6 +103,10 @@ function AppInner() { // Auto-collapse sidebar on mobile by default const isMobile = typeof window !== "undefined" && window.innerWidth <= 768; + + // Fix iOS Safari visual viewport scroll issue on keyboard dismiss + useMobileKeyboardFix(); + const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile, { listener: true, }); diff --git a/src/browser/hooks/useMobileKeyboardFix.ts b/src/browser/hooks/useMobileKeyboardFix.ts new file mode 100644 index 0000000000..c0ffb99f42 --- /dev/null +++ b/src/browser/hooks/useMobileKeyboardFix.ts @@ -0,0 +1,52 @@ +/** + * useMobileKeyboardFix + * + * iOS Safari has a known issue where the visual viewport can remain scrolled + * after the virtual keyboard dismisses. This leaves fixed-position elements + * (like our mobile header) appearing offset from their intended position. + * + * This hook listens for visual viewport resize events (which fire when the + * keyboard opens/closes) and resets window scroll when the viewport height + * increases (keyboard closing). + * + * Only active on mobile touch devices where this bug occurs. + */ +import { useEffect } from "react"; + +export function useMobileKeyboardFix(): void { + useEffect(() => { + // Only apply fix on mobile touch devices + const isMobileTouch = + typeof window !== "undefined" && + window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches; + + if (!isMobileTouch || !window.visualViewport) { + return; + } + + const vv = window.visualViewport; + let lastHeight = vv.height; + + const handleResize = () => { + const currentHeight = vv.height; + + // When viewport height increases significantly (keyboard closing), + // reset scroll position to fix the offset header issue + if (currentHeight > lastHeight + 50) { + // Use requestAnimationFrame to ensure DOM has settled + requestAnimationFrame(() => { + // Scroll the window to top to reset any viewport offset + window.scrollTo(0, 0); + }); + } + + lastHeight = currentHeight; + }; + + vv.addEventListener("resize", handleResize); + + return () => { + vv.removeEventListener("resize", handleResize); + }; + }, []); +}