Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
});
Expand Down
52 changes: 52 additions & 0 deletions src/browser/hooks/useMobileKeyboardFix.ts
Original file line number Diff line number Diff line change
@@ -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);
};
}, []);
}
Loading