From 41893806d987134233d007fd7e004453206a0c6d Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 21 Mar 2023 13:44:32 -0700 Subject: [PATCH 01/26] Squish dynamic changes for rebase --- .../etc/react-components.unstable.api.md | 42 +++++++ .../react-components/src/unstable/index.ts | 14 +++ .../etc/react-virtualizer.api.md | 55 +++++++++ .../react-virtualizer/src/Utilities.ts | 1 + .../src/VirtualizerScrollViewDynamic.ts | 1 + .../Virtualizer/Virtualizer.types.ts | 10 -- .../Virtualizer/renderVirtualizer.tsx | 9 ++ .../components/Virtualizer/useVirtualizer.ts | 63 +++++----- .../useVirtualizerScrollViewStyles.ts | 3 +- .../VirtualizerScrollViewDynamic.ts | 35 ++++++ .../VirtualizerScrollViewDynamic.types.ts | 38 ++++++ .../VirtualizerScrollViewDynamic/index.ts | 5 + .../renderVirtualizerScrollViewDynamic.tsx | 39 +++++++ .../useVirtualizerScrollViewDynamic.ts | 57 +++++++++ .../useVirtualizerScrollViewDynamicStyles.ts | 71 ++++++++++++ .../src/hooks/useVirtualizerMeasure.ts | 109 +++++++++++++++++- .../src/hooks/useVirtualizerMeasure.types.ts | 8 ++ .../react-virtualizer/src/index.ts | 23 +++- .../VirtualizerContext/VirtualizerContext.ts | 15 +++ .../src/utilities/VirtualizerContext/index.ts | 2 + .../src/utilities/VirtualizerContext/types.ts | 7 ++ .../react-virtualizer/src/utilities/index.ts | 1 + .../stories/Virtualizer/Dynamic.stories.tsx | 59 ++++++---- .../Default.stories.tsx | 57 +++++++++ ...VirtualizerScrollViewDynamicDescription.md | 12 ++ .../index.stories.ts | 16 +++ 26 files changed, 685 insertions(+), 67 deletions(-) create mode 100644 packages/react-components/react-virtualizer/src/Utilities.ts create mode 100644 packages/react-components/react-virtualizer/src/VirtualizerScrollViewDynamic.ts create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/index.ts create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts create mode 100644 packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts create mode 100644 packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts create mode 100644 packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/index.ts create mode 100644 packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts create mode 100644 packages/react-components/react-virtualizer/src/utilities/index.ts create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts diff --git a/packages/react-components/react-components/etc/react-components.unstable.api.md b/packages/react-components/react-components/etc/react-components.unstable.api.md index 16f7745139fa90..7bad91802b01bd 100644 --- a/packages/react-components/react-components/etc/react-components.unstable.api.md +++ b/packages/react-components/react-components/etc/react-components.unstable.api.md @@ -38,6 +38,7 @@ import { InfoLabelState } from '@fluentui/react-infobutton'; import { InputField_unstable as InputField } from '@fluentui/react-input'; import { inputFieldClassNames } from '@fluentui/react-input'; import { InputFieldProps_unstable as InputFieldProps } from '@fluentui/react-input'; +import { IVirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { NestedTreeItem } from '@fluentui/react-tree'; import { ProgressField_unstable as ProgressField } from '@fluentui/react-progress'; import { progressFieldClassNames } from '@fluentui/react-progress'; @@ -49,6 +50,7 @@ import { renderAlert_unstable } from '@fluentui/react-alert'; import { renderField_unstable } from '@fluentui/react-field'; import { renderInfoButton_unstable } from '@fluentui/react-infobutton'; import { renderInfoLabel_unstable } from '@fluentui/react-infobutton'; +import { renderIVirtualizerScrollViewDynamic_unstable } from '@fluentui/react-virtualizer'; import { renderSkeleton_unstable } from '@fluentui/react-skeleton'; import { renderSkeletonItem_unstable } from '@fluentui/react-skeleton'; import { renderTree_unstable } from '@fluentui/react-tree'; @@ -57,6 +59,7 @@ import { renderTreeItemLayout_unstable } from '@fluentui/react-tree'; import { renderTreeItemPersonaLayout_unstable } from '@fluentui/react-tree'; import { renderVirtualizer_unstable } from '@fluentui/react-virtualizer'; import { renderVirtualizerScrollView_unstable } from '@fluentui/react-virtualizer'; +import { renderVirtualizerScrollViewDynamic_unstable } from '@fluentui/react-virtualizer'; import { SelectField_unstable as SelectField } from '@fluentui/react-select'; import { selectFieldClassNames } from '@fluentui/react-select'; import { SelectFieldProps_unstable as SelectFieldProps } from '@fluentui/react-select'; @@ -115,6 +118,7 @@ import { TreeSlots } from '@fluentui/react-tree'; import { TreeState } from '@fluentui/react-tree'; import { useAlert_unstable } from '@fluentui/react-alert'; import { useAlertStyles_unstable } from '@fluentui/react-alert'; +import { useDynamicVirtualizerMeasure } from '@fluentui/react-virtualizer'; import { useField_unstable } from '@fluentui/react-field'; import { useFieldStyles_unstable } from '@fluentui/react-field'; import { useFlatTree_unstable } from '@fluentui/react-tree'; @@ -140,15 +144,25 @@ import { useTreeItemPersonaLayoutStyles_unstable } from '@fluentui/react-tree'; import { useTreeItemStyles_unstable } from '@fluentui/react-tree'; import { useTreeStyles_unstable } from '@fluentui/react-tree'; import { useVirtualizer_unstable } from '@fluentui/react-virtualizer'; +import { useVirtualizerContext } from '@fluentui/react-virtualizer'; import { useVirtualizerScrollView_unstable } from '@fluentui/react-virtualizer'; +import { useVirtualizerScrollViewDynamic_unstable } from '@fluentui/react-virtualizer'; +import { useVirtualizerScrollViewDynamicStyles_unstable } from '@fluentui/react-virtualizer'; import { useVirtualizerScrollViewStyles_unstable } from '@fluentui/react-virtualizer'; import { useVirtualizerStyles_unstable } from '@fluentui/react-virtualizer'; import { Virtualizer } from '@fluentui/react-virtualizer'; import { VirtualizerChildRenderFunction } from '@fluentui/react-virtualizer'; import { virtualizerClassNames } from '@fluentui/react-virtualizer'; +import { VirtualizerContext } from '@fluentui/react-virtualizer'; +import { VirtualizerContextProps } from '@fluentui/react-virtualizer'; import { VirtualizerProps } from '@fluentui/react-virtualizer'; import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; import { virtualizerScrollViewClassNames } from '@fluentui/react-virtualizer'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; +import { virtualizerScrollViewDynamicClassNames } from '@fluentui/react-virtualizer'; +import { VirtualizerScrollViewDynamicProps } from '@fluentui/react-virtualizer'; +import { VirtualizerScrollViewDynamicSlots } from '@fluentui/react-virtualizer'; +import { VirtualizerScrollViewDynamicState } from '@fluentui/react-virtualizer'; import { VirtualizerScrollViewProps } from '@fluentui/react-virtualizer'; import { VirtualizerScrollViewSlots } from '@fluentui/react-virtualizer'; import { VirtualizerScrollViewState } from '@fluentui/react-virtualizer'; @@ -223,6 +237,8 @@ export { inputFieldClassNames } export { InputFieldProps } +export { IVirtualizerScrollViewDynamic } + export { NestedTreeItem } export { ProgressField } @@ -245,6 +261,8 @@ export { renderInfoButton_unstable } export { renderInfoLabel_unstable } +export { renderIVirtualizerScrollViewDynamic_unstable } + export { renderSkeleton_unstable } export { renderSkeletonItem_unstable } @@ -261,6 +279,8 @@ export { renderVirtualizer_unstable } export { renderVirtualizerScrollView_unstable } +export { renderVirtualizerScrollViewDynamic_unstable } + export { SelectField } export { selectFieldClassNames } @@ -377,6 +397,8 @@ export { useAlert_unstable } export { useAlertStyles_unstable } +export { useDynamicVirtualizerMeasure } + export { useField_unstable } export { useFieldStyles_unstable } @@ -427,8 +449,14 @@ export { useTreeStyles_unstable } export { useVirtualizer_unstable } +export { useVirtualizerContext } + export { useVirtualizerScrollView_unstable } +export { useVirtualizerScrollViewDynamic_unstable } + +export { useVirtualizerScrollViewDynamicStyles_unstable } + export { useVirtualizerScrollViewStyles_unstable } export { useVirtualizerStyles_unstable } @@ -439,12 +467,26 @@ export { VirtualizerChildRenderFunction } export { virtualizerClassNames } +export { VirtualizerContext } + +export { VirtualizerContextProps } + export { VirtualizerProps } export { VirtualizerScrollView } export { virtualizerScrollViewClassNames } +export { VirtualizerScrollViewDynamic } + +export { virtualizerScrollViewDynamicClassNames } + +export { VirtualizerScrollViewDynamicProps } + +export { VirtualizerScrollViewDynamicSlots } + +export { VirtualizerScrollViewDynamicState } + export { VirtualizerScrollViewProps } export { VirtualizerScrollViewSlots } diff --git a/packages/react-components/react-components/src/unstable/index.ts b/packages/react-components/react-components/src/unstable/index.ts index f6728de03d1b2e..ee0dfafeacd413 100644 --- a/packages/react-components/react-components/src/unstable/index.ts +++ b/packages/react-components/react-components/src/unstable/index.ts @@ -121,11 +121,21 @@ export { useVirtualizerStyles_unstable, useIntersectionObserver, useStaticVirtualizerMeasure, + useDynamicVirtualizerMeasure, + VirtualizerContext, + useVirtualizerContext, VirtualizerScrollView, virtualizerScrollViewClassNames, useVirtualizerScrollView_unstable, renderVirtualizerScrollView_unstable, useVirtualizerScrollViewStyles_unstable, + VirtualizerScrollViewDynamic, + IVirtualizerScrollViewDynamic, + virtualizerScrollViewDynamicClassNames, + useVirtualizerScrollViewDynamic_unstable, + renderVirtualizerScrollViewDynamic_unstable, + renderIVirtualizerScrollViewDynamic_unstable, + useVirtualizerScrollViewDynamicStyles_unstable, } from '@fluentui/react-virtualizer'; export type { VirtualizerProps, @@ -135,6 +145,10 @@ export type { VirtualizerScrollViewProps, VirtualizerScrollViewState, VirtualizerScrollViewSlots, + VirtualizerContextProps, + VirtualizerScrollViewDynamicProps, + VirtualizerScrollViewDynamicState, + VirtualizerScrollViewDynamicSlots, } from '@fluentui/react-virtualizer'; export { diff --git a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md index a00c9dec69b41e..3956595936d3bd 100644 --- a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md +++ b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md @@ -14,12 +14,29 @@ import type { SetStateAction } from 'react'; import { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +// @public +export const IVirtualizerScrollViewDynamic: React_2.FC; + +// @public (undocumented) +export const renderIVirtualizerScrollViewDynamic_unstable: (state: VirtualizerScrollViewDynamicState) => JSX.Element; + // @public (undocumented) export const renderVirtualizer_unstable: (state: VirtualizerState) => JSX.Element; // @public (undocumented) export const renderVirtualizerScrollView_unstable: (state: VirtualizerScrollViewState) => JSX.Element; +// @public (undocumented) +export const renderVirtualizerScrollViewDynamic_unstable: (props: VirtualizerScrollViewDynamicProps, context: VirtualizerContextProps) => JSX.Element; + +// @public +export const useDynamicVirtualizerMeasure: (virtualizerProps: VirtualizerMeasureDynamicProps) => { + virtualizerLength: number; + bufferItems: number; + bufferSize: number; + scrollRef: (instance: HTMLElement | HTMLDivElement | null) => void; +}; + // @public export const useIntersectionObserver: (callback: IntersectionObserverCallback, options?: IntersectionObserverInit | undefined) => { setObserverList: Dispatch>; @@ -38,9 +55,18 @@ export const useStaticVirtualizerMeasure: (virtualizerProps: VirtualizerMeasureP // @public (undocumented) export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerState; +// @public (undocumented) +export const useVirtualizerContext: () => VirtualizerContextProps; + // @public (undocumented) export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewProps): VirtualizerScrollViewState; +// @public (undocumented) +export function useVirtualizerScrollViewDynamic_unstable(props: VirtualizerScrollViewDynamicProps): VirtualizerScrollViewDynamicState; + +// @public +export const useVirtualizerScrollViewDynamicStyles_unstable: (state: VirtualizerScrollViewDynamicState) => VirtualizerScrollViewDynamicState; + // @public export const useVirtualizerScrollViewStyles_unstable: (state: VirtualizerScrollViewState) => VirtualizerScrollViewState; @@ -56,6 +82,15 @@ export type VirtualizerChildRenderFunction = (index: number) => React_2.ReactNod // @public (undocumented) export const virtualizerClassNames: SlotClassNames; +// @public (undocumented) +export const VirtualizerContext: React_2.Context; + +// @public (undocumented) +export type VirtualizerContextProps = { + contextIndex: number; + setContextIndex: (index: number) => void; +}; + // @public (undocumented) export type VirtualizerProps = ComponentProps> & VirtualizerConfigProps; @@ -65,6 +100,26 @@ export const VirtualizerScrollView: React_2.FC; // @public (undocumented) export const virtualizerScrollViewClassNames: SlotClassNames; +// @public (undocumented) +export const VirtualizerScrollViewDynamic: React_2.FC; + +// @public (undocumented) +export const virtualizerScrollViewDynamicClassNames: SlotClassNames; + +// @public (undocumented) +export type VirtualizerScrollViewDynamicProps = ComponentProps> & Partial> & { + itemSize: number; + getItemSize: (index: number) => number; + numItems: number; + children: VirtualizerChildRenderFunction; +}; + +// @public (undocumented) +export type VirtualizerScrollViewDynamicSlots = VirtualizerScrollViewSlots; + +// @public (undocumented) +export type VirtualizerScrollViewDynamicState = ComponentState & VirtualizerConfigState; + // @public (undocumented) export type VirtualizerScrollViewProps = ComponentProps> & Partial> & { itemSize: number; diff --git a/packages/react-components/react-virtualizer/src/Utilities.ts b/packages/react-components/react-virtualizer/src/Utilities.ts new file mode 100644 index 00000000000000..91e9ad875428e2 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/Utilities.ts @@ -0,0 +1 @@ +export * from './utilities/index'; diff --git a/packages/react-components/react-virtualizer/src/VirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/VirtualizerScrollViewDynamic.ts new file mode 100644 index 00000000000000..dd94f542e41270 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/VirtualizerScrollViewDynamic.ts @@ -0,0 +1 @@ +export * from './components/VirtualizerScrollViewDynamic/index'; diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts index dbdfb52d21c222..0699effdd76293 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts @@ -133,16 +133,6 @@ export type VirtualizerConfigProps = { * @param index - the index of the requested size's child */ getItemSize?: (index: number) => number; - - /** - * Notify users of index changes - */ - onUpdateIndex?: (index: number, prevIndex: number) => void; - - /** - * Allow users to intervene in index calculation changes - */ - onCalculateIndex?: (newIndex: number) => number; }; export type VirtualizerProps = ComponentProps> & VirtualizerConfigProps; diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx b/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx index bcc999560a09e4..b3fcd78507a753 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; import { VirtualizerSlots, VirtualizerState } from './Virtualizer.types'; +import { ReactNode } from 'react'; export const renderVirtualizer_unstable = (state: VirtualizerState) => { const { slots, slotProps } = getSlots(state); @@ -20,3 +21,11 @@ export const renderVirtualizer_unstable = (state: VirtualizerState) => { ); }; + +export const renderVirtualizerChildPlaceholder = (child: ReactNode, index: number) => { + return ( + + {child} + + ); +}; diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index 90577e0efc412e..58138a61d1e523 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -6,6 +6,9 @@ import type { VirtualizerProps, VirtualizerState } from './Virtualizer.types'; import { resolveShorthand } from '@fluentui/react-utilities'; import { flushSync } from 'react-dom'; +import { useVirtualizerContext } from '../../Utilities'; +import { renderVirtualizerChildPlaceholder } from './renderVirtualizer'; + export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerState { const { itemSize, @@ -18,13 +21,16 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta scrollViewRef, axis = 'vertical', reversed = false, - onUpdateIndex, - onCalculateIndex, } = props; + /* The context is optional, it's useful for injecting additional index logic, or performing uniform state updates*/ + const virtualizerContext = useVirtualizerContext(); // Tracks the initial item to start virtualizer at, -1 implies first render cycle const [virtualizerStartIndex, setVirtualizerStartIndex] = useState(-1); + const actualIndex = virtualizerContext ? virtualizerContext.contextIndex : virtualizerStartIndex; + const setActualIndex = virtualizerContext ? virtualizerContext.setContextIndex : setVirtualizerStartIndex; + // Store ref to before padding element const beforeElementRef = useRef(null); @@ -73,20 +79,21 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta const batchUpdateNewIndex = (index: number) => { // Local updates - onUpdateIndex?.(index, virtualizerStartIndex); updateChildRows(index); updateCurrentItemSizes(index); // State setters - setVirtualizerStartIndex(index); + setActualIndex(index); }; // Observe intersections of virtualized components const { setObserverList } = useIntersectionObserver( (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { + const currentIndex = actualIndex; + /* Sanity check - do we even need virtualization? */ if (virtualizerLength > numItems) { - if (virtualizerStartIndex !== 0) { + if (currentIndex !== 0) { batchUpdateNewIndex(0); } // No-op @@ -159,21 +166,14 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // For now lets use hardcoded size to assess current element to paginate on const startIndex = getIndexFromScrollPosition(measurementPos); - let bufferedIndex = Math.max(startIndex - bufferCount, 0); - - if (onCalculateIndex) { - // User has chance to intervene/customize prior to render - // They may want to normalize this value. - bufferedIndex = onCalculateIndex(bufferedIndex); - } + 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); - if (virtualizerStartIndex !== newStartIndex) { + if (currentIndex !== newStartIndex) { // We flush sync this and perform an immediate state update - // due to virtualizerStartIndex invalidation. flushSync(() => { batchUpdateNewIndex(newStartIndex); }); @@ -189,7 +189,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta 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 virtualizerStartIndex; + return actualIndex; } const midpoint = Math.floor((lowIndex + highIndex) / 2); const iBefore = Math.max(midpoint - 1, 0); @@ -247,17 +247,19 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta }; const calculateBefore = () => { + const currentIndex = Math.min(actualIndex, numItems); + if (!getItemSize) { // The missing items from before virtualization starts height - return virtualizerStartIndex * itemSize; + return currentIndex * itemSize; } - if (virtualizerStartIndex <= 0) { + if (currentIndex <= 0) { return 0; } // Time for custom size calcs - return childProgressiveSizes.current[virtualizerStartIndex - 1]; + return childProgressiveSizes.current[currentIndex - 1]; }; const calculateAfter = () => { @@ -265,7 +267,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta return 0; } - const lastItemIndex = Math.min(virtualizerStartIndex + virtualizerLength, numItems - 1); + const lastItemIndex = Math.min(actualIndex + virtualizerLength, numItems - 1); if (!getItemSize) { // The missing items from after virtualization ends height const remainingItems = numItems - lastItemIndex - 1; @@ -287,11 +289,11 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta if (childArray.current.length !== numItems) { childArray.current = new Array(virtualizerLength); } - const actualIndex = Math.max(newIndex, 0); - const end = Math.min(actualIndex + virtualizerLength, numItems); + 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); + for (let i = _actualIndex; i < end; i++) { + childArray.current[i - _actualIndex] = renderVirtualizerChildPlaceholder(renderChild(i), i); } }, [numItems, renderChild, virtualizerLength], @@ -377,7 +379,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // Initialization on mount - update array index to 0 (ready state). // Only fire on mount (no deps). useEffect(() => { - if (virtualizerStartIndex < 0) { + if (actualIndex < 0) { batchUpdateNewIndex(0); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -385,8 +387,9 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // If the user passes in an updated renderChild function - update current children useEffect(() => { - if (virtualizerStartIndex >= 0) { - updateChildRows(virtualizerStartIndex); + const currentIndex = actualIndex; + if (currentIndex >= 0) { + updateChildRows(currentIndex); forceUpdate(); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -402,11 +405,11 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // Ensure we recalc if virtualizer length changes const maxCompare = Math.min(virtualizerLength, numItems); - if (childArray.current.length !== maxCompare && virtualizerStartIndex + childArray.current.length < numItems) { - updateChildRows(virtualizerStartIndex); + if (childArray.current.length !== maxCompare && actualIndex + childArray.current.length < numItems) { + updateChildRows(actualIndex); } - const isFullyInitialized = hasInitialized.current && virtualizerStartIndex >= 0; + const isFullyInitialized = hasInitialized.current && actualIndex >= 0; return { components: { before: 'div', @@ -444,7 +447,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta beforeBufferHeight: isFullyInitialized ? calculateBefore() : 0, afterBufferHeight: isFullyInitialized ? calculateAfter() : 0, totalVirtualizerHeight: isFullyInitialized ? calculateTotalSize() : virtualizerLength * itemSize, - virtualizerStartIndex, + virtualizerStartIndex: actualIndex, axis, bufferSize, reversed, diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollViewStyles.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollViewStyles.ts index b457f20bcef4c1..023fdc40b5739c 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollViewStyles.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollViewStyles.ts @@ -45,7 +45,7 @@ export const useVirtualizerScrollViewStyles_unstable = ( ): VirtualizerScrollViewState => { const styles = useStyles(); - // For now - just return default style mods + // Default virtualizer styles base useVirtualizerStyles_unstable(state); const containerStyle = @@ -57,6 +57,7 @@ export const useVirtualizerScrollViewStyles_unstable = ( ? styles.verticalReversed : styles.vertical; + // Add container styles state.container.className = mergeClasses( virtualizerScrollViewClassNames.container, styles.base, diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts new file mode 100644 index 00000000000000..9f159f83428dae --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts @@ -0,0 +1,35 @@ +import { VirtualizerScrollViewDynamicProps } from './VirtualizerScrollViewDynamic.types'; +import { useVirtualizerScrollViewDynamic_unstable } from './useVirtualizerScrollViewDynamic'; +import { + renderIVirtualizerScrollViewDynamic_unstable, + renderVirtualizerScrollViewDynamic_unstable, +} from './renderVirtualizerScrollViewDynamic'; +import { useVirtualizerScrollViewDynamicStyles_unstable } from './useVirtualizerScrollViewDynamicStyles'; +import * as React from 'react'; +import { useVirtualizerContextState, VirtualizerContextProps } from '../../Utilities'; + +/** + * Virtualizer ScrollView + */ + +export const IVirtualizerScrollViewDynamic: React.FC = ( + props: VirtualizerScrollViewDynamicProps, + context: React.Context, +) => { + const state = useVirtualizerScrollViewDynamic_unstable(props); + + useVirtualizerScrollViewDynamicStyles_unstable(state); + + return renderIVirtualizerScrollViewDynamic_unstable(state); +}; + +IVirtualizerScrollViewDynamic.displayName = 'IVirtualizerScrollViewDynamic'; + +export const VirtualizerScrollViewDynamic: React.FC = ( + props: VirtualizerScrollViewDynamicProps, +) => { + const context = useVirtualizerContextState(); + return renderVirtualizerScrollViewDynamic_unstable(props, context); +}; + +VirtualizerScrollViewDynamic.displayName = 'VirtualizerScrollViewDynamic'; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts new file mode 100644 index 00000000000000..0df4061f12ed7d --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts @@ -0,0 +1,38 @@ +import { ComponentProps, ComponentState } from '@fluentui/react-utilities'; +import { + VirtualizerConfigProps, + VirtualizerConfigState, + VirtualizerChildRenderFunction, +} from '../Virtualizer/Virtualizer.types'; + +import { VirtualizerScrollViewSlots } from '../VirtualizerScrollView/VirtualizerScrollView.types'; + +export type VirtualizerScrollViewDynamicSlots = VirtualizerScrollViewSlots; + +export type VirtualizerScrollViewDynamicProps = ComponentProps> & + Partial> & { + /** + * Set as the minimum item size. + * Axis: 'vertical' = Height + * Axis: 'horizontal' = Width + */ + itemSize: number; + /** + * Callback for acquiring size of individual items + * @param index - the index of the requested size's child + */ + getItemSize: (index: number) => number; + /** + * The total number of items to be virtualized. + */ + numItems: number; + /** + * Child render function. + * Iteratively called to return current virtualizer DOM children. + * Will act as a row or column indexer depending on Virtualizer settings. + */ + children: VirtualizerChildRenderFunction; + }; + +export type VirtualizerScrollViewDynamicState = ComponentState & + VirtualizerConfigState; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/index.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/index.ts new file mode 100644 index 00000000000000..0c5e4e353fab9e --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/index.ts @@ -0,0 +1,5 @@ +export * from './VirtualizerScrollViewDynamic'; +export * from './VirtualizerScrollViewDynamic.types'; +export * from './useVirtualizerScrollViewDynamic'; +export * from './renderVirtualizerScrollViewDynamic'; +export * from './useVirtualizerScrollViewDynamicStyles'; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx new file mode 100644 index 00000000000000..cb74dedafb7222 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; +import { + VirtualizerScrollViewDynamicProps, + VirtualizerScrollViewDynamicSlots, + VirtualizerScrollViewDynamicState, +} from './VirtualizerScrollViewDynamic.types'; +import { VirtualizerContext, VirtualizerContextProps } from '../../Utilities'; +import { IVirtualizerScrollViewDynamic } from './VirtualizerScrollViewDynamic'; + +export const renderIVirtualizerScrollViewDynamic_unstable = (state: VirtualizerScrollViewDynamicState) => { + const { slots, slotProps } = getSlots(state); + + return ( + + {/* The 'before' bookend to hold items in place and detect scroll previous */} + + + + {/* The reduced list of non-virtualized children to be rendered */} + {state.virtualizedChildren} + {/* The 'after' bookend to hold items in place and detect scroll next */} + + + + + ); +}; + +export const renderVirtualizerScrollViewDynamic_unstable = ( + props: VirtualizerScrollViewDynamicProps, + context: VirtualizerContextProps, +) => { + return ( + + + + ); +}; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts new file mode 100644 index 00000000000000..9cd91c0010300c --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { resolveShorthand } from '@fluentui/react-utilities'; +import { useVirtualizer_unstable } from '../Virtualizer/useVirtualizer'; +import { + VirtualizerScrollViewDynamicProps, + VirtualizerScrollViewDynamicState, +} from './VirtualizerScrollViewDynamic.types'; +import { useDynamicVirtualizerMeasure } from '../../Hooks'; +import { useVirtualizerContext } from '../../Utilities'; + +export function useVirtualizerScrollViewDynamic_unstable( + props: VirtualizerScrollViewDynamicProps, +): VirtualizerScrollViewDynamicState { + const { contextIndex } = useVirtualizerContext(); + const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useDynamicVirtualizerMeasure({ + defaultItemSize: props.itemSize, + direction: props.axis ?? 'vertical', + getItemSize: props.getItemSize, + currentIndex: contextIndex, + numItems: 0, + }); + + const iScrollRef = React.useRef(null); + + const setScrollRef = React.useCallback( + (element: HTMLDivElement) => { + if (iScrollRef.current === element) { + return; + } + scrollRef(element); + iScrollRef.current = element; + }, + [scrollRef], + ); + + const virtualizerState = useVirtualizer_unstable({ + ...props, + virtualizerLength, + bufferItems, + bufferSize, + scrollViewRef: iScrollRef, + }); + + return { + ...virtualizerState, + components: { + ...virtualizerState.components, + container: 'div', + }, + container: resolveShorthand(props.container, { + required: true, + defaultProps: { + ref: setScrollRef, + }, + }), + }; +} diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts new file mode 100644 index 00000000000000..0f0d4e29ae4d83 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts @@ -0,0 +1,71 @@ +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { + VirtualizerScrollViewDynamicSlots, + VirtualizerScrollViewDynamicState, +} from './VirtualizerScrollViewDynamic.types'; +import { useVirtualizerStyles_unstable, virtualizerClassNames } from '../Virtualizer/useVirtualizerStyles'; +import { makeStyles, mergeClasses } from '@griffel/react'; + +const virtualizerScrollViewDynamicClassName = 'fui-Virtualizer-Scroll-View-Dynamic'; + +export const virtualizerScrollViewDynamicClassNames: SlotClassNames = { + ...virtualizerClassNames, + container: `${virtualizerScrollViewDynamicClassName}__container`, +}; + +const useStyles = makeStyles({ + base: { + display: 'flex', + width: '100%', + height: '100%', + overflowAnchor: 'none', + }, + vertical: { + flexDirection: 'column', + overflowAnchor: 'none', + overflowY: 'auto', + }, + horizontal: { + flexDirection: 'row', + overflowX: 'auto', + }, + verticalReversed: { + flexDirection: 'column-reverse', + overflowY: 'auto', + }, + horizontalReversed: { + flexDirection: 'row-reverse', + overflowX: 'auto', + }, +}); + +/** + * Apply styling to the Virtualizer states + */ +export const useVirtualizerScrollViewDynamicStyles_unstable = ( + state: VirtualizerScrollViewDynamicState, +): VirtualizerScrollViewDynamicState => { + const styles = useStyles(); + + // Default virtualizer styles base + useVirtualizerStyles_unstable(state); + + const containerStyle = + state.axis === 'horizontal' + ? state.reversed + ? styles.horizontalReversed + : styles.horizontal + : state.reversed + ? styles.verticalReversed + : styles.vertical; + + // Add container styles + state.container.className = mergeClasses( + virtualizerScrollViewDynamicClassNames.container, + styles.base, + containerStyle, + state.container.className, + ); + + return state; +}; diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index d5f92320ecb233..28d64c7ae162f9 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { canUseDOM } from '@fluentui/react-utilities'; -import { VirtualizerMeasureProps } from './useVirtualizerMeasure.types'; +import { VirtualizerMeasureDynamicProps, VirtualizerMeasureProps } from './useVirtualizerMeasure.types'; import { debounce } from '../utilities/debounce'; /** @@ -97,3 +97,110 @@ export const useStaticVirtualizerMeasure = ( scrollRef, }; }; + +/** + * React hook that measures virtualized space dynamically to ensure optimized virtualization length. + */ +export const useDynamicVirtualizerMeasure = ( + virtualizerProps: VirtualizerMeasureDynamicProps, +): { + virtualizerLength: number; + bufferItems: number; + bufferSize: number; + scrollRef: (instance: HTMLElement | HTMLDivElement | null) => void; +} => { + const { defaultItemSize, direction = 'vertical', numItems, getItemSize, currentIndex } = virtualizerProps; + + const sizeTracker = React.useRef(new Array(numItems).fill(defaultItemSize)); + + if (sizeTracker.current.length !== numItems) { + // numItems changed, morph array - keep previously explored values. + const newItems = numItems - sizeTracker.current.length; + if (newItems > 0) { + sizeTracker.current = [...sizeTracker.current, ...Array(newItems).fill(defaultItemSize)]; + } else { + sizeTracker.current.splice(numItems, newItems * -1); + } + } + + const [state, setState] = React.useState({ + virtualizerLength: 0, + virtualizerBufferItems: 0, + virtualizerBufferSize: 0, + }); + + const { virtualizerLength, virtualizerBufferItems, virtualizerBufferSize } = state; + + const container = React.useRef(null); + + const resizeCallback = () => { + if (!container.current) { + // Error? ignore? + return; + } + + const containerSize = + direction === 'vertical' + ? container.current?.getBoundingClientRect().height + : container.current?.getBoundingClientRect().width; + + let indexSizer = 0; + let length = 0; + + while (indexSizer <= containerSize) { + const iItemSize = getItemSize(currentIndex + length); + sizeTracker.current[currentIndex + length] = iItemSize; + + // Increment + indexSizer += iItemSize; + length++; + } + + /* + * Number of items to append at each end, i.e. 'preload' each side before entering view. + */ + const bufferItems = Math.max(Math.floor(length / 4), 2); + + /* + * 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; + + setState({ + virtualizerLength: totalLength, + virtualizerBufferSize: bufferSize, + virtualizerBufferItems: bufferItems, + }); + }; + + // the handler for resize observer + const handleResize = debounce(resizeCallback); + + // Keep the reference of ResizeObserver in the state, as it should live through renders + const [resizeObserver] = React.useState(canUseDOM() ? new ResizeObserver(handleResize) : undefined); + + const scrollRef = React.useCallback( + (el: HTMLElement | null) => { + if (container.current !== el) { + if (container.current) { + resizeObserver?.unobserve(container.current); + } + + container.current = el; + if (container.current) { + resizeObserver?.observe(container.current); + } + } + }, + [resizeObserver], + ); + + return { + virtualizerLength, + bufferItems: virtualizerBufferItems, + bufferSize: virtualizerBufferSize, + scrollRef, + }; +}; diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts index dffd7c801c7b89..eab7ad32e7860e 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts @@ -2,3 +2,11 @@ export type VirtualizerMeasureProps = { defaultItemSize: number; direction?: 'vertical' | 'horizontal'; }; + +export type VirtualizerMeasureDynamicProps = { + defaultItemSize: number; + currentIndex: number; + numItems: number; + getItemSize: (index: number) => number; + direction?: 'vertical' | 'horizontal'; +}; diff --git a/packages/react-components/react-virtualizer/src/index.ts b/packages/react-components/react-virtualizer/src/index.ts index eff1e6574b1946..3ca43dc63dc7ad 100644 --- a/packages/react-components/react-virtualizer/src/index.ts +++ b/packages/react-components/react-virtualizer/src/index.ts @@ -11,7 +11,12 @@ export type { VirtualizerSlots, VirtualizerChildRenderFunction, } from './Virtualizer'; -export { useIntersectionObserver, useStaticVirtualizerMeasure } from './Hooks'; + +export { useIntersectionObserver, useStaticVirtualizerMeasure, useDynamicVirtualizerMeasure } from './Hooks'; + +export { VirtualizerContext, useVirtualizerContext } from './Utilities'; + +export type { VirtualizerContextProps } from './Utilities'; export { VirtualizerScrollView, @@ -26,3 +31,19 @@ export type { VirtualizerScrollViewState, VirtualizerScrollViewSlots, } from './VirtualizerScrollView'; + +export { + VirtualizerScrollViewDynamic, + IVirtualizerScrollViewDynamic, + virtualizerScrollViewDynamicClassNames, + useVirtualizerScrollViewDynamic_unstable, + renderVirtualizerScrollViewDynamic_unstable, + renderIVirtualizerScrollViewDynamic_unstable, + useVirtualizerScrollViewDynamicStyles_unstable, +} from './VirtualizerScrollViewDynamic'; + +export type { + VirtualizerScrollViewDynamicProps, + VirtualizerScrollViewDynamicState, + VirtualizerScrollViewDynamicSlots, +} from './VirtualizerScrollViewDynamic'; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts new file mode 100644 index 00000000000000..986bd880302748 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import type { VirtualizerContextProps } from './types'; + +export const VirtualizerContext = React.createContext( + undefined, +) as React.Context; + +export const useVirtualizerContext = () => { + return React.useContext(VirtualizerContext); +}; + +export const useVirtualizerContextState = (): VirtualizerContextProps => { + const [contextIndex, setContextIndex] = React.useState(0); + return { contextIndex, setContextIndex }; +}; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/index.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/index.ts new file mode 100644 index 00000000000000..3eee78749512cc --- /dev/null +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/index.ts @@ -0,0 +1,2 @@ +export * from './VirtualizerContext'; +export * from './types'; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts new file mode 100644 index 00000000000000..408d3773f4228e --- /dev/null +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts @@ -0,0 +1,7 @@ +/** + * {@docCategory Virtualizer} + */ +export type VirtualizerContextProps = { + contextIndex: number; + setContextIndex: (index: number) => void; +}; diff --git a/packages/react-components/react-virtualizer/src/utilities/index.ts b/packages/react-components/react-virtualizer/src/utilities/index.ts new file mode 100644 index 00000000000000..a4f06905012b63 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/utilities/index.ts @@ -0,0 +1 @@ +export * from './VirtualizerContext'; diff --git a/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx b/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx index 73000e8da9fe5d..ca5cbf5642f49c 100644 --- a/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Virtualizer } from '@fluentui/react-components/unstable'; +import { Virtualizer, useDynamicVirtualizerMeasure, VirtualizerContext } from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; const smallSize = 100; @@ -29,6 +29,7 @@ const useStyles = makeStyles({ }); export const Dynamic = () => { + const [currentIndex, setCurrentIndex] = React.useState(-1); const [flag, toggleFlag] = React.useState(false); const styles = useStyles(); const childLength = 1000; @@ -52,29 +53,39 @@ export const Dynamic = () => { return sizeValue; }; + const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useDynamicVirtualizerMeasure({ + defaultItemSize: 100, + getItemSize: getSizeForIndex, + numItems: childLength, + currentIndex, + }); + return ( -
- getSizeForIndex(index)} - numItems={childLength} - bufferSize={50} - virtualizerLength={25} - itemSize={100} - > - {(index: number) => { - const sizeValue = getSizeForIndex(index); - const sizeClass = sizeValue === smallSize ? styles.child : styles.childLarge; - return ( -
{`Node-${index}-size-${sizeValue}`}
- ); - }} -
-
+ +
+ getSizeForIndex(index)} + numItems={childLength} + bufferSize={bufferSize} + bufferItems={bufferItems} + virtualizerLength={virtualizerLength} + itemSize={100} + > + {(index: number) => { + const sizeValue = getSizeForIndex(index); + const sizeClass = sizeValue === smallSize ? styles.child : styles.childLarge; + return ( +
{`Node-${index}-size-${sizeValue}`}
+ ); + }} +
+
+
); }; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx new file mode 100644 index 00000000000000..f3b0e61af8261c --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { makeStyles } from '@fluentui/react-components'; +import { ThemeProvider } from '@fluentui/react'; +import { array } from 'yargs'; +import { useEffect } from '@storybook/addons'; + +const useStyles = makeStyles({ + root: { + maxHeight: '100vh', + }, + child: { + lineHeight: '42px', + width: '100%', + minHeight: '42px', + }, +}); + +export const Default = () => { + const styles = useStyles(); + const childLength = 1000; + const minHeight = 42; + const arraySize = new Array(childLength).fill(minHeight); + + useEffect(() => { + for (let i = 0; i < childLength; i++) { + arraySize[i] = Math.random() * 250 + minHeight; + } + }, [arraySize]); + + return ( + + { + return arraySize[index]; + }} + container={{ role: 'list', style: { maxHeight: '100vh' } }} + > + {(index: number) => { + const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; + return ( +
{`Node-${index} - size: ${arraySize[index]}`}
+ ); + }} +
+
+ ); +}; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md new file mode 100644 index 00000000000000..78973fe04255e6 --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamicDescription.md @@ -0,0 +1,12 @@ + + +> **⚠️ Preview components are considered unstable:** +> +> ```jsx +> +> import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +> +> ``` +> +> - Features and APIs may change before final release +> - Please contact us if you intend to use this in your product diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts new file mode 100644 index 00000000000000..ce2edca660abcd --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts @@ -0,0 +1,16 @@ +import { VirtualizerScrollViewDynamic } from '../../src/VirtualizerScrollViewDynamic'; +import descriptionMd from './VirtualizerScrollViewDynamicDescription.md'; + +export { Default } from './Default.stories'; + +export default { + title: 'Preview Components/VirtualizerScrollViewDynamic', + component: VirtualizerScrollViewDynamic, + parameters: { + docs: { + description: { + component: [descriptionMd].join('\n'), + }, + }, + }, +}; From ab58ef071c2eeb31e840df299d94680deda6125e Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Wed, 22 Mar 2023 17:42:38 -0700 Subject: [PATCH 02/26] Update size function to use callback and be updateable if changed --- .../components/Virtualizer/useVirtualizer.ts | 9 +++++- .../Default.stories.tsx | 31 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index 58138a61d1e523..a18a351e3a063f 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -68,7 +68,6 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta for (let index = 0; index < numItems; index++) { childSizes.current[index] = getItemSize(index); - if (index === 0) { childProgressiveSizes.current[index] = childSizes.current[index]; } else { @@ -395,6 +394,14 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // eslint-disable-next-line react-hooks/exhaustive-deps }, [renderChild, updateChildRows]); + useEffect(() => { + // Ensure we repopulate if getItemSize callback changes + populateSizeArrays(); + + // We only run this effect on getItemSize change (recalc dynamic sizes) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getItemSize]); + // Ensure we have run through and updated the whole size list array at least once. initializeSizeArray(); diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx index f3b0e61af8261c..53ebd299bdecf4 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -2,12 +2,11 @@ import * as React from 'react'; import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; import { ThemeProvider } from '@fluentui/react'; -import { array } from 'yargs'; import { useEffect } from '@storybook/addons'; const useStyles = makeStyles({ root: { - maxHeight: '100vh', + maxHeight: '60vh', }, child: { lineHeight: '42px', @@ -20,22 +19,34 @@ export const Default = () => { const styles = useStyles(); const childLength = 1000; const minHeight = 42; - const arraySize = new Array(childLength).fill(minHeight); + // 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 + const [totalSize, setTotalSize] = React.useState(minHeight * childLength); useEffect(() => { + let _totalSize = 0; for (let i = 0; i < childLength; i++) { - arraySize[i] = Math.random() * 250 + minHeight; + arraySize.current[i] = Math.random() * 250 + minHeight; + _totalSize += arraySize.current[i]; } - }, [arraySize]); + setTotalSize(_totalSize); + }, []); + + const getItemSizeCallback = React.useCallback( + (index: number) => { + return arraySize.current[index]; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [arraySize, totalSize], + ); return ( { - return arraySize[index]; - }} + getItemSize={getItemSizeCallback} container={{ role: 'list', style: { maxHeight: '100vh' } }} > {(index: number) => { @@ -47,8 +58,8 @@ export const Default = () => { aria-setsize={childLength} key={`test-virtualizer-child-${index}`} className={styles.child} - style={{ minHeight: arraySize[index], backgroundColor }} - >{`Node-${index} - size: ${arraySize[index]}`} + style={{ minHeight: arraySize.current[index], backgroundColor }} + >{`Node-${index} - size: ${arraySize.current[index]}`} ); }} From c6b3afb97d294baa902414412df3563bbf12226c Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Wed, 22 Mar 2023 17:54:29 -0700 Subject: [PATCH 03/26] Removed unneeded style --- .../useVirtualizerScrollViewDynamicStyles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts index 0f0d4e29ae4d83..bbf6deb5da7edc 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamicStyles.ts @@ -22,7 +22,6 @@ const useStyles = makeStyles({ }, vertical: { flexDirection: 'column', - overflowAnchor: 'none', overflowY: 'auto', }, horizontal: { From 96c6f06a15ea1120a28c646b6317e9759b262fc2 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Wed, 22 Mar 2023 17:59:01 -0700 Subject: [PATCH 04/26] Change files --- ...ct-components-333c9bda-d493-4ea4-842a-af649fd2001e.json | 7 +++++++ ...t-virtualizer-e15ce267-f3e2-4c24-a3ca-6d20e617025f.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-react-components-333c9bda-d493-4ea4-842a-af649fd2001e.json create mode 100644 change/@fluentui-react-virtualizer-e15ce267-f3e2-4c24-a3ca-6d20e617025f.json diff --git a/change/@fluentui-react-components-333c9bda-d493-4ea4-842a-af649fd2001e.json b/change/@fluentui-react-components-333c9bda-d493-4ea4-842a-af649fd2001e.json new file mode 100644 index 00000000000000..9989cacc4816a9 --- /dev/null +++ b/change/@fluentui-react-components-333c9bda-d493-4ea4-842a-af649fd2001e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feature: Add dynamically sized virtualizer scroll view", + "packageName": "@fluentui/react-components", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-virtualizer-e15ce267-f3e2-4c24-a3ca-6d20e617025f.json b/change/@fluentui-react-virtualizer-e15ce267-f3e2-4c24-a3ca-6d20e617025f.json new file mode 100644 index 00000000000000..28f90bea75cb7c --- /dev/null +++ b/change/@fluentui-react-virtualizer-e15ce267-f3e2-4c24-a3ca-6d20e617025f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feature: Add dynamically sized virtualizer scroll view", + "packageName": "@fluentui/react-virtualizer", + "email": "mifraser@microsoft.com", + "dependentChangeType": "patch" +} From 8cc6259885ad6417211af35d6329ed9171329eb2 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Thu, 23 Mar 2023 10:54:03 -0700 Subject: [PATCH 05/26] Dont export context itself --- .../react-components/src/unstable/index.ts | 2 +- .../renderVirtualizerScrollViewDynamic.tsx | 6 +++--- .../react-components/react-virtualizer/src/index.ts | 2 +- .../utilities/VirtualizerContext/VirtualizerContext.ts | 2 ++ .../stories/Virtualizer/Dynamic.stories.tsx | 10 +++++++--- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-components/src/unstable/index.ts b/packages/react-components/react-components/src/unstable/index.ts index ee0dfafeacd413..83955a224b8041 100644 --- a/packages/react-components/react-components/src/unstable/index.ts +++ b/packages/react-components/react-components/src/unstable/index.ts @@ -122,7 +122,7 @@ export { useIntersectionObserver, useStaticVirtualizerMeasure, useDynamicVirtualizerMeasure, - VirtualizerContext, + VirtualizerContextProvider, useVirtualizerContext, VirtualizerScrollView, virtualizerScrollViewClassNames, diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx index cb74dedafb7222..996ddf64870235 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx @@ -5,7 +5,7 @@ import { VirtualizerScrollViewDynamicSlots, VirtualizerScrollViewDynamicState, } from './VirtualizerScrollViewDynamic.types'; -import { VirtualizerContext, VirtualizerContextProps } from '../../Utilities'; +import { VirtualizerContextProvider, VirtualizerContextProps } from '../../Utilities'; import { IVirtualizerScrollViewDynamic } from './VirtualizerScrollViewDynamic'; export const renderIVirtualizerScrollViewDynamic_unstable = (state: VirtualizerScrollViewDynamicState) => { @@ -32,8 +32,8 @@ export const renderVirtualizerScrollViewDynamic_unstable = ( context: VirtualizerContextProps, ) => { return ( - + - + ); }; diff --git a/packages/react-components/react-virtualizer/src/index.ts b/packages/react-components/react-virtualizer/src/index.ts index 3ca43dc63dc7ad..f8e9fd46b57afd 100644 --- a/packages/react-components/react-virtualizer/src/index.ts +++ b/packages/react-components/react-virtualizer/src/index.ts @@ -14,7 +14,7 @@ export type { export { useIntersectionObserver, useStaticVirtualizerMeasure, useDynamicVirtualizerMeasure } from './Hooks'; -export { VirtualizerContext, useVirtualizerContext } from './Utilities'; +export { VirtualizerContextProvider, useVirtualizerContext } from './Utilities'; export type { VirtualizerContextProps } from './Utilities'; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts index 986bd880302748..e70af42b09dcbf 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -5,6 +5,8 @@ export const VirtualizerContext = React.createContext; +export const VirtualizerContextProvider = VirtualizerContext.Provider; + export const useVirtualizerContext = () => { return React.useContext(VirtualizerContext); }; diff --git a/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx b/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx index ca5cbf5642f49c..5d30169516e2c6 100644 --- a/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/Virtualizer/Dynamic.stories.tsx @@ -1,5 +1,9 @@ import * as React from 'react'; -import { Virtualizer, useDynamicVirtualizerMeasure, VirtualizerContext } from '@fluentui/react-components/unstable'; +import { + Virtualizer, + useDynamicVirtualizerMeasure, + VirtualizerContextProvider, +} from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; const smallSize = 100; @@ -61,7 +65,7 @@ export const Dynamic = () => { }); return ( - +
getSizeForIndex(index)} @@ -86,6 +90,6 @@ export const Dynamic = () => { }}
-
+ ); }; From 5edf804b6cfa104e937d0c52a0df3a8482a09a71 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Thu, 23 Mar 2023 11:20:02 -0700 Subject: [PATCH 06/26] Update api --- .../react-components/etc/react-components.unstable.api.md | 6 +++--- .../react-virtualizer/etc/react-virtualizer.api.md | 6 +++--- .../src/utilities/VirtualizerContext/VirtualizerContext.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-components/react-components/etc/react-components.unstable.api.md b/packages/react-components/react-components/etc/react-components.unstable.api.md index 7bad91802b01bd..0a8d1c09465dfc 100644 --- a/packages/react-components/react-components/etc/react-components.unstable.api.md +++ b/packages/react-components/react-components/etc/react-components.unstable.api.md @@ -153,8 +153,8 @@ import { useVirtualizerStyles_unstable } from '@fluentui/react-virtualizer'; import { Virtualizer } from '@fluentui/react-virtualizer'; import { VirtualizerChildRenderFunction } from '@fluentui/react-virtualizer'; import { virtualizerClassNames } from '@fluentui/react-virtualizer'; -import { VirtualizerContext } from '@fluentui/react-virtualizer'; import { VirtualizerContextProps } from '@fluentui/react-virtualizer'; +import { VirtualizerContextProvider } from '@fluentui/react-virtualizer'; import { VirtualizerProps } from '@fluentui/react-virtualizer'; import { VirtualizerScrollView } from '@fluentui/react-virtualizer'; import { virtualizerScrollViewClassNames } from '@fluentui/react-virtualizer'; @@ -467,10 +467,10 @@ export { VirtualizerChildRenderFunction } export { virtualizerClassNames } -export { VirtualizerContext } - export { VirtualizerContextProps } +export { VirtualizerContextProvider } + export { VirtualizerProps } export { VirtualizerScrollView } diff --git a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md index 3956595936d3bd..acb1f5a1449699 100644 --- a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md +++ b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md @@ -82,15 +82,15 @@ export type VirtualizerChildRenderFunction = (index: number) => React_2.ReactNod // @public (undocumented) export const virtualizerClassNames: SlotClassNames; -// @public (undocumented) -export const VirtualizerContext: React_2.Context; - // @public (undocumented) export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; }; +// @public (undocumented) +export const VirtualizerContextProvider: React_2.Provider; + // @public (undocumented) export type VirtualizerProps = ComponentProps> & VirtualizerConfigProps; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts index e70af42b09dcbf..7b114695ce809b 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { VirtualizerContextProps } from './types'; -export const VirtualizerContext = React.createContext( +const VirtualizerContext = React.createContext( undefined, ) as React.Context; From 39d20de0bbd8274888f349a1a2af28308f265aa0 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 28 Mar 2023 14:20:36 -0700 Subject: [PATCH 07/26] Remove Theme Provider and ensure all demos have a max height set --- .../etc/react-components.unstable.api.md | 12 ++--- .../react-components/src/unstable/index.ts | 4 +- .../etc/react-virtualizer.api.md | 4 +- .../VirtualizerScrollViewDynamic.ts | 8 ++-- .../renderVirtualizerScrollViewDynamic.tsx | 6 +-- .../react-virtualizer/src/index.ts | 4 +- .../stories/Virtualizer/Default.stories.tsx | 2 +- .../Virtualizer/DefaultUnbounded.stories.tsx | 3 -- .../VirtualizerScrollView/Default.stories.tsx | 3 -- .../Default.stories.tsx | 46 ++++++++----------- 10 files changed, 40 insertions(+), 52 deletions(-) diff --git a/packages/react-components/react-components/etc/react-components.unstable.api.md b/packages/react-components/react-components/etc/react-components.unstable.api.md index 0a8d1c09465dfc..cf82a8b7b8d940 100644 --- a/packages/react-components/react-components/etc/react-components.unstable.api.md +++ b/packages/react-components/react-components/etc/react-components.unstable.api.md @@ -15,6 +15,7 @@ import { CheckboxFieldProps_unstable as CheckboxFieldProps } from '@fluentui/rea import { ComboboxField_unstable as ComboboxField } from '@fluentui/react-combobox'; import { comboboxFieldClassNames } from '@fluentui/react-combobox'; import { ComboboxFieldProps_unstable as ComboboxFieldProps } from '@fluentui/react-combobox'; +import { ContextlessVirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { Field } from '@fluentui/react-field'; import { fieldClassNames } from '@fluentui/react-field'; import { FieldProps } from '@fluentui/react-field'; @@ -38,7 +39,6 @@ import { InfoLabelState } from '@fluentui/react-infobutton'; import { InputField_unstable as InputField } from '@fluentui/react-input'; import { inputFieldClassNames } from '@fluentui/react-input'; import { InputFieldProps_unstable as InputFieldProps } from '@fluentui/react-input'; -import { IVirtualizerScrollViewDynamic } from '@fluentui/react-virtualizer'; import { NestedTreeItem } from '@fluentui/react-tree'; import { ProgressField_unstable as ProgressField } from '@fluentui/react-progress'; import { progressFieldClassNames } from '@fluentui/react-progress'; @@ -47,10 +47,10 @@ import { RadioGroupField_unstable as RadioGroupField } from '@fluentui/react-rad import { radioGroupFieldClassNames } from '@fluentui/react-radio'; import { RadioGroupFieldProps_unstable as RadioGroupFieldProps } from '@fluentui/react-radio'; import { renderAlert_unstable } from '@fluentui/react-alert'; +import { renderContextlessVirtualizerScrollViewDynamic_unstable } from '@fluentui/react-virtualizer'; import { renderField_unstable } from '@fluentui/react-field'; import { renderInfoButton_unstable } from '@fluentui/react-infobutton'; import { renderInfoLabel_unstable } from '@fluentui/react-infobutton'; -import { renderIVirtualizerScrollViewDynamic_unstable } from '@fluentui/react-virtualizer'; import { renderSkeleton_unstable } from '@fluentui/react-skeleton'; import { renderSkeletonItem_unstable } from '@fluentui/react-skeleton'; import { renderTree_unstable } from '@fluentui/react-tree'; @@ -191,6 +191,8 @@ export { comboboxFieldClassNames } export { ComboboxFieldProps } +export { ContextlessVirtualizerScrollViewDynamic } + export { Field } export { fieldClassNames } @@ -237,8 +239,6 @@ export { inputFieldClassNames } export { InputFieldProps } -export { IVirtualizerScrollViewDynamic } - export { NestedTreeItem } export { ProgressField } @@ -255,14 +255,14 @@ export { RadioGroupFieldProps } export { renderAlert_unstable } +export { renderContextlessVirtualizerScrollViewDynamic_unstable } + export { renderField_unstable } export { renderInfoButton_unstable } export { renderInfoLabel_unstable } -export { renderIVirtualizerScrollViewDynamic_unstable } - export { renderSkeleton_unstable } export { renderSkeletonItem_unstable } diff --git a/packages/react-components/react-components/src/unstable/index.ts b/packages/react-components/react-components/src/unstable/index.ts index 83955a224b8041..f9e619a5e5fb58 100644 --- a/packages/react-components/react-components/src/unstable/index.ts +++ b/packages/react-components/react-components/src/unstable/index.ts @@ -130,11 +130,11 @@ export { renderVirtualizerScrollView_unstable, useVirtualizerScrollViewStyles_unstable, VirtualizerScrollViewDynamic, - IVirtualizerScrollViewDynamic, + ContextlessVirtualizerScrollViewDynamic, virtualizerScrollViewDynamicClassNames, useVirtualizerScrollViewDynamic_unstable, renderVirtualizerScrollViewDynamic_unstable, - renderIVirtualizerScrollViewDynamic_unstable, + renderContextlessVirtualizerScrollViewDynamic_unstable, useVirtualizerScrollViewDynamicStyles_unstable, } from '@fluentui/react-virtualizer'; export type { diff --git a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md index acb1f5a1449699..813643663925a5 100644 --- a/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md +++ b/packages/react-components/react-virtualizer/etc/react-virtualizer.api.md @@ -15,10 +15,10 @@ import { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; // @public -export const IVirtualizerScrollViewDynamic: React_2.FC; +export const ContextlessVirtualizerScrollViewDynamic: React_2.FC; // @public (undocumented) -export const renderIVirtualizerScrollViewDynamic_unstable: (state: VirtualizerScrollViewDynamicState) => JSX.Element; +export const renderContextlessVirtualizerScrollViewDynamic_unstable: (state: VirtualizerScrollViewDynamicState) => JSX.Element; // @public (undocumented) export const renderVirtualizer_unstable: (state: VirtualizerState) => JSX.Element; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts index 9f159f83428dae..64894fda84c078 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.ts @@ -1,7 +1,7 @@ import { VirtualizerScrollViewDynamicProps } from './VirtualizerScrollViewDynamic.types'; import { useVirtualizerScrollViewDynamic_unstable } from './useVirtualizerScrollViewDynamic'; import { - renderIVirtualizerScrollViewDynamic_unstable, + renderContextlessVirtualizerScrollViewDynamic_unstable, renderVirtualizerScrollViewDynamic_unstable, } from './renderVirtualizerScrollViewDynamic'; import { useVirtualizerScrollViewDynamicStyles_unstable } from './useVirtualizerScrollViewDynamicStyles'; @@ -12,7 +12,7 @@ import { useVirtualizerContextState, VirtualizerContextProps } from '../../Utili * Virtualizer ScrollView */ -export const IVirtualizerScrollViewDynamic: React.FC = ( +export const ContextlessVirtualizerScrollViewDynamic: React.FC = ( props: VirtualizerScrollViewDynamicProps, context: React.Context, ) => { @@ -20,10 +20,10 @@ export const IVirtualizerScrollViewDynamic: React.FC = ( props: VirtualizerScrollViewDynamicProps, diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx index 996ddf64870235..3118e7879aea58 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx @@ -6,9 +6,9 @@ import { VirtualizerScrollViewDynamicState, } from './VirtualizerScrollViewDynamic.types'; import { VirtualizerContextProvider, VirtualizerContextProps } from '../../Utilities'; -import { IVirtualizerScrollViewDynamic } from './VirtualizerScrollViewDynamic'; +import { ContextlessVirtualizerScrollViewDynamic } from './VirtualizerScrollViewDynamic'; -export const renderIVirtualizerScrollViewDynamic_unstable = (state: VirtualizerScrollViewDynamicState) => { +export const renderContextlessVirtualizerScrollViewDynamic_unstable = (state: VirtualizerScrollViewDynamicState) => { const { slots, slotProps } = getSlots(state); return ( @@ -33,7 +33,7 @@ export const renderVirtualizerScrollViewDynamic_unstable = ( ) => { return ( - + ); }; diff --git a/packages/react-components/react-virtualizer/src/index.ts b/packages/react-components/react-virtualizer/src/index.ts index f8e9fd46b57afd..918ca9fdfe066a 100644 --- a/packages/react-components/react-virtualizer/src/index.ts +++ b/packages/react-components/react-virtualizer/src/index.ts @@ -34,11 +34,11 @@ export type { export { VirtualizerScrollViewDynamic, - IVirtualizerScrollViewDynamic, + ContextlessVirtualizerScrollViewDynamic, virtualizerScrollViewDynamicClassNames, useVirtualizerScrollViewDynamic_unstable, renderVirtualizerScrollViewDynamic_unstable, - renderIVirtualizerScrollViewDynamic_unstable, + renderContextlessVirtualizerScrollViewDynamic_unstable, useVirtualizerScrollViewDynamicStyles_unstable, } from './VirtualizerScrollViewDynamic'; diff --git a/packages/react-components/react-virtualizer/stories/Virtualizer/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/Virtualizer/Default.stories.tsx index 00ab141a061181..28b75b3f13a2f1 100644 --- a/packages/react-components/react-virtualizer/stories/Virtualizer/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/Virtualizer/Default.stories.tsx @@ -10,7 +10,7 @@ const useStyles = makeStyles({ overflowY: 'auto', width: '100%', height: '100%', - maxHeight: '60vh', + maxHeight: '750px', }, child: { height: '100px', diff --git a/packages/react-components/react-virtualizer/stories/Virtualizer/DefaultUnbounded.stories.tsx b/packages/react-components/react-virtualizer/stories/Virtualizer/DefaultUnbounded.stories.tsx index fea0580d0945c2..ded4e0d6c1f553 100644 --- a/packages/react-components/react-virtualizer/stories/Virtualizer/DefaultUnbounded.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/Virtualizer/DefaultUnbounded.stories.tsx @@ -5,9 +5,6 @@ import { makeStyles } from '@fluentui/react-components'; import { useFluent } from '@fluentui/react-components'; const useStyles = makeStyles({ - root: { - maxHeight: '100vh', - }, container: { display: 'flex', flexDirection: 'column', diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/Default.stories.tsx index 458ead434fee54..b515b6a7b24046 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/Default.stories.tsx @@ -3,9 +3,6 @@ import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ - root: { - maxHeight: '100vh', - }, child: { height: '100px', lineHeight: '100px', diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx index 53ebd299bdecf4..b21247f521166e 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -1,13 +1,9 @@ import * as React from 'react'; import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; -import { ThemeProvider } from '@fluentui/react'; import { useEffect } from '@storybook/addons'; const useStyles = makeStyles({ - root: { - maxHeight: '60vh', - }, child: { lineHeight: '42px', width: '100%', @@ -42,27 +38,25 @@ export const Default = () => { ); return ( - - - {(index: number) => { - const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; - return ( -
{`Node-${index} - size: ${arraySize.current[index]}`}
- ); - }} -
-
+ + {(index: number) => { + const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; + return ( +
{`Node-${index} - size: ${arraySize.current[index]}`}
+ ); + }} +
); }; From 3555dd8cf20531d740d2ec4c37e0fdde821341db Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 28 Mar 2023 14:24:27 -0700 Subject: [PATCH 08/26] Enable virtualizer render in dynamic scroll view --- .../renderVirtualizerScrollViewDynamic.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx index 3118e7879aea58..502362489f5115 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/renderVirtualizerScrollViewDynamic.tsx @@ -7,24 +7,12 @@ import { } from './VirtualizerScrollViewDynamic.types'; import { VirtualizerContextProvider, VirtualizerContextProps } from '../../Utilities'; import { ContextlessVirtualizerScrollViewDynamic } from './VirtualizerScrollViewDynamic'; +import { renderVirtualizer_unstable } from '../Virtualizer/renderVirtualizer'; export const renderContextlessVirtualizerScrollViewDynamic_unstable = (state: VirtualizerScrollViewDynamicState) => { const { slots, slotProps } = getSlots(state); - return ( - - {/* The 'before' bookend to hold items in place and detect scroll previous */} - - - - {/* The reduced list of non-virtualized children to be rendered */} - {state.virtualizedChildren} - {/* The 'after' bookend to hold items in place and detect scroll next */} - - - - - ); + return {renderVirtualizer_unstable(state)}; }; export const renderVirtualizerScrollViewDynamic_unstable = ( From 964595650cf91c47d6b7ff1e67030283574b12a4 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 28 Mar 2023 14:53:49 -0700 Subject: [PATCH 09/26] Fix comment --- .../react-virtualizer/src/hooks/useVirtualizerMeasure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 28d64c7ae162f9..9a35188dd0cce0 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -24,7 +24,7 @@ export const useStaticVirtualizerMeasure = ( const { virtualizerLength, bufferItems, bufferSize } = state; - // The ref the user sets on their scrollView - Defaults to document.body to ensure no null on init + // The ref the user sets on their scrollView const container = React.useRef(null); const resizeCallback = () => { From bde38772b2358f398eff01c238a66a022d2b4ebb Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 28 Mar 2023 15:31:02 -0700 Subject: [PATCH 10/26] Fix up storybook use effect import --- .../stories/VirtualizerScrollViewDynamic/Default.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx index b21247f521166e..df650095bd603d 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; import { makeStyles } from '@fluentui/react-components'; -import { useEffect } from '@storybook/addons'; +import { useEffect } from 'react'; const useStyles = makeStyles({ child: { From fb7811ae065cc47f9d61cc4a8678812805168876 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 11:25:01 -0700 Subject: [PATCH 11/26] Tidy up ref and fix numItems link --- .../useVirtualizerScrollViewDynamic.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 9cd91c0010300c..7072fc97cab37a 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { resolveShorthand } from '@fluentui/react-utilities'; +import { resolveShorthand, useMergedRefs } from '@fluentui/react-utilities'; import { useVirtualizer_unstable } from '../Virtualizer/useVirtualizer'; import { VirtualizerScrollViewDynamicProps, @@ -17,21 +17,10 @@ export function useVirtualizerScrollViewDynamic_unstable( direction: props.axis ?? 'vertical', getItemSize: props.getItemSize, currentIndex: contextIndex, - numItems: 0, + numItems: props.numItems, }); - const iScrollRef = React.useRef(null); - - const setScrollRef = React.useCallback( - (element: HTMLDivElement) => { - if (iScrollRef.current === element) { - return; - } - scrollRef(element); - iScrollRef.current = element; - }, - [scrollRef], - ); + const iScrollRef = useMergedRefs(React.useRef(null), scrollRef) as React.RefObject; const virtualizerState = useVirtualizer_unstable({ ...props, @@ -50,7 +39,7 @@ export function useVirtualizerScrollViewDynamic_unstable( container: resolveShorthand(props.container, { required: true, defaultProps: { - ref: setScrollRef, + ref: iScrollRef, }, }), }; From d4700aa9872f8a76714984c0d1dd727e3c943d69 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 13:01:10 -0700 Subject: [PATCH 12/26] Update dynamic sizing algo to ensure optimization prior to render (dynamic sizes may have changed) --- .../src/hooks/useVirtualizerMeasure.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 9a35188dd0cce0..7e581800e4ddba 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -197,6 +197,33 @@ export const useDynamicVirtualizerMeasure = ( [resizeObserver], ); + if (container.current) { + const containerSize = + direction === 'vertical' + ? container.current?.getBoundingClientRect().height + : container.current?.getBoundingClientRect().width; + + let couldBeSmaller = false; + let recheckTotal = 0; + for (let i = currentIndex; i < currentIndex + virtualizerLength; i++) { + const newItemSize = getItemSize(currentIndex + i); + sizeTracker.current[currentIndex + i] = newItemSize; + recheckTotal += newItemSize; + + const newLength = i - currentIndex; + const bufferItems = Math.max(Math.floor(newLength / 4), 2); + const checkLength = newLength + bufferItems * 2 + 1; + if (recheckTotal > containerSize && checkLength < virtualizerLength - 1) { + couldBeSmaller = true; + } + } + + // Check if the render has caused us to need a re-calc of virtualizer length + if (recheckTotal < containerSize || couldBeSmaller) { + handleResize(); + } + } + return { virtualizerLength, bufferItems: virtualizerBufferItems, From 5f7be10cd0a8ff3b79ecf31f06392f9069214198 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 13:29:53 -0700 Subject: [PATCH 13/26] Remove smaller check (for now) --- .../src/hooks/useVirtualizerMeasure.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 7e581800e4ddba..e780e0cc6b35d1 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -203,23 +203,15 @@ export const useDynamicVirtualizerMeasure = ( ? container.current?.getBoundingClientRect().height : container.current?.getBoundingClientRect().width; - let couldBeSmaller = false; let recheckTotal = 0; for (let i = currentIndex; i < currentIndex + virtualizerLength; i++) { const newItemSize = getItemSize(currentIndex + i); sizeTracker.current[currentIndex + i] = newItemSize; recheckTotal += newItemSize; - - const newLength = i - currentIndex; - const bufferItems = Math.max(Math.floor(newLength / 4), 2); - const checkLength = newLength + bufferItems * 2 + 1; - if (recheckTotal > containerSize && checkLength < virtualizerLength - 1) { - couldBeSmaller = true; - } } // Check if the render has caused us to need a re-calc of virtualizer length - if (recheckTotal < containerSize || couldBeSmaller) { + if (recheckTotal < containerSize) { handleResize(); } } From 765cf336b3a8e04ed92f1e2f350ab0eead82c4a9 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 14:10:56 -0700 Subject: [PATCH 14/26] Fix up smaller/larger dynamic size virtualizer length detection --- .../src/hooks/useVirtualizerMeasure.ts | 23 ++++++++++++++----- .../Default.stories.tsx | 9 ++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index e780e0cc6b35d1..3b836d7affcc53 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -166,7 +166,7 @@ export const useDynamicVirtualizerMeasure = ( */ const bufferSize = Math.max(Math.floor((length / 8) * defaultItemSize), 1); - const totalLength = length + bufferItems * 2 + 1; + const totalLength = length + bufferItems * 2 + 2; setState({ virtualizerLength: totalLength, @@ -200,18 +200,29 @@ export const useDynamicVirtualizerMeasure = ( if (container.current) { const containerSize = direction === 'vertical' - ? container.current?.getBoundingClientRect().height - : container.current?.getBoundingClientRect().width; + ? 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(currentIndex + i); - sizeTracker.current[currentIndex + i] = newItemSize; + const newItemSize = getItemSize(i); + sizeTracker.current[i] = newItemSize; recheckTotal += newItemSize; + + const newLength = i - currentIndex; + + const bufferItems = Math.max(Math.floor(newLength / 4), 2); + const totalNewLength = newLength + bufferItems * 2 + 3; + const compareLengths = totalNewLength < virtualizerLength; + + if (recheckTotal > containerSize && compareLengths && !couldBeSmaller) { + couldBeSmaller = true; + } } // Check if the render has caused us to need a re-calc of virtualizer length - if (recheckTotal < containerSize) { + if (recheckTotal < containerSize || couldBeSmaller) { handleResize(); } } diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx index df650095bd603d..878fea92caa0ed 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -23,7 +23,12 @@ export const Default = () => { useEffect(() => { let _totalSize = 0; for (let i = 0; i < childLength; i++) { - arraySize.current[i] = Math.random() * 250 + minHeight; + if (i < 100) { + // arraySize.current[i] = Math.random() * 150 + minHeight; + arraySize.current[i] = minHeight; + } else { + arraySize.current[i] = 420; + } _totalSize += arraySize.current[i]; } setTotalSize(_totalSize); @@ -40,7 +45,7 @@ export const Default = () => { return ( From dff5d76ccdbbe007cbffd3bd2d664c8fb5d1facf Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 14:13:28 -0700 Subject: [PATCH 15/26] Update demo to return randomization --- .../VirtualizerScrollViewDynamic/Default.stories.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx index 878fea92caa0ed..0b166fd70ca617 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/Default.stories.tsx @@ -23,12 +23,7 @@ export const Default = () => { useEffect(() => { let _totalSize = 0; for (let i = 0; i < childLength; i++) { - if (i < 100) { - // arraySize.current[i] = Math.random() * 150 + minHeight; - arraySize.current[i] = minHeight; - } else { - arraySize.current[i] = 420; - } + arraySize.current[i] = Math.random() * 150 + minHeight; _totalSize += arraySize.current[i]; } setTotalSize(_totalSize); From a157094ad30e78855c5f95b8ccd82fdd12129513 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 14:40:10 -0700 Subject: [PATCH 16/26] Dynamic function requires an additional item for buffer for safer measuring --- .../react-virtualizer/src/hooks/useVirtualizerMeasure.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 3b836d7affcc53..cdfe90cf14cacd 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -166,7 +166,7 @@ export const useDynamicVirtualizerMeasure = ( */ const bufferSize = Math.max(Math.floor((length / 8) * defaultItemSize), 1); - const totalLength = length + bufferItems * 2 + 2; + const totalLength = length + bufferItems * 2 + 3; setState({ virtualizerLength: totalLength, @@ -213,7 +213,7 @@ export const useDynamicVirtualizerMeasure = ( const newLength = i - currentIndex; const bufferItems = Math.max(Math.floor(newLength / 4), 2); - const totalNewLength = newLength + bufferItems * 2 + 3; + const totalNewLength = newLength + bufferItems * 2 + 4; const compareLengths = totalNewLength < virtualizerLength; if (recheckTotal > containerSize && compareLengths && !couldBeSmaller) { From 5ba9d3317880370c98e1d0801e5734b82675ecee Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Fri, 31 Mar 2023 14:30:21 -0700 Subject: [PATCH 17/26] Initial callback framework --- .../VirtualizerScrollView.types.ts | 5 ++ .../src/hooks/useImperativeScrolling.ts | 57 +++++++++++++++++++ .../src/hooks/useImperativeScrolling.types.ts | 16 ++++++ 3 files changed, 78 insertions(+) create mode 100644 packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts create mode 100644 packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/VirtualizerScrollView.types.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/VirtualizerScrollView.types.ts index 198789fef9c81d..1377f473d2bab5 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/VirtualizerScrollView.types.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/VirtualizerScrollView.types.ts @@ -5,6 +5,7 @@ import { VirtualizerConfigState, VirtualizerChildRenderFunction, } from '../Virtualizer/Virtualizer.types'; +import { VirtualizerScrollCallbacks } from '../../hooks/useImperativeScrolling.types'; export type VirtualizerScrollViewSlots = VirtualizerSlots & { /** @@ -31,6 +32,10 @@ export type VirtualizerScrollViewProps = ComponentProps & VirtualizerConfigState; diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts new file mode 100644 index 00000000000000..76ab11a569219f --- /dev/null +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -0,0 +1,57 @@ +import { ScrollToItemStatic, VirtualizerScrollCallbacks } from './useImperativeScrolling.types'; +import { RefObject, useRef } from 'react'; + +export const useImperativeScrolling = (): VirtualizerScrollCallbacks => { + // The virtualizer scroll container will override this function for the user to call + const scrollToItem = useRef<(index: number) => void>(); + // The user will override this function for the virtualizer scroll container to call + const didScrollToItem = useRef<(index: number) => void>(); + + return { + scrollToItem, + didScrollToItem, + }; +}; + +export const _scrollToItemStatic = (params: ScrollToItemStatic) => { + const { index, itemSize, totalItems, scrollView, axis = 'vertical', reversed = false, indexToNotify } = params; + + // We store the index in a ref for scrollView to handle once it is detected. + indexToNotify.current = index; + + if (axis === 'horizontal') { + if (reversed) { + scrollView.current?.scrollTo({ + left: totalItems * itemSize - itemSize * index, + behavior: 'smooth', + }); + } else { + scrollView.current?.scrollTo({ + left: itemSize * index, + behavior: 'smooth', + }); + } + } else { + if (reversed) { + scrollView.current?.scrollTo({ + top: totalItems * itemSize - itemSize * index, + behavior: 'smooth', + }); + } else { + scrollView.current?.scrollTo({ + top: itemSize * index, + behavior: 'smooth', + }); + } + } + + return; +}; + +export const _scrollToItemDynamic = ( + index: number, + progressiveItemSizes: number[], + scrollView: RefObject, +) => { + return; +}; diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts new file mode 100644 index 00000000000000..3ac4844eea0de7 --- /dev/null +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -0,0 +1,16 @@ +import { MutableRefObject, RefObject } from 'react'; + +export type VirtualizerScrollCallbacks = { + scrollToItem: MutableRefObject<((index: number) => void) | undefined>; + didScrollToItem: MutableRefObject<((index: number) => void) | undefined>; +}; + +export type ScrollToItemStatic = { + index: number; + itemSize: number; + totalItems: number; + scrollView: RefObject; + axis?: 'horizontal' | 'vertical'; + reversed?: boolean; + indexToNotify: MutableRefObject; +}; From f714bdafc4843f26eb5f90ea7e1fa0f2a473dbb4 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 10:35:53 -0700 Subject: [PATCH 18/26] Completed PoC for static --- .../Virtualizer/Virtualizer.types.ts | 22 ++++++++ .../components/Virtualizer/useVirtualizer.ts | 12 ++++ .../useVirtualizerScrollView.ts | 32 ++++++++++- .../src/hooks/useImperativeScrolling.ts | 17 +++--- .../src/hooks/useImperativeScrolling.types.ts | 3 +- .../ScrollTo.stories.tsx | 56 +++++++++++++++++++ .../VirtualizerScrollView/index.stories.ts | 1 + 7 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollView/ScrollTo.stories.tsx diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts index 0699effdd76293..392a193d649340 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts @@ -133,6 +133,28 @@ export type VirtualizerConfigProps = { * @param index - the index of the requested size's child */ getItemSize?: (index: number) => number; + + /** + * Callback for notifying when a flagged index has been rendered + */ + onRenderedFlaggedIndex?: (index: number) => number; + + /* + * Callback object to notify when a flagged index has been rendered + */ + flagIndex?: FlaggedIndexCallback; +}; + +export type FlaggedIndexCallback = { + /** + * Callback for notifying when a flagged index has been rendered + */ + onRenderedFlaggedIndex: (index: number) => number; + + /* + * Callback for notifying when a flagged index has been rendered + */ + flaggedIndex: React.MutableRefObject; }; export type VirtualizerProps = ComponentProps> & VirtualizerConfigProps; diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index a18a351e3a063f..ff116ed61c832b 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -21,6 +21,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta scrollViewRef, axis = 'vertical', reversed = false, + flagIndex, } = props; /* The context is optional, it's useful for injecting additional index logic, or performing uniform state updates*/ @@ -402,6 +403,17 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // eslint-disable-next-line react-hooks/exhaustive-deps }, [getItemSize]); + useEffect(() => { + if (!flagIndex || flagIndex.flaggedIndex.current === null) { + return; + } + const checkIndex = flagIndex.flaggedIndex.current; + if (actualIndex <= checkIndex && actualIndex + virtualizerLength >= checkIndex) { + flagIndex.flaggedIndex.current = null; + flagIndex?.onRenderedFlaggedIndex(checkIndex); + } + }, [actualIndex, flagIndex, virtualizerLength, virtualizerStartIndex]); + // Ensure we have run through and updated the whole size list array at least once. initializeSizeArray(); diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts index 331752a1376f31..2d78b76f1b492f 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts @@ -3,6 +3,8 @@ import { resolveShorthand, useMergedRefs } from '@fluentui/react-utilities'; import { useVirtualizer_unstable } from '../Virtualizer/useVirtualizer'; import { VirtualizerScrollViewProps, VirtualizerScrollViewState } from './VirtualizerScrollView.types'; import { useStaticVirtualizerMeasure } from '../../Hooks'; +import { FlaggedIndexCallback } from '../Virtualizer/Virtualizer.types'; +import { _scrollToItemStatic } from '../../hooks/useImperativeScrolling'; export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewProps): VirtualizerScrollViewState { const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useStaticVirtualizerMeasure({ @@ -10,7 +12,27 @@ export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewPr direction: props.axis ?? 'vertical', }); - const iScrollRef = useMergedRefs(React.useRef(null), scrollRef); + const iScrollRef = useMergedRefs(React.useRef(null), scrollRef) as React.RefObject; + + const scrollCallbackIndex = React.useRef(null); + + React.useEffect(() => { + const { itemSize, numItems, axis = 'vertical', reversed } = props; + if (props.scrollCallbacks) { + props.scrollCallbacks.scrollToItem.current = (index: number) => { + scrollCallbackIndex.current = index; + _scrollToItemStatic({ + indexRef: scrollCallbackIndex, + itemSize, + totalItems: numItems, + scrollView: iScrollRef, + axis, + reversed, + }); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [iScrollRef, props.scrollCallbacks]); const virtualizerState = useVirtualizer_unstable({ ...props, @@ -18,6 +40,12 @@ export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewPr bufferItems, bufferSize, scrollViewRef: iScrollRef, + flagIndex: props.scrollCallbacks + ? ({ + flaggedIndex: scrollCallbackIndex, + onRenderedFlaggedIndex: props.scrollCallbacks.didScrollToItem.current, + } as FlaggedIndexCallback) + : undefined, }); return { @@ -29,7 +57,7 @@ export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewPr container: resolveShorthand(props.container, { required: true, defaultProps: { - ref: iScrollRef as React.RefObject, + ref: iScrollRef, }, }), }; diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 76ab11a569219f..7a7729b8c577cc 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -14,32 +14,35 @@ export const useImperativeScrolling = (): VirtualizerScrollCallbacks => { }; export const _scrollToItemStatic = (params: ScrollToItemStatic) => { - const { index, itemSize, totalItems, scrollView, axis = 'vertical', reversed = false, indexToNotify } = params; + const { indexRef, itemSize, totalItems, scrollView, axis = 'vertical', reversed = false } = params; - // We store the index in a ref for scrollView to handle once it is detected. - indexToNotify.current = index; + if (indexRef.current === null) { + // null check - abort + return; + } + // We store the index in a ref for scrollView to handle once it is detected. if (axis === 'horizontal') { if (reversed) { scrollView.current?.scrollTo({ - left: totalItems * itemSize - itemSize * index, + left: totalItems * itemSize - itemSize * indexRef.current, behavior: 'smooth', }); } else { scrollView.current?.scrollTo({ - left: itemSize * index, + left: itemSize * indexRef.current, behavior: 'smooth', }); } } else { if (reversed) { scrollView.current?.scrollTo({ - top: totalItems * itemSize - itemSize * index, + top: totalItems * itemSize - itemSize * indexRef.current, behavior: 'smooth', }); } else { scrollView.current?.scrollTo({ - top: itemSize * index, + top: itemSize * indexRef.current, behavior: 'smooth', }); } diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts index 3ac4844eea0de7..5a310eddee4ac4 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -6,11 +6,10 @@ export type VirtualizerScrollCallbacks = { }; export type ScrollToItemStatic = { - index: number; + indexRef: MutableRefObject; itemSize: number; totalItems: number; scrollView: RefObject; axis?: 'horizontal' | 'vertical'; reversed?: boolean; - indexToNotify: MutableRefObject; }; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/ScrollTo.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/ScrollTo.stories.tsx new file mode 100644 index 00000000000000..736cb67e35dc8d --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/ScrollTo.stories.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { VirtualizerScrollView } from '@fluentui/react-components/unstable'; +import { makeStyles } from '@fluentui/react-components'; +import { useImperativeScrolling } from '../../src/hooks/useImperativeScrolling'; +import { Button } from '@fluentui/react-components'; +const useStyles = makeStyles({ + child: { + height: '100px', + lineHeight: '100px', + width: '100%', + }, +}); + +export const ScrollTo = () => { + const styles = useStyles(); + const childLength = 1000; + const scrollCallbacks = useImperativeScrolling(); + + const scrollToRandomIndex = () => { + const randomIndex = Math.floor(Math.random() * childLength); + if (scrollCallbacks.scrollToItem.current) { + console.log('Scrolling to random index: ', randomIndex); + scrollCallbacks.scrollToItem.current(randomIndex); + } + }; + + const reachedIndexCallback = (index: number) => { + console.log('Reached index: ', index); + }; + + scrollCallbacks.didScrollToItem.current = reachedIndexCallback; + + return ( +
+ + + {(index: number) => { + return ( +
{`Node-${index}`}
+ ); + }} +
+
+ ); +}; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/index.stories.ts b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/index.stories.ts index db5040009bd414..456f3e7ae733f8 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollView/index.stories.ts @@ -2,6 +2,7 @@ import { VirtualizerScrollView } from '../../src/VirtualizerScrollView'; import descriptionMd from './VirtualizerScrollViewDescription.md'; export { Default } from './Default.stories'; +export { ScrollTo } from './ScrollTo.stories'; export default { title: 'Preview Components/VirtualizerScrollView', From 99820ce96163a252c91fc5c5c06b0f56eef98c26 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 11:22:22 -0700 Subject: [PATCH 19/26] Stash dynamic changes --- .../VirtualizerScrollViewDynamic.types.ts | 6 ++ .../useVirtualizerScrollViewDynamic.ts | 28 +++++- .../src/hooks/useImperativeScrolling.ts | 73 +++++++++++++--- .../src/hooks/useImperativeScrolling.types.ts | 9 ++ .../src/hooks/useVirtualizerMeasure.ts | 13 ++- .../ScrollTo.stories.tsx | 86 +++++++++++++++++++ .../index.stories.ts | 1 + 7 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts index 0df4061f12ed7d..4305aa6ab79642 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/VirtualizerScrollViewDynamic.types.ts @@ -6,6 +6,7 @@ import { } from '../Virtualizer/Virtualizer.types'; import { VirtualizerScrollViewSlots } from '../VirtualizerScrollView/VirtualizerScrollView.types'; +import { VirtualizerScrollCallbacks } from '../../hooks/useImperativeScrolling.types'; export type VirtualizerScrollViewDynamicSlots = VirtualizerScrollViewSlots; @@ -32,6 +33,11 @@ export type VirtualizerScrollViewDynamicProps = ComponentProps & diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 7072fc97cab37a..09e1275dde2517 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -7,12 +7,13 @@ import { } from './VirtualizerScrollViewDynamic.types'; import { useDynamicVirtualizerMeasure } from '../../Hooks'; import { useVirtualizerContext } from '../../Utilities'; +import { _scrollToItemDynamic } from '../../hooks/useImperativeScrolling'; export function useVirtualizerScrollViewDynamic_unstable( props: VirtualizerScrollViewDynamicProps, ): VirtualizerScrollViewDynamicState { const { contextIndex } = useVirtualizerContext(); - const { virtualizerLength, bufferItems, bufferSize, scrollRef } = useDynamicVirtualizerMeasure({ + const { virtualizerLength, bufferItems, bufferSize, scrollRef, sizingArray } = useDynamicVirtualizerMeasure({ defaultItemSize: props.itemSize, direction: props.axis ?? 'vertical', getItemSize: props.getItemSize, @@ -20,8 +21,33 @@ export function useVirtualizerScrollViewDynamic_unstable( numItems: props.numItems, }); + console.log('Sizing array:', sizingArray); + const iScrollRef = useMergedRefs(React.useRef(null), scrollRef) as React.RefObject; + const scrollCallbackIndex = React.useRef(null); + + React.useEffect(() => { + console.log('Setting value'); + const { axis = 'vertical', reversed } = props; + if (props.scrollCallbacks) { + console.log('Setting value - 2'); + props.scrollCallbacks.scrollToItem.current = (index: number) => { + console.log('Calling value'); + scrollCallbackIndex.current = index; + _scrollToItemDynamic({ + indexRef: scrollCallbackIndex, + currentIndex: contextIndex, + itemSizes: sizingArray, + scrollView: iScrollRef, + axis, + reversed, + }); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [iScrollRef, props.scrollCallbacks]); + const virtualizerState = useVirtualizer_unstable({ ...props, virtualizerLength, diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 7a7729b8c577cc..1d58eec7f9b982 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -1,5 +1,5 @@ -import { ScrollToItemStatic, VirtualizerScrollCallbacks } from './useImperativeScrolling.types'; -import { RefObject, useRef } from 'react'; +import { ScrollToItemDynamic, ScrollToItemStatic, VirtualizerScrollCallbacks } from './useImperativeScrolling.types'; +import { useRef } from 'react'; export const useImperativeScrolling = (): VirtualizerScrollCallbacks => { // The virtualizer scroll container will override this function for the user to call @@ -26,35 +26,80 @@ export const _scrollToItemStatic = (params: ScrollToItemStatic) => { if (reversed) { scrollView.current?.scrollTo({ left: totalItems * itemSize - itemSize * indexRef.current, - behavior: 'smooth', + behavior: 'auto', }); } else { scrollView.current?.scrollTo({ left: itemSize * indexRef.current, - behavior: 'smooth', + behavior: 'auto', }); } } else { if (reversed) { scrollView.current?.scrollTo({ top: totalItems * itemSize - itemSize * indexRef.current, - behavior: 'smooth', + behavior: 'auto', }); } else { scrollView.current?.scrollTo({ top: itemSize * indexRef.current, - behavior: 'smooth', + behavior: 'auto', }); } } - - return; }; -export const _scrollToItemDynamic = ( - index: number, - progressiveItemSizes: number[], - scrollView: RefObject, -) => { - return; +export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { + const { currentIndex, indexRef, itemSizes, scrollView, axis = 'vertical', reversed = false } = params; + + if (!itemSizes.current) { + return; + } + console.log('Current index:', currentIndex); + + if (indexRef.current === null || itemSizes.current === null || itemSizes.current.length < indexRef.current) { + // null check - abort + console.log('ABORTING'); + console.log('indexRef.current:', indexRef.current); + console.log('itemSizes.current:', itemSizes.current); + console.log('itemSizes.current.length:', itemSizes.current.length); + return; + } + + let itemDepth = 0; + let totalSize = 0; + const trackTill = reversed ? itemSizes.current.length : indexRef.current; + for (let i = 0; i < trackTill; i++) { + if (i < indexRef.current) { + itemDepth += itemSizes.current[i]; + } + totalSize += itemSizes.current[i]; + } + + // We store the index in a ref for scrollView to handle once it is detected. + if (axis === 'horizontal') { + if (reversed) { + scrollView.current?.scrollTo({ + left: totalSize - itemDepth, + behavior: 'auto', + }); + } else { + scrollView.current?.scrollTo({ + left: itemDepth, + behavior: 'auto', + }); + } + } else { + if (reversed) { + scrollView.current?.scrollTo({ + top: totalSize - itemDepth, + behavior: 'auto', + }); + } else { + scrollView.current?.scrollTo({ + top: itemDepth, + behavior: 'auto', + }); + } + } }; diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts index 5a310eddee4ac4..8577fe126dc023 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -13,3 +13,12 @@ export type ScrollToItemStatic = { axis?: 'horizontal' | 'vertical'; reversed?: boolean; }; + +export type ScrollToItemDynamic = { + currentIndex: number; + indexRef: MutableRefObject; + itemSizes: RefObject; + scrollView: RefObject; + axis?: 'horizontal' | 'vertical'; + reversed?: boolean; +}; diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index cdfe90cf14cacd..2df85061eb8744 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -108,17 +108,26 @@ export const useDynamicVirtualizerMeasure = ( bufferItems: number; bufferSize: number; scrollRef: (instance: HTMLElement | HTMLDivElement | null) => void; + sizingArray: React.RefObject; } => { const { defaultItemSize, direction = 'vertical', numItems, getItemSize, currentIndex } = virtualizerProps; const sizeTracker = React.useRef(new Array(numItems).fill(defaultItemSize)); + console.log('sizeTracker.current:', sizeTracker.current); + console.log('numItems:', numItems); + console.log('sizeTracker.current.length:', sizeTracker.current.length); if (sizeTracker.current.length !== numItems) { + console.log('Size tracker issue?'); // numItems changed, morph array - keep previously explored values. const newItems = numItems - sizeTracker.current.length; if (newItems > 0) { + console.log('Size tracker issue - 2?'); sizeTracker.current = [...sizeTracker.current, ...Array(newItems).fill(defaultItemSize)]; - } else { + } else if (numItems > 0) { + console.log('Size tracker issue - 3?'); + console.log('numItems:', numItems); + console.log('newItems:', newItems); sizeTracker.current.splice(numItems, newItems * -1); } } @@ -155,6 +164,7 @@ export const useDynamicVirtualizerMeasure = ( indexSizer += iItemSize; length++; } + console.log('sizeTracker.current:', sizeTracker.current); /* * Number of items to append at each end, i.e. 'preload' each side before entering view. @@ -232,5 +242,6 @@ export const useDynamicVirtualizerMeasure = ( bufferItems: virtualizerBufferItems, bufferSize: virtualizerBufferSize, scrollRef, + sizingArray: sizeTracker, }; }; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx new file mode 100644 index 00000000000000..b2252cc438a5cc --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { Button, makeStyles } from '@fluentui/react-components'; +import { useEffect } from 'react'; +import { useImperativeScrolling } from '../../src/hooks/useImperativeScrolling'; + +const useStyles = makeStyles({ + child: { + lineHeight: '42px', + width: '100%', + minHeight: '42px', + }, +}); + +export const ScrollTo = () => { + const styles = useStyles(); + const childLength = 1000; + const minHeight = 42; + // 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 + const [totalSize, setTotalSize] = React.useState(minHeight * childLength); + + useEffect(() => { + let _totalSize = 0; + for (let i = 0; i < childLength; i++) { + arraySize.current[i] = Math.random() * 250 + minHeight; + _totalSize += arraySize.current[i]; + } + setTotalSize(_totalSize); + }, []); + + const getItemSizeCallback = React.useCallback( + (index: number) => { + return arraySize.current[index]; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [arraySize, totalSize], + ); + + /* Imperative scrolling hooks */ + + const scrollCallbacks = useImperativeScrolling(); + + const scrollToRandomIndex = () => { + const randomIndex = Math.floor(Math.random() * childLength); + console.log('Scrolling to random index - 1: ', randomIndex); + if (scrollCallbacks.scrollToItem.current) { + console.log('Scrolling to random index - 2: ', randomIndex); + scrollCallbacks.scrollToItem.current(randomIndex); + } + }; + + const reachedIndexCallback = (index: number) => { + console.log('Reached index: ', index); + }; + + scrollCallbacks.didScrollToItem.current = reachedIndexCallback; + + return ( +
+ + + {(index: number) => { + const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; + return ( +
{`Node-${index} - size: ${arraySize.current[index]}`}
+ ); + }} +
+
+ ); +}; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts index ce2edca660abcd..62dc80d9d2978a 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts @@ -2,6 +2,7 @@ import { VirtualizerScrollViewDynamic } from '../../src/VirtualizerScrollViewDyn import descriptionMd from './VirtualizerScrollViewDynamicDescription.md'; export { Default } from './Default.stories'; +export { ScrollTo } from './ScrollTo.stories'; export default { title: 'Preview Components/VirtualizerScrollViewDynamic', From 42344cae11331331edd626985a09bf351f78638c Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 15:25:54 -0700 Subject: [PATCH 20/26] Ensure dynamic can track most up to date internal sizing --- .../src/components/Virtualizer/useVirtualizer.ts | 5 +++++ .../useVirtualizerScrollViewDynamic.ts | 16 +++++++++------- .../src/hooks/useImperativeScrolling.ts | 6 ------ .../src/hooks/useImperativeScrolling.types.ts | 1 + .../src/hooks/useVirtualizerMeasure.ts | 9 --------- .../src/hooks/useVirtualizerMeasure.types.ts | 2 ++ .../VirtualizerContext/VirtualizerContext.ts | 3 ++- .../src/utilities/VirtualizerContext/types.ts | 3 +++ .../ScrollTo.stories.tsx | 3 +-- 9 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index ff116ed61c832b..a942ac7a9aee86 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -75,6 +75,11 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta childProgressiveSizes.current[index] = childProgressiveSizes.current[index - 1] + childSizes.current[index]; } } + + if (virtualizerContext.currentChildSizes) { + // We keep an up to date context reference for external hooks + virtualizerContext.currentChildSizes.current = childSizes.current; + } }; const batchUpdateNewIndex = (index: number) => { diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 09e1275dde2517..72b7f63e82e552 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -8,11 +8,12 @@ import { import { useDynamicVirtualizerMeasure } from '../../Hooks'; import { useVirtualizerContext } from '../../Utilities'; import { _scrollToItemDynamic } from '../../hooks/useImperativeScrolling'; +import { FlaggedIndexCallback } from '../Virtualizer/Virtualizer.types'; export function useVirtualizerScrollViewDynamic_unstable( props: VirtualizerScrollViewDynamicProps, ): VirtualizerScrollViewDynamicState { - const { contextIndex } = useVirtualizerContext(); + const { contextIndex, currentChildSizes } = useVirtualizerContext(); const { virtualizerLength, bufferItems, bufferSize, scrollRef, sizingArray } = useDynamicVirtualizerMeasure({ defaultItemSize: props.itemSize, direction: props.axis ?? 'vertical', @@ -21,24 +22,19 @@ export function useVirtualizerScrollViewDynamic_unstable( numItems: props.numItems, }); - console.log('Sizing array:', sizingArray); - const iScrollRef = useMergedRefs(React.useRef(null), scrollRef) as React.RefObject; const scrollCallbackIndex = React.useRef(null); React.useEffect(() => { - console.log('Setting value'); const { axis = 'vertical', reversed } = props; if (props.scrollCallbacks) { - console.log('Setting value - 2'); props.scrollCallbacks.scrollToItem.current = (index: number) => { - console.log('Calling value'); scrollCallbackIndex.current = index; _scrollToItemDynamic({ indexRef: scrollCallbackIndex, currentIndex: contextIndex, - itemSizes: sizingArray, + itemSizes: currentChildSizes ?? sizingArray, scrollView: iScrollRef, axis, reversed, @@ -54,6 +50,12 @@ export function useVirtualizerScrollViewDynamic_unstable( bufferItems, bufferSize, scrollViewRef: iScrollRef, + flagIndex: props.scrollCallbacks + ? ({ + flaggedIndex: scrollCallbackIndex, + onRenderedFlaggedIndex: props.scrollCallbacks.didScrollToItem.current, + } as FlaggedIndexCallback) + : undefined, }); return { diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 1d58eec7f9b982..1bb4ee76e9e6c6 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -51,18 +51,12 @@ export const _scrollToItemStatic = (params: ScrollToItemStatic) => { export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { const { currentIndex, indexRef, itemSizes, scrollView, axis = 'vertical', reversed = false } = params; - if (!itemSizes.current) { return; } - console.log('Current index:', currentIndex); if (indexRef.current === null || itemSizes.current === null || itemSizes.current.length < indexRef.current) { // null check - abort - console.log('ABORTING'); - console.log('indexRef.current:', indexRef.current); - console.log('itemSizes.current:', itemSizes.current); - console.log('itemSizes.current.length:', itemSizes.current.length); return; } diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts index 8577fe126dc023..91ecdcf192d78b 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -21,4 +21,5 @@ export type ScrollToItemDynamic = { scrollView: RefObject; axis?: 'horizontal' | 'vertical'; reversed?: boolean; + currentChildSizes: MutableRefObject; }; diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 2df85061eb8744..1e77ff8c7c2da4 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -114,20 +114,12 @@ export const useDynamicVirtualizerMeasure = ( const sizeTracker = React.useRef(new Array(numItems).fill(defaultItemSize)); - console.log('sizeTracker.current:', sizeTracker.current); - console.log('numItems:', numItems); - console.log('sizeTracker.current.length:', sizeTracker.current.length); if (sizeTracker.current.length !== numItems) { - console.log('Size tracker issue?'); // numItems changed, morph array - keep previously explored values. const newItems = numItems - sizeTracker.current.length; if (newItems > 0) { - console.log('Size tracker issue - 2?'); sizeTracker.current = [...sizeTracker.current, ...Array(newItems).fill(defaultItemSize)]; } else if (numItems > 0) { - console.log('Size tracker issue - 3?'); - console.log('numItems:', numItems); - console.log('newItems:', newItems); sizeTracker.current.splice(numItems, newItems * -1); } } @@ -164,7 +156,6 @@ export const useDynamicVirtualizerMeasure = ( indexSizer += iItemSize; length++; } - console.log('sizeTracker.current:', sizeTracker.current); /* * Number of items to append at each end, i.e. 'preload' each side before entering view. diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts index eab7ad32e7860e..8e468be400110c 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.types.ts @@ -1,3 +1,5 @@ +import { MutableRefObject } from 'react'; + export type VirtualizerMeasureProps = { defaultItemSize: number; direction?: 'vertical' | 'horizontal'; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts index 7b114695ce809b..3530cd34831145 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -13,5 +13,6 @@ export const useVirtualizerContext = () => { export const useVirtualizerContextState = (): VirtualizerContextProps => { const [contextIndex, setContextIndex] = React.useState(0); - return { contextIndex, setContextIndex }; + const currentChildSizes = React.useRef([]); + return { contextIndex, setContextIndex, currentChildSizes }; }; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts index 408d3773f4228e..410be428b785ba 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts @@ -1,7 +1,10 @@ +import { MutableRefObject } from 'react'; + /** * {@docCategory Virtualizer} */ export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; + currentChildSizes?: MutableRefObject; }; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx index b2252cc438a5cc..78916e6fa5a04e 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollTo.stories.tsx @@ -44,9 +44,8 @@ export const ScrollTo = () => { const scrollToRandomIndex = () => { const randomIndex = Math.floor(Math.random() * childLength); - console.log('Scrolling to random index - 1: ', randomIndex); if (scrollCallbacks.scrollToItem.current) { - console.log('Scrolling to random index - 2: ', randomIndex); + console.log('Scrolling to random index: ', randomIndex); scrollCallbacks.scrollToItem.current(randomIndex); } }; From 92c4acf9b427c63f7ea2932a22dbc1c3f01e6236 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 15:31:38 -0700 Subject: [PATCH 21/26] Remove content index (for now) --- .../useVirtualizerScrollViewDynamic.ts | 1 - .../src/hooks/useImperativeScrolling.ts | 10 +++++----- .../src/hooks/useImperativeScrolling.types.ts | 2 -- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 72b7f63e82e552..730576df982e7f 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -33,7 +33,6 @@ export function useVirtualizerScrollViewDynamic_unstable( scrollCallbackIndex.current = index; _scrollToItemDynamic({ indexRef: scrollCallbackIndex, - currentIndex: contextIndex, itemSizes: currentChildSizes ?? sizingArray, scrollView: iScrollRef, axis, diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 1bb4ee76e9e6c6..559b881691129c 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -50,7 +50,7 @@ export const _scrollToItemStatic = (params: ScrollToItemStatic) => { }; export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { - const { currentIndex, indexRef, itemSizes, scrollView, axis = 'vertical', reversed = false } = params; + const { indexRef, itemSizes, scrollView, axis = 'vertical', reversed = false } = params; if (!itemSizes.current) { return; } @@ -75,24 +75,24 @@ export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { if (reversed) { scrollView.current?.scrollTo({ left: totalSize - itemDepth, - behavior: 'auto', + behavior: 'smooth', }); } else { scrollView.current?.scrollTo({ left: itemDepth, - behavior: 'auto', + behavior: 'smooth', }); } } else { if (reversed) { scrollView.current?.scrollTo({ top: totalSize - itemDepth, - behavior: 'auto', + behavior: 'smooth', }); } else { scrollView.current?.scrollTo({ top: itemDepth, - behavior: 'auto', + behavior: 'smooth', }); } } diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts index 91ecdcf192d78b..015cdc2afb6bc0 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -15,11 +15,9 @@ export type ScrollToItemStatic = { }; export type ScrollToItemDynamic = { - currentIndex: number; indexRef: MutableRefObject; itemSizes: RefObject; scrollView: RefObject; axis?: 'horizontal' | 'vertical'; reversed?: boolean; - currentChildSizes: MutableRefObject; }; From c67a009fb3033494f1015eeb06370e3628d523d5 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 16:06:15 -0700 Subject: [PATCH 22/26] remove comment --- .../react-virtualizer/src/hooks/useImperativeScrolling.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 559b881691129c..1dbc9793d780e1 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -21,7 +21,6 @@ export const _scrollToItemStatic = (params: ScrollToItemStatic) => { return; } - // We store the index in a ref for scrollView to handle once it is detected. if (axis === 'horizontal') { if (reversed) { scrollView.current?.scrollTo({ @@ -70,7 +69,6 @@ export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { totalSize += itemSizes.current[i]; } - // We store the index in a ref for scrollView to handle once it is detected. if (axis === 'horizontal') { if (reversed) { scrollView.current?.scrollTo({ From b34623ca50ca0966af3663c7922cfe4f56fb8272 Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Mon, 3 Apr 2023 16:45:01 -0700 Subject: [PATCH 23/26] We are gonna have to rely on our pre-calcs via context to be fully accurate --- .../src/components/Virtualizer/useVirtualizer.ts | 10 +++++++++- .../useVirtualizerScrollViewDynamic.ts | 3 ++- .../src/hooks/useImperativeScrolling.ts | 7 ++----- .../src/hooks/useImperativeScrolling.types.ts | 1 + .../utilities/VirtualizerContext/VirtualizerContext.ts | 3 ++- .../src/utilities/VirtualizerContext/types.ts | 1 + 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index a942ac7a9aee86..2c83b43091d444 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -244,9 +244,17 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta const calculateTotalSize = () => { if (!getItemSize) { - return itemSize * numItems; + const size = itemSize * numItems; + if (virtualizerContext?.totalSize) { + virtualizerContext.totalSize.current = size; + } + return size; } + // Ensure context has access to totalSize for any reverse calculations + if (virtualizerContext?.totalSize) { + virtualizerContext.totalSize.current = childProgressiveSizes.current[numItems - 1]; + } // Time for custom size calcs return childProgressiveSizes.current[numItems - 1]; }; diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 730576df982e7f..0996c6a9880699 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -13,7 +13,7 @@ import { FlaggedIndexCallback } from '../Virtualizer/Virtualizer.types'; export function useVirtualizerScrollViewDynamic_unstable( props: VirtualizerScrollViewDynamicProps, ): VirtualizerScrollViewDynamicState { - const { contextIndex, currentChildSizes } = useVirtualizerContext(); + const { contextIndex, currentChildSizes, totalSize } = useVirtualizerContext(); const { virtualizerLength, bufferItems, bufferSize, scrollRef, sizingArray } = useDynamicVirtualizerMeasure({ defaultItemSize: props.itemSize, direction: props.axis ?? 'vertical', @@ -34,6 +34,7 @@ export function useVirtualizerScrollViewDynamic_unstable( _scrollToItemDynamic({ indexRef: scrollCallbackIndex, itemSizes: currentChildSizes ?? sizingArray, + totalSize, scrollView: iScrollRef, axis, reversed, diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts index 1dbc9793d780e1..4f2812fbc42a28 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.ts @@ -49,7 +49,7 @@ export const _scrollToItemStatic = (params: ScrollToItemStatic) => { }; export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { - const { indexRef, itemSizes, scrollView, axis = 'vertical', reversed = false } = params; + const { indexRef, itemSizes, totalSize, scrollView, axis = 'vertical', reversed = false } = params; if (!itemSizes.current) { return; } @@ -60,13 +60,10 @@ export const _scrollToItemDynamic = (params: ScrollToItemDynamic) => { } let itemDepth = 0; - let totalSize = 0; - const trackTill = reversed ? itemSizes.current.length : indexRef.current; - for (let i = 0; i < trackTill; i++) { + for (let i = 0; i < indexRef.current; i++) { if (i < indexRef.current) { itemDepth += itemSizes.current[i]; } - totalSize += itemSizes.current[i]; } if (axis === 'horizontal') { diff --git a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts index 015cdc2afb6bc0..521e4ca6414adb 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useImperativeScrolling.types.ts @@ -17,6 +17,7 @@ export type ScrollToItemStatic = { export type ScrollToItemDynamic = { indexRef: MutableRefObject; itemSizes: RefObject; + totalSize: number; scrollView: RefObject; axis?: 'horizontal' | 'vertical'; reversed?: boolean; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts index 3530cd34831145..5736d8f7739408 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -14,5 +14,6 @@ export const useVirtualizerContext = () => { export const useVirtualizerContextState = (): VirtualizerContextProps => { const [contextIndex, setContextIndex] = React.useState(0); const currentChildSizes = React.useRef([]); - return { contextIndex, setContextIndex, currentChildSizes }; + const totalSize = React.useRef(0); + return { contextIndex, setContextIndex, currentChildSizes, totalSize }; }; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts index 410be428b785ba..3b5300943b0d3b 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts @@ -7,4 +7,5 @@ export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; currentChildSizes?: MutableRefObject; + totalSize?: MutableRefObject; }; From 280463048e817178528c2a9d9880e2a0f2a7d3ad Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 4 Apr 2023 09:12:48 -0700 Subject: [PATCH 24/26] unneeded dep --- .../src/components/Virtualizer/useVirtualizer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index 2c83b43091d444..b173e60105b401 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -425,7 +425,7 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta flagIndex.flaggedIndex.current = null; flagIndex?.onRenderedFlaggedIndex(checkIndex); } - }, [actualIndex, flagIndex, virtualizerLength, virtualizerStartIndex]); + }, [actualIndex, flagIndex, virtualizerLength]); // Ensure we have run through and updated the whole size list array at least once. initializeSizeArray(); From b25bcae657b27afe5473b99da22a4b7cce1cc89f Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 4 Apr 2023 09:41:45 -0700 Subject: [PATCH 25/26] Add optional 'isScrolling' flag in case users want to render placeholders --- .../Virtualizer/Virtualizer.types.ts | 2 +- .../Virtualizer/renderVirtualizer.tsx | 2 +- .../components/Virtualizer/useVirtualizer.ts | 37 ++++++++++- .../ScrollLoading.stories.tsx | 64 +++++++++++++++++++ .../index.stories.ts | 1 + 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts index 392a193d649340..3217d39f56d4f7 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/Virtualizer.types.ts @@ -61,7 +61,7 @@ export type VirtualizerState = ComponentState & VirtualizerCon // 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? -export type VirtualizerChildRenderFunction = (index: number) => React.ReactNode; +export type VirtualizerChildRenderFunction = (index: number, isScrolling?: boolean) => React.ReactNode; export type VirtualizerConfigProps = { /** diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx b/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx index b3fcd78507a753..b1dac3fe1b452c 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/renderVirtualizer.tsx @@ -22,7 +22,7 @@ export const renderVirtualizer_unstable = (state: VirtualizerState) => { ); }; -export const renderVirtualizerChildPlaceholder = (child: ReactNode, index: number) => { +export const renderVirtualizerChildPlaceholder = (child: ReactNode, index: number, isScrolling?: boolean) => { return ( {child} diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index b173e60105b401..ce1b25457ebe32 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -82,6 +82,35 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta } }; + const [isScrolling, setIsScrolling] = useState(false); + const scrollTimer = useRef | null>(); + const scrollCounter = useRef(0); + + const initializeScrollingTimer = () => { + /* + * This can be considered the 'velocity' required to start scroll + * - Maybe we should let users pass these in. + */ + const INIT_SCROLL_FLAG_REQ = 10; + const INIT_SCROLL_FLAG_DELAY = 100; + + scrollCounter.current++; + if (scrollCounter.current >= INIT_SCROLL_FLAG_REQ) { + setIsScrolling(true); + } + if (scrollTimer.current) { + clearTimeout(scrollTimer.current); + } + scrollTimer.current = setTimeout(() => { + setIsScrolling(false); + scrollCounter.current = 0; + }, INIT_SCROLL_FLAG_DELAY); + }; + + useEffect(() => { + initializeScrollingTimer(); + }, [actualIndex]); + const batchUpdateNewIndex = (index: number) => { // Local updates updateChildRows(index); @@ -306,10 +335,14 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta const end = Math.min(_actualIndex + virtualizerLength, numItems); for (let i = _actualIndex; i < end; i++) { - childArray.current[i - _actualIndex] = renderVirtualizerChildPlaceholder(renderChild(i), i); + childArray.current[i - _actualIndex] = renderVirtualizerChildPlaceholder( + renderChild(i, isScrolling), + i, + isScrolling, + ); } }, - [numItems, renderChild, virtualizerLength], + [numItems, renderChild, virtualizerLength, isScrolling], ); const setBeforeRef = useCallback( diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx new file mode 100644 index 00000000000000..16ed658f185d55 --- /dev/null +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/ScrollLoading.stories.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { VirtualizerScrollViewDynamic } from '@fluentui/react-components/unstable'; +import { makeStyles } from '@fluentui/react-components'; +import { useEffect } from 'react'; + +const useStyles = makeStyles({ + child: { + lineHeight: '42px', + width: '100%', + minHeight: '42px', + }, +}); + +export const ScrollLoading = () => { + const styles = useStyles(); + const childLength = 1000; + const minHeight = 42; + // 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 + const [totalSize, setTotalSize] = React.useState(minHeight * childLength); + + useEffect(() => { + let _totalSize = 0; + for (let i = 0; i < childLength; i++) { + arraySize.current[i] = Math.random() * 150 + minHeight; + _totalSize += arraySize.current[i]; + } + setTotalSize(_totalSize); + }, []); + + const getItemSizeCallback = React.useCallback( + (index: number) => { + return arraySize.current[index]; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [arraySize, totalSize], + ); + + return ( + + {(index: number, isScrolling = false) => { + const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; + return isScrolling ? ( +
LOADING
+ ) : ( +
{`Node-${index} - size: ${arraySize.current[index]}`}
+ ); + }} +
+ ); +}; diff --git a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts index 62dc80d9d2978a..cf5ac78e1cf052 100644 --- a/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts +++ b/packages/react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts @@ -3,6 +3,7 @@ import descriptionMd from './VirtualizerScrollViewDynamicDescription.md'; export { Default } from './Default.stories'; export { ScrollTo } from './ScrollTo.stories'; +export { ScrollLoading } from './ScrollLoading.stories'; export default { title: 'Preview Components/VirtualizerScrollViewDynamic', From 1038aa3be7649b924bdc929eab34a69dee7676ef Mon Sep 17 00:00:00 2001 From: Mitch-At-Work Date: Tue, 4 Apr 2023 15:58:59 -0700 Subject: [PATCH 26/26] Lets expose progressive sizes also so that users can inject any async dynamic updates --- .../src/components/Virtualizer/useVirtualizer.ts | 7 ++++++- .../useVirtualizerScrollViewDynamic.ts | 3 +-- .../VirtualizerContext/VirtualizerContext.ts | 3 ++- .../src/utilities/VirtualizerContext/types.ts | 14 +++++++++++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index ce1b25457ebe32..4bd9edc03a2697 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -76,10 +76,15 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta } } + /* + * We keep an up to date context reference for external hooks or user injected changes + */ if (virtualizerContext.currentChildSizes) { - // We keep an up to date context reference for external hooks virtualizerContext.currentChildSizes.current = childSizes.current; } + if (virtualizerContext.progressiveChildSizes) { + virtualizerContext.progressiveChildSizes.current = childProgressiveSizes.current; + } }; const [isScrolling, setIsScrolling] = useState(false); diff --git a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts index 0996c6a9880699..4f496ed9ddaed5 100644 --- a/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts +++ b/packages/react-components/react-virtualizer/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.ts @@ -23,7 +23,6 @@ export function useVirtualizerScrollViewDynamic_unstable( }); const iScrollRef = useMergedRefs(React.useRef(null), scrollRef) as React.RefObject; - const scrollCallbackIndex = React.useRef(null); React.useEffect(() => { @@ -34,7 +33,7 @@ export function useVirtualizerScrollViewDynamic_unstable( _scrollToItemDynamic({ indexRef: scrollCallbackIndex, itemSizes: currentChildSizes ?? sizingArray, - totalSize, + totalSize: totalSize?.current ?? 0, scrollView: iScrollRef, axis, reversed, diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts index 5736d8f7739408..90b39a6ab4687e 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/VirtualizerContext.ts @@ -14,6 +14,7 @@ export const useVirtualizerContext = () => { export const useVirtualizerContextState = (): VirtualizerContextProps => { const [contextIndex, setContextIndex] = React.useState(0); const currentChildSizes = React.useRef([]); + const progressiveChildSizes = React.useRef([]); const totalSize = React.useRef(0); - return { contextIndex, setContextIndex, currentChildSizes, totalSize }; + return { contextIndex, setContextIndex, currentChildSizes, totalSize, progressiveChildSizes }; }; diff --git a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts index 3b5300943b0d3b..d726e3119c4653 100644 --- a/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts +++ b/packages/react-components/react-virtualizer/src/utilities/VirtualizerContext/types.ts @@ -6,6 +6,18 @@ import { MutableRefObject } from 'react'; export type VirtualizerContextProps = { contextIndex: number; setContextIndex: (index: number) => void; - currentChildSizes?: MutableRefObject; totalSize?: MutableRefObject; + + /* + * Virtualizer's current item sizes - external hooks can track/update. + * Users can update sizing at any time to handle dynamic updates. + */ + currentChildSizes?: MutableRefObject; + /* + * Virtualizer's current progressive sizes. + * + * Updating these in-sync instead of waiting for Virtualizer to catch a mismatch + * will ensure accuracy from any external currentChildSizes updates. + */ + progressiveChildSizes?: MutableRefObject; };