diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 18fe7cc3..a03400b1 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -36,6 +36,7 @@ import { } from "../lib/navigation-scroll"; import { queryKeys } from "../lib/queryKeys"; import { scheduleMainContentFocus } from "../lib/main-content-focus"; +import { pinDocumentScrollToZero } from "../lib/pin-document-scroll"; import { cn } from "../lib/utils"; import { NotFoundPage } from "../pages/NotFound"; import { PluginSlotMount, resolveRouteSidebarSlot, usePluginSlots } from "../plugins/slots"; @@ -424,13 +425,24 @@ export function Layout() { useEffect(() => { const previousOverflow = document.body.style.overflow; - document.body.style.overflow = isMobile ? "visible" : "hidden"; + document.body.style.overflow = isMobile ? "visible" : "clip"; return () => { document.body.style.overflow = previousOverflow; }; }, [isMobile]); + // `scrollIntoView` walks every ancestor scroll container. On a long thread + // the post-submit `scrollIntoView` on the new comment reaches `` and + // animates `documentElement.scrollTop` via the browser's internal scroll + // algorithm, which bypasses the CSS `overflow` on the root element and + // visually shifts the entire shell (sidebar included) off-screen. Pin + // both roots to scrollTop=0 on every scroll tick. + useEffect(() => { + if (isMobile) return; + return pinDocumentScrollToZero(); + }, [isMobile]); + useEffect(() => { if (typeof document === "undefined") return; const mainContent = mainContentRef.current; @@ -456,7 +468,7 @@ export function Layout() {