diff --git a/examples/react/dynamic/src/main.tsx b/examples/react/dynamic/src/main.tsx index 36b1173c8..ee73d276e 100644 --- a/examples/react/dynamic/src/main.tsx +++ b/examples/react/dynamic/src/main.tsx @@ -16,8 +16,9 @@ const sentences = new Array(10000) function RowVirtualizerDynamic() { const parentRef = React.useRef(null) + const count = sentences.length const virtualizer = useVirtualizer({ - count: sentences.length, + count, getScrollElement: () => parentRef.current, estimateSize: () => 45, }) @@ -25,40 +26,65 @@ function RowVirtualizerDynamic() { const items = virtualizer.getVirtualItems() return ( -
+
+ + + +
- {items.map((virtualRow) => ( -
-
-
Row {virtualRow.index}
-
{sentences[virtualRow.index]}
+
+ {items.map((virtualRow) => ( +
+
+
Row {virtualRow.index}
+
{sentences[virtualRow.index]}
+
-
- ))} + ))} +
diff --git a/packages/react-virtual/__tests__/index.test.tsx b/packages/react-virtual/__tests__/index.test.tsx index 923bd8286..2c63e2a24 100644 --- a/packages/react-virtual/__tests__/index.test.tsx +++ b/packages/react-virtual/__tests__/index.test.tsx @@ -40,6 +40,8 @@ function List({ rangeExtractor, }) + const measureElement = dynamic ? rowVirtualizer.measureElement : undefined + return (
- dynamic ? rowVirtualizer.measureElement(el) : undefined - } + ref={measureElement} style={{ position: 'absolute', top: 0, diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index d12c29994..7bfbc79c2 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -288,6 +288,7 @@ export class Virtualizer< scrollElement: TScrollElement | null = null isScrolling: boolean = false private isScrollingTimeoutId: ReturnType | null = null + private scrollToIndexTimeoutId: ReturnType | null = null measurementsCache: VirtualItem[] = [] private itemSizeCache: Record = {} private pendingMeasuredCacheIndexes: number[] = [] @@ -296,7 +297,6 @@ export class Virtualizer< scrollDirection: ScrollDirection | null = null private scrollAdjustments: number = 0 private measureElementCache: Record = {} - private pendingScrollToIndexCallback: (() => void) | null = null private getResizeObserver = (() => { let _ro: ResizeObserver | null = null @@ -380,8 +380,6 @@ export class Virtualizer< } _willUpdate = () => { - this.pendingScrollToIndexCallback?.() - const scrollElement = this.options.getScrollElement() if (this.scrollElement !== scrollElement) { @@ -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) } @@ -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, @@ -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 { @@ -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, }) }