diff --git a/src/components/SearchPanel.tsx b/src/components/SearchPanel.tsx index 41b9c49..460e9c2 100644 --- a/src/components/SearchPanel.tsx +++ b/src/components/SearchPanel.tsx @@ -70,7 +70,11 @@ export function SearchPanel({ const langParam = useMemo(() => langQuery(lang), [lang]); useEffect(() => { - inputRef.current?.focus(); + // Avoid scroll-into-view: browsers default-scroll the focused element + // into the viewport, which moves the underlying page when the search + // overlay opens from a scrolled position. `preventScroll` keeps the page + // exactly where it was. + inputRef.current?.focus({ preventScroll: true }); }, []); useEffect(() => { @@ -133,7 +137,7 @@ export function SearchPanel({ }; return ( -
+
diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index c48b6ca..68cf05a 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -331,6 +331,35 @@ export function PublicLayout() { }; }, [open]); + // Lock background scroll while the mobile search overlay is open. + // Uses the iOS-compatible position-fixed pattern so the underlying page + // doesn't move at all (overflow:hidden alone is not enough on iOS Safari). + useEffect(() => { + if (!mobileSearchOpen) return; + const scrollY = window.scrollY; + const body = document.body; + const prev = { + position: body.style.position, + top: body.style.top, + left: body.style.left, + right: body.style.right, + width: body.style.width, + }; + body.style.position = "fixed"; + body.style.top = `-${scrollY}px`; + body.style.left = "0"; + body.style.right = "0"; + body.style.width = "100%"; + return () => { + body.style.position = prev.position; + body.style.top = prev.top; + body.style.left = prev.left; + body.style.right = prev.right; + body.style.width = prev.width; + window.scrollTo(0, scrollY); + }; + }, [mobileSearchOpen]); + return (