diff --git a/change/@fluentui-react-virtualizer-701a2cd4-bd8c-48af-a2d4-753903d62124.json b/change/@fluentui-react-virtualizer-701a2cd4-bd8c-48af-a2d4-753903d62124.json new file mode 100644 index 0000000000000..509786e8a35c1 --- /dev/null +++ b/change/@fluentui-react-virtualizer-701a2cd4-bd8c-48af-a2d4-753903d62124.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: Enable virtualizer to fall back to most recent IO event if none intersecting", + "packageName": "@fluentui/react-virtualizer", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-virtualizer/library/etc/react-virtualizer.api.md b/packages/react-components/react-virtualizer/library/etc/react-virtualizer.api.md index 62e0171de50f1..88f83e9b44995 100644 --- a/packages/react-components/react-virtualizer/library/etc/react-virtualizer.api.md +++ b/packages/react-components/react-virtualizer/library/etc/react-virtualizer.api.md @@ -81,6 +81,7 @@ export const useDynamicVirtualizerMeasure: (virtua bufferSize: number; scrollRef: (instance: TElement | null) => void; containerSizeRef: React_2.RefObject; + updateScrollPosition: (scrollPosition: number) => void; }; // @public @@ -145,8 +146,6 @@ export const virtualizerClassNames: SlotClassNames; export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; - contextPosition?: number; - setContextPosition?: (index: number) => void; childProgressiveSizes?: React_2.MutableRefObject; }; diff --git a/packages/react-components/react-virtualizer/library/src/components/Virtualizer/Virtualizer.types.ts b/packages/react-components/react-virtualizer/library/src/components/Virtualizer/Virtualizer.types.ts index cd5661c86eec2..b0f6802e04c78 100644 --- a/packages/react-components/react-virtualizer/library/src/components/Virtualizer/Virtualizer.types.ts +++ b/packages/react-components/react-virtualizer/library/src/components/Virtualizer/Virtualizer.types.ts @@ -140,8 +140,7 @@ export type VirtualizerConfigProps = { /** * Enables users to override the intersectionObserverRoot. - * RECOMMEND: DO NOT PASS THIS IN, as it can cause side effects - * when overlapping with other scroll views + * We recommend passing this in for accurate distance assessment in IO */ scrollViewRef?: React.MutableRefObject; @@ -195,6 +194,12 @@ export type VirtualizerConfigProps = { * Virtualizer Measure hooks provide a suitable reference. */ containerSizeRef: RefObject; + + /** + * A callback that enables updating scroll position for calculating required dynamic lengths, + * this should be passed in from useDynamicVirtualizerMeasure + */ + updateScrollPosition?: (position: number) => void; }; export type VirtualizerProps = ComponentProps> & VirtualizerConfigProps; diff --git a/packages/react-components/react-virtualizer/library/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/library/src/components/Virtualizer/useVirtualizer.ts index bd6976b03c132..e095aebd0779f 100644 --- a/packages/react-components/react-virtualizer/library/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/library/src/components/Virtualizer/useVirtualizer.ts @@ -26,6 +26,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta containerSizeRef, scrollViewRef, enableScrollLoad, + updateScrollPosition, } = props; /* The context is optional, it's useful for injecting additional index logic, or performing uniform state updates*/ @@ -33,9 +34,10 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // We use this ref as a constant source to access the virtualizer's state imperatively const actualIndexRef = useRef(_virtualizerContext.contextIndex); - const flaggedIndex = useRef(null); + const flaggedIndex = useRef(null); const actualIndex = _virtualizerContext.contextIndex; + // Just in case our ref gets out of date vs the context during a re-render if (_virtualizerContext.contextIndex !== actualIndexRef.current) { actualIndexRef.current = _virtualizerContext.contextIndex; @@ -49,10 +51,10 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta ); // Store ref to before padding element - const beforeElementRef = useRef(null); + const beforeElementRef = useRef(null); // Store ref to before padding element - const afterElementRef = useRef(null); + const afterElementRef = useRef(null); // We need to store an array to track dynamic sizes, we can use this to incrementally update changes const childSizes = useRef(new Array(getItemSize ? numItems : 0)); @@ -306,18 +308,17 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta return; } - // Grab latest entry that is intersecting - const latestEntry = - entries.length === 1 - ? entries[0] - : entries - .sort((entry1, entry2) => entry2.time - entry1.time) - .find(entry => { - return entry.intersectionRatio > 0; - }); - - if (!latestEntry || !latestEntry.isIntersecting) { - // If we don't find an intersecting area, ignore for now. + if (entries.length === 0) { + // No entries found, return. + return; + } + // Find the latest entry that is intersecting + const sortedEntries = entries.sort((entry1, entry2) => entry2.time - entry1.time); + const latestEntry = sortedEntries.find(entry => { + return entry.isIntersecting; + }); + + if (!latestEntry) { return; } @@ -364,14 +365,15 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta } else if (latestEntry.target === beforeElementRef.current) { // Get before buffers position measurementPos = calculateBefore(); + // Get exact intersection position based on overflow size (how far into window did we scroll IO?) const overflowAmount = axis === 'vertical' ? latestEntry.intersectionRect.height : latestEntry.intersectionRect.width; + // Minus from original before position measurementPos -= overflowAmount; // Ignore buffer size (IO offset) measurementPos += bufferSize; - // Calculate how far past the window bounds we are (this will be zero if IO is within window) const hOverflow = latestEntry.boundingClientRect.bottom - latestEntry.intersectionRect.bottom; const hOverflowReversed = latestEntry.boundingClientRect.top - latestEntry.intersectionRect.top; @@ -400,30 +402,34 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // Safety limits const newStartIndex = Math.min(Math.max(startIndex, 0), maxIndex); - flushSync(() => { - _virtualizerContext.setContextPosition(measurementPos); + // Callback to allow measure functions to check virtualizer length + if (newStartIndex + virtualizerLength >= numItems && actualIndex + virtualizerLength >= numItems) { + // We've already hit the end, no need to update state. + return; + } + updateScrollPosition?.(measurementPos); if (actualIndex !== newStartIndex) { batchUpdateNewIndex(newStartIndex); } }); }, [ - _virtualizerContext, actualIndex, + virtualizerLength, axis, - batchUpdateNewIndex, - bufferItems, + reversed, + numItems, bufferSize, + bufferItems, + containerSizeRef, + updateScrollPosition, + batchUpdateNewIndex, calculateAfter, calculateBefore, calculateTotalSize, - containerSizeRef, getIndexFromScrollPosition, - numItems, - reversed, updateCurrentItemSizes, - virtualizerLength, ], ), { diff --git a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx index 64dff6c4a8601..c223a37ec30bd 100644 --- a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx +++ b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx @@ -49,15 +49,16 @@ export function useVirtualizerScrollViewDynamic_unstable( [sizeTrackingArray, props.itemSize, sizeUpdateCount], ); - const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useDynamicVirtualizerMeasure({ - defaultItemSize: props.itemSize, - direction: props.axis ?? 'vertical', - getItemSize: props.getItemSize ?? getChildSizeAuto, - virtualizerContext: contextState, - numItems: props.numItems, - bufferItems: _bufferItems, - bufferSize: _bufferSize, - }); + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef, updateScrollPosition } = + useDynamicVirtualizerMeasure({ + defaultItemSize: props.itemSize, + direction: props.axis ?? 'vertical', + getItemSize: props.getItemSize ?? getChildSizeAuto, + virtualizerContext: contextState, + numItems: props.numItems, + bufferItems: _bufferItems, + bufferSize: _bufferSize, + }); const _imperativeVirtualizerRef = useMergedRefs(React.useRef(null), imperativeVirtualizerRef); @@ -76,7 +77,7 @@ export function useVirtualizerScrollViewDynamic_unstable( if (virtualizerLengthRef.current !== virtualizerLength) { virtualizerLengthRef.current = virtualizerLength; } - const scrollViewRef = useMergedRefs(props.scrollViewRef, scrollRef, paginationRef) as React.RefObject; + const scrollViewRef = useMergedRefs(props.scrollViewRef, scrollRef, paginationRef); const scrollCallbackRef = React.useRef void)>(null); useImperativeHandle( @@ -97,7 +98,7 @@ export function useVirtualizerScrollViewDynamic_unstable( index, itemSizes: _imperativeVirtualizerRef.current?.nodeSizes, totalSize, - scrollViewRef, + scrollViewRef: scrollViewRef as React.RefObject, axis, reversed, behavior, @@ -127,6 +128,8 @@ export function useVirtualizerScrollViewDynamic_unstable( imperativeVirtualizerRef: _imperativeVirtualizerRef, onRenderedFlaggedIndex: handleRenderedIndex, containerSizeRef, + scrollViewRef, + updateScrollPosition, }); const measureObject = useMeasureList( diff --git a/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts index f980e99e20425..0017bd2cf2939 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts @@ -2,7 +2,6 @@ import { useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilit import * as React from 'react'; import { VirtualizerMeasureDynamicProps } from './hooks.types'; import { useResizeObserverRef_unstable } from './useResizeObserverRef'; -import { useRef } from 'react'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** @@ -16,6 +15,7 @@ export const useDynamicVirtualizerMeasure = ( bufferSize: number; scrollRef: (instance: TElement | null) => void; containerSizeRef: React.RefObject; + updateScrollPosition: (scrollPosition: number) => void; } => { const { defaultItemSize, @@ -26,8 +26,6 @@ export const useDynamicVirtualizerMeasure = ( bufferSize, virtualizerContext, } = virtualizerProps; - const indexRef = useRef(virtualizerContext.contextIndex); - indexRef.current = virtualizerContext.contextIndex; const [state, setState] = React.useState({ virtualizerLength: 0, @@ -36,6 +34,7 @@ export const useDynamicVirtualizerMeasure = ( }); const containerSizeRef = React.useRef(0); + const scrollPosition = React.useRef(0); const { virtualizerLength, virtualizerBufferItems, virtualizerBufferSize } = state; const { targetDocument } = useFluent(); @@ -63,9 +62,11 @@ export const useDynamicVirtualizerMeasure = ( let i = 0; let length = 0; + const startIndex = virtualizerContext.contextIndex; const sizeToBeat = containerSizeRef.current + virtualizerBufferSize * 2; - while (indexSizer <= sizeToBeat && i + virtualizerContext.contextIndex < numItems) { - const iItemSize = getItemSize(indexRef.current + i); + + while (indexSizer <= sizeToBeat && i + startIndex < numItems) { + const iItemSize = getItemSize(startIndex + i); if (virtualizerContext.childProgressiveSizes.current.length < numItems) { /* We are in unknown territory, either an initial render or an update in virtualizer item length has occurred. @@ -73,8 +74,8 @@ export const useDynamicVirtualizerMeasure = ( return virtualizerLength - virtualizerBufferSize * 2; } - const currentScrollPos = virtualizerContext.contextPosition; - const currentItemPos = virtualizerContext.childProgressiveSizes.current[indexRef.current + i] - iItemSize; + const currentScrollPos = scrollPosition.current; + const currentItemPos = virtualizerContext.childProgressiveSizes.current[startIndex + i] - iItemSize; if (currentScrollPos > currentItemPos + iItemSize) { // The item isn't in view, ignore for now. @@ -102,8 +103,9 @@ export const useDynamicVirtualizerMeasure = ( /* * This is how far we deviate into the bufferItems to detect a redraw. */ - const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2.0, 1); + const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2, 1); const totalLength = length + newBufferItems * 2; + setState({ virtualizerLength: totalLength, virtualizerBufferSize: newBufferSize, @@ -122,7 +124,6 @@ export const useDynamicVirtualizerMeasure = ( virtualizerBufferSize, virtualizerContext.childProgressiveSizes, virtualizerContext.contextIndex, - virtualizerContext.contextPosition, virtualizerLength, ], ); @@ -151,11 +152,21 @@ export const useDynamicVirtualizerMeasure = ( } }, [handleScrollResize, numItems, virtualizerContext.contextIndex, virtualizerLength]); + const updateScrollPosition = React.useCallback( + (_scrollPosition: number) => { + scrollPosition.current = _scrollPosition; + // Check if our vLength's need recalculating + handleScrollResize(scrollRef); + }, + [handleScrollResize, scrollRef], + ); + return { virtualizerLength, bufferItems: virtualizerBufferItems, bufferSize: virtualizerBufferSize, scrollRef, containerSizeRef, + updateScrollPosition, }; }; diff --git a/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/VirtualizerContext.ts index 63bec41d3f61e..7b5b5e43cfd2c 100644 --- a/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -17,7 +17,6 @@ export const useVirtualizerContextState_unstable = ( ): DynamicVirtualizerContextProps => { const virtualizerContext = useVirtualizerContext_unstable(); const [_contextIndex, _setContextIndex] = useState(-1); - const [_contextPosition, _setContextPosition] = useState(0); const childProgressiveSizes = useRef([]); /* We respect any wrapped providers while also ensuring defaults or passed through @@ -27,12 +26,9 @@ export const useVirtualizerContextState_unstable = ( () => ({ contextIndex: passedContext?.contextIndex ?? virtualizerContext?.contextIndex ?? _contextIndex, setContextIndex: passedContext?.setContextIndex ?? virtualizerContext?.setContextIndex ?? _setContextIndex, - contextPosition: passedContext?.contextPosition ?? virtualizerContext?.contextPosition ?? _contextPosition, - setContextPosition: - passedContext?.setContextPosition ?? virtualizerContext?.setContextPosition ?? _setContextPosition, childProgressiveSizes, }), - [_contextIndex, _contextPosition, passedContext, virtualizerContext], + [_contextIndex, passedContext, virtualizerContext], ); return context; diff --git a/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/types.ts b/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/types.ts index 47a91d04c71ee..9eba6b7dd8a00 100644 --- a/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/types.ts +++ b/packages/react-components/react-virtualizer/library/src/utilities/VirtualizerContext/types.ts @@ -6,10 +6,8 @@ export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; /* - * These option props are used in dynamic virtualizer + * These optional props are used in dynamic virtualizer */ - contextPosition?: number; - setContextPosition?: (index: number) => void; childProgressiveSizes?: React.MutableRefObject; }; diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Dynamic.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Dynamic.stories.tsx index 09f78c9ef429c..4a3f7d78b4689 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Dynamic.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Dynamic.stories.tsx @@ -31,7 +31,6 @@ const useStyles = makeStyles({ export const Dynamic = () => { const [currentIndex, setCurrentIndex] = React.useState(-1); - const [currentPosition, setCurrentPosition] = React.useState(0); const childProgressiveSizes = React.useRef([]); const [flag, toggleFlag] = React.useState(false); const styles = useStyles(); @@ -65,17 +64,16 @@ export const Dynamic = () => { const contextState: DynamicVirtualizerContextProps = { contextIndex: currentIndex, setContextIndex: setCurrentIndex, - contextPosition: currentPosition, - setContextPosition: setCurrentPosition, childProgressiveSizes, }; - const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useDynamicVirtualizerMeasure({ - defaultItemSize: 100, - getItemSize: getSizeForIndex, - numItems: childLength, - virtualizerContext: contextState, - }); + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef, updateScrollPosition } = + useDynamicVirtualizerMeasure({ + defaultItemSize: 100, + getItemSize: getSizeForIndex, + numItems: childLength, + virtualizerContext: contextState, + }); return ( @@ -89,6 +87,7 @@ export const Dynamic = () => { itemSize={100} containerSizeRef={containerSizeRef} virtualizerContext={contextState} + updateScrollPosition={updateScrollPosition} > {useCallback( (index: number) => { diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/AutoMeasure.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/AutoMeasure.stories.tsx index 728cb8e0b0d37..04bf54a1248e9 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/AutoMeasure.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/AutoMeasure.stories.tsx @@ -43,7 +43,7 @@ export const AutoMeasure = () => { bufferSize={minHeight / 2.0} > {(index: number) => { - const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; + const backgroundColor = index % 2 ? '#CCCCCC' : '#ABABAB'; return (