Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(virtual-core): add enabled option #741

Merged
merged 1 commit into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/api/virtualizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ This function is passed the index of each item and should return the actual size

## Optional Options

### `enabled`

```tsx
enabled?: boolean
```

Set to `false` to disable scrollElement observers and reset the virtualizer's state

### `debug`

```tsx
Expand Down
11 changes: 11 additions & 0 deletions examples/react/dynamic/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ const sentences = new Array(10000)
function RowVirtualizerDynamic() {
const parentRef = React.useRef<HTMLDivElement>(null)

const [enabled, setEnabled] = React.useState(true)

const count = sentences.length
const virtualizer = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
estimateSize: () => 45,
enabled,
})

const items = virtualizer.getVirtualItems()
Expand Down Expand Up @@ -50,6 +53,14 @@ function RowVirtualizerDynamic() {
>
scroll to the end
</button>
<span style={{ padding: '0 4px' }} />
<button
onClick={() => {
setEnabled((prev) => !prev)
}}
>
turn {enabled ? 'off' : 'on'} virtualizer
</button>
<hr />
<div
ref={parentRef}
Expand Down
175 changes: 111 additions & 64 deletions packages/virtual-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ export interface VirtualizerOptions<
initialMeasurementsCache?: VirtualItem[]
lanes?: number
isScrollingResetDelay?: number
enabled?: boolean
}

export class Virtualizer<
Expand All @@ -333,8 +334,8 @@ export class Virtualizer<
measurementsCache: VirtualItem[] = []
private itemSizeCache = new Map<Key, number>()
private pendingMeasuredCacheIndexes: number[] = []
scrollRect: Rect
scrollOffset: number
scrollRect: Rect | null = null
scrollOffset: number | null = null
scrollDirection: ScrollDirection | null = null
private scrollAdjustments: number = 0
shouldAdjustScrollPositionOnItemSizeChange:
Expand Down Expand Up @@ -375,17 +376,6 @@ export class Virtualizer<

constructor(opts: VirtualizerOptions<TScrollElement, TItemElement>) {
this.setOptions(opts)
this.scrollRect = this.options.initialRect
this.scrollOffset =
typeof this.options.initialOffset === 'function'
? this.options.initialOffset()
: this.options.initialOffset
this.measurementsCache = this.options.initialMeasurementsCache
this.measurementsCache.forEach((item) => {
this.itemSizeCache.set(item.key, item.size)
})

this.notify(false, false)
}

setOptions = (opts: VirtualizerOptions<TScrollElement, TItemElement>) => {
Expand Down Expand Up @@ -413,6 +403,7 @@ export class Virtualizer<
initialMeasurementsCache: [],
lanes: 1,
isScrollingResetDelay: 150,
enabled: true,
...opts,
}
}
Expand All @@ -437,22 +428,30 @@ export class Virtualizer<
this.unsubs.filter(Boolean).forEach((d) => d!())
this.unsubs = []
this.scrollElement = null
this.targetWindow = null
this.observer.disconnect()
this.measureElementCache.clear()
}

_didMount = () => {
this.measureElementCache.forEach(this.observer.observe)
return () => {
this.observer.disconnect()
this.cleanup()
}
}

_willUpdate = () => {
const scrollElement = this.options.getScrollElement()
const scrollElement = this.options.enabled
? this.options.getScrollElement()
: null

if (this.scrollElement !== scrollElement) {
this.cleanup()

if (!scrollElement) {
this.notify(false, false)
return
}

this.scrollElement = scrollElement

if (this.scrollElement && 'ownerDocument' in this.scrollElement) {
Expand All @@ -461,7 +460,7 @@ export class Virtualizer<
this.targetWindow = this.scrollElement?.window ?? null
}

this._scrollToOffset(this.scrollOffset, {
this._scrollToOffset(this.getScrollOffset(), {
adjustments: undefined,
behavior: undefined,
})
Expand All @@ -477,7 +476,7 @@ export class Virtualizer<
this.options.observeElementOffset(this, (offset, isScrolling) => {
this.scrollAdjustments = 0
this.scrollDirection = isScrolling
? this.scrollOffset < offset
? this.getScrollOffset() < offset
? 'forward'
: 'backward'
: null
Expand All @@ -493,29 +492,30 @@ export class Virtualizer<
}

private getSize = () => {
if (!this.options.enabled) {
this.scrollRect = null
return 0
}

this.scrollRect = this.scrollRect ?? this.options.initialRect

return this.scrollRect[this.options.horizontal ? 'width' : 'height']
}

private getMeasurementOptions = memo(
() => [
this.options.count,
this.options.paddingStart,
this.options.scrollMargin,
this.options.getItemKey,
],
(count, paddingStart, scrollMargin, getItemKey) => {
this.pendingMeasuredCacheIndexes = []
return {
count,
paddingStart,
scrollMargin,
getItemKey,
}
},
{
key: false,
},
)
private getScrollOffset = () => {
if (!this.options.enabled) {
this.scrollOffset = null
return 0
}

this.scrollOffset =
this.scrollOffset ??
(typeof this.options.initialOffset === 'function'
? this.options.initialOffset()
: this.options.initialOffset)

return this.scrollOffset
}

private getFurthestMeasurement = (
measurements: VirtualItem[],
Expand Down Expand Up @@ -558,9 +558,48 @@ export class Virtualizer<
: undefined
}

private getMeasurementOptions = memo(
() => [
this.options.count,
this.options.paddingStart,
this.options.scrollMargin,
this.options.getItemKey,
this.options.enabled,
],
(count, paddingStart, scrollMargin, getItemKey, enabled) => {
this.pendingMeasuredCacheIndexes = []
return {
count,
paddingStart,
scrollMargin,
getItemKey,
enabled,
}
},
{
key: false,
},
)

private getMeasurements = memo(
() => [this.getMeasurementOptions(), this.itemSizeCache],
({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => {
(
{ count, paddingStart, scrollMargin, getItemKey, enabled },
itemSizeCache,
) => {
if (!enabled) {
this.measurementsCache = []
this.itemSizeCache.clear()
return []
}

if (this.measurementsCache.length === 0) {
this.measurementsCache = this.options.initialMeasurementsCache
this.measurementsCache.forEach((item) => {
this.itemSizeCache.set(item.key, item.size)
})
}

const min =
this.pendingMeasuredCacheIndexes.length > 0
? Math.min(...this.pendingMeasuredCacheIndexes)
Expand Down Expand Up @@ -614,7 +653,7 @@ export class Virtualizer<
)

calculateRange = memo(
() => [this.getMeasurements(), this.getSize(), this.scrollOffset],
() => [this.getMeasurements(), this.getSize(), this.getScrollOffset()],
(measurements, outerSize, scrollOffset) => {
return (this.range =
measurements.length > 0 && outerSize > 0
Expand Down Expand Up @@ -672,7 +711,7 @@ export class Virtualizer<
node: TItemElement,
entry: ResizeObserverEntry | undefined,
) => {
const item = this.measurementsCache[this.indexFromElement(node)]
const item = this.getMeasurements()[this.indexFromElement(node)]

if (!item || !node.isConnected) {
this.measureElementCache.forEach((cached, key) => {
Expand Down Expand Up @@ -707,13 +746,13 @@ export class Virtualizer<
if (
this.shouldAdjustScrollPositionOnItemSizeChange !== undefined
? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this)
: item.start < this.scrollOffset + this.scrollAdjustments
: item.start < this.getScrollOffset() + this.scrollAdjustments
) {
if (process.env.NODE_ENV !== 'production' && this.options.debug) {
console.info('correction', delta)
}

this._scrollToOffset(this.scrollOffset, {
this._scrollToOffset(this.getScrollOffset(), {
adjustments: (this.scrollAdjustments += delta),
behavior: undefined,
})
Expand Down Expand Up @@ -756,7 +795,9 @@ export class Virtualizer<

getVirtualItemForOffset = (offset: number) => {
const measurements = this.getMeasurements()

if (measurements.length === 0) {
return undefined
}
return notUndefined(
measurements[
findNearestBinarySearch(
Expand All @@ -771,11 +812,12 @@ export class Virtualizer<

getOffsetForAlignment = (toOffset: number, align: ScrollAlignment) => {
const size = this.getSize()
const scrollOffset = this.getScrollOffset()

if (align === 'auto') {
if (toOffset <= this.scrollOffset) {
if (toOffset <= scrollOffset) {
align = 'start'
} else if (toOffset >= this.scrollOffset + size) {
} else if (toOffset >= scrollOffset + size) {
align = 'end'
} else {
align = 'start'
Expand All @@ -799,36 +841,36 @@ export class Virtualizer<
: this.scrollElement[scrollSizeProp]
: 0

const maxOffset = scrollSize - this.getSize()
const maxOffset = scrollSize - size

return Math.max(Math.min(maxOffset, toOffset), 0)
}

getOffsetForIndex = (index: number, align: ScrollAlignment = 'auto') => {
index = Math.max(0, Math.min(index, this.options.count - 1))

const measurement = notUndefined(this.getMeasurements()[index])
const item = this.getMeasurements()[index]
if (!item) {
return undefined
}

const size = this.getSize()
const scrollOffset = this.getScrollOffset()

if (align === 'auto') {
if (
measurement.end >=
this.scrollOffset + this.getSize() - this.options.scrollPaddingEnd
) {
if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) {
align = 'end'
} else if (
measurement.start <=
this.scrollOffset + this.options.scrollPaddingStart
) {
} else if (item.start <= scrollOffset + this.options.scrollPaddingStart) {
align = 'start'
} else {
return [this.scrollOffset, align] as const
return [scrollOffset, align] as const
}
}

const toOffset =
align === 'end'
? measurement.end + this.options.scrollPaddingEnd
: measurement.start - this.options.scrollPaddingStart
? item.end + this.options.scrollPaddingEnd
: item.start - this.options.scrollPaddingStart

return [this.getOffsetForAlignment(toOffset, align), align] as const
}
Expand Down Expand Up @@ -874,9 +916,12 @@ export class Virtualizer<
)
}

const [toOffset, align] = this.getOffsetForIndex(index, initialAlign)
const offsetAndAlign = this.getOffsetForIndex(index, initialAlign)
if (!offsetAndAlign) return

const [offset, align] = offsetAndAlign

this._scrollToOffset(toOffset, { adjustments: undefined, behavior })
this._scrollToOffset(offset, { adjustments: undefined, behavior })

if (behavior !== 'smooth' && this.isDynamicMode() && this.targetWindow) {
this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => {
Expand All @@ -887,9 +932,11 @@ export class Virtualizer<
)

if (elementInDOM) {
const [toOffset] = this.getOffsetForIndex(index, align)
const [latestOffset] = notUndefined(
this.getOffsetForIndex(index, align),
)

if (!approxEqual(toOffset, this.scrollOffset)) {
if (!approxEqual(latestOffset, this.getScrollOffset())) {
this.scrollToIndex(index, { align, behavior })
}
} else {
Expand All @@ -908,7 +955,7 @@ export class Virtualizer<
)
}

this._scrollToOffset(this.scrollOffset + delta, {
this._scrollToOffset(this.getScrollOffset() + delta, {
adjustments: undefined,
behavior,
})
Expand Down
Loading