SkateSpots/src/components/ScrollToTop.tsx

85 lines
2.4 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { useLocation, useNavigationType } from 'react-router-dom';
const KEY = 'skatespots:scroll-positions';
const MAX = 80;
interface SavedPosition {
path: string;
y: number;
at: number;
}
function readPositions(): Record<string, SavedPosition> {
try {
const raw = sessionStorage.getItem(KEY);
if (!raw) return {};
const parsed = JSON.parse(raw) as unknown;
return parsed && typeof parsed === 'object' ? parsed as Record<string, SavedPosition> : {};
} catch {
return {};
}
}
function writePositions(positions: Record<string, SavedPosition>): void {
try {
const entries = Object.entries(positions)
.sort(([, a], [, b]) => b.at - a.at)
.slice(0, MAX);
sessionStorage.setItem(KEY, JSON.stringify(Object.fromEntries(entries)));
} catch {
// storage may be unavailable
}
}
/**
* Scroll behaviour:
* - new navigation starts at the top;
* - browser Back/Forward restores the saved position for that history entry;
* - query/tab changes on the same page keep the current scroll.
*/
export function ScrollToTop() {
const location = useLocation();
const navigationType = useNavigationType();
const previousPath = useRef(location.pathname);
const raf = useRef<number | null>(null);
useEffect(() => {
const positions = readPositions();
const pathChanged = previousPath.current !== location.pathname;
previousPath.current = location.pathname;
if (!pathChanged) return;
if (navigationType === 'POP') {
const saved = positions[location.key]?.y ?? 0;
requestAnimationFrame(() => window.scrollTo(0, saved));
} else {
window.scrollTo(0, 0);
}
}, [location.key, location.pathname, navigationType]);
useEffect(() => {
const save = () => {
if (raf.current !== null) return;
raf.current = window.requestAnimationFrame(() => {
raf.current = null;
const positions = readPositions();
positions[location.key] = {
path: location.pathname,
y: window.scrollY,
at: Date.now(),
};
writePositions(positions);
});
};
window.addEventListener('scroll', save, { passive: true });
return () => {
window.removeEventListener('scroll', save);
if (raf.current !== null) window.cancelAnimationFrame(raf.current);
raf.current = null;
};
}, [location.key, location.pathname]);
return null;
}