From 850fb71ddda9f39ffa5a2618a9e0fe5ebb3a89bb Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 3 May 2026 11:51:27 +0800 Subject: [PATCH] fix(web): force scroll-to-top on tab switch (iOS Safari quirk) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js App Router *should* reset scroll on route change, but iOS Safari (and occasionally Chrome on Android) preserves the document scroll position when navigating between routes that share a layout — the user lands on the new tab halfway down the page and has to scroll manually. Adds a useEffect([]) in both AccountsTable and UsersTable that calls window.scrollTo({top:0, left:0, behavior:'instant'}) on mount. Route transitions remount the table component (different page module), so the empty deps array fires exactly once per visit. 'instant' behavior avoids a smooth-scroll animation that would compete with the loading-skeleton swap. --- web/components/accounts-table.tsx | 8 ++++++++ web/components/users-table.tsx | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/web/components/accounts-table.tsx b/web/components/accounts-table.tsx index 11c07de..9693db7 100644 --- a/web/components/accounts-table.tsx +++ b/web/components/accounts-table.tsx @@ -75,6 +75,14 @@ export default function AccountsTable({ const [createOpen, setCreateOpen] = useState(false); const [toast, setToast] = useState(null); + // iOS Safari (and sometimes Chrome on Android) keeps the document + // scroll position across SPA route changes, so tab-switching from + // /users back to / leaves the user halfway down the page. Force + // scroll-to-top on mount; route transitions remount the table. + useEffect(() => { + window.scrollTo({ top: 0, left: 0, behavior: "instant" }); + }, []); + // Accumulated rows from initial server-side fetch + every loadMore. const [rows, setRows] = useState(initial); const [hasMore, setHasMore] = useState(initialHasMore); diff --git a/web/components/users-table.tsx b/web/components/users-table.tsx index 6b64b73..2d561d5 100644 --- a/web/components/users-table.tsx +++ b/web/components/users-table.tsx @@ -77,6 +77,13 @@ export default function UsersTable({ const [createOpen, setCreateOpen] = useState(false); const [toast, setToast] = useState(null); + // Force scroll-to-top on mount — iOS Safari preserves document scroll + // across SPA route changes, so tab-switching leaves the new page + // halfway down. Route transitions remount the table, so [] deps fire. + useEffect(() => { + window.scrollTo({ top: 0, left: 0, behavior: "instant" }); + }, []); + const [rows, setRows] = useState(initial); const [hasMore, setHasMore] = useState(initialHasMore); const [total, setTotal] = useState(initialTotal);