Skip to content

Commit b77fbfe

Browse files
committed
fix(#2766): Virtualizer does not scroll items into view within the browser viewport
Add IntersectionObserver to detect whether or not a focused item is visible within the browser window, and if not scroll it into view.
1 parent cae83ff commit b77fbfe

File tree

1 file changed

+33
-2
lines changed

1 file changed

+33
-2
lines changed

packages/@react-aria/virtualizer/src/Virtualizer.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {Collection} from '@react-types/shared';
1414
import {focusWithoutScrolling, mergeProps, useLayoutEffect} from '@react-aria/utils';
1515
import {Layout, Rect, ReusableView, useVirtualizerState, VirtualizerState} from '@react-stately/virtualizer';
16-
import React, {FocusEvent, HTMLAttributes, Key, ReactElement, RefObject, useCallback, useEffect, useRef} from 'react';
16+
import React, {FocusEvent, HTMLAttributes, Key, ReactElement, RefObject, useCallback, useEffect, useMemo, useRef} from 'react';
1717
import {ScrollView} from './ScrollView';
1818
import {VirtualizerItem} from './VirtualizerItem';
1919

@@ -140,6 +140,32 @@ export function useVirtualizer<T extends object, V, W>(props: VirtualizerOptions
140140
lastFocusedKey.current = focusedKey;
141141
}, [focusedKey, virtualizer.visibleRect.height, virtualizer, lastFocusedKey, scrollToItem]);
142142

143+
// Define an IntersectionObserver to evaluate whether the browser should scroll the element receiving focus into view.
144+
let intersectionObserver = useMemo(() => {
145+
const intersectionObserverOptions:IntersectionObserverInit = {
146+
root: undefined,
147+
rootMargin: '0px',
148+
threshold: 1
149+
};
150+
151+
const scrollIntoViewOptions:ScrollIntoViewOptions = {
152+
behavior: 'auto',
153+
block: 'nearest',
154+
inline: 'nearest'
155+
};
156+
157+
const intersectionObserverCallback = (entries:Array<IntersectionObserverEntry>, observer:IntersectionObserver) => {
158+
entries.forEach(entry => {
159+
if (!entry.isIntersecting) {
160+
entry.target.scrollIntoView(scrollIntoViewOptions);
161+
}
162+
observer.unobserve(entry.target);
163+
});
164+
};
165+
166+
return new IntersectionObserver(intersectionObserverCallback, intersectionObserverOptions);
167+
}, []);
168+
143169
let isFocusWithin = useRef(false);
144170
let onFocus = useCallback((e: FocusEvent) => {
145171
// If the focused item is scrolled out of view and is not in the DOM, the collection
@@ -154,7 +180,12 @@ export function useVirtualizer<T extends object, V, W>(props: VirtualizerOptions
154180
}
155181

156182
isFocusWithin.current = e.target !== ref.current;
157-
}, [ref, virtualizer, focusedKey, scrollToItem]);
183+
184+
// Evaluate whether the browser should scroll to bring element receiving focus into view.
185+
if (isFocusWithin.current) {
186+
intersectionObserver.observe(e.target);
187+
}
188+
}, [ref, virtualizer, focusedKey, scrollToItem, intersectionObserver]);
158189

159190
let onBlur = useCallback((e: FocusEvent) => {
160191
isFocusWithin.current = ref.current.contains(e.relatedTarget as Element);

0 commit comments

Comments
 (0)