@tanstack/virtual-core 3.0.0-beta.34 → 3.0.0-beta.36

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
@@ -82,10 +82,16 @@ export const observeElementRect = (
82
82
  cb: (rect: Rect) => void,
83
83
  ) => {
84
84
  const observer = new ResizeObserver((entries) => {
85
- cb({
86
- width: entries[0]?.contentRect.width as number,
87
- height: entries[0]?.contentRect.height as number,
88
- })
85
+ const entry = entries[0]
86
+ if (entry) {
87
+ const { width, height } = entry.contentRect
88
+ cb({
89
+ width: Math.round(width),
90
+ height: Math.round(height),
91
+ })
92
+ } else {
93
+ cb({ width: 0, height: 0 })
94
+ }
89
95
  })
90
96
 
91
97
  if (!instance.scrollElement) {
@@ -282,6 +288,7 @@ export class Virtualizer<
282
288
  scrollElement: TScrollElement | null = null
283
289
  isScrolling: boolean = false
284
290
  private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
291
+ private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
285
292
  measurementsCache: VirtualItem[] = []
286
293
  private itemSizeCache: Record<Key, number> = {}
287
294
  private pendingMeasuredCacheIndexes: number[] = []
@@ -290,7 +297,6 @@ export class Virtualizer<
290
297
  scrollDirection: ScrollDirection | null = null
291
298
  private scrollAdjustments: number = 0
292
299
  private measureElementCache: Record<Key, TItemElement> = {}
293
- private pendingScrollToIndexCallback: (() => void) | null = null
294
300
  private getResizeObserver = (() => {
295
301
  let _ro: ResizeObserver | null = null
296
302
 
@@ -374,8 +380,6 @@ export class Virtualizer<
374
380
  }
375
381
 
376
382
  _willUpdate = () => {
377
- this.pendingScrollToIndexCallback?.()
378
-
379
383
  const scrollElement = this.options.getScrollElement()
380
384
 
381
385
  if (this.scrollElement !== scrollElement) {
@@ -573,11 +577,7 @@ export class Virtualizer<
573
577
  const delta = measuredItemSize - itemSize
574
578
 
575
579
  if (delta !== 0) {
576
- if (
577
- item.start < this.scrollOffset &&
578
- this.isScrolling &&
579
- this.scrollDirection === 'backward'
580
- ) {
580
+ if (item.start < this.scrollOffset) {
581
581
  if (process.env.NODE_ENV !== 'production' && this.options.debug) {
582
582
  console.info('correction', delta)
583
583
  }
@@ -653,6 +653,15 @@ export class Virtualizer<
653
653
  toOffset: number,
654
654
  { align = 'start', behavior }: ScrollToOffsetOptions = {},
655
655
  ) => {
656
+ const isDynamic = Object.keys(this.measureElementCache).length > 0
657
+
658
+ if (isDynamic && behavior === 'smooth') {
659
+ console.warn(
660
+ 'The `smooth` scroll behavior is not supported with dynamic size.',
661
+ )
662
+ return
663
+ }
664
+
656
665
  const options = {
657
666
  adjustments: undefined,
658
667
  behavior,
@@ -665,25 +674,43 @@ export class Virtualizer<
665
674
  index: number,
666
675
  { align = 'auto', behavior }: ScrollToIndexOptions = {},
667
676
  ) => {
668
- this.pendingScrollToIndexCallback = null
677
+ if (this.scrollToIndexTimeoutId !== null) {
678
+ clearTimeout(this.scrollToIndexTimeoutId)
679
+ this.scrollToIndexTimeoutId = null
680
+ }
669
681
 
670
- const offset = this.scrollOffset
671
- const size = this.getSize()
672
- const { count } = this.options
682
+ const isDynamic = Object.keys(this.measureElementCache).length > 0
673
683
 
674
- const measurements = this.getMeasurements()
675
- const measurement = measurements[Math.max(0, Math.min(index, count - 1))]
684
+ if (isDynamic && behavior === 'smooth') {
685
+ console.warn(
686
+ 'The `smooth` scroll behavior is not supported with dynamic size.',
687
+ )
688
+ return
689
+ }
690
+
691
+ const getMeasurement = () => {
692
+ const measurements = this.getMeasurements()
693
+ const measurement =
694
+ measurements[Math.max(0, Math.min(index, this.options.count - 1))]
676
695
 
677
- if (!measurement) {
678
- throw new Error(`VirtualItem not found for index = ${index}`)
696
+ if (!measurement) {
697
+ throw new Error(`VirtualItem not found for index = ${index}`)
698
+ }
699
+
700
+ return measurement
679
701
  }
680
702
 
703
+ const measurement = getMeasurement()
704
+
681
705
  if (align === 'auto') {
682
- if (measurement.end >= offset + size - this.options.scrollPaddingEnd) {
706
+ if (
707
+ measurement.end >=
708
+ this.scrollOffset + this.getSize() - this.options.scrollPaddingEnd
709
+ ) {
683
710
  align = 'end'
684
711
  } else if (
685
712
  measurement.start <=
686
- offset + this.options.scrollPaddingStart
713
+ this.scrollOffset + this.options.scrollPaddingStart
687
714
  ) {
688
715
  align = 'start'
689
716
  } else {
@@ -697,34 +724,61 @@ export class Virtualizer<
697
724
  ? measurement.end + this.options.scrollPaddingEnd
698
725
  : measurement.start - this.options.scrollPaddingStart
699
726
 
700
- return this.getOffsetForAlignment(toOffset, align)
701
- }
727
+ const sizeProp = this.options.horizontal ? 'scrollWidth' : 'scrollHeight'
728
+ const scrollSize = this.scrollElement
729
+ ? 'document' in this.scrollElement
730
+ ? this.scrollElement.document.documentElement[sizeProp]
731
+ : this.scrollElement[sizeProp]
732
+ : 0
702
733
 
703
- const toOffset = getOffsetForIndexAndAlignment(measurement)
734
+ const maxOffset = scrollSize - this.getSize()
704
735
 
705
- if (Math.round(toOffset) === Math.round(offset)) {
706
- return
736
+ return Math.min(maxOffset, this.getOffsetForAlignment(toOffset, align))
707
737
  }
708
738
 
739
+ const toOffset = getOffsetForIndexAndAlignment(measurement)
740
+
709
741
  const options = {
710
742
  adjustments: undefined,
711
743
  behavior,
712
744
  }
713
745
  this._scrollToOffset(toOffset, options)
714
746
 
715
- const isDynamic = Object.keys(this.measureElementCache).length > 0
747
+ const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1
716
748
 
717
749
  if (isDynamic) {
718
- this.pendingScrollToIndexCallback = () => {
719
- this.scrollToIndex(index, { align, behavior })
720
- }
750
+ this.scrollToIndexTimeoutId = setTimeout(() => {
751
+ this.scrollToIndexTimeoutId = null
752
+
753
+ const elementInDOM =
754
+ !!this.measureElementCache[this.options.getItemKey(index)]
755
+
756
+ if (elementInDOM) {
757
+ const toOffset = getOffsetForIndexAndAlignment(getMeasurement())
758
+
759
+ if (!approxEqual(toOffset, this.scrollOffset)) {
760
+ this.scrollToIndex(index, { align, behavior })
761
+ }
762
+ } else {
763
+ this.scrollToIndex(index, { align, behavior })
764
+ }
765
+ })
721
766
  }
722
767
  }
723
768
 
724
- scrollBy = (adjustments: number, options?: { behavior: ScrollBehavior }) => {
725
- this._scrollToOffset(this.scrollOffset, {
726
- adjustments,
727
- behavior: options?.behavior,
769
+ scrollBy = (delta: number, { behavior }: ScrollToOffsetOptions = {}) => {
770
+ const isDynamic = Object.keys(this.measureElementCache).length > 0
771
+
772
+ if (isDynamic && behavior === 'smooth') {
773
+ console.warn(
774
+ 'The `smooth` scroll behavior is not supported with dynamic size.',
775
+ )
776
+ return
777
+ }
778
+
779
+ this._scrollToOffset(this.scrollOffset + delta, {
780
+ adjustments: undefined,
781
+ behavior,
728
782
  })
729
783
  }
730
784