В библиотеке React Aria управление скроллом реализуется через хуки и утилиты, обеспечивающие доступность и предсказуемое поведение в интерфейсе. Основная задача — сохранять и восстанавливать позицию прокрутки при переходах между компонентами, обновлении контента или изменении состояния страницы.
React Aria не навязывает конкретную реализацию скролла, а предоставляет инструменты для отслеживания и управления состоянием прокрутки через refs, события и хуки.
useScroll для отслеживания позицииХук useScroll позволяет привязать обработчики к элементу
с прокруткой и отслеживать текущую позицию:
import {useScroll} from '@react-aria/interactions';
import {useRef} from 'react';
function ScrollableList() {
const scrollRef = useRef(null);
const {scrollProps} = useScroll({
onScroll: (e) => {
console.log('Текущая позиция scrollTop:', e.target.scrollTop);
}
});
return (
<div {...scrollProps} ref={scrollRef} style={{overflowY: 'auto', height: 300}}>
{/* Контент */}
</div>
);
}
Ключевые моменты:
scrollRef используется для прямого доступа к
DOM-элементу.scrollProps включает стандартные события
onScroll и другие атрибуты, обеспечивающие корректное
поведение для ассистивных технологий.onScroll срабатывает при любом изменении позиции
прокрутки.Чтобы сохранять позицию при смене страниц или компонент, можно хранить координаты скролла в состоянии или контексте:
import {useState, useEffect, useRef} from 'react';
function ScrollManager({children}) {
const [scrollPositions, setScrollPositions] = useState({});
const scrollRef = useRef(null);
useEffect(() => {
const handleScroll = () => {
setScrollPositions(prev => ({
...prev,
main: scrollRef.current.scrollTop
}));
};
const node = scrollRef.current;
node.addEventListener('scroll', handleScroll);
return () => node.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (scrollRef.current && scrollPositions.main !== undefined) {
scrollRef.current.scrollTop = scrollPositions.main;
}
}, [scrollPositions]);
return (
<div ref={scrollRef} style={{overflowY: 'auto', height: '100%'}}>
{children}
</div>
);
}
Особенности реализации:
scrollPositions по
ключам, соответствующим различным областям интерфейса.scrollTop
восстанавливается из состояния.При асинхронной подгрузке элементов высота контейнера может
изменяться, что сдвигает позицию скролла. Для корректного восстановления
используется requestAnimationFrame или
setTimeout:
useEffect(() => {
if (scrollRef.current && scrollPositions.main !== undefined) {
requestAnimationFrame(() => {
scrollRef.current.scrollTop = scrollPositions.main;
});
}
}, [scrollPositions, children]);
Пояснение:
requestAnimationFrame откладывает установку позиции до
следующего рендера, когда DOM уже полностью обновлен.useVirtualizer для больших списковReact Aria совместим с библиотекой React Stately и
useVirtualizer, позволяя эффективно работать с длинными
списками без потери позиции скролла:
import {useVirtualizer} from '@tanstack/react-virtual';
function VirtualList({items}) {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50
});
return (
<div ref={parentRef} style={{height: 400, overflowY: 'auto'}}>
<div style={{height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative'}}>
{rowVirtualizer.getVirtualItems().map(virtualRow => (
<div key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`
}}>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
Преимущества:
При восстановлении скролла важно учитывать фокус. React Aria
предоставляет хуки для синхронизации фокуса с прокруткой, например
useFocusRing и useFocusManager:
import {useFocusManager} from '@react-aria/focus';
function FocusScroll() {
const focusManager = useFocusManager();
const scrollToFocused = () => {
const focused = document.activeElement;
if (focused && focused.scrollIntoView) {
focused.scrollIntoView({beh * avior: 'smooth', block: 'nearest'});
}
};
return (
<button onCl ick={() => {
focusManager.focusNext();
scrollToFocused();
}}>
Следующий элемент
</button>
);
}
Ключевой момент:
scrollIntoView корректно работает с динамическим
контентом и виртуализированными списками.ref для контейнеров скролла, чтобы
напрямую управлять scrollTop.