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

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,20 @@ 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
232
+ scrollPaddingStart?: number
233
+ scrollPaddingEnd?: number
228
234
  initialOffset?: number
229
- keyExtractor?: (index: number) => Key
235
+ getItemKey?: (index: number) => Key
230
236
  rangeExtractor?: (range: Range) => number[]
231
237
  enableSmoothScroll?: boolean
232
238
  }
233
239
 
234
240
  export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
235
- unsubs: (void | (() => void))[] = []
241
+ private unsubs: (void | (() => void))[] = []
236
242
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
237
243
  scrollElement: TScrollElement | null = null
238
244
  private measurementsCache: Item[] = []
@@ -240,13 +246,8 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
240
246
  private pendingMeasuredCacheIndexes: number[] = []
241
247
  private scrollRect: Rect
242
248
  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
249
+ private destinationOffset: undefined | number
250
+ private scrollCheckFrame!: ReturnType<typeof setTimeout>
250
251
 
251
252
  constructor(opts: VirtualizerOptions<TScrollElement, TItemElement>) {
252
253
  this.setOptions(opts)
@@ -262,16 +263,17 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
262
263
  this.options = {
263
264
  debug: false,
264
265
  initialOffset: 0,
265
- estimateSize: defaultEstimateSize,
266
266
  overscan: 1,
267
267
  paddingStart: 0,
268
268
  paddingEnd: 0,
269
+ scrollPaddingStart: 0,
270
+ scrollPaddingEnd: 0,
269
271
  horizontal: false,
270
- keyExtractor: defaultKeyExtractor,
272
+ getItemKey: defaultKeyExtractor,
271
273
  rangeExtractor: defaultRangeExtractor,
272
- enableSmoothScroll: false,
274
+ enableSmoothScroll: true,
273
275
  onChange: () => {},
274
- measureElement: defaultMeasureElement,
276
+ measureElement,
275
277
  initialRect: { width: 0, height: 0 },
276
278
  ...opts,
277
279
  }
@@ -317,18 +319,17 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
317
319
  }
318
320
 
319
321
  private getSize = () => {
320
- return this.scrollRect[this.getSizeKey()]
322
+ return this.scrollRect[this.options.horizontal ? 'width' : 'height']
321
323
  }
322
324
 
323
325
  private getMeasurements = memo(
324
326
  () => [
325
327
  this.options.count,
326
328
  this.options.paddingStart,
327
- this.getEstimateSizeFn(),
328
- this.options.keyExtractor,
329
+ this.options.getItemKey,
329
330
  this.itemMeasurementsCache,
330
331
  ],
331
- (count, paddingStart, estimateSize, keyExtractor, measurementsCache) => {
332
+ (count, paddingStart, getItemKey, measurementsCache) => {
332
333
  const min =
333
334
  this.pendingMeasuredCacheIndexes.length > 0
334
335
  ? Math.min(...this.pendingMeasuredCacheIndexes)
@@ -338,13 +339,15 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
338
339
  const measurements = this.measurementsCache.slice(0, min)
339
340
 
340
341
  for (let i = min; i < count; i++) {
341
- const key = keyExtractor(i)
342
+ const key = getItemKey(i)
342
343
  const measuredSize = measurementsCache[key]
343
344
  const start = measurements[i - 1]
344
345
  ? measurements[i - 1]!.end
345
346
  : paddingStart
346
347
  const size =
347
- typeof measuredSize === 'number' ? measuredSize : estimateSize(i)
348
+ typeof measuredSize === 'number'
349
+ ? measuredSize
350
+ : this.options.estimateSize(i)
348
351
  const end = start + size
349
352
  measurements[i] = { index: i, start, size, end, key }
350
353
  }
@@ -419,10 +422,12 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
419
422
  )
420
423
  console.info('correction', measuredItemSize - item.size)
421
424
 
422
- this._scrollToOffset(
423
- this.scrollOffset + (measuredItemSize - item.size),
424
- false,
425
- )
425
+ if (!this.destinationOffset) {
426
+ this._scrollToOffset(
427
+ this.scrollOffset + (measuredItemSize - item.size),
428
+ false,
429
+ )
430
+ }
426
431
  }
427
432
 
428
433
  this.pendingMeasuredCacheIndexes.push(i)
@@ -450,29 +455,36 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
450
455
  toOffset: number,
451
456
  { align }: ScrollToOffsetOptions = { align: 'start' },
452
457
  ) => {
453
- const offset = this.scrollOffset
454
- const size = this.getSize()
458
+ const attempt = () => {
459
+ const offset = this.scrollOffset
460
+ const size = this.getSize()
461
+
462
+ if (align === 'auto') {
463
+ if (toOffset <= offset) {
464
+ align = 'start'
465
+ } else if (toOffset >= offset + size) {
466
+ align = 'end'
467
+ } else {
468
+ align = 'start'
469
+ }
470
+ }
455
471
 
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'
472
+ if (align === 'start') {
473
+ this._scrollToOffset(toOffset, true)
474
+ } else if (align === 'end') {
475
+ this._scrollToOffset(toOffset - size, true)
476
+ } else if (align === 'center') {
477
+ this._scrollToOffset(toOffset - size / 2, true)
463
478
  }
464
479
  }
465
480
 
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
- }
481
+ attempt()
482
+ requestAnimationFrame(() => {
483
+ attempt()
484
+ })
473
485
  }
