@tanstack/virtual-core 3.0.0-beta.30 → 3.0.0-beta.33

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
@@ -4,6 +4,8 @@ export * from './utils'
4
4
 
5
5
  //
6
6
 
7
+ type ScrollDirection = 'forward' | 'backward'
8
+
7
9
  type ScrollAlignment = 'start' | 'center' | 'end' | 'auto'
8
10
 
9
11
  type ScrollBehavior = 'auto' | 'smooth'
@@ -149,7 +151,7 @@ const createOffsetObserver = (mode: ObserverMode) => {
149
151
  const offset =
150
152
  instance.scrollElement[instance.options.horizontal ? propX : propY]
151
153
 
152
- cb(Math.max(0, offset - instance.options.scrollMargin))
154
+ cb(offset)
153
155
  }
154
156
 
155
157
  scroll()
@@ -195,15 +197,12 @@ export const measureElement = <TItemElement extends Element>(
195
197
  export const windowScroll = <T extends Window>(
196
198
  offset: number,
197
199
  {
198
- adjustments,
200
+ adjustments = 0,
199
201
  behavior,
200
- sync,
201
- }: { adjustments?: number; behavior?: ScrollBehavior; sync: boolean },
202
+ }: { adjustments?: number; behavior?: ScrollBehavior },
202
203
  instance: Virtualizer<T, any>,
203
204
  ) => {
204
- const toOffset =
205
- (sync ? offset : offset + instance.options.scrollMargin) +
206
- (adjustments ?? 0)
205
+ const toOffset = offset + adjustments
207
206
 
208
207
  instance.scrollElement?.scrollTo?.({
209
208
  [instance.options.horizontal ? 'left' : 'top']: toOffset,
@@ -214,15 +213,12 @@ export const windowScroll = <T extends Window>(
214
213
  export const elementScroll = <T extends Element>(
215
214
  offset: number,
216
215
  {
217
- adjustments,
216
+ adjustments = 0,
218
217
  behavior,
219
- sync,
220
- }: { adjustments?: number; behavior?: ScrollBehavior; sync: boolean },
218
+ }: { adjustments?: number; behavior?: ScrollBehavior },
221
219
  instance: Virtualizer<T, any>,
222
220
  ) => {
223
- const toOffset =
224
- (sync ? offset : offset + instance.options.scrollMargin) +
225
- (adjustments ?? 0)
221
+ const toOffset = offset + adjustments
226
222
 
227
223
  instance.scrollElement?.scrollTo?.({
228
224
  [instance.options.horizontal ? 'left' : 'top']: toOffset,
@@ -242,7 +238,7 @@ export interface VirtualizerOptions<
242
238
  // Required from the framework adapter (but can be overridden)
243
239
  scrollToFn: (
244
240
  offset: number,
245
- options: { adjustments?: number; behavior?: ScrollBehavior; sync: boolean },
241
+ options: { adjustments?: number; behavior?: ScrollBehavior },
246
242
  instance: Virtualizer<TScrollElement, TItemElement>,
247
243
  ) => void
248
244
  observeElementRect: (
@@ -274,6 +270,7 @@ export interface VirtualizerOptions<
274
270
  scrollMargin?: number
275
271
  scrollingDelay?: number
276
272
  indexAttribute?: string
273
+ initialMeasurementsCache?: VirtualItem[]
277
274
  }
278
275
 
279
276
  export class Virtualizer<
@@ -286,10 +283,11 @@ export class Virtualizer<
286
283
  isScrolling: boolean = false
287
284
  private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
288
285
  measurementsCache: VirtualItem[] = []
289
- private itemMeasurementsCache: Record<Key, number> = {}
286
+ private itemSizeCache: Record<Key, number> = {}
290
287
  private pendingMeasuredCacheIndexes: number[] = []
291
288
  private scrollRect: Rect
292
- private scrollOffset: number
289
+ scrollOffset: number
290
+ scrollDirection: ScrollDirection | null = null
293
291
  private scrollAdjustments: number = 0
294
292
  private measureElementCache: Record<Key, TItemElement> = {}
295
293
  private pendingScrollToIndexCallback: (() => void) | null = null
@@ -319,6 +317,10 @@ export class Virtualizer<
319
317
  this.setOptions(opts)
320
318
  this.scrollRect = this.options.initialRect
321
319
  this.scrollOffset = this.options.initialOffset
320
+ this.measurementsCache = this.options.initialMeasurementsCache
321
+ this.measurementsCache.forEach((item) => {
322
+ this.itemSizeCache[item.key] = item.size
323
+ })
322
324
 
323
325
  this.calculateRange()
324
326
  }
@@ -345,6 +347,7 @@ export class Virtualizer<
345
347
  scrollMargin: 0,
346
348
  scrollingDelay: 150,
347
349
  indexAttribute: 'data-index',
350
+ initialMeasurementsCache: [],
348
351
  ...opts,
349
352
  }
350
353
  }
@@ -383,7 +386,6 @@ export class Virtualizer<
383
386
  this._scrollToOffset(this.scrollOffset, {
384
387
  adjustments: undefined,
385
388
  behavior: undefined,
386
- sync: true,
387
389
  })
388
390
 
389
391
  this.unsubs.push(
@@ -395,6 +397,12 @@ export class Virtualizer<
395
397
 
396
398
  this.unsubs.push(
397
399
  this.options.observeElementOffset(this, (offset) => {
400
+ this.scrollAdjustments = 0
401
+
402
+ if (this.scrollOffset === offset) {
403
+ return
404
+ }
405
+
398
406
  if (this.isScrollingTimeoutId !== null) {
399
407
  clearTimeout(this.isScrollingTimeoutId)
400
408
  this.isScrollingTimeoutId = null
@@ -407,17 +415,18 @@ export class Virtualizer<
407
415
  }
408
416
  }
409
417
 
410
- this.scrollAdjustments = 0
418
+ this.scrollDirection =
419
+ this.scrollOffset < offset ? 'forward' : 'backward'
411
420
 
412
- if (this.scrollOffset !== offset) {
413
- this.scrollOffset = offset
414
- onIsScrollingChange(true)
415
- }
421
+ this.scrollOffset = offset
416
422
 
417
423
  this.calculateRange()
418
424
 
425
+ onIsScrollingChange(true)
426
+
419
427
  this.isScrollingTimeoutId = setTimeout(() => {
420
428
  this.isScrollingTimeoutId = null
429
+ this.scrollDirection = null
421
430
  onIsScrollingChange(false)
422
431
  }, this.options.scrollingDelay)
423
432
  }),
@@ -435,10 +444,11 @@ export class Virtualizer<
435
444
  () => [
436
445
  this.options.count,
437
446
  this.options.paddingStart,
447
+ this.options.scrollMargin,
438
448
  this.options.getItemKey,
439
- this.itemMeasurementsCache,
449
+ this.itemSizeCache,
440
450
  ],
441
- (count, paddingStart, getItemKey, measurementsCache) => {
451
+ (count, paddingStart, scrollMargin, getItemKey, itemSizeCache) => {
442
452
  const min =
443
453
  this.pendingMeasuredCacheIndexes.length > 0
444
454
  ? Math.min(...this.pendingMeasuredCacheIndexes)
@@ -449,10 +459,10 @@ export class Virtualizer<
449
459
 
450
460
  for (let i = min; i < count; i++) {
451
461
  const key = getItemKey(i)
452
- const measuredSize = measurementsCache[key]
462
+ const measuredSize = itemSizeCache[key]
453
463
  const start = measurements[i - 1]
454
464
  ? measurements[i - 1]!.end
455
- : paddingStart
465
+ : paddingStart + scrollMargin
456
466
  const size =
457
467
  typeof measuredSize === 'number'
458
468
  ? measuredSize
@@ -462,6 +472,7 @@ export class Virtualizer<
462
472
  }
463
473
 
464
474
  this.measurementsCache = measurements
475
+
465
476
  return measurements
466
477
  },
467
478
  {
@@ -557,12 +568,16 @@ export class Virtualizer<
557
568
 
558
569
  const measuredItemSize = this.options.measureElement(node, this)
559
570
 
560
- const itemSize = this.itemMeasurementsCache[item.key] ?? item.size
571
+ const itemSize = this.itemSizeCache[item.key] ?? item.size
561
572
 
562
573
  const delta = measuredItemSize - itemSize
563
574
 
564
575
  if (delta !== 0) {
565
- if (item.start < this.scrollOffset && this.isScrolling) {
576
+ if (
577
+ item.start < this.scrollOffset &&
578
+ this.isScrolling &&
579
+ this.scrollDirection === 'backward'
580
+ ) {
566
581
  if (process.env.NODE_ENV !== 'production' && this.options.debug) {
567
582
  console.info('correction', delta)
568
583
  }
@@ -570,13 +585,12 @@ export class Virtualizer<
570
585
  this._scrollToOffset(this.scrollOffset, {
571
586
  adjustments: (this.scrollAdjustments += delta),
572
587
  behavior: undefined,
573
- sync: false,
574
588
  })
575
589
  }
576
590
 
577
591
  this.pendingMeasuredCacheIndexes.push(index)
578
- this.itemMeasurementsCache = {
579
- ...this.itemMeasurementsCache,
592
+ this.itemSizeCache = {
593
+ ...this.itemSizeCache,
580
594
  [item.key]: measuredItemSize,
581
595
  }
582
596
  this.notify()
@@ -611,10 +625,7 @@ export class Virtualizer<
611
625
  },
612
626
  )
613
627
 
614
- scrollToOffset = (
615
- toOffset: number,
616
- { align = 'start', behavior }: ScrollToOffsetOptions = {},
617
- ) => {
628
+ getOffsetForAlignment = (toOffset: number, align: ScrollAlignment) => {
618
629
  const offset = this.scrollOffset
619
630
  const size = this.getSize()
620
631
 
@@ -628,35 +639,43 @@ export class Virtualizer<
628
639
  }
629
640
  }
630
641
 
642
+ if (align === 'start') {
643
+ return toOffset
644
+ } else if (align === 'end') {
645
+ return toOffset - size
646
+ } else if (align === 'center') {
647
+ return toOffset - size / 2
648
+ }
649
+ return toOffset
650
+ }
651
+
652
+ scrollToOffset = (
653
+ toOffset: number,
654
+ { align = 'start', behavior }: ScrollToOffsetOptions = {},
655
+ ) => {
631
656
  const options = {
632
657
  adjustments: undefined,
633
658
  behavior,
634
659
  sync: false,
635
660
  }
636
- if (align === 'start') {
637
- this._scrollToOffset(toOffset, options)
638
- } else if (align === 'end') {
639
- this._scrollToOffset(toOffset - size, options)
640
- } else if (align === 'center') {
641
- this._scrollToOffset(toOffset - size / 2, options)
642
- }
661
+ this._scrollToOffset(this.getOffsetForAlignment(toOffset, align), options)
643
662
  }
644
663
 
645
664
  scrollToIndex = (
646
665
  index: number,
647
- { align = 'auto', ...rest }: ScrollToIndexOptions = {},
666
+ { align = 'auto', behavior }: ScrollToIndexOptions = {},
648
667
  ) => {
649
668
  this.pendingScrollToIndexCallback = null
650
669
 
651
- const measurements = this.getMeasurements()
652
670
  const offset = this.scrollOffset
653
671
  const size = this.getSize()
654
672
  const { count } = this.options
655
673
 
674
+ const measurements = this.getMeasurements()
656
675
  const measurement = measurements[Math.max(0, Math.min(index, count - 1))]
657
676
 
658
677
  if (!measurement) {
659
- return
678
+ throw new Error(`VirtualItem not found for index = ${index}`)
660
679
  }
661
680
 
662
681
  if (align === 'auto') {
@@ -672,52 +691,64 @@ export class Virtualizer<
672
691
  }
673
692
  }
674
693
 
675
- const toOffset =
676
- align === 'end'
677
- ? measurement.end + this.options.scrollPaddingEnd
678
- : measurement.start - this.options.scrollPaddingStart
694
+ const getOffsetForIndexAndAlignment = (measurement: VirtualItem) => {
695
+ const toOffset =
696
+ align === 'end'
697
+ ? measurement.end + this.options.scrollPaddingEnd
698
+ : measurement.start - this.options.scrollPaddingStart
699
+
700
+ return this.getOffsetForAlignment(toOffset, align)
701
+ }
679
702
 
680
- this.scrollToOffset(toOffset, { align, ...rest })
703
+ const toOffset = getOffsetForIndexAndAlignment(measurement)
704
+
705
+ if (toOffset === offset) {
706
+ return
707
+ }
708
+
709
+ const options = {
710
+ adjustments: undefined,
711
+ behavior,
712
+ }
713
+ this._scrollToOffset(toOffset, options)
681
714
 
682
715
  const isDynamic = Object.keys(this.measureElementCache).length > 0
683
716
 
684
717
  if (isDynamic) {
685
- const didSeen = () =>
686
- typeof this.itemMeasurementsCache[this.options.getItemKey(index)] ===
687
- 'number'
688
-
689
- if (!didSeen()) {
690
- this.pendingScrollToIndexCallback = () => {
691
- if (didSeen()) {
692
- this.pendingScrollToIndexCallback = null
693
- this.scrollToIndex(index, { align, ...rest })
694
- }
695
- }
718
+ this.pendingScrollToIndexCallback = () => {
719
+ this.scrollToIndex(index, { align, behavior })
696
720
  }
697
721
  }
698
722
  }
699
723
 
724
+ scrollBy = (adjustments: number, options?: { behavior: ScrollBehavior }) => {
725
+ this._scrollToOffset(this.scrollOffset, {
726
+ adjustments,
727
+ behavior: options?.behavior,
728
+ })
729
+ }
730
+
700
731
  getTotalSize = () =>
701
732
  (this.getMeasurements()[this.options.count - 1]?.end ||
702
- this.options.paddingStart) + this.options.paddingEnd
733
+ this.options.paddingStart) -
734
+ this.options.scrollMargin +
735
+ this.options.paddingEnd
703
736
 
704
737
  private _scrollToOffset = (
705
738
  offset: number,
706
739
  {
707
740
  adjustments,
708
741
  behavior,
709
- sync,
710
742
  }: {
711
743
  adjustments: number | undefined
712
744
  behavior: ScrollBehavior | undefined
713
- sync: boolean
714
745
  },
715
746
  ) => {
716
- this.options.scrollToFn(offset, { behavior, sync, adjustments }, this)
747
+ this.options.scrollToFn(offset, { behavior, adjustments }, this)
717
748
  }
718
749
 
719
750
  measure = () => {
720
- this.itemMeasurementsCache = {}
751
+ this.itemSizeCache = {}
721
752
  this.notify()
722
753
  }
723
754
  }