Skip to content

Commit

Permalink
feat: add measureElement on virtual item, when we can't relay on node…
Browse files Browse the repository at this point in the history
….isConnected
  • Loading branch information
piecyk committed Jun 21, 2024
1 parent 20fc206 commit d49ef9f
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 26 deletions.
3 changes: 2 additions & 1 deletion docs/api/virtual-item.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ title: VirtualItem
The `VirtualItem` object represents a single item returned by the virtualizer. It contains information you need to render the item in the coordinate space within your virtualizer's scrollElement and other helpful properties/functions.

```tsx
export interface VirtualItem {
export interface VirtualItem<TItemElement extends Element> {
key: string | number
index: number
start: number
end: number
size: number
measureElement: (node: TItemElement | null | undefined) => void
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/api/virtualizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ By default the `measureElement` virtualizer option is configured to measure elem
### `resizeItem`
```tsx
resizeItem: (item: VirtualItem, size: number) => void
resizeItem: (index: number, size: number) => void
```
Change the virtualized item's size manually. Use this function to manually set the size calculated for this item. Useful in occations when using some custom morphing transition and you know the morphed item's size beforehand.
Change the virtualized item's size manually. Use this function to manually set the size calculated for this index. Useful in occations when using some custom morphing transition and you know the morphed item's size beforehand.
You can also use this method with a throttled ResizeObserver instead of `Virtualizer.measureElement` to reduce re-rendering.
Expand Down
83 changes: 60 additions & 23 deletions packages/virtual-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ export interface Range {

type Key = number | string

export interface VirtualItem {
export interface VirtualItem<TItemElement extends Element> {
key: Key
index: number
start: number
end: number
size: number
lane: number
measureElement: (node: TItemElement | null | undefined) => void
}

export interface Rect {
Expand Down Expand Up @@ -315,7 +316,7 @@ export interface VirtualizerOptions<
scrollMargin?: number
gap?: number
indexAttribute?: string
initialMeasurementsCache?: VirtualItem[]
initialMeasurementsCache?: VirtualItem<TItemElement>[]
lanes?: number
isScrollingResetDelay?: number
enabled?: boolean
Expand All @@ -331,7 +332,7 @@ export class Virtualizer<
targetWindow: (Window & typeof globalThis) | null = null
isScrolling: boolean = false
private scrollToIndexTimeoutId: number | null = null
measurementsCache: VirtualItem[] = []
measurementsCache: VirtualItem<TItemElement>[] = []
private itemSizeCache = new Map<Key, number>()
private pendingMeasuredCacheIndexes: number[] = []
scrollRect: Rect | null = null
Expand All @@ -341,11 +342,11 @@ export class Virtualizer<
shouldAdjustScrollPositionOnItemSizeChange:
| undefined
| ((
item: VirtualItem,
item: VirtualItem<TItemElement>,
delta: number,
instance: Virtualizer<TScrollElement, TItemElement>,
) => boolean)
measureElementCache = new Map<Key, TItemElement>()
elementCache = new Map<Key, TItemElement>()
private observer = (() => {
let _ro: ResizeObserver | null = null

Expand Down Expand Up @@ -430,7 +431,7 @@ export class Virtualizer<
this.scrollElement = null
this.targetWindow = null
this.observer.disconnect()
this.measureElementCache.clear()
this.elementCache.clear()
}

_didMount = () => {
Expand Down Expand Up @@ -518,11 +519,11 @@ export class Virtualizer<
}

private getFurthestMeasurement = (
measurements: VirtualItem[],
measurements: VirtualItem<TItemElement>[],
index: number,
) => {
const furthestMeasurementsFound = new Map<number, true>()
const furthestMeasurements = new Map<number, VirtualItem>()
const furthestMeasurements = new Map<number, VirtualItem<TItemElement>>()
for (let m = index - 1; m >= 0; m--) {
const measurement = measurements[m]!

Expand Down Expand Up @@ -609,6 +610,38 @@ export class Virtualizer<
const measurements = this.measurementsCache.slice(0, min)

for (let i = min; i < count; i++) {
let measureElement = this.measurementsCache[i]?.measureElement

if (!measureElement) {
measureElement = (node: TItemElement | null | undefined) => {
const key = getItemKey(i)
const prevNode = this.elementCache.get(key)

if (!node) {
if (prevNode) {
this.observer.unobserve(prevNode)
this.elementCache.delete(key)
}
return
}

if (prevNode !== node) {
if (prevNode) {
this.observer.unobserve(prevNode)
}
this.observer.observe(node)
this.elementCache.set(key, node)
}

if (node.isConnected) {
this.resizeItem(
i,
this.options.measureElement(node, undefined, this),
)
}
}
}

const key = getItemKey(i)

const furthestMeasurement =
Expand Down Expand Up @@ -639,6 +672,7 @@ export class Virtualizer<
end,
key,
lane,
measureElement,
}
}

Expand Down Expand Up @@ -711,34 +745,37 @@ export class Virtualizer<
node: TItemElement,
entry: ResizeObserverEntry | undefined,
) => {
const item = this.getMeasurements()[this.indexFromElement(node)]
const i = this.indexFromElement(node)
const item = this.getMeasurements()[i]

if (!item || !node.isConnected) {
this.measureElementCache.forEach((cached, key) => {
this.elementCache.forEach((cached, key) => {
if (cached === node) {
this.observer.unobserve(node)
this.measureElementCache.delete(key)
this.elementCache.delete(key)
}
})
return
}

const prevNode = this.measureElementCache.get(item.key)
const prevNode = this.elementCache.get(item.key)

if (prevNode !== node) {
if (prevNode) {
this.observer.unobserve(prevNode)
}
this.observer.observe(node)
this.measureElementCache.set(item.key, node)
this.elementCache.set(item.key, node)
}

const measuredItemSize = this.options.measureElement(node, entry, this)

this.resizeItem(item, measuredItemSize)
this.resizeItem(i, this.options.measureElement(node, entry, this))
}

resizeItem = (item: VirtualItem, size: number) => {
resizeItem = (index: number, size: number) => {
const item = this.getMeasurements()[index]
if (!item) {
return
}
const itemSize = this.itemSizeCache.get(item.key) ?? item.size
const delta = size - itemSize

Expand All @@ -765,7 +802,7 @@ export class Virtualizer<
}
}

measureElement = (node: TItemElement | null) => {
measureElement = (node: TItemElement | null | undefined) => {
if (!node) {
return
}
Expand All @@ -776,7 +813,7 @@ export class Virtualizer<
getVirtualItems = memo(
() => [this.getIndexes(), this.getMeasurements()],
(indexes, measurements) => {
const virtualItems: VirtualItem[] = []
const virtualItems: VirtualItem<TItemElement>[] = []

for (let k = 0, len = indexes.length; k < len; k++) {
const i = indexes[k]!
Expand Down Expand Up @@ -875,7 +912,7 @@ export class Virtualizer<
return [this.getOffsetForAlignment(toOffset, align), align] as const
}

private isDynamicMode = () => this.measureElementCache.size > 0
private isDynamicMode = () => this.elementCache.size > 0

private cancelScrollToIndex = () => {
if (this.scrollToIndexTimeoutId !== null && this.targetWindow) {
Expand Down Expand Up @@ -927,7 +964,7 @@ export class Virtualizer<
this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => {
this.scrollToIndexTimeoutId = null

const elementInDOM = this.measureElementCache.has(
const elementInDOM = this.elementCache.has(
this.options.getItemKey(index),
)

Expand Down Expand Up @@ -1026,12 +1063,12 @@ const findNearestBinarySearch = (
}
}

function calculateRange({
function calculateRange<TItemElement extends Element>({
measurements,
outerSize,
scrollOffset,
}: {
measurements: VirtualItem[]
measurements: VirtualItem<TItemElement>[]
outerSize: number
scrollOffset: number
}) {
Expand Down

0 comments on commit d49ef9f

Please sign in to comment.