@tanstack/virtual-core 3.0.0-beta.20 → 3.0.0-beta.21

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
@@ -24,7 +24,7 @@ export interface Range {
24
24
 
25
25
  type Key = number | string
26
26
 
27
- interface Item {
27
+ export interface VirtualItem {
28
28
  key: Key
29
29
  index: number
30
30
  start: number
@@ -37,10 +37,6 @@ interface Rect {
37
37
  height: number
38
38
  }
39
39
 
40
- export interface VirtualItem<TItemElement> extends Item {
41
- measureElement: (el: TItemElement | null) => void
42
- }
43
-
44
40
  //
45
41
 
46
42
  export const defaultKeyExtractor = (index: number) => index
@@ -183,13 +179,15 @@ const createOffsetObserver = (mode: ObserverMode) => {
183
179
  export const observeElementOffset = createOffsetObserver('element')
184
180
  export const observeWindowOffset = createOffsetObserver('window')
185
181
 
186
- export const measureElement = (
187
- element: unknown,
188
- instance: Virtualizer<any, any>,
182
+ export const measureElement = <TItemElement extends Element>(
183
+ element: TItemElement,
184
+ instance: Virtualizer<any, TItemElement>,
189
185
  ) => {
190
- return (element as Element).getBoundingClientRect()[
191
- instance.options.horizontal ? 'width' : 'height'
192
- ]
186
+ return Math.round(
187
+ element.getBoundingClientRect()[
188
+ instance.options.horizontal ? 'width' : 'height'
189
+ ],
190
+ )
193
191
  }
194
192
 
195
193
  export const windowScroll = (
@@ -219,12 +217,12 @@ export const elementScroll = (
219
217
  }
220
218
 
221
219
  export interface VirtualizerOptions<
222
- TScrollElement = unknown,
223
- TItemElement = unknown,
220
+ TScrollElement extends unknown,
221
+ TItemElement extends Element,
224
222
  > {
225
223
  // Required from the user
226
224
  count: number
227
- getScrollElement: () => TScrollElement
225
+ getScrollElement: () => TScrollElement | null
228
226
  estimateSize: (index: number) => number
229
227
 
230
228
  // Required from the framework adapter (but can be overridden)
@@ -262,15 +260,19 @@ export interface VirtualizerOptions<
262
260
  enableSmoothScroll?: boolean
263
261
  scrollMargin?: number
264
262
  scrollingDelay?: number
263
+ indexAttribute?: string
265
264
  }
266
265
 
267
- export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
266
+ export class Virtualizer<
267
+ TScrollElement extends unknown,
268
+ TItemElement extends Element,
269
+ > {
268
270
  private unsubs: (void | (() => void))[] = []
269
271
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
270
272
  scrollElement: TScrollElement | null = null
271
273
  isScrolling: boolean = false
272
274
  private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
273
- private measurementsCache: Item[] = []
275
+ measurementsCache: VirtualItem[] = []
274
276
  private itemMeasurementsCache: Record<Key, number> = {}
275
277
  private pendingMeasuredCacheIndexes: number[] = []
276
278
  private scrollRect: Rect
@@ -278,10 +280,12 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
278
280
  private scrollDelta: number = 0
279
281
  private destinationOffset: undefined | number
280
282
  private scrollCheckFrame!: ReturnType<typeof setTimeout>
281
- private measureElementCache: Record<
282
- number,
283
- (measurableItem: TItemElement | null) => void
284
- > = {}
283
+ private measureElementCache: Record<string, TItemElement> = {}
284
+ private ro = new ResizeObserver((entries) => {
285
+ entries.forEach((entry) => {
286
+ this._measureElement(entry.target as TItemElement, false)
287
+ })
288
+ })
285
289
  range: { startIndex: number; endIndex: number } = {
286
290
  startIndex: 0,
287
291
  endIndex: 0,
@@ -317,6 +321,7 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
317
321
  initialRect: { width: 0, height: 0 },
318
322
  scrollMargin: 0,
319
323
  scrollingDelay: 150,
324
+ indexAttribute: 'data-index',
320
325
  ...opts,
321
326
  }
322
327
  }
@@ -333,6 +338,9 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
333
338
 
334
339
  _didMount = () => {
335
340
  return () => {
341
+ this.ro.disconnect()
342
+ this.measureElementCache = {}
343
+
336
344
  this.cleanup()
337
345
  }
338
346
  }
@@ -474,68 +482,97 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
474
482
  },
475
483
  )
476
484
 
477
- getVirtualItems = memo(
478
- () => [
479
- this.getIndexes(),
480
- this.getMeasurements(),
481
- this.options.measureElement,
482
- ],
483
- (indexes, measurements, measureElement) => {
484
- const makeMeasureElement =
485
- (index: number) => (measurableItem: TItemElement | null) => {
486
- const item = this.measurementsCache[index]!
485
+ indexFromElement = (node: TItemElement) => {
486
+ const attributeName = this.options.indexAttribute
487
+ const indexStr = node.getAttribute(attributeName)
487
488
 
488
- if (!measurableItem) {
489
- return
490
- }
489
+ if (!indexStr) {
490
+ console.warn(
491
+ `Missing attribute name '${attributeName}={index}' on measured element.`,
492
+ )
493
+ return -1
494
+ }
491
495
 
492
- const measuredItemSize = measureElement(measurableItem, this)
493
- const itemSize = this.itemMeasurementsCache[item.key] ?? item.size
494
-
495
- if (measuredItemSize !== itemSize) {
496
- if (item.start < this.scrollOffset) {
497
- if (process.env.NODE_ENV !== 'production' && this.options.debug) {
498
- console.info('correction', measuredItemSize - itemSize)
499
- }
500
-
501
- if (this.destinationOffset === undefined) {
502
- this.scrollDelta += measuredItemSize - itemSize
503
-
504
- this._scrollToOffset(this.scrollOffset + this.scrollDelta, {
505
- canSmooth: false,
506
- sync: false,
507
- requested: false,
508
- })
509
- }
510
- }
511
-
512
- this.pendingMeasuredCacheIndexes.push(index)
513
- this.itemMeasurementsCache = {
514
- ...this.itemMeasurementsCache,
515
- [item.key]: measuredItemSize,
516
- }
517
- this.notify()
518
- }
496
+ return parseInt(indexStr, 10)
497
+ }
498
+
499
+ _measureElement = (node: TItemElement, _sync: boolean) => {
500
+ const index = this.indexFromElement(node)
501
+
502
+ const item = this.measurementsCache[index]
503
+ if (!item) {
504
+ return
505
+ }
506
+ const key = String(item.key)
507
+
508
+ const prevNode = this.measureElementCache[key]
509
+
510
+ if (!node.isConnected) {
511
+ if (prevNode) {
512
+ this.ro.unobserve(prevNode)
513
+ delete this.measureElementCache[key]
514
+ }
515
+ return
516
+ }
517
+
518
+ if (!prevNode || prevNode !== node) {
519
+ if (prevNode) {
520
+ this.ro.unobserve(prevNode)
521
+ }
522
+ this.measureElementCache[key] = node
523
+ this.ro.observe(node)
524
+ }
525
+
526
+ const measuredItemSize = this.options.measureElement(node, this)
527
+
528
+ const itemSize = this.itemMeasurementsCache[item.key] ?? item.size
529
+
530
+ if (measuredItemSize !== itemSize) {
531
+ if (item.start < this.scrollOffset) {
532
+ if (process.env.NODE_ENV !== 'production' && this.options.debug) {
533
+ console.info('correction', measuredItemSize - itemSize)
519
534
  }
520
535
 
521
- const virtualItems: VirtualItem<TItemElement>[] = []
536
+ if (this.destinationOffset === undefined) {
537
+ this.scrollDelta += measuredItemSize - itemSize
522
538
 
523
- const currentMeasureElements: typeof this.measureElementCache = {}
539
+ this._scrollToOffset(this.scrollOffset + this.scrollDelta, {
540
+ canSmooth: false,
541
+ sync: false,
542
+ requested: false,
543
+ })
544
+ }
545
+ }
546
+
547
+ this.pendingMeasuredCacheIndexes.push(index)
548
+ this.itemMeasurementsCache = {
549
+ ...this.itemMeasurementsCache,
550
+ [item.key]: measuredItemSize,
551
+ }
552
+ this.notify()
553
+ }
554
+ }
555
+
556
+ measureElement = (node: TItemElement | null) => {
557
+ if (!node) {
558
+ return
559
+ }
560
+
561
+ this._measureElement(node, true)
562
+ }
563
+
564
+ getVirtualItems = memo(
565
+ () => [this.getIndexes(), this.getMeasurements()],
566
+ (indexes, measurements) => {
567
+ const virtualItems: VirtualItem[] = []
524
568
 
525
569
  for (let k = 0, len = indexes.length; k < len; k++) {
526
570
  const i = indexes[k]!
527
571
  const measurement = measurements[i]!
528
572
 
529
- const item = {
530
- ...measurement,
531
- measureElement: (currentMeasureElements[i] =
532
- this.measureElementCache[i] ?? makeMeasureElement(i)),
533
- }
534
- virtualItems.push(item)
573
+ virtualItems.push(measurement)
535
574
  }
536
575
 
537
- this.measureElementCache = currentMeasureElements
538
-
539
576
  return virtualItems
540
577
  },
541
578
  {
@@ -695,7 +732,7 @@ function calculateRange({
695
732
  outerSize,
696
733
  scrollOffset,
697
734
  }: {
698
- measurements: Item[]
735
+ measurements: VirtualItem[]
699
736
  outerSize: number
700
737
  scrollOffset: number
701
738
  }) {