44 lines
1.9 KiB
TypeScript
44 lines
1.9 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
||
import { useLocation } from 'wouter';
|
||
|
||
// Позиции скролла по пути — живут на уровне модуля, переживают размонтирование страниц
|
||
const scrollPositions = new Map<string, number>();
|
||
|
||
/**
|
||
* Запоминает scrollTop скролл-контейнера при уходе со страницы и восстанавливает
|
||
* его при возврате на тот же путь (после рендера, через requestAnimationFrame).
|
||
*
|
||
* Скроллящим элементом может быть как сам контейнер (десктоп: overflow-auto),
|
||
* так и родительский <main> (мобильный Sidebar) — читаем/пишем оба, берём максимум.
|
||
*
|
||
* noScroll-страницы и виртуализированные списки пропускаются через disabled —
|
||
* они управляют скроллом сами.
|
||
*/
|
||
export function useScrollRestoration<T extends HTMLElement>(disabled?: boolean) {
|
||
const ref = useRef<T>(null);
|
||
const [location] = useLocation();
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el || disabled) return;
|
||
// Ключ включает search, чтобы ?tab=... и подобные варианты страницы не делили позицию
|
||
const key = location + window.location.search;
|
||
const main = el.closest('main');
|
||
|
||
const raf = requestAnimationFrame(() => {
|
||
const saved = scrollPositions.get(key);
|
||
if (saved == null) return;
|
||
el.scrollTop = saved;
|
||
if (main instanceof HTMLElement) main.scrollTop = saved;
|
||
});
|
||
|
||
return () => {
|
||
cancelAnimationFrame(raf);
|
||
const top = Math.max(el.scrollTop, main instanceof HTMLElement ? main.scrollTop : 0);
|
||
scrollPositions.set(key, top);
|
||
};
|
||
}, [location, disabled]);
|
||
|
||
return ref;
|
||
}
|