Fix iOS Safari zoom-in on lock/unlock

Adds a tiny client component mounted in the root layout that listens for
pageshow with persisted=true (bfcache resume) and nudges scroll + forces
a reflow. Resolves the zoom-desync bug where viewportFit=cover +
maximumScale=1 leaves the visual viewport stale after device lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-04-15 22:11:59 +08:00
parent cd1fd6ad64
commit 9681dddc2e
2 changed files with 22 additions and 0 deletions

View File

@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import { Geist } from "next/font/google"; import { Geist } from "next/font/google";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { BottomNav } from "@/components/BottomNav"; import { BottomNav } from "@/components/BottomNav";
import { ViewportRedrawOnResume } from "@/components/ViewportRedrawOnResume";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const geistSans = Geist({
@ -48,6 +49,7 @@ export default function RootLayout({
suppressHydrationWarning suppressHydrationWarning
> >
<body className="min-h-dvh flex flex-col bg-background text-foreground"> <body className="min-h-dvh flex flex-col bg-background text-foreground">
<ViewportRedrawOnResume />
<Header /> <Header />
<main className="flex-1 bg-background">{children}</main> <main className="flex-1 bg-background">{children}</main>
<BottomNav /> <BottomNav />

View File

@ -0,0 +1,20 @@
"use client";
import { useEffect } from "react";
// iOS Safari bug: with viewportFit=cover + maximumScale=1, restoring from
// bfcache after lock/unlock can leave the visual viewport in a stale
// "zoomed" state. Nudging scroll + forcing a reflow on pageshow
// (persisted) realigns it without changing the zoom level.
export function ViewportRedrawOnResume() {
useEffect(() => {
const onShow = (e: PageTransitionEvent) => {
if (!e.persisted) return;
window.scrollTo(window.scrollX, window.scrollY);
void document.body.offsetHeight;
};
window.addEventListener("pageshow", onShow);
return () => window.removeEventListener("pageshow", onShow);
}, []);
return null;
}