474
486
 
475
- private tryScrollToIndex = (
487
+ scrollToIndex = (
476
488
  index: number,
477
489
  { align, ...rest }: ScrollToIndexOptions = { align: 'auto' },
478
490
  ) => {
@@ -488,9 +500,12 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
488
500
  }
489
501
 
490
502
  if (align === 'auto') {
491
- if (measurement.end >= offset + size) {
503
+ if (measurement.end >= offset + size - this.options.scrollPaddingEnd) {
492
504
  align = 'end'
493
- } else if (measurement.start <= offset) {
505
+ } else if (
506
+ measurement.start <=
507
+ offset + this.options.scrollPaddingStart
508
+ ) {
494
509
  align = 'start'
495
510
  } else {
496
511
  return
@@ -498,51 +513,47 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
498
513
  }
499
514
 
500
515
  const toOffset =
501
- align === 'center'
502
- ? measurement.start + measurement.size / 2
503
- : align === 'end'
504
- ? measurement.end
505
- : measurement.start
516
+ align === 'end'
517
+ ? measurement.end + this.options.scrollPaddingEnd
518
+ : measurement.start - this.options.scrollPaddingStart
506
519
 
507
520
  this.scrollToOffset(toOffset, { align, ...rest })
508
521
  }
509
522
 
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
523
  getTotalSize = () =>
523
524
  (this.getMeasurements()[this.options.count - 1]?.end ||
524
525
  this.options.paddingStart) + this.options.paddingEnd
525
526
 
526
- getSizeKey = () => (this.options.horizontal ? 'width' : 'height')
527
-
528
527
  private _scrollToOffset = (offset: number, canSmooth: boolean) => {
528
+ clearTimeout(this.scrollCheckFrame)
529
+
530
+ this.destinationOffset = offset
529
531
  this.options.scrollToFn(
530
532
  offset,
531
533
  this.options.enableSmoothScroll && canSmooth,
532
534
  this,
533
535
  )
534
- }
535
536
 
536
- private getEstimateSizeFn = memo(
537
- () => [this.options.estimateSize],
538
- (d) => d,
539
- {
540
- key: false,
541
- onChange: () => {
542
- this.itemMeasurementsCache = {}
543
- },
544
- },
545
- )
537
+ let scrollCheckFrame: ReturnType<typeof setTimeout>
538
+
539
+ const check = () => {
540
+ let lastOffset = this.scrollOffset
541
+ this.scrollCheckFrame = scrollCheckFrame = setTimeout(() => {
542
+ if (this.scrollCheckFrame !== scrollCheckFrame) {
543
+ return
544
+ }
545
+
546
+ if (this.scrollOffset === lastOffset) {
547
+ this.destinationOffset = undefined
548
+ return
549
+ }
550
+ lastOffset = this.scrollOffset
551
+ check()
552
+ }, 100)
553
+ }
554
+
555
+ check()
556
+ }
546
557
 
547
558
  measure = () => {
548
559
  this.itemMeasurementsCache = {}