Skip to content
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
76 changes: 51 additions & 25 deletions examples/react/dynamic/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,75 @@ const sentences = new Array(10000)
function RowVirtualizerDynamic() {
const parentRef = React.useRef<HTMLDivElement>(null)

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

const items = virtualizer.getVirtualItems()

return (
<div
ref={parentRef}
className="List"
style={{ height: 400, width: 400, overflowY: 'auto' }}
>
<div>
<button
onClick={() => {
virtualizer.scrollToIndex(count / 2)
}}
>
scroll to the middle
</button>
<span style={{ padding: '0 4px' }} />
<button
onClick={() => {
virtualizer.scrollToIndex(count - 1)
}}
>
scroll to the last
</button>
<hr />
<div
ref={parentRef}
className="List"
style={{
height: virtualizer.getTotalSize(),
width: '100%',
position: 'relative',
height: 400,
width: 400,
overflowY: 'auto',
contain: 'strict',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
height: virtualizer.getTotalSize(),
width: '100%',
transform: `translateY(${items[0].start}px)`,
position: 'relative',
}}
>
{items.map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
>
<div style={{ padding: '10px 0' }}>
<div>Row {virtualRow.index}</div>
<div>{sentences[virtualRow.index]}</div>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className={
virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'
}
>
<div style={{ padding: '10px 0' }}>
<div>Row {virtualRow.index}</div>
<div>{sentences[virtualRow.index]}</div>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/react-virtual/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ function List({
rangeExtractor,
})

const measureElement = dynamic ? rowVirtualizer.measureElement : undefined

return (
<div
ref={parentRef}
Expand All @@ -58,9 +60,7 @@ function List({
data-testid={`item-${virtualRow.key}`}
key={virtualRow.key}
data-index={virtualRow.index}
ref={(el) =>
dynamic ? rowVirtualizer.measureElement(el) : undefined
}
ref={measureElement}
style={{
position: 'absolute',
top: 0,
Expand Down
110 changes: 79 additions & 31 deletions packages/virtual-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class Virtualizer<
scrollElement: TScrollElement | null = null
isScrolling: boolean = false
private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
measurementsCache: VirtualItem[] = []
private itemSizeCache: Record<Key, number> = {}
private pendingMeasuredCacheIndexes: number[] = []
Expand All @@ -296,7 +297,6 @@ export class Virtualizer<
scrollDirection: ScrollDirection | null = null
private scrollAdjustments: number = 0
private measureElementCache: Record<Key, TItemElement> = {}
private pendingScrollToIndexCallback: (() => void) | null = null
private getResizeObserver = (() => {
let _ro: ResizeObserver | null = null

Expand Down Expand Up @@ -380,8 +380,6 @@ export class Virtualizer<
}

_willUpdate = () => {
this.pendingScrollToIndexCallback?.()

const scrollElement = this.options.getScrollElement()

if (this.scrollElement !== scrollElement) {
Expand Down Expand Up @@ -579,11 +577,7 @@ export class Virtualizer<
const delta = measuredItemSize - itemSize

if (delta !== 0) {
if (
item.start < this.scrollOffset &&
this.isScrolling &&
this.scrollDirection === 'backward'
) {
if (item.start < this.scrollOffset) {
if (process.env.NODE_ENV !== 'production' && this.options.debug) {
console.info('correction', delta)
}
Expand Down Expand Up @@ -659,6 +653,15 @@ export class Virtualizer<
toOffset: number,
{ align = 'start', behavior }: ScrollToOffsetOptions = {},
) => {
const isDynamic = Object.keys(this.measureElementCache).length > 0

if (isDynamic && behavior === 'smooth') {
console.warn(
'The `smooth` scroll behavior is not supported with dynamic size.',
)
return
}

const options = {
adjustments: undefined,
behavior,
Expand All @@ -671,25 +674,43 @@ export class Virtualizer<
index: number,
{ align = 'auto', behavior }: ScrollToIndexOptions = {},
) => {
this.pendingScrollToIndexCallback = null
if (this.scrollToIndexTimeoutId !== null) {
clearTimeout(this.scrollToIndexTimeoutId)
this.scrollToIndexTimeoutId = null
}

const offset = this.scrollOffset
const size = this.getSize()
const { count } = this.options
const isDynamic = Object.keys(this.measureElementCache).length > 0

if (isDynamic && behavior === 'smooth') {
console.warn(
'The `smooth` scroll behavior is not supported with dynamic size.',
)
return
}

const getMeasurement = () => {
const measurements = this.getMeasurements()
const measurement =
measurements[Math.max(0, Math.min(index, this.options.count - 1))]

const measurements = this.getMeasurements()
const measurement = measurements[Math.max(0, Math.min(index, count - 1))]
if (!measurement) {
throw new Error(`VirtualItem not found for index = ${index}`)
}

if (!measurement) {
throw new Error(`VirtualItem not found for index = ${index}`)
return measurement
}

const measurement = getMeasurement()

if (align === 'auto') {
if (measurement.end >= offset + size - this.options.scrollPaddingEnd) {
if (
measurement.end >=
this.scrollOffset + this.getSize() - this.options.scrollPaddingEnd
) {
align = 'end'
} else if (
measurement.start <=
offset + this.options.scrollPaddingStart
this.scrollOffset + this.options.scrollPaddingStart
) {
align = 'start'
} else {
Expand All @@ -703,34 +724,61 @@ export class Virtualizer<
? measurement.end + this.options.scrollPaddingEnd
: measurement.start - this.options.scrollPaddingStart

return this.getOffsetForAlignment(toOffset, align)
}
const sizeProp = this.options.horizontal ? 'scrollWidth' : 'scrollHeight'
const scrollSize = this.scrollElement
? 'document' in this.scrollElement
? this.scrollElement.document.documentElement[sizeProp]
: this.scrollElement[sizeProp]
: 0

const toOffset = getOffsetForIndexAndAlignment(measurement)
const maxOffset = scrollSize - this.getSize()

if (toOffset === offset) {
return
return Math.min(maxOffset, this.getOffsetForAlignment(toOffset, align))
}

const toOffset = getOffsetForIndexAndAlignment(measurement)

const options = {
adjustments: undefined,
behavior,
}
this._scrollToOffset(toOffset, options)

const isDynamic = Object.keys(this.measureElementCache).length > 0
const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1

if (isDynamic) {
this.pendingScrollToIndexCallback = () => {
this.scrollToIndex(index, { align, behavior })
}
this.scrollToIndexTimeoutId = setTimeout(() => {
this.scrollToIndexTimeoutId = null

const elementInDOM =
!!this.measureElementCache[this.options.getItemKey(index)]

if (elementInDOM) {
const toOffset = getOffsetForIndexAndAlignment(getMeasurement())

if (!approxEqual(toOffset, this.scrollOffset)) {
this.scrollToIndex(index, { align, behavior })
}
} else {
this.scrollToIndex(index, { align, behavior })
}
})
}
}

scrollBy = (adjustments: number, options?: { behavior: ScrollBehavior }) => {
this._scrollToOffset(this.scrollOffset, {
adjustments,
behavior: options?.behavior,
scrollBy = (delta: number, { behavior }: ScrollToOffsetOptions = {}) => {
const isDynamic = Object.keys(this.measureElementCache).length > 0

if (isDynamic && behavior === 'smooth') {
console.warn(
'The `smooth` scroll behavior is not supported with dynamic size.',
)
return
}

this._scrollToOffset(this.scrollOffset + delta, {
adjustments: undefined,
behavior,
})
}

Expand Down