2026-06-04 17:06:29 +08:00
|
|
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
2026-05-29 13:09:09 +08:00
|
|
|
import { useLocation } from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-30 23:47:26 +08:00
|
|
|
* Resets the window to the top on client navigation. React Router does not
|
|
|
|
|
* restore scroll on its own, so without this a short new page would clamp to
|
|
|
|
|
* wherever the previous (taller) page was scrolled — e.g. landing at the bottom
|
|
|
|
|
* of a category page after clicking a card far down the home grid.
|
2026-05-29 13:09:09 +08:00
|
|
|
*
|
2026-05-30 23:47:26 +08:00
|
|
|
* Navigating to a *different* page always jumps to the top, even when the URL
|
|
|
|
|
* carries `?post=<id>`. The destination's own deep-link logic then aligns to
|
|
|
|
|
* that post by scrolling DOWN from the top, instead of animating UP from the
|
|
|
|
|
* previous page's bottom scroll — which looked like the page "scrolling up from
|
|
|
|
|
* the bottom" after tapping a popular-section card.
|
|
|
|
|
*
|
|
|
|
|
* A hash (`#post-<id>`, `#categories`, …) is left alone so the target page can
|
|
|
|
|
* handle its own anchor alignment. A same-page `?post=` change (clicking
|
|
|
|
|
* another card while already on the list) is also left alone so it doesn't
|
|
|
|
|
* fight the in-page alignment.
|
2026-05-29 13:09:09 +08:00
|
|
|
*/
|
|
|
|
|
export function ScrollToTop() {
|
2026-05-29 13:22:40 +08:00
|
|
|
const { pathname, search, hash } = useLocation();
|
2026-05-30 23:47:26 +08:00
|
|
|
const prevPathname = useRef(pathname);
|
2026-05-29 13:09:09 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-04 17:06:29 +08:00
|
|
|
if (!("scrollRestoration" in window.history)) return;
|
|
|
|
|
const previous = window.history.scrollRestoration;
|
|
|
|
|
window.history.scrollRestoration = "manual";
|
|
|
|
|
return () => {
|
|
|
|
|
window.history.scrollRestoration = previous;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
2026-05-30 23:47:26 +08:00
|
|
|
const pathnameChanged = prevPathname.current !== pathname;
|
|
|
|
|
prevPathname.current = pathname;
|
|
|
|
|
|
|
|
|
|
if (hash) return;
|
|
|
|
|
|
|
|
|
|
// Entering a new page: always start at the top (post deep-links align
|
|
|
|
|
// afterwards from here).
|
|
|
|
|
if (pathnameChanged) {
|
|
|
|
|
window.scrollTo({ top: 0, left: 0 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Same page, search-only change (e.g. switching sort/filter): reset to top
|
|
|
|
|
// unless it's an in-page post deep-link, which handles its own alignment.
|
|
|
|
|
if (!new URLSearchParams(search).has("post")) {
|
|
|
|
|
window.scrollTo({ top: 0, left: 0 });
|
|
|
|
|
}
|
2026-05-29 13:22:40 +08:00
|
|
|
}, [pathname, search, hash]);
|
2026-05-29 13:09:09 +08:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|