@tanstack/virtual-core 3.0.0-alpha.1 → 3.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import observeRect from '@reach/observe-rect'
2
+ import { check } from 'prettier'
2
3
  import React from 'react'
3
4
  import { memo } from './utils'
4
5
 
@@ -44,7 +45,6 @@ export interface VirtualItem<TItemElement> extends Item {
44
45
 
45
46
  //
46
47
 
47
- export const defaultEstimateSize = () => 50
48
48
  export const defaultKeyExtractor = (index: number) => index
49
49
 
50
50
  export const defaultRangeExtractor = (range: Range) => {
@@ -162,11 +162,13 @@ export const observeWindowOffset = (
162
162
  }
163
163
  }
164
164
 
165
- export const defaultMeasureElement = (
165
+ export const measureElement = (
166
166
  element: unknown,
167
167
  instance: Virtualizer<any, any>,
168
168
  ) => {
169
- return (element as Element).getBoundingClientRect()[instance.getSizeKey()]
169
+ return (element as Element).getBoundingClientRect()[
170
+ instance.options.horizontal ? 'width' : 'height'
171
+ ]
170
172
  }
171
173
 
172
174
  export const windowScroll = (
@@ -195,13 +197,17 @@ export interface VirtualizerOptions<
195
197
  TScrollElement = unknown,
196
198
  TItemElement = unknown,
197
199
  > {
200
+ // Required from the user
198
201
  count: number
202
+ getScrollElement: () => TScrollElement
203
+ estimateSize: (index: number) => number
204
+
205
+ // Required from the framework adapter (but can be overridden)
199
206
  scrollToFn: (
200
207
  offset: number,
201
208
  canSmooth: boolean,
202
209
  instance: Virtualizer<TScrollElement, TItemElement>,
203
210
  ) => void
204
- getScrollElement: () => TScrollElement
205
211
  observeElementRect: (
206
212
  instance: Virtualizer<TScrollElement, TItemElement>,
207
213
  cb: (rect: Rect) => void,
@@ -211,8 +217,7 @@ export interface VirtualizerOptions<
211
217
  cb: (offset: number) => void,
212
218
  ) => void | (() => void)
213
219
 
214
- //
215
-
220
+ // Optional
216
221
  debug?: any
217
222
  initialRect?: Rect
218
223
  onChange?: (instance: Virtualizer<TScrollElement, TItemElement>) => void
@@ -220,19 +225,18 @@ export interface VirtualizerOptions<
220
225
  el: TItemElement,
221
226
  instance: Virtualizer<TScrollElement, TItemElement>,
222
227
  ) => number
223
- estimateSize?: (index: number) => number
224
228
  overscan?: number
225
229
  horizontal?: boolean
226
230
  paddingStart?: number
227
231
  paddingEnd?: number
228
232
  initialOffset?: number
229
- keyExtractor?: (index: number) => Key
233
+ getItemKey?: (index: number) => Key
230
234
  rangeExtractor?: (range: Range) => number[]
231
235
  enableSmoothScroll?: boolean
232
236
  }
233
237
 
234
238
  export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
235
- unsubs: (void | (() => void))[] = []
239
+ private unsubs: (void | (() => void))[] = []
236
240
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
237
241
  scrollElement: TScrollElement | null = null
238
242
  private measurementsCache: Item[] = []
@@ -240,13 +244,8 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
240
244
  private pendingMeasuredCacheIndexes: number[] = []
241
245
  private scrollRect: Rect
242
246
  private scrollOffset: number
243
-
244
- //
245
- // virtualItems: VirtualItem<TItemElement>[]
246
- // totalSize: number
247
- // scrollToOffset: (offset: number, options?: ScrollToOffsetOptions) => void
248
- // scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void
249
- // measure: (index: number) => void
247
+ private destinationOffset: undefined | number
248
+ private scrollCheckFrame!: ReturnType<typeof setTimeout>
250
249
 
251
250
  constructor(opts: VirtualizerOptions<TScrollElement, TItemElement>) {
252
251
  this.setOptions(opts)
@@ -262,16 +261,15 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
262
261
  this.options = {
263
262
  debug: false,
264
263
  initialOffset: 0,
265
- estimateSize: defaultEstimateSize,
266
264
  overscan: 1,
267
265
  paddingStart: 0,
268
266
  paddingEnd: 0,
269
267
  horizontal: false,
270
- keyExtractor: defaultKeyExtractor,
268
+ getItemKey: defaultKeyExtractor,
271
269
  rangeExtractor: defaultRangeExtractor,
272
270
  enableSmoothScroll: false,
273
271
  onChange: () => {},
274
- measureElement: defaultMeasureElement,
272
+ measureElement,
275
273
  initialRect: { width: 0, height: 0 },
276
274
  ...opts,
277
275
  }
@@ -317,18 +315,17 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
317
315
  }
318
316
 
319
317
  private getSize = () => {
320
- return this.scrollRect[this.getSizeKey()]
318
+ return this.scrollRect[this.options.horizontal ? 'width' : 'height']
321
319
  }
322
320
 
323
321
  private getMeasurements = memo(
324
322
  () => [
325
323
  this.options.count,
326
324
  this.options.paddingStart,
327
- this.getEstimateSizeFn(),
328
- this.options.keyExtractor,
325
+ this.options.getItemKey,
329
326
  this.itemMeasurementsCache,
330
327
  ],
331
- (count, paddingStart, estimateSize, keyExtractor, measurementsCache) => {
328
+ (count, paddingStart, getItemKey, measurementsCache) => {
332
329
  const min =
333
330
  this.pendingMeasuredCacheIndexes.length > 0
334
331
  ? Math.min(...this.pendingMeasuredCacheIndexes)
@@ -338,13 +335,15 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
338
335
  const measurements = this.measurementsCache.slice(0, min)
339
336
 
340
337
  for (let i = min; i < count; i++) {
341
- const key = keyExtractor(i)
338
+ const key = getItemKey(i)
342
339
  const measuredSize = measurementsCache[key]
343
340
  const start = measurements[i - 1]
344
341
  ? measurements[i - 1]!.end
345
342
  : paddingStart
346
343
  const size =
347
- typeof measuredSize === 'number' ? measuredSize : estimateSize(i)
344
+ typeof measuredSize === 'number'
345
+ ? measuredSize
346
+ : this.options.estimateSize(i)
348
347
  const end = start + size
349
348
  measurements[i] = { index: i, start, size, end, key }
350
349
  }
@@ -419,10 +418,12 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
419
418
  )
420
419
  console.info('correction', measuredItemSize - item.size)
421
420
 
422
- this._scrollToOffset(
423
- this.scrollOffset + (measuredItemSize - item.size),
424
- false,
425
- )
421
+ if (!this.destinationOffset) {
422
+ this._scrollToOffset(
423
+ this.scrollOffset + (measuredItemSize - item.size),
424
+ false,
425
+ )
426
+ }
426
427
  }
427
428
 
428
429
  this.pendingMeasuredCacheIndexes.push(i)
@@ -450,29 +451,36 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
450
451
  toOffset: number,
451
452
  { align }: ScrollToOffsetOptions = { align: 'start' },
452
453
  ) => {
453
- const offset = this.scrollOffset
454
- const size = this.getSize()
454
+ const attempt = () => {
455
+ const offset = this.scrollOffset
456
+ const size = this.getSize()
457
+
458
+ if (align === 'auto') {
459
+ if (toOffset <= offset) {
460
+ align = 'start'
461
+ } else if (toOffset >= offset + size) {
462
+ align = 'end'
463
+ } else {
464
+ align = 'start'
465
+ }
466
+ }
455
467
 
456
- if (align === 'auto') {
457
- if (toOffset <= offset) {
458
- align = 'start'
459
- } else if (toOffset >= offset + size) {
460
- align = 'end'
461
- } else {
462
- align = 'start'
468
+ if (align === 'start') {
469
+ this._scrollToOffset(toOffset, true)
470
+ } else if (align === 'end') {
471
+ this._scrollToOffset(toOffset - size, true)
472
+ } else if (align === 'center') {
473
+ this._scrollToOffset(toOffset - size / 2, true)
463
474
  }
464
475
  }
465
476
 
466
- if (align === 'start') {
467
- this._scrollToOffset(toOffset, true)
468
- } else if (align === 'end') {
469
- this._scrollToOffset(toOffset - size, true)
470
- } else if (align === 'center') {
471
- this._scrollToOffset(toOffset - size / 2, true)
472
- }
477
+ attempt()
478
+ requestAnimationFrame(() => {
479
+ attempt()
480
+ })
473
481
  }
474
482
 
475
- private tryScrollToIndex = (
483
+ scrollToIndex = (
476
484
  index: number,
477
485
  { align, ...rest }: ScrollToIndexOptions = { align: 'auto' },
478
486
  ) => {
@@ -507,42 +515,36 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
507
515
  this.scrollToOffset(toOffset, { align, ...rest })
508
516
  }
509
517
 
510
- scrollToIndex = (index: number, options?: ScrollToIndexOptions) => {
511
- // We do a double request here because of
512
- // dynamic sizes which can cause offset shift
513
- // and end up in the wrong spot. Unfortunately,
514
- // we can't know about those dynamic sizes until
515
- // we try and render them. So double down!
516
- this.tryScrollToIndex(index, options)
517
- requestAnimationFrame(() => {
518
- this.tryScrollToIndex(index, options)
519
- })
520
- }
521
-
522
518
  getTotalSize = () =>
523
519
  (this.getMeasurements()[this.options.count - 1]?.end ||
524
520
  this.options.paddingStart) + this.options.paddingEnd
525
521
 
526
- getSizeKey = () => (this.options.horizontal ? 'width' : 'height')
527
-
528
522
  private _scrollToOffset = (offset: number, canSmooth: boolean) => {
529
- this.options.scrollToFn(
530
- offset,
531
- this.options.enableSmoothScroll && canSmooth,
532
- this,
533
- )
534
- }
523
+ clearTimeout(this.scrollCheckFrame)
535
524
 
536
- private getEstimateSizeFn = memo(
537
- () => [this.options.estimateSize],
538
- (d) => d,
539
- {
540
- key: false,
541
- onChange: () => {
542
- this.itemMeasurementsCache = {}
543
- },
544
- },
545
- )
525
+ this.destinationOffset = offset
526
+ this.options.scrollToFn(offset, canSmooth, this)
527
+
528
+ let scrollCheckFrame: ReturnType<typeof setTimeout>
529
+
530
+ const check = () => {
531
+ let lastOffset = this.scrollOffset
532
+ this.scrollCheckFrame = scrollCheckFrame = setTimeout(() => {
533
+ if (this.scrollCheckFrame !== scrollCheckFrame) {
534
+ return
535
+ }
536
+
537
+ if (this.scrollOffset === lastOffset) {
538
+ this.destinationOffset = undefined
539
+ return
540
+ }
541
+ lastOffset = this.scrollOffset
542
+ check()
543
+ }, 100)
544
+ }
545
+
546
+ check()
547
+ }
546
548
 
547
549
  measure = () => {
548
550
  this.itemMeasurementsCache = {}