@tanstack/virtual-core 3.5.1 → 3.6.0

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
@@ -318,6 +318,7 @@ export interface VirtualizerOptions<
318
318
  initialMeasurementsCache?: VirtualItem[]
319
319
  lanes?: number
320
320
  isScrollingResetDelay?: number
321
+ enabled?: boolean
321
322
  }
322
323
 
323
324
  export class Virtualizer<
@@ -333,8 +334,8 @@ export class Virtualizer<
333
334
  measurementsCache: VirtualItem[] = []
334
335
  private itemSizeCache = new Map<Key, number>()
335
336
  private pendingMeasuredCacheIndexes: number[] = []
336
- scrollRect: Rect
337
- scrollOffset: number
337
+ scrollRect: Rect | null = null
338
+ scrollOffset: number | null = null
338
339
  scrollDirection: ScrollDirection | null = null
339
340
  private scrollAdjustments: number = 0
340
341
  shouldAdjustScrollPositionOnItemSizeChange:
@@ -375,17 +376,6 @@ export class Virtualizer<
375
376
 
376
377
  constructor(opts: VirtualizerOptions<TScrollElement, TItemElement>) {
377
378
  this.setOptions(opts)
378
- this.scrollRect = this.options.initialRect
379
- this.scrollOffset =
380
- typeof this.options.initialOffset === 'function'
381
- ? this.options.initialOffset()
382
- : this.options.initialOffset
383
- this.measurementsCache = this.options.initialMeasurementsCache
384
- this.measurementsCache.forEach((item) => {
385
- this.itemSizeCache.set(item.key, item.size)
386
- })
387
-
388
- this.notify(false, false)
389
379
  }
390
380
 
391
381
  setOptions = (opts: VirtualizerOptions<TScrollElement, TItemElement>) => {
@@ -413,6 +403,7 @@ export class Virtualizer<
413
403
  initialMeasurementsCache: [],
414
404
  lanes: 1,
415
405
  isScrollingResetDelay: 150,
406
+ enabled: true,
416
407
  ...opts,
417
408
  }
418
409
  }
@@ -437,22 +428,30 @@ export class Virtualizer<
437
428
  this.unsubs.filter(Boolean).forEach((d) => d!())
438
429
  this.unsubs = []
439
430
  this.scrollElement = null
431
+ this.targetWindow = null
432
+ this.observer.disconnect()
433
+ this.measureElementCache.clear()
440
434
  }
441
435
 
442
436
  _didMount = () => {
443
- this.measureElementCache.forEach(this.observer.observe)
444
437
  return () => {
445
- this.observer.disconnect()
446
438
  this.cleanup()
447
439
  }
448
440
  }
449
441
 
450
442
  _willUpdate = () => {
451
- const scrollElement = this.options.getScrollElement()
443
+ const scrollElement = this.options.enabled
444
+ ? this.options.getScrollElement()
445
+ : null
452
446
 
453
447
  if (this.scrollElement !== scrollElement) {
454
448
  this.cleanup()
455
449
 
450
+ if (!scrollElement) {
451
+ this.notify(false, false)
452
+ return
453
+ }
454
+
456
455
  this.scrollElement = scrollElement
457
456
 
458
457
  if (this.scrollElement && 'ownerDocument' in this.scrollElement) {
@@ -461,7 +460,7 @@ export class Virtualizer<
461
460
  this.targetWindow = this.scrollElement?.window ?? null
462
461
  }
463
462
 
464
- this._scrollToOffset(this.scrollOffset, {
463
+ this._scrollToOffset(this.getScrollOffset(), {
465
464
  adjustments: undefined,
466
465
  behavior: undefined,
467
466
  })
@@ -477,7 +476,7 @@ export class Virtualizer<
477
476
  this.options.observeElementOffset(this, (offset, isScrolling) => {
478
477
  this.scrollAdjustments = 0
479
478
  this.scrollDirection = isScrolling
480
- ? this.scrollOffset < offset
479
+ ? this.getScrollOffset() < offset
481
480
  ? 'forward'
482
481
  : 'backward'
483
482
  : null
@@ -493,29 +492,30 @@ export class Virtualizer<
493
492
  }
494
493
 
495
494
  private getSize = () => {
495
+ if (!this.options.enabled) {
496
+ this.scrollRect = null
497
+ return 0
498
+ }
499
+
500
+ this.scrollRect = this.scrollRect ?? this.options.initialRect
501
+
496
502
  return this.scrollRect[this.options.horizontal ? 'width' : 'height']
497
503
  }
498
504
 
499
- private getMeasurementOptions = memo(
500
- () => [
501
- this.options.count,
502
- this.options.paddingStart,
503
- this.options.scrollMargin,
504
- this.options.getItemKey,
505
- ],
506
- (count, paddingStart, scrollMargin, getItemKey) => {
507
- this.pendingMeasuredCacheIndexes = []
508
- return {
509
- count,
510
- paddingStart,
511
- scrollMargin,
512
- getItemKey,
513
- }
514
- },
515
- {
516
- key: false,
517
- },
518
- )
505
+ private getScrollOffset = () => {
506
+ if (!this.options.enabled) {
507
+ this.scrollOffset = null
508
+ return 0
509
+ }
510
+
511
+ this.scrollOffset =
512
+ this.scrollOffset ??
513
+ (typeof this.options.initialOffset === 'function'
514
+ ? this.options.initialOffset()
515
+ : this.options.initialOffset)
516
+
517
+ return this.scrollOffset
518
+ }
519
519
 
520
520
  private getFurthestMeasurement = (
521
521
  measurements: VirtualItem[],
@@ -558,9 +558,48 @@ export class Virtualizer<
558
558
  : undefined
559
559
  }
560
560
 
561
+ private getMeasurementOptions = memo(
562
+ () => [
563
+ this.options.count,
564
+ this.options.paddingStart,
565
+ this.options.scrollMargin,
566
+ this.options.getItemKey,
567
+ this.options.enabled,
568
+ ],
569
+ (count, paddingStart, scrollMargin, getItemKey, enabled) => {
570
+ this.pendingMeasuredCacheIndexes = []
571
+ return {
572
+ count,
573
+ paddingStart,
574
+ scrollMargin,
575
+ getItemKey,
576
+ enabled,
577
+ }
578
+ },
579
+ {
580
+ key: false,
581
+ },
582
+ )
583
+
561
584
  private getMeasurements = memo(
562
585
  () => [this.getMeasurementOptions(), this.itemSizeCache],
563
- ({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => {
586
+ (
587
+ { count, paddingStart, scrollMargin, getItemKey, enabled },
588
+ itemSizeCache,
589
+ ) => {
590
+ if (!enabled) {
591
+ this.measurementsCache = []
592
+ this.itemSizeCache.clear()
593
+ return []
594
+ }
595
+
596
+ if (this.measurementsCache.length === 0) {
597
+ this.measurementsCache = this.options.initialMeasurementsCache
598
+ this.measurementsCache.forEach((item) => {
599
+ this.itemSizeCache.set(item.key, item.size)
600
+ })
601
+ }
602
+
564
603
  const min =
565
604
  this.pendingMeasuredCacheIndexes.length > 0
566
605
  ? Math.min(...this.pendingMeasuredCacheIndexes)
@@ -614,7 +653,7 @@ export class Virtualizer<
614
653
  )
615
654
 
616
655
  calculateRange = memo(
617
- () => [this.getMeasurements(), this.getSize(), this.scrollOffset],
656
+ () => [this.getMeasurements(), this.getSize(), this.getScrollOffset()],
618
657
  (measurements, outerSize, scrollOffset) => {
619
658
  return (this.range =
620
659
  measurements.length > 0 && outerSize > 0
@@ -672,7 +711,7 @@ export class Virtualizer<
672
711
  node: TItemElement,
673
712
  entry: ResizeObserverEntry | undefined,
674
713
  ) => {
675
- const item = this.measurementsCache[this.indexFromElement(node)]
714
+ const item = this.getMeasurements()[this.indexFromElement(node)]
676
715
 
677
716
  if (!item || !node.isConnected) {
678
717
  this.measureElementCache.forEach((cached, key) => {
@@ -707,13 +746,13 @@ export class Virtualizer<
707
746
  if (
708
747
  this.shouldAdjustScrollPositionOnItemSizeChange !== undefined
709
748
  ? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this)
710
- : item.start < this.scrollOffset + this.scrollAdjustments
749
+ : item.start < this.getScrollOffset() + this.scrollAdjustments
711
750
  ) {
712
751
  if (process.env.NODE_ENV !== 'production' && this.options.debug) {
713
752
  console.info('correction', delta)
714
753
  }
715
754
 
716
- this._scrollToOffset(this.scrollOffset, {
755
+ this._scrollToOffset(this.getScrollOffset(), {
717
756
  adjustments: (this.scrollAdjustments += delta),
718
757
  behavior: undefined,
719
758
  })
@@ -756,7 +795,9 @@ export class Virtualizer<
756
795
 
757
796
  getVirtualItemForOffset = (offset: number) => {
758
797
  const measurements = this.getMeasurements()
759
-
798
+ if (measurements.length === 0) {
799
+ return undefined
800
+ }
760
801
  return notUndefined(
761
802
  measurements[
762
803
  findNearestBinarySearch(
@@ -771,11 +812,12 @@ export class Virtualizer<
771
812
 
772
813
  getOffsetForAlignment = (toOffset: number, align: ScrollAlignment) => {
773
814
  const size = this.getSize()
815
+ const scrollOffset = this.getScrollOffset()
774
816
 
775
817
  if (align === 'auto') {
776
- if (toOffset <= this.scrollOffset) {
818
+ if (toOffset <= scrollOffset) {
777
819
  align = 'start'
778
- } else if (toOffset >= this.scrollOffset + size) {
820
+ } else if (toOffset >= scrollOffset + size) {
779
821
  align = 'end'
780
822
  } else {
781
823
  align = 'start'
@@ -799,7 +841,7 @@ export class Virtualizer<
799
841
  : this.scrollElement[scrollSizeProp]
800
842
  : 0
801
843
 
802
- const maxOffset = scrollSize - this.getSize()
844
+ const maxOffset = scrollSize - size
803
845
 
804
846
  return Math.max(Math.min(maxOffset, toOffset), 0)
805
847
  }
@@ -807,28 +849,28 @@ export class Virtualizer<
807
849
  getOffsetForIndex = (index: number, align: ScrollAlignment = 'auto') => {
808
850
  index = Math.max(0, Math.min(index, this.options.count - 1))
809
851
 
810
- const measurement = notUndefined(this.getMeasurements()[index])
852
+ const item = this.getMeasurements()[index]
853
+ if (!item) {
854
+ return undefined
855
+ }
856
+
857
+ const size = this.getSize()
858
+ const scrollOffset = this.getScrollOffset()
811
859
 
812
860
  if (align === 'auto') {
813
- if (
814
- measurement.end >=
815
- this.scrollOffset + this.getSize() - this.options.scrollPaddingEnd
816
- ) {
861
+ if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) {
817
862
  align = 'end'
818
- } else if (
819
- measurement.start <=
820
- this.scrollOffset + this.options.scrollPaddingStart
821
- ) {
863
+ } else if (item.start <= scrollOffset + this.options.scrollPaddingStart) {
822
864
  align = 'start'
823
865
  } else {
824
- return [this.scrollOffset, align] as const
866
+ return [scrollOffset, align] as const
825
867
  }
826
868
  }
827
869
 
828
870
  const toOffset =
829
871
  align === 'end'
830
- ? measurement.end + this.options.scrollPaddingEnd
831
- : measurement.start - this.options.scrollPaddingStart
872
+ ? item.end + this.options.scrollPaddingEnd
873
+ : item.start - this.options.scrollPaddingStart
832
874
 
833
875
  return [this.getOffsetForAlignment(toOffset, align), align] as const
834
876
  }
@@ -874,9 +916,12 @@ export class Virtualizer<
874
916
  )
875
917
  }
876
918
 
877
- const [toOffset, align] = this.getOffsetForIndex(index, initialAlign)
919
+ const offsetAndAlign = this.getOffsetForIndex(index, initialAlign)
920
+ if (!offsetAndAlign) return
921
+
922
+ const [offset, align] = offsetAndAlign
878
923
 
879
- this._scrollToOffset(toOffset, { adjustments: undefined, behavior })
924
+ this._scrollToOffset(offset, { adjustments: undefined, behavior })
880
925
 
881
926
  if (behavior !== 'smooth' && this.isDynamicMode() && this.targetWindow) {
882
927
  this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => {
@@ -887,9 +932,11 @@ export class Virtualizer<
887
932
  )
888
933
 
889
934
  if (elementInDOM) {
890
- const [toOffset] = this.getOffsetForIndex(index, align)
935
+ const [latestOffset] = notUndefined(
936
+ this.getOffsetForIndex(index, align),
937
+ )
891
938
 
892
- if (!approxEqual(toOffset, this.scrollOffset)) {
939
+ if (!approxEqual(latestOffset, this.getScrollOffset())) {
893
940
  this.scrollToIndex(index, { align, behavior })
894
941
  }
895
942
  } else {
@@ -908,7 +955,7 @@ export class Virtualizer<
908
955
  )
909
956
  }
910
957
 
911
- this._scrollToOffset(this.scrollOffset + delta, {
958
+ this._scrollToOffset(this.getScrollOffset() + delta, {
912
959
  adjustments: undefined,
913
960
  behavior,
914
961
  })