forked from microsoft/fluentui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Virtualizer - feat: Auto-measurement (microsoft#29868)
* Store first concept of auto-measure * Add back in suspense * hook up sizing * Connect end to end * Add key and direct ref call * change file * Remove react.suspense from default as it hurts our reference measure * Update API * lint and update for parser * remove total length * Update change and use average size * Only access bounding box once per call * Address comments
- Loading branch information
1 parent
f72acbf
commit 4dddcd6
Showing
9 changed files
with
255 additions
and
5 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-virtualizer-40268eff-578b-4dfe-8ddc-046946884b0e.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "prerelease", | ||
"comment": "feat: Add default auto-measuring on dynamic virtualizezr if no sizing function provided", | ||
"packageName": "@fluentui/react-virtualizer", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
packages/react-components/react-virtualizer/src/hooks/useMeasureList.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import * as React from 'react'; | ||
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; | ||
|
||
export interface IndexedResizeCallbackElement { | ||
handleResize: () => void; | ||
} | ||
/** | ||
* Provides a way of automating size in the virtualizer | ||
* Returns | ||
* `width` - element width ref (0 by default), | ||
* `height` - element height ref (0 by default), | ||
* `measureElementRef` - a ref function to be passed as `ref` to the element you want to measure | ||
*/ | ||
export function useMeasureList< | ||
TElement extends HTMLElement & IndexedResizeCallbackElement = HTMLElement & IndexedResizeCallbackElement, | ||
>(currentIndex: number, refLength: number, totalLength: number, defaultItemSize: number) { | ||
const widthArray = React.useRef(new Array(totalLength).fill(defaultItemSize)); | ||
const heightArray = React.useRef(new Array(totalLength).fill(defaultItemSize)); | ||
|
||
const refArray = React.useRef<Array<TElement | undefined | null>>([]); | ||
const { targetDocument } = useFluent(); | ||
|
||
// the handler for resize observer | ||
const handleIndexUpdate = React.useCallback( | ||
(index: number) => { | ||
const boundClientRect = refArray.current[index]?.getBoundingClientRect(); | ||
const containerWidth = boundClientRect?.width; | ||
widthArray.current[currentIndex + index] = containerWidth || defaultItemSize; | ||
|
||
const containerHeight = boundClientRect?.height; | ||
heightArray.current[currentIndex + index] = containerHeight || defaultItemSize; | ||
}, | ||
[currentIndex, defaultItemSize], | ||
); | ||
|
||
const handleElementResizeCallback = (entries: ResizeObserverEntry[]) => { | ||
for (const entry of entries) { | ||
const target = entry.target as TElement; | ||
// Call the elements own resize handler (indexed) | ||
target.handleResize(); | ||
} | ||
}; | ||
|
||
React.useEffect(() => { | ||
widthArray.current = new Array(totalLength).fill(defaultItemSize); | ||
heightArray.current = new Array(totalLength).fill(defaultItemSize); | ||
}, [defaultItemSize, totalLength]); | ||
|
||
// Keep the reference of ResizeObserver as a ref, as it should live through renders | ||
const resizeObserver = React.useRef(createResizeObserverFromDocument(targetDocument, handleElementResizeCallback)); | ||
|
||
/* createIndexedRef provides a dynamic function to create an undefined number of refs at render time | ||
* these refs then provide an indexed callback via attaching 'handleResize' to the element itself | ||
* this function is then called on resize by handleElementResize and relies on indexing | ||
* to track continuous sizes throughout renders while releasing all virtualized element refs each render cycle. | ||
*/ | ||
const createIndexedRef = React.useCallback( | ||
(index: number) => { | ||
const measureElementRef = (el: TElement) => { | ||
if (!targetDocument || !resizeObserver.current) { | ||
return; | ||
} | ||
|
||
if (el) { | ||
el.handleResize = () => { | ||
handleIndexUpdate(index); | ||
}; | ||
} | ||
|
||
// cleanup previous container | ||
if (refArray.current[index] !== undefined && refArray.current[index] !== null) { | ||
resizeObserver.current.unobserve(refArray.current[index]!); | ||
} | ||
|
||
refArray.current[index] = undefined; | ||
if (el) { | ||
refArray.current[index] = el; | ||
resizeObserver.current.observe(el); | ||
handleIndexUpdate(index); | ||
} | ||
}; | ||
|
||
return measureElementRef; | ||
}, | ||
[handleIndexUpdate, resizeObserver, targetDocument], | ||
); | ||
|
||
React.useEffect(() => { | ||
const _resizeObserver = resizeObserver; | ||
return () => _resizeObserver.current?.disconnect(); | ||
}, [resizeObserver]); | ||
|
||
return { widthArray, heightArray, createIndexedRef, refArray }; | ||
} | ||
|
||
/** | ||
* FIXME - TS 3.8/3.9 don't have ResizeObserver types by default, move this to a shared utility once we bump the minbar | ||
* A utility method that creates a ResizeObserver from a target document | ||
* @param targetDocument - document to use to create the ResizeObserver | ||
* @param callback - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback | ||
* @returns a ResizeObserver instance or null if the global does not exist on the document | ||
*/ | ||
export function createResizeObserverFromDocument( | ||
targetDocument: Document | null | undefined, | ||
callback: ResizeObserverCallback, | ||
) { | ||
if (!targetDocument?.defaultView?.ResizeObserver) { | ||
return null; | ||
} | ||
|
||
return new targetDocument.defaultView.ResizeObserver(callback); | ||
} |
51 changes: 51 additions & 0 deletions
51
...components/react-virtualizer/stories/VirtualizerScrollViewDynamic/AutoMeasure.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
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 AutoMeasure = () => { | ||
const styles = useStyles(); | ||
const childLength = 1000; | ||
const minHeight = 42; | ||
const maxHeightIncrease = 150; | ||
// Array size ref stores a list of random num for div sizing and callbacks | ||
const arraySize = React.useRef(new Array<number>(childLength).fill(minHeight)); | ||
|
||
useEffect(() => { | ||
// Set random heights on init (to be measured) | ||
for (let i = 0; i < childLength; i++) { | ||
arraySize.current[i] = Math.floor(Math.random() * maxHeightIncrease + minHeight); | ||
} | ||
}, []); | ||
|
||
return ( | ||
<VirtualizerScrollViewDynamic | ||
numItems={childLength} | ||
// We can use itemSize to set an average height for minimal size change impact | ||
itemSize={minHeight + maxHeightIncrease / 2} | ||
container={{ role: 'list', style: { maxHeight: '100vh' } }} | ||
> | ||
{(index: number) => { | ||
const backgroundColor = index % 2 ? '#FFFFFF' : '#ABABAB'; | ||
return ( | ||
<div | ||
role={'listitem'} | ||
aria-posinset={index} | ||
aria-setsize={childLength} | ||
key={`test-virtualizer-child-${index}`} | ||
className={styles.child} | ||
style={{ minHeight: arraySize.current[index], backgroundColor }} | ||
>{`Node-${index} - size: ${arraySize.current[index]}`}</div> | ||
); | ||
}} | ||
</VirtualizerScrollViewDynamic> | ||
); | ||
}; |
1 change: 1 addition & 0 deletions
1
.../react-components/react-virtualizer/stories/VirtualizerScrollViewDynamic/index.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters