85 lines
2.4 KiB
TypeScript
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;
|
|
}
|