diff --git a/change/@fluentui-react-virtualizer-bab73d05-7372-4052-8d22-ca75b5fadfe2.json b/change/@fluentui-react-virtualizer-bab73d05-7372-4052-8d22-ca75b5fadfe2.json new file mode 100644 index 0000000000000..23f3859fc95bd --- /dev/null +++ b/change/@fluentui-react-virtualizer-bab73d05-7372-4052-8d22-ca75b5fadfe2.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "BREAKING CHANGE (useVirtualizerDynamicMeasure): optimized with scrollPos state and children height reference, updated algorithm to be more accurate, and exported measurement hook", + "packageName": "@fluentui/react-virtualizer", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-combobox/stories/src/Combobox/ComboboxVirtualizer.stories.tsx b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxVirtualizer.stories.tsx index e30a5a3d8e3c4..4de580c3b373c 100644 --- a/packages/react-components/react-combobox/stories/src/Combobox/ComboboxVirtualizer.stories.tsx +++ b/packages/react-components/react-combobox/stories/src/Combobox/ComboboxVirtualizer.stories.tsx @@ -17,10 +17,11 @@ const useStyles = makeStyles({ export const ComboboxVirtualizer = (props: Partial) => { const comboId = useId('combobox'); - const itemHeight = 32; //This should match the height of each item in the listbox + //This should include the item height (32px) and account for rowGap (2px) + const itemHeight = 34; const numberOfItems = 10000; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: itemHeight, direction: 'vertical', }); @@ -43,6 +44,7 @@ export const ComboboxVirtualizer = (props: Partial) => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={itemHeight} + containerSizeRef={containerSizeRef} > {index => { return ( 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 6b47e848983c2..62e0171de50f1 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 @@ -15,6 +15,15 @@ import type { SetStateAction } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +// @public +export type DynamicVirtualizerContextProps = Required; + +// @public (undocumented) +export interface IndexedResizeCallbackElement { + // (undocumented) + handleResize: () => void; +} + // @public (undocumented) export const renderVirtualizer_unstable: (state: VirtualizerState) => JSX.Element; @@ -71,6 +80,7 @@ export const useDynamicVirtualizerMeasure: (virtua bufferItems: number; bufferSize: number; scrollRef: (instance: TElement | null) => void; + containerSizeRef: React_2.RefObject; }; // @public @@ -80,6 +90,15 @@ export const useIntersectionObserver: (callback: IntersectionObserverCallback, o observer: MutableRefObject; }; +// @public +export function useMeasureList(currentIndex: number, refLength: number, totalLength: number, defaultItemSize: number): { + widthArray: React_2.MutableRefObject; + heightArray: React_2.MutableRefObject; + createIndexedRef: (index: number) => (el: TElement) => void; + refArray: React_2.MutableRefObject<(TElement | null | undefined)[]>; + sizeUpdateCount: number; +}; + // @public export const useResizeObserverRef_unstable: (resizeCallback: ResizeCallbackWithRef) => (instance: HTMLElement | HTMLDivElement | null) => void; @@ -89,6 +108,7 @@ export const useStaticVirtualizerMeasure: (virtual bufferItems: number; bufferSize: number; scrollRef: (instance: TElement | null) => void; + containerSizeRef: React_2.MutableRefObject; }; // @public (undocumented) @@ -115,7 +135,7 @@ export const useVirtualizerStyles_unstable: (state: VirtualizerState) => Virtual // @public export const Virtualizer: FC; -// @public (undocumented) +// @public export type VirtualizerChildRenderFunction = (index: number, isScrolling: boolean) => React_2.ReactNode; // @public (undocumented) @@ -125,6 +145,9 @@ export const virtualizerClassNames: SlotClassNames; export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; + contextPosition?: number; + setContextPosition?: (index: number) => void; + childProgressiveSizes?: React_2.MutableRefObject; }; // @public (undocumented) @@ -141,16 +164,20 @@ export type VirtualizerDataRef = { // @public (undocumented) export type VirtualizerMeasureDynamicProps = { defaultItemSize: number; - currentIndex: number; + virtualizerContext: DynamicVirtualizerContextProps; numItems: number; getItemSize: (index: number) => number; direction?: 'vertical' | 'horizontal'; + bufferItems?: number; + bufferSize?: number; }; // @public (undocumented) export type VirtualizerMeasureProps = { defaultItemSize: number; direction?: 'vertical' | 'horizontal'; + bufferItems?: number; + bufferSize?: number; }; // @public (undocumented) @@ -169,13 +196,14 @@ export const VirtualizerScrollViewDynamic: React_2.FC; // @public (undocumented) -export type VirtualizerScrollViewDynamicProps = ComponentProps> & Partial> & { +export type VirtualizerScrollViewDynamicProps = ComponentProps> & Partial> & { itemSize: number; getItemSize?: (index: number) => number; numItems: number; children: VirtualizerChildRenderFunction; imperativeRef?: RefObject; enablePagination?: boolean; + virtualizerContext?: DynamicVirtualizerContextProps; }; // @public (undocumented) 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 f2664f6d6d9f6..cd5661c86eec2 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 @@ -52,6 +52,11 @@ export type VirtualizerConfigState = { * Tells the virtualizer to measure in the reverse direction (for column-reverse order etc.) */ reversed?: boolean; + /** + * Enables the isScrolling property in the child render function + * Default: false - to prevent nessecary render function calls + */ + enableScrollLoad?: boolean; /** * Pixel size of intersection observers and how much they 'cross over' into the bufferItems index. * Minimum 1px. @@ -69,8 +74,10 @@ export type VirtualizerConfigState = { export type VirtualizerState = ComponentState & VirtualizerConfigState; -// Virtualizer render function to procedurally generate children elements as rows or columns via index. -// Q: Use generic typing and passing through object data or a simple index system? +/** + * The main child render method of Virtualization + * isScrolling will only be enabled when enableScrollLoad is set to true. + */ export type VirtualizerChildRenderFunction = (index: number, isScrolling: boolean) => React.ReactNode; export type VirtualizerDataRef = { @@ -109,16 +116,19 @@ export type VirtualizerConfigProps = { virtualizerLength: number; /** - * Defaults to 1/4th of virtualizerLength. + * Defaults to 1/4th (or 1/3rd for dynamic items) of virtualizerLength. + * RECOMMEND: Override this with a consistent value if using a dynamic virtualizer. + * * Controls the number of elements rendered before the current index entering the virtualized viewport. * Constraints: * - Large enough to cover bufferSize (prevents buffers intersecting into the viewport during rest state). - * - Small enough that the end buffer and end index (start index + virtualizerLength) is not within viewport at rest. + * - Small enough that the virtualizer only renders a few items outside of view. */ bufferItems?: number; /** - * Defaults to half of bufferItems size (in pixels). + * Defaults to half of bufferItems * itemSize size (in pixels). + * RECOMMEND: Override this with a consistent minimum item size value if using a dynamic virtualizer. * The length (in pixels) before the end/start DOM index where the virtualizer recalculation will be triggered. * Increasing this reduces whitespace on ultra-fast scroll, as additional elements * are buffered to appear while virtualization recalculates. @@ -130,6 +140,8 @@ 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 */ scrollViewRef?: React.MutableRefObject; @@ -145,6 +157,12 @@ export type VirtualizerConfigProps = { */ reversed?: boolean; + /** + * Enables the isScrolling property in the child render function + * Default: false - to prevent nessecary render function calls + */ + enableScrollLoad?: boolean; + /** * Callback for acquiring size of individual items * @param index - the index of the requested size's child @@ -171,6 +189,12 @@ export type VirtualizerConfigProps = { * Imperative ref contains our scrollTo index functionality for user control. */ imperativeVirtualizerRef?: RefObject; + + /** + * A ref that provides the size of container (vertical - height, horizontal - width), set by a resize observer. + * Virtualizer Measure hooks provide a suitable reference. + */ + containerSizeRef: RefObject; }; 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 27957cbfc4c6a..bd6976b03c132 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 @@ -1,11 +1,11 @@ import type { ReactNode } from 'react'; import type { VirtualizerProps, VirtualizerState } from './Virtualizer.types'; -import { useEffect, useRef, useCallback, useReducer, useImperativeHandle, useState } from 'react'; +import { useEffect, useRef, useCallback, useImperativeHandle, useState, useReducer } from 'react'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; -import { flushSync } from 'react-dom'; import { useVirtualizerContextState_unstable } from '../../Utilities'; import { slot, useTimeout } from '@fluentui/react-utilities'; +import { flushSync } from 'react-dom'; export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerState { 'use no memo'; @@ -18,12 +18,14 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta getItemSize, bufferItems = Math.round(virtualizerLength / 4.0), bufferSize = Math.floor(bufferItems / 2.0) * itemSize, - scrollViewRef, axis = 'vertical', reversed = false, virtualizerContext, onRenderedFlaggedIndex, imperativeVirtualizerRef, + containerSizeRef, + scrollViewRef, + enableScrollLoad, } = props; /* The context is optional, it's useful for injecting additional index logic, or performing uniform state updates*/ @@ -31,13 +33,20 @@ 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); - if (actualIndexRef.current !== _virtualizerContext.contextIndex) { - actualIndexRef.current = _virtualizerContext.contextIndex; - } const flaggedIndex = useRef(null); const actualIndex = _virtualizerContext.contextIndex; - const setActualIndex = _virtualizerContext.setContextIndex; + // 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; + } + const setActualIndex = useCallback( + (index: number) => { + actualIndexRef.current = index; + _virtualizerContext.setContextIndex(index); + }, + [_virtualizerContext], + ); // Store ref to before padding element const beforeElementRef = useRef(null); @@ -51,15 +60,13 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta /* We keep track of the progressive sizing/placement down the list, this helps us skip re-calculations unless children/size changes */ const childProgressiveSizes = useRef(new Array(getItemSize ? numItems : 0)); + if (virtualizerContext?.childProgressiveSizes) { + virtualizerContext.childProgressiveSizes.current = childProgressiveSizes.current; + } // The internal tracking REF for child array (updates often). const childArray = useRef(new Array(virtualizerLength)); - // We want to be methodical about updating the render with child reference array - const forceUpdate = useReducer(() => ({}), {})[1]; - - const horizontal = axis === 'horizontal'; - const populateSizeArrays = () => { if (!getItemSize) { // Static sizes, never mind! @@ -72,6 +79,9 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta if (numItems !== childProgressiveSizes.current.length) { childProgressiveSizes.current = new Array(numItems); + if (virtualizerContext?.childProgressiveSizes) { + virtualizerContext.childProgressiveSizes.current = childProgressiveSizes.current; + } } for (let index = 0; index < numItems; index++) { @@ -89,6 +99,10 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta const scrollCounter = useRef(0); const initializeScrollingTimer = useCallback(() => { + if (!enableScrollLoad) { + // Disabled by default for reduction of render callbacks + return; + } /* * This can be considered the 'velocity' required to start 'isScrolling' * INIT_SCROLL_FLAG_REQ: Number of renders required to activate isScrolling @@ -107,175 +121,134 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta setIsScrolling(false); scrollCounter.current = 0; }, INIT_SCROLL_FLAG_DELAY); - }, [clearScrollTimer, setScrollTimer]); + }, [clearScrollTimer, setScrollTimer, enableScrollLoad]); useEffect(() => { initializeScrollingTimer(); }, [actualIndex, initializeScrollingTimer]); - const batchUpdateNewIndex = (index: number) => { - // Local updates - updateChildRows(index); - updateCurrentItemSizes(index); - - // Set before 'setActualIndex' call - // If it changes before render, or injected via context, re-render will update ref. - actualIndexRef.current = index; - - // State setters - setActualIndex(index); - }; - - // Observe intersections of virtualized components - const { setObserverList } = useIntersectionObserver( - // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 - // eslint-disable-next-line no-restricted-globals - (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { - /* Sanity check - do we even need virtualization? */ - if (virtualizerLength > numItems) { - if (actualIndex !== 0) { - batchUpdateNewIndex(0); - } - // No-op + const updateChildRows = useCallback( + (newIndex: number) => { + if (numItems === 0) { + /* Nothing to virtualize */ return; } - /* IO initiates this function when needed (bookend entering view) */ - let measurementPos = 0; - let bufferCount = bufferItems; - - // 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) { - // If we don't find an intersecting area, ignore for now. - return; + /* + We reset the array every time to ensure children are re-rendered + This function should only be called when update is nessecary + */ + childArray.current = new Array(virtualizerLength); + const _actualIndex = Math.max(newIndex, 0); + const end = Math.min(_actualIndex + virtualizerLength, numItems); + for (let i = _actualIndex; i < end; i++) { + childArray.current[i - _actualIndex] = renderChild(i, isScrolling); } + }, + [isScrolling, numItems, renderChild, virtualizerLength], + ); - if (latestEntry.target === afterElementRef.current) { - // We need to inverse the buffer count - bufferCount = virtualizerLength - bufferItems; - measurementPos = reversed ? calculateAfter() : calculateTotalSize() - calculateAfter(); - if (!horizontal) { - if (reversed) { - // Scrolling 'up' and hit the after element below - measurementPos -= Math.abs(latestEntry.boundingClientRect.bottom); - } else if (latestEntry.boundingClientRect.top < 0) { - // Scrolling 'down' and hit the after element above top: 0 - measurementPos -= latestEntry.boundingClientRect.top; - } - } else { - if (reversed) { - // Scrolling 'left' and hit the after element - measurementPos -= Math.abs(latestEntry.boundingClientRect.right); - } else if (latestEntry.boundingClientRect.left < 0) { - // Scrolling 'right' and hit the after element - measurementPos -= latestEntry.boundingClientRect.left; - } - } - } else if (latestEntry.target === beforeElementRef.current) { - measurementPos = reversed ? calculateTotalSize() - calculateBefore() : calculateBefore(); - if (!horizontal) { - if (!reversed) { - measurementPos -= Math.abs(latestEntry.boundingClientRect.bottom); - } else if (latestEntry.boundingClientRect.top < 0) { - // Scrolling 'down' in reverse order and hit the before element above top: 0 - measurementPos -= latestEntry.boundingClientRect.top; - } - } else { - if (!reversed) { - measurementPos -= Math.abs(latestEntry.boundingClientRect.right); - } else if (latestEntry.boundingClientRect.left < 0) { - // Scrolling 'left' and hit before element - measurementPos -= latestEntry.boundingClientRect.left; - } + const updateCurrentItemSizes = useCallback( + (newIndex: number) => { + if (!getItemSize) { + // Static sizes, not required. + return; + } + // We should always call our size function on index change (only for the items that will be rendered) + // This ensures we request the latest data for incoming items in case sizing has changed. + const endIndex = Math.min(newIndex + virtualizerLength, numItems); + const startIndex = Math.max(newIndex, 0); + + let didUpdate = false; + for (let i = startIndex; i < endIndex; i++) { + const newSize = getItemSize(i); + if (newSize !== childSizes.current[i]) { + childSizes.current[i] = newSize; + didUpdate = true; } } - if (reversed) { - // We're reversed, up is down, left is right, invert the scroll measure. - measurementPos = Math.max(calculateTotalSize() - Math.abs(measurementPos), 0); + if (didUpdate) { + // Update our progressive size array + for (let i = startIndex; i < numItems; i++) { + const prevSize = i > 0 ? childProgressiveSizes.current[i - 1] : 0; + childProgressiveSizes.current[i] = prevSize + childSizes.current[i]; + } } + }, + [getItemSize, numItems, virtualizerLength], + ); - // For now lets use hardcoded size to assess current element to paginate on - const startIndex = getIndexFromScrollPosition(measurementPos); - const bufferedIndex = Math.max(startIndex - bufferCount, 0); - - // Safety limits - const maxIndex = Math.max(numItems - virtualizerLength, 0); - const newStartIndex = Math.min(Math.max(bufferedIndex, 0), maxIndex); + const batchUpdateNewIndex = useCallback( + (index: number) => { + // Local updates + updateChildRows(index); + updateCurrentItemSizes(index); - if (actualIndex !== newStartIndex) { - // We flush sync this and perform an immediate state update - flushSync(() => { - batchUpdateNewIndex(newStartIndex); - }); - } - }, - { - root: scrollViewRef ? scrollViewRef?.current : null, - rootMargin: '0px', - threshold: 0, + // State setters + setActualIndex(index); }, + [setActualIndex, updateChildRows, updateCurrentItemSizes], ); - const findIndexRecursive = (scrollPos: number, lowIndex: number, highIndex: number): number => { - if (lowIndex > highIndex) { - // We shouldn't get here - but no-op the index if we do. - return actualIndex; - } - const midpoint = Math.floor((lowIndex + highIndex) / 2); - const iBefore = Math.max(midpoint - 1, 0); - const iAfter = Math.min(midpoint + 1, childProgressiveSizes.current.length - 1); - const indexValue = childProgressiveSizes.current[midpoint]; - const afterIndexValue = childProgressiveSizes.current[iAfter]; - const beforeIndexValue = childProgressiveSizes.current[iBefore]; - if (scrollPos <= afterIndexValue && scrollPos >= beforeIndexValue) { - /* We've found our index - if we are exactly matching before/after index that's ok, + const findIndexRecursive = useCallback( + (scrollPos: number, lowIndex: number, highIndex: number): number => { + if (lowIndex > highIndex) { + // We shouldn't get here - but no-op the index if we do. + return actualIndex; + } + const midpoint = Math.floor((lowIndex + highIndex) / 2); + const iBefore = Math.max(midpoint - 1, 0); + const iAfter = Math.min(midpoint + 1, childProgressiveSizes.current.length - 1); + const indexValue = childProgressiveSizes.current[midpoint]; + const afterIndexValue = childProgressiveSizes.current[iAfter]; + const beforeIndexValue = childProgressiveSizes.current[iBefore]; + if (scrollPos <= afterIndexValue && scrollPos >= beforeIndexValue) { + /* We've found our index - if we are exactly matching before/after index that's ok, better to reduce checks if it's right on the boundary. */ - return midpoint; - } - - if (indexValue > scrollPos) { - return findIndexRecursive(scrollPos, lowIndex, midpoint - 1); - } else { - return findIndexRecursive(scrollPos, midpoint + 1, highIndex); - } - }; + return midpoint; + } - const getIndexFromSizeArray = (scrollPos: number): number => { - /* Quick searches our progressive height array */ - if ( - scrollPos === 0 || - childProgressiveSizes.current.length === 0 || - scrollPos <= childProgressiveSizes.current[0] - ) { - // Check start - return 0; - } + if (indexValue > scrollPos) { + return findIndexRecursive(scrollPos, lowIndex, midpoint - 1); + } else { + return findIndexRecursive(scrollPos, midpoint + 1, highIndex); + } + }, + [actualIndex], + ); - if (scrollPos >= childProgressiveSizes.current[childProgressiveSizes.current.length - 1]) { - // Check end - return childProgressiveSizes.current.length - 1; - } + const getIndexFromSizeArray = useCallback( + (scrollPos: number): number => { + /* Quick searches our progressive height array */ + if ( + scrollPos === 0 || + childProgressiveSizes.current.length === 0 || + scrollPos <= childProgressiveSizes.current[0] + ) { + // Check start + return 0; + } - return findIndexRecursive(scrollPos, 0, childProgressiveSizes.current.length - 1); - }; + if (scrollPos >= childProgressiveSizes.current[childProgressiveSizes.current.length - 1]) { + // Check end + return childProgressiveSizes.current.length - 1; + } - const getIndexFromScrollPosition = (scrollPos: number) => { - if (!getItemSize) { - return Math.round(scrollPos / itemSize); - } + return findIndexRecursive(scrollPos, 0, childProgressiveSizes.current.length - 1); + }, + [findIndexRecursive], + ); + const getIndexFromScrollPosition = useCallback( + (scrollPos: number) => { + if (!getItemSize) { + return Math.round(scrollPos / itemSize); + } - return getIndexFromSizeArray(scrollPos); - }; + return getIndexFromSizeArray(scrollPos); + }, + [getIndexFromSizeArray, getItemSize, itemSize], + ); const calculateTotalSize = useCallback(() => { if (!getItemSize) { @@ -318,25 +291,146 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta return childProgressiveSizes.current[numItems - 1] - childProgressiveSizes.current[lastItemIndex - 1]; }, [actualIndex, getItemSize, itemSize, numItems, virtualizerLength]); - const updateChildRows = useCallback( - (newIndex: number) => { - if (numItems === 0) { - /* Nothing to virtualize */ - return; - } + // Observe intersections of virtualized components + const { setObserverList } = useIntersectionObserver( + useCallback( + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals + (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { + /* Sanity check - do we even need virtualization? */ + if (virtualizerLength > numItems) { + if (actualIndex !== 0) { + batchUpdateNewIndex(0); + } + // No-op + return; + } - /* - We reset the array every time to ensure children are re-rendered - This function should only be called when update is nessecary - */ - childArray.current = new Array(virtualizerLength); - const _actualIndex = Math.max(newIndex, 0); - const end = Math.min(_actualIndex + virtualizerLength, numItems); - for (let i = _actualIndex; i < end; i++) { - childArray.current[i - _actualIndex] = renderChild(i, isScrolling); - } + // 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. + return; + } + + // We have to be sure our item sizes are up to date with current indexed ref before calculation + // Check if we still need + updateCurrentItemSizes(actualIndexRef.current); + + const calculateOverBuffer = (): number => { + /** + * We avoid using the scroll ref scrollTop, it may be incorrect + * as virtualization may exist within a scroll view with other elements + * The benefit of using IO is that we can detect relative scrolls, + * so any items can be placed around the virtualizer in the scroll view + */ + let measurementPos = 0; + if (latestEntry.target === afterElementRef.current) { + // Get after buffers position + measurementPos = calculateTotalSize() - calculateAfter(); + + // Get exact intersection position based on overflow size (how far into IO did we scroll?) + const overflowAmount = + axis === 'vertical' ? latestEntry.intersectionRect.height : latestEntry.intersectionRect.width; + // Add to original after position + measurementPos += overflowAmount; + // Ignore buffer size (IO offset) + measurementPos -= bufferSize; + // we hit the after buffer and detected the end of view, we need to find the start index. + measurementPos -= containerSizeRef.current ?? 0; + + // Calculate how far past the window bounds we are (this will be zero if IO is within window) + const hOverflow = latestEntry.boundingClientRect.top - latestEntry.intersectionRect.top; + const hOverflowReversed = latestEntry.boundingClientRect.bottom - latestEntry.intersectionRect.bottom; + const wOverflow = latestEntry.boundingClientRect.left - latestEntry.intersectionRect.left; + const wOverflowReversed = latestEntry.boundingClientRect.right - latestEntry.intersectionRect.right; + const widthOverflow = reversed ? wOverflowReversed : wOverflow; + const heightOverflow = reversed ? hOverflowReversed : hOverflow; + const additionalOverflow = axis === 'vertical' ? heightOverflow : widthOverflow; + + if (reversed) { + measurementPos += additionalOverflow; + } else { + measurementPos -= additionalOverflow; + } + } 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; + const wOverflow = latestEntry.boundingClientRect.right - latestEntry.intersectionRect.right; + const wOverflowReversed = latestEntry.boundingClientRect.left - latestEntry.intersectionRect.left; + const widthOverflow = reversed ? wOverflowReversed : wOverflow; + const heightOverflow = reversed ? hOverflowReversed : hOverflow; + const additionalOverflow = axis === 'vertical' ? heightOverflow : widthOverflow; + + if (reversed) { + measurementPos += additionalOverflow; + } else { + measurementPos -= additionalOverflow; + } + } + + return measurementPos; + }; + + // Get exact relative 'scrollTop' via IO values + const measurementPos = calculateOverBuffer(); + + const maxIndex = Math.max(numItems - virtualizerLength, 0); + + const startIndex = getIndexFromScrollPosition(measurementPos) - bufferItems; + + // Safety limits + const newStartIndex = Math.min(Math.max(startIndex, 0), maxIndex); + + flushSync(() => { + _virtualizerContext.setContextPosition(measurementPos); + if (actualIndex !== newStartIndex) { + batchUpdateNewIndex(newStartIndex); + } + }); + }, + [ + _virtualizerContext, + actualIndex, + axis, + batchUpdateNewIndex, + bufferItems, + bufferSize, + calculateAfter, + calculateBefore, + calculateTotalSize, + containerSizeRef, + getIndexFromScrollPosition, + numItems, + reversed, + updateCurrentItemSizes, + virtualizerLength, + ], + ), + { + root: scrollViewRef ? scrollViewRef?.current : null, + rootMargin: '0px', + threshold: 0, }, - [isScrolling, numItems, renderChild, virtualizerLength], ); const setBeforeRef = useCallback( @@ -379,34 +473,6 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta [setObserverList], ); - const updateCurrentItemSizes = (newIndex: number) => { - if (!getItemSize) { - // Static sizes, not required. - return; - } - // We should always call our size function on index change (only for the items that will be rendered) - // This ensures we request the latest data for incoming items in case sizing has changed. - const endIndex = Math.min(newIndex + virtualizerLength, numItems); - const startIndex = Math.max(newIndex, 0); - - let didUpdate = false; - for (let i = startIndex; i < endIndex; i++) { - const newSize = getItemSize(i); - if (newSize !== childSizes.current[i]) { - childSizes.current[i] = newSize; - didUpdate = true; - } - } - - if (didUpdate) { - // Update our progressive size array - for (let i = startIndex; i < numItems; i++) { - const prevSize = i > 0 ? childProgressiveSizes.current[i - 1] : 0; - childProgressiveSizes.current[i] = prevSize + childSizes.current[i]; - } - } - }; - // Initialize the size array before first render. const hasInitialized = useRef(false); const initializeSizeArray = () => { @@ -438,14 +504,22 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + /* + * forceUpdate: + * We only want to trigger this when scrollLoading is enabled and set to false, + * it will force re-render all children elements + */ + const forceUpdate = useReducer(() => ({}), {})[1]; // If the user passes in an updated renderChild function - update current children useEffect(() => { if (actualIndex >= 0) { updateChildRows(actualIndex); - forceUpdate(); + if (enableScrollLoad && !isScrolling) { + forceUpdate(); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [renderChild, updateChildRows]); + }, [renderChild, isScrolling]); useEffect(() => { // Ensure we repopulate if getItemSize callback changes diff --git a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts index a1aebaca2b782..93d7e640b0f34 100644 --- a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts +++ b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts @@ -10,7 +10,7 @@ import { useStaticVirtualizerPagination } from '../../hooks/useStaticPagination' export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewProps): VirtualizerScrollViewState { const { imperativeRef, itemSize, numItems, axis = 'vertical', reversed, enablePagination = false } = props; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: props.itemSize, direction: props.axis ?? 'vertical', }); @@ -61,9 +61,9 @@ export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewPr virtualizerLength, bufferItems, bufferSize, - scrollViewRef, onRenderedFlaggedIndex: handleRenderedIndex, imperativeVirtualizerRef, + containerSizeRef, }); return { diff --git a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts index 8ff695b652275..3121398286109 100644 --- a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts +++ b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts @@ -7,12 +7,17 @@ import type { import type { VirtualizerScrollViewSlots } from '../VirtualizerScrollView/VirtualizerScrollView.types'; import type { RefObject } from 'react'; -import type { ScrollToInterface } from '../../Utilities'; +import type { DynamicVirtualizerContextProps, ScrollToInterface } from '../../Utilities'; export type VirtualizerScrollViewDynamicSlots = VirtualizerScrollViewSlots; export type VirtualizerScrollViewDynamicProps = ComponentProps> & - Partial> & { + Partial< + Omit< + VirtualizerConfigProps, + 'itemSize' | 'numItems' | 'getItemSize' | 'children' | 'flagIndex' | 'virtualizerContext' + > + > & { /** * Set as the minimum item size. * Axis: 'vertical' = Height @@ -43,6 +48,10 @@ export type VirtualizerScrollViewDynamicProps = ComponentProps & 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 7957635acbdf4..64dff6c4a8601 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 @@ -19,7 +19,15 @@ export function useVirtualizerScrollViewDynamic_unstable( 'use no memo'; const contextState = useVirtualizerContextState_unstable(props.virtualizerContext); - const { imperativeRef, axis = 'vertical', reversed, imperativeVirtualizerRef, enablePagination = false } = props; + const { + imperativeRef, + axis = 'vertical', + reversed, + imperativeVirtualizerRef, + enablePagination = false, + bufferItems: _bufferItems, + bufferSize: _bufferSize, + } = props; let sizeTrackingArray = React.useRef(new Array(props.numItems).fill(props.itemSize)); @@ -41,12 +49,14 @@ export function useVirtualizerScrollViewDynamic_unstable( [sizeTrackingArray, props.itemSize, sizeUpdateCount], ); - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useDynamicVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useDynamicVirtualizerMeasure({ defaultItemSize: props.itemSize, direction: props.axis ?? 'vertical', getItemSize: props.getItemSize ?? getChildSizeAuto, - currentIndex: contextState?.contextIndex ?? 0, + virtualizerContext: contextState, numItems: props.numItems, + bufferItems: _bufferItems, + bufferSize: _bufferSize, }); const _imperativeVirtualizerRef = useMergedRefs(React.useRef(null), imperativeVirtualizerRef); @@ -113,10 +123,10 @@ export function useVirtualizerScrollViewDynamic_unstable( virtualizerLength, bufferItems, bufferSize, - scrollViewRef, virtualizerContext: contextState, imperativeVirtualizerRef: _imperativeVirtualizerRef, onRenderedFlaggedIndex: handleRenderedIndex, + containerSizeRef, }); const measureObject = useMeasureList( diff --git a/packages/react-components/react-virtualizer/library/src/hooks/hooks.types.ts b/packages/react-components/react-virtualizer/library/src/hooks/hooks.types.ts index 7d8d27dcb730c..72323c793048e 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/hooks.types.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/hooks.types.ts @@ -1,16 +1,37 @@ import { MutableRefObject, RefObject } from 'react'; +import { DynamicVirtualizerContextProps } from '../Utilities'; export type VirtualizerMeasureProps = { defaultItemSize: number; direction?: 'vertical' | 'horizontal'; + + /** + * Override recommended number of buffer items + */ + bufferItems?: number; + + /** + * Override recommended buffer size (px) + */ + bufferSize?: number; }; export type VirtualizerMeasureDynamicProps = { defaultItemSize: number; - currentIndex: number; + virtualizerContext: DynamicVirtualizerContextProps; numItems: number; getItemSize: (index: number) => number; direction?: 'vertical' | 'horizontal'; + + /** + * Override recommended number of buffer items + */ + bufferItems?: number; + + /** + * Override recommended buffer size (px) + */ + bufferSize?: number; }; export type VirtualizerStaticPaginationProps = { diff --git a/packages/react-components/react-virtualizer/library/src/hooks/index.ts b/packages/react-components/react-virtualizer/library/src/hooks/index.ts index 4b9defe52372d..1b64be17abdbb 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/index.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/index.ts @@ -3,3 +3,4 @@ export * from './useVirtualizerMeasure'; export * from './useDynamicVirtualizerMeasure'; export * from './useResizeObserverRef'; export * from './hooks.types'; +export * from './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 9eda6d577c275..f980e99e20425 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/useDynamicVirtualizerMeasure.ts @@ -1,8 +1,9 @@ -import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilities'; 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'; /** * React hook that measures virtualized space dynamically to ensure optimized virtualization length. @@ -14,10 +15,19 @@ export const useDynamicVirtualizerMeasure = ( bufferItems: number; bufferSize: number; scrollRef: (instance: TElement | null) => void; + containerSizeRef: React.RefObject; } => { - const { defaultItemSize, direction = 'vertical', numItems, getItemSize, currentIndex } = virtualizerProps; - const indexRef = useRef(currentIndex); - indexRef.current = currentIndex; + const { + defaultItemSize, + direction = 'vertical', + numItems, + getItemSize, + bufferItems, + bufferSize, + virtualizerContext, + } = virtualizerProps; + const indexRef = useRef(virtualizerContext.contextIndex); + indexRef.current = virtualizerContext.contextIndex; const [state, setState] = React.useState({ virtualizerLength: 0, @@ -25,8 +35,10 @@ export const useDynamicVirtualizerMeasure = ( virtualizerBufferSize: 0, }); + const containerSizeRef = React.useRef(0); const { virtualizerLength, virtualizerBufferItems, virtualizerBufferSize } = state; + const { targetDocument } = useFluent(); const container = React.useRef(null); const handleScrollResize = React.useCallback( (scrollRef: React.MutableRefObject) => { @@ -35,44 +47,84 @@ export const useDynamicVirtualizerMeasure = ( return; } - if (scrollRef.current !== container.current) { - container.current = scrollRef.current; + if (scrollRef.current !== targetDocument?.body) { + // We have a local scroll container + containerSizeRef.current = + direction === 'vertical' + ? scrollRef?.current.getBoundingClientRect().height + : scrollRef?.current.getBoundingClientRect().width; + } else if (targetDocument?.defaultView) { + // If our scroll ref is the document body, we should check window height + containerSizeRef.current = + direction === 'vertical' ? targetDocument?.defaultView?.innerHeight : targetDocument?.defaultView?.innerWidth; } - const containerSize = - direction === 'vertical' - ? scrollRef.current.getBoundingClientRect().height - : scrollRef.current.getBoundingClientRect().width; - let indexSizer = 0; + let i = 0; let length = 0; - while (indexSizer <= containerSize && length < numItems) { - const iItemSize = getItemSize(indexRef.current + length); - + const sizeToBeat = containerSizeRef.current + virtualizerBufferSize * 2; + while (indexSizer <= sizeToBeat && i + virtualizerContext.contextIndex < numItems) { + const iItemSize = getItemSize(indexRef.current + 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. + We need to let the new items render first then we can accurately assess.*/ + return virtualizerLength - virtualizerBufferSize * 2; + } + + const currentScrollPos = virtualizerContext.contextPosition; + const currentItemPos = virtualizerContext.childProgressiveSizes.current[indexRef.current + i] - iItemSize; + + if (currentScrollPos > currentItemPos + iItemSize) { + // The item isn't in view, ignore for now. + i++; + continue; + } else if (currentScrollPos > currentItemPos) { + // The item is partially out of view, ignore the out of bounds portion + const variance = currentItemPos + iItemSize - currentScrollPos; + indexSizer += variance; + } else { + // Item is in view + indexSizer += iItemSize; + } // Increment - indexSizer += iItemSize; + i++; length++; } /* * Number of items to append at each end, i.e. 'preload' each side before entering view. + * Minimum: 2 - we give slightly more buffer for dynamic version. */ - const bufferItems = Math.max(Math.floor(length / 4), 4); + const newBufferItems = bufferItems ?? Math.max(Math.ceil(length / 3), 1); /* * This is how far we deviate into the bufferItems to detect a redraw. */ - const bufferSize = Math.max(Math.floor((length / 8) * defaultItemSize), 1); - - const totalLength = length + bufferItems * 2 + 1; + const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2.0, 1); + const totalLength = length + newBufferItems * 2; setState({ virtualizerLength: totalLength, - virtualizerBufferSize: bufferSize, - virtualizerBufferItems: bufferItems, + virtualizerBufferSize: newBufferSize, + virtualizerBufferItems: newBufferItems, }); }, - [defaultItemSize, direction, getItemSize, numItems], + [ + bufferItems, + bufferSize, + defaultItemSize, + direction, + getItemSize, + numItems, + targetDocument?.body, + targetDocument?.defaultView, + virtualizerBufferSize, + virtualizerContext.childProgressiveSizes, + virtualizerContext.contextIndex, + virtualizerContext.contextPosition, + virtualizerLength, + ], ); const resizeCallback = React.useCallback( @@ -90,46 +142,20 @@ export const useDynamicVirtualizerMeasure = ( [handleScrollResize], ); - const scrollRef = useResizeObserverRef_unstable(resizeCallback); + const scrollRef = useMergedRefs(container, useResizeObserverRef_unstable(resizeCallback)); useIsomorphicLayoutEffect(() => { - if (!container.current) { - return; - } - - const containerSize = - direction === 'vertical' - ? container.current?.getBoundingClientRect().height * 1.5 - : container.current?.getBoundingClientRect().width * 1.5; - - let couldBeSmaller = false; - let recheckTotal = 0; - for (let i = currentIndex; i < currentIndex + virtualizerLength; i++) { - const newItemSize = getItemSize(i); - recheckTotal += newItemSize; - - const newLength = i - currentIndex; - - const bufferItems = Math.max(Math.floor(newLength / 4), 2); - const totalNewLength = newLength + bufferItems * 2 + 4; - const compareLengths = totalNewLength < virtualizerLength; - - if (recheckTotal > containerSize && compareLengths) { - couldBeSmaller = true; - break; - } - } - - // Check if the render has caused us to need a re-calc of virtualizer length - if (recheckTotal < containerSize || couldBeSmaller) { + if (virtualizerContext.contextIndex + virtualizerLength < numItems) { + // Avoid re-rendering/re-calculating when the end index has already been reached handleScrollResize(container); } - }, [getItemSize, currentIndex, direction, virtualizerLength, resizeCallback, handleScrollResize]); + }, [handleScrollResize, numItems, virtualizerContext.contextIndex, virtualizerLength]); return { virtualizerLength, bufferItems: virtualizerBufferItems, bufferSize: virtualizerBufferSize, scrollRef, + containerSizeRef, }; }; diff --git a/packages/react-components/react-virtualizer/library/src/hooks/useMeasureList.ts b/packages/react-components/react-virtualizer/library/src/hooks/useMeasureList.ts index ee03e649091d2..318aa3e7f883e 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/useMeasureList.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/useMeasureList.ts @@ -58,8 +58,21 @@ export function useMeasureList< }; React.useEffect(() => { - widthArray.current = new Array(totalLength).fill(defaultItemSize); - heightArray.current = new Array(totalLength).fill(defaultItemSize); + const newHeightLength = totalLength - heightArray.current.length; + const newWidthLength = totalLength - widthArray.current.length; + /* Ensure we grow or truncate arrays with prior properties, + keeping the existing values is important for whitespace assumptions. + Even if items in the 'middle' are deleted, we will recalc the whitespace as it is explored.*/ + if (newWidthLength > 0) { + widthArray.current = widthArray.current.concat(new Array(newWidthLength).fill(defaultItemSize)); + } else if (newWidthLength < 0) { + widthArray.current = widthArray.current.slice(0, totalLength); + } + if (newHeightLength > 0) { + heightArray.current = heightArray.current.concat(new Array(newHeightLength).fill(defaultItemSize)); + } else if (newHeightLength < 0) { + heightArray.current = heightArray.current.slice(0, totalLength); + } }, [defaultItemSize, totalLength]); // Keep the reference of ResizeObserver as a ref, as it should live through renders diff --git a/packages/react-components/react-virtualizer/library/src/hooks/useResizeObserverRef.ts b/packages/react-components/react-virtualizer/library/src/hooks/useResizeObserverRef.ts index aabb9113db19e..fa44e72e928d5 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/useResizeObserverRef.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/useResizeObserverRef.ts @@ -12,11 +12,20 @@ export const useResizeObserverRef_unstable = (resizeCallback: ResizeCallbackWith const { targetDocument } = useFluent(); const container = React.useRef(null); + const containerHeightRef = React.useRef(0); + const containerWidthRef = React.useRef(0); // the handler for resize observer // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 // eslint-disable-next-line no-restricted-globals const handleResize = debounce((entries: ResizeObserverEntry[], observer: ResizeObserver) => { - resizeCallback(entries, observer, container); + const containerHeight = container.current?.clientHeight; + const containerWidth = container.current?.clientWidth; + // Our resize observer will fire on scroll resize, let index change handle that instead. + if (containerHeightRef.current !== containerHeight || containerWidth !== containerWidthRef.current) { + containerWidthRef.current = containerWidth ?? 0; + containerHeightRef.current = containerHeight ?? 0; + resizeCallback(entries, observer, container); + } }); // Keep the reference of ResizeObserver in the state, as it should live through renders diff --git a/packages/react-components/react-virtualizer/library/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/library/src/hooks/useVirtualizerMeasure.ts index aeeec5bb5a6db..026125d50647c 100644 --- a/packages/react-components/react-virtualizer/library/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/library/src/hooks/useVirtualizerMeasure.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { VirtualizerMeasureProps } from './hooks.types'; import { useResizeObserverRef_unstable } from './useResizeObserverRef'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** * React hook that measures virtualized space based on a static size to ensure optimized virtualization length. @@ -12,16 +13,20 @@ export const useStaticVirtualizerMeasure = ( bufferItems: number; bufferSize: number; scrollRef: (instance: TElement | null) => void; + containerSizeRef: React.MutableRefObject; } => { - const { defaultItemSize, direction = 'vertical' } = virtualizerProps; + const { defaultItemSize, direction = 'vertical', bufferItems, bufferSize } = virtualizerProps; const [state, setState] = React.useState({ virtualizerLength: 0, - bufferSize: 0, - bufferItems: 0, + _bufferSize: 0, + _bufferItems: 0, }); - const { virtualizerLength, bufferItems, bufferSize } = state; + const containerSizeRef = React.useRef(0); + const { targetDocument } = useFluent(); + + const { virtualizerLength, _bufferItems, _bufferSize } = state; const resizeCallback = React.useCallback( ( @@ -35,43 +40,51 @@ export const useStaticVirtualizerMeasure = ( return; } - const containerSize = - direction === 'vertical' - ? scrollRef?.current.getBoundingClientRect().height - : scrollRef?.current.getBoundingClientRect().width; - + if (scrollRef.current !== targetDocument?.body) { + // We have a local scroll container + containerSizeRef.current = + direction === 'vertical' + ? scrollRef?.current.getBoundingClientRect().height + : scrollRef?.current.getBoundingClientRect().width; + } else if (targetDocument?.defaultView) { + // If our scroll ref is the document body, we should check window height + containerSizeRef.current = + direction === 'vertical' ? targetDocument?.defaultView?.innerHeight : targetDocument?.defaultView?.innerWidth; + } /* * Number of items required to cover viewport. */ - const length = Math.ceil(containerSize / defaultItemSize + 1); + const length = Math.ceil(containerSizeRef.current / defaultItemSize + 1); /* * Number of items to append at each end, i.e. 'preload' each side before entering view. + * Minimum: 1 */ - const newBufferItems = Math.max(Math.floor(length / 4), 2); + const newBufferItems = bufferItems ?? Math.max(Math.ceil(length / 4), 1); /* * This is how far we deviate into the bufferItems to detect a redraw. */ - const newBufferSize = Math.max(Math.floor((length / 8) * defaultItemSize), 1); + const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2.0, 1); - const totalLength = length + newBufferItems * 2 + 1; + const totalLength = length + newBufferItems * 2; setState({ virtualizerLength: totalLength, - bufferItems: newBufferItems, - bufferSize: newBufferSize, + _bufferItems: newBufferItems, + _bufferSize: newBufferSize, }); }, - [defaultItemSize, direction], + [bufferItems, bufferSize, defaultItemSize, direction, targetDocument?.body, targetDocument?.defaultView], ); const scrollRef = useResizeObserverRef_unstable(resizeCallback); return { virtualizerLength, - bufferItems, - bufferSize, + bufferItems: _bufferItems, + bufferSize: _bufferSize, scrollRef, + containerSizeRef, }; }; diff --git a/packages/react-components/react-virtualizer/library/src/index.ts b/packages/react-components/react-virtualizer/library/src/index.ts index bad0518470b8f..6912c0057e4a7 100644 --- a/packages/react-components/react-virtualizer/library/src/index.ts +++ b/packages/react-components/react-virtualizer/library/src/index.ts @@ -19,9 +19,15 @@ export { useStaticVirtualizerMeasure, useDynamicVirtualizerMeasure, useResizeObserverRef_unstable, + useMeasureList, } from './Hooks'; -export type { ResizeCallbackWithRef, VirtualizerMeasureDynamicProps, VirtualizerMeasureProps } from './Hooks'; +export type { + ResizeCallbackWithRef, + VirtualizerMeasureDynamicProps, + VirtualizerMeasureProps, + IndexedResizeCallbackElement, +} from './Hooks'; export type { ScrollToItemDynamicParams, ScrollToItemStaticParams, ScrollToInterface } from './Utilities'; @@ -32,7 +38,7 @@ export { scrollToItemDynamic, } from './Utilities'; -export type { VirtualizerContextProps } from './Utilities'; +export type { VirtualizerContextProps, DynamicVirtualizerContextProps } from './Utilities'; export { VirtualizerScrollView, diff --git a/packages/react-components/react-virtualizer/library/src/testing/useVirtualizer.test.ts b/packages/react-components/react-virtualizer/library/src/testing/useVirtualizer.test.ts index 1205684eb4d34..0290dd4cf94a0 100644 --- a/packages/react-components/react-virtualizer/library/src/testing/useVirtualizer.test.ts +++ b/packages/react-components/react-virtualizer/library/src/testing/useVirtualizer.test.ts @@ -17,6 +17,9 @@ describe('useVirtualizer', () => { const virtualizerLength = 50; const actualLength = 250; const divArr = new Array(actualLength).fill('Test-Node'); + const containerSizeRef = { + current: 300, + }; const rowFunc = (index: number) => { return divArr[index]; @@ -27,6 +30,7 @@ describe('useVirtualizer', () => { virtualizerLength, itemSize: 100, // 100 pixels children: rowFunc, + containerSizeRef, }), ); 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 5d0cc41d69ead..63bec41d3f61e 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 @@ -1,6 +1,6 @@ import * as React from 'react'; -import type { VirtualizerContextProps } from './types'; -import { useMemo, useState } from 'react'; +import type { DynamicVirtualizerContextProps, VirtualizerContextProps } from './types'; +import { useMemo, useState, useRef } from 'react'; const VirtualizerContext = React.createContext( undefined, @@ -14,20 +14,26 @@ export const useVirtualizerContext_unstable = () => { export const useVirtualizerContextState_unstable = ( passedContext?: VirtualizerContextProps, -): VirtualizerContextProps => { +): 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 * Order of usage -> Passed Prop -> Provider Context -> Internal State default */ - const _context = useMemo( - () => passedContext ?? virtualizerContext ?? { contextIndex: _contextIndex, setContextIndex: _setContextIndex }, - [_contextIndex, passedContext, virtualizerContext], + const context = useMemo( + () => ({ + 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], ); - const context = useMemo(() => { - return { contextIndex: _context.contextIndex, setContextIndex: _context.setContextIndex }; - }, [_context]); 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 408d3773f4228..47a91d04c71ee 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 @@ -1,7 +1,19 @@ +import * as React from 'react'; /** * {@docCategory Virtualizer} */ export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; + /* + * These option props are used in dynamic virtualizer + */ + contextPosition?: number; + setContextPosition?: (index: number) => void; + childProgressiveSizes?: React.MutableRefObject; }; + +/** + * Some props are optional on static virtualizer, but required for dynamic. + */ +export type DynamicVirtualizerContextProps = Required; diff --git a/packages/react-components/react-virtualizer/stories/.storybook/preview-body.html b/packages/react-components/react-virtualizer/stories/.storybook/preview-body.html deleted file mode 100644 index 22c02a93fea16..0000000000000 --- a/packages/react-components/react-virtualizer/stories/.storybook/preview-body.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Default.stories.tsx index a1e8b98ff9145..e198568fe03f9 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Default.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-components/unstable'; +import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -9,12 +9,13 @@ const useStyles = makeStyles({ overflowY: 'auto', width: '100%', height: '100%', - maxHeight: '750px', + maxHeight: '80vh', }, child: { - height: '100px', - lineHeight: '100px', + height: '25px', + lineHeight: '25px', width: '100%', + minHeight: '25px', }, }); @@ -22,31 +23,34 @@ export const Default = () => { const styles = useStyles(); const childLength = 1000; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ - defaultItemSize: 100, + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ + defaultItemSize: 25, }); return ( -
- - {index => { - return ( - {`Node-${index}`} - ); - }} - +
+
+ + {index => { + return ( + {`Node-${index}`} + ); + }} + +
); }; diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/DefaultUnbounded.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/DefaultUnbounded.stories.tsx index a22d4758951ae..6a160404fc5ab 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/DefaultUnbounded.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/DefaultUnbounded.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-components/unstable'; +import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useFluent } from '@fluentui/react-components'; @@ -20,7 +20,6 @@ const useStyles = makeStyles({ }, child: { display: 'flex', - height: '100px', lineHeight: '100px', width: '100%', }, @@ -30,13 +29,14 @@ const useStyles = makeStyles({ paddingBottom: '100px', fontSize: '36px', textAlign: 'center', + minHeight: '100px', }, }); export const DefaultUnbounded = () => { const styles = useStyles(); const childLength = 1000; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: 100, }); @@ -54,6 +54,7 @@ export const DefaultUnbounded = () => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={100} + containerSizeRef={containerSizeRef} > {(index, isScrolling) => { return ( 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 1a4e4ad3774a1..09f78c9ef429c 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 @@ -1,9 +1,6 @@ import * as React from 'react'; -import { - Virtualizer, - useDynamicVirtualizerMeasure, - VirtualizerContextProvider, -} from '@fluentui/react-components/unstable'; +import { Virtualizer, useDynamicVirtualizerMeasure, VirtualizerContextProvider } from '@fluentui/react-virtualizer'; +import type { DynamicVirtualizerContextProps } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useCallback, useRef } from 'react'; @@ -34,6 +31,8 @@ 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(); const childLength = 1000; @@ -63,15 +62,23 @@ export const Dynamic = () => { [flag], ); - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useDynamicVirtualizerMeasure({ + 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, - currentIndex, + virtualizerContext: contextState, }); return ( - +
{ bufferItems={bufferItems} virtualizerLength={virtualizerLength} itemSize={100} + containerSizeRef={containerSizeRef} + virtualizerContext={contextState} > {useCallback( (index: number) => { diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Horizontal.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Horizontal.stories.tsx index 16edbe43f39f4..ca017a693e59d 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Horizontal.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Horizontal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-components/unstable'; +import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -23,7 +23,7 @@ export const Horizontal = () => { const childLength = 1000; const itemWidth = 100; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: itemWidth, direction: 'horizontal', }); @@ -37,6 +37,7 @@ export const Horizontal = () => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={itemWidth} + containerSizeRef={containerSizeRef} > {index => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/MultiUnbounded.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/MultiUnbounded.stories.tsx index e6bf79745fd88..bc03277a444b3 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/MultiUnbounded.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/MultiUnbounded.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-components/unstable'; +import { Virtualizer, useStaticVirtualizerMeasure } from '@fluentui/react-virtualizer'; import { makeStyles, useFluent } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -36,7 +36,7 @@ export const MultiUnbounded = () => { const childLength = 100; const repeatingVirtualizers = 5; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: 100, }); @@ -58,6 +58,7 @@ export const MultiUnbounded = () => { bufferSize={bufferSize} itemSize={100} key={`virtualizer-container-${index}`} + containerSizeRef={containerSizeRef} > {rowIndex => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/RTL.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/RTL.stories.tsx index dcb16f951cb12..8f4f1333c46ca 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/RTL.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/RTL.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-components/unstable'; +import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -24,7 +24,7 @@ export const RTL = () => { const childLength = 1000; const itemWidth = 100; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: itemWidth, direction: 'horizontal', }); @@ -39,6 +39,7 @@ export const RTL = () => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={100} + containerSizeRef={containerSizeRef} > {index => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Reversed.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Reversed.stories.tsx index 109f2da1f82c9..56cc09b6257ca 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/Reversed.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/Reversed.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-components/unstable'; +import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -23,7 +23,7 @@ export const Reversed = () => { const childLength = 1000; const itemSize = 100; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: itemSize, }); @@ -36,6 +36,7 @@ export const Reversed = () => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={itemSize} + containerSizeRef={containerSizeRef} > {index => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/ReversedHorizontal.stories.tsx b/packages/react-components/react-virtualizer/stories/src/Virtualizer/ReversedHorizontal.stories.tsx index bdb10df418cf6..0dca7e63f0d69 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/ReversedHorizontal.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/ReversedHorizontal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-components/unstable'; +import { useStaticVirtualizerMeasure, Virtualizer } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -24,7 +24,7 @@ export const ReversedHorizontal = () => { const itemWidth = 100; - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({ defaultItemSize: itemWidth, direction: 'horizontal', }); @@ -39,6 +39,7 @@ export const ReversedHorizontal = () => { bufferItems={bufferItems} bufferSize={bufferSize} itemSize={itemWidth} + containerSizeRef={containerSizeRef} > {index => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/VirtualizerDescription.md b/packages/react-components/react-virtualizer/stories/src/Virtualizer/VirtualizerDescription.md index 1bfbae160e2c3..99a7bd52f594f 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/VirtualizerDescription.md +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/VirtualizerDescription.md @@ -4,7 +4,7 @@ > > ```jsx > -> import { Virtualizer } from '@fluentui/react-components/unstable'; +> import { Virtualizer } from '@fluentui/react-virtualizer'; > > ``` > diff --git a/packages/react-components/react-virtualizer/stories/src/Virtualizer/index.stories.ts b/packages/react-components/react-virtualizer/stories/src/Virtualizer/index.stories.ts index 6632b13e634bd..b5a8886213d2a 100644 --- a/packages/react-components/react-virtualizer/stories/src/Virtualizer/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/src/Virtualizer/index.stories.ts @@ -1,4 +1,4 @@ -import { Virtualizer } from '@fluentui/react-components/unstable'; +import { Virtualizer } from '@fluentui/react-virtualizer'; import descriptionMd from './VirtualizerDescription.md'; export { Default } from './Default.stories'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/Default.stories.tsx index e9f90c5b82320..b3253bb6930ee 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/Default.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -12,13 +12,13 @@ const useStyles = makeStyles({ export const Default = () => { const styles = useStyles(); - const childLength = 100; + const childLength = 10000; return ( {(index: number) => { return ( diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/ScrollTo.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/ScrollTo.stories.tsx index e8d7f93ae9ed8..bc5bcd0bcf183 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/ScrollTo.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/ScrollTo.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; -import type { ScrollToInterface } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; +import type { ScrollToInterface } from '@fluentui/react-virtualizer'; import { Text, Input, makeStyles } from '@fluentui/react-components'; import { Button } from '@fluentui/react-components'; @@ -42,7 +42,7 @@ export const ScrollTo = () => { {(index: number) => { diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/SnapToAlignment.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/SnapToAlignment.stories.tsx index 963c6332a1187..cb1621b11c55b 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/SnapToAlignment.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/SnapToAlignment.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/VirtualizerScrollViewDescription.md b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/VirtualizerScrollViewDescription.md index 6e866566c1075..abd58d5aef2f3 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/VirtualizerScrollViewDescription.md +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/VirtualizerScrollViewDescription.md @@ -4,7 +4,7 @@ > > ```jsx > -> import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; +> import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; > > ``` > diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/index.stories.ts b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/index.stories.ts index 97a1c28f8afb4..9114b2abdc353 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollView/index.stories.ts @@ -1,4 +1,4 @@ -import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; import descriptionMd from './VirtualizerScrollViewDescription.md'; export { Default } from './Default.stories'; 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 9df460624fbcc..728cb8e0b0d37 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 @@ -1,37 +1,46 @@ import * as React from 'react'; -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useEffect } from 'react'; const useStyles = makeStyles({ child: { - lineHeight: '42px', + lineHeight: '25px', width: '100%', - minHeight: '42px', + minHeight: '25px', }, }); export const AutoMeasure = () => { const styles = useStyles(); - const childLength = 1000; - const minHeight = 42; - const maxHeightIncrease = 150; + const childLength = 100; + const minHeight = 25; + const maxHeightIncrease = 55; // Array size ref stores a list of random num for div sizing and callbacks const arraySize = React.useRef(new Array(childLength).fill(minHeight)); useEffect(() => { // Set random heights on init (to be measured) for (let i = 0; i < childLength; i++) { - arraySize.current[i] = Math.floor(Math.random() * maxHeightIncrease + minHeight); + // if (i % 10 == 0) { + if (i < 10) { + arraySize.current[i] = 1000; + } else { + arraySize.current[i] = Math.floor(Math.random() * maxHeightIncrease + minHeight); + } + + // arraySize.current[i] = Math.floor(Math.random() * maxHeightIncrease + minHeight); } }, []); return ( {(index: number) => { const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/Default.stories.tsx index 63fff1f9724f8..940583aaeae1f 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useEffect } from 'react'; @@ -13,8 +13,9 @@ const useStyles = makeStyles({ export const Default = () => { const styles = useStyles(); - const childLength = 1000; + const childLength = 10000; const minHeight = 42; + const maxHeightMod = 150; // Array size ref stores a list of random num for div sizing and callbacks const arraySize = React.useRef(new Array(childLength).fill(minHeight)); // totalSize flag drives our callback update @@ -23,7 +24,7 @@ export const Default = () => { useEffect(() => { let _totalSize = 0; for (let i = 0; i < childLength; i++) { - arraySize.current[i] = Math.floor(Math.random() * 150 + minHeight); + arraySize.current[i] = Math.floor(Math.random() * maxHeightMod + minHeight); _totalSize += arraySize.current[i]; } setTotalSize(_totalSize); @@ -40,9 +41,11 @@ export const Default = () => { return ( {(index: number) => { const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx index c3e30de72025b..3fb548125546e 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useEffect } from 'react'; @@ -42,7 +42,8 @@ export const ScrollLoading = () => { numItems={childLength} itemSize={minHeight} getItemSize={getItemSizeCallback} - container={{ role: 'list', style: { maxHeight: '100vh' } }} + container={{ role: 'list', style: { maxHeight: '80vh' } }} + enableScrollLoad={true} > {(index: number, isScrolling = false) => { const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx index 2400818360479..52b43d38b0734 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; -import type { ScrollToInterface } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; +import type { ScrollToInterface } from '@fluentui/react-virtualizer'; import type { VirtualizerDataRef } from '@fluentui/react-virtualizer'; import { Button, Input, makeStyles, Text } from '@fluentui/react-components'; import { useEffect } from 'react'; @@ -70,7 +70,7 @@ export const ScrollTo = () => { getItemSize={getItemSizeCallback} imperativeRef={scrollRef} imperativeVirtualizerRef={sizeRef} - container={{ role: 'list', style: { maxHeight: '100vh' } }} + container={{ role: 'list', style: { maxHeight: '80vh' } }} > {(index: number) => { const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/SnapToAlignment.stories.tsx b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/SnapToAlignment.stories.tsx index 722ff97990641..c44577f587a7c 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/SnapToAlignment.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/SnapToAlignment.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { makeStyles } from '@fluentui/react-components'; import { useEffect } from 'react'; diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md index 78973fe04255e..bc2261aa25f03 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md @@ -4,7 +4,7 @@ > > ```jsx > -> import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +> import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; > > ``` > diff --git a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/index.stories.ts b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/index.stories.ts index 5aa34ef060e83..9dee7b7a99216 100644 --- a/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/src/VirtualizerScrollViewDynamic/index.stories.ts @@ -1,4 +1,4 @@ -import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import descriptionMd from './VirtualizerScrollViewDynamicDescription.md'; export { AutoMeasure } from './AutoMeasure.stories';