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

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,24 @@ 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<Key, TItemElement> = {}
284
+ private getResizeObserver = (() => {
285
+ let _ro: ResizeObserver | null = null
286
+
287
+ return () => {
288
+ if (_ro) {
289
+ return _ro
290
+ } else if (typeof ResizeObserver !== 'undefined') {
291
+ return (_ro = new ResizeObserver((entries) => {
292
+ entries.forEach((entry) => {
293
+ this._measureElement(entry.target as TItemElement, false)
294
+ })
295
+ }))
296
+ } else {
297
+ return null
298
+ }
299
+ }
300
+ })()
285
301
  range: { startIndex: number; endIndex: number } = {
286
302
  startIndex: 0,
287
303
  endIndex: 0,
@@ -317,6 +333,7 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
317
333
  initialRect: { width: 0, height: 0 },
318
334
  scrollMargin: 0,
319
335
  scrollingDelay: 150,
336
+ indexAttribute: 'data-index',
320
337
  ...opts,
321
338
  }
322
339
  }
@@ -333,6 +350,9 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
333
350
 
334
351
  _didMount = () => {
335
352
  return () => {
353
+ this.getResizeObserver()?.disconnect()
354
+ this.measureElementCache = {}
355
+
336
356
  this.cleanup()
337
357
  }
338
358
  }
@@ -474,68 +494,98 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
474
494
  },
475
495
  )
476
496
 
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]!
497
+ indexFromElement = (node: TItemElement) => {
498
+ const attributeName = this.options.indexAttribute
499
+ const indexStr = node.getAttribute(attributeName)
487
500
 
488
- if (!measurableItem) {
489
- return
490
- }
501
+ if (!indexStr) {
502
+ console.warn(
503
+ `Missing attribute name '${attributeName}={index}' on measured element.`,
504
+ )
505
+ return -1
506
+ }
491
507
 
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
- }
508
+ return parseInt(indexStr, 10)
509
+ }
510
+
511
+ private _measureElement = (node: TItemElement, _sync: boolean) => {
512
+ const index = this.indexFromElement(node)
513
+
514
+ const item = this.measurementsCache[index]
515
+ if (!item) {
516
+ return
517
+ }
518
+
519
+ const prevNode = this.measureElementCache[item.key]
520
+
521
+ const ro = this.getResizeObserver()
522
+
523
+ if (!node.isConnected) {
524
+ if (prevNode) {
525
+ ro?.unobserve(prevNode)
526
+ delete this.measureElementCache[item.key]
527
+ }
528
+ return
529
+ }
530
+
531
+ if (!prevNode || prevNode !== node) {
532
+ if (prevNode) {
533
+ ro?.unobserve(prevNode)
534
+ }
535
+ this.measureElementCache[item.key] = node
536
+ ro?.observe(node)
537
+ }
538
+
539
+ const measuredItemSize = this.options.measureElement(node, this)
540
+
541
+ const itemSize = this.itemMeasurementsCache[item.key] ?? item.size
542
+
543
+ if (measuredItemSize !== itemSize) {
544
+ if (item.start < this.scrollOffset) {
545
+ if (process.env.NODE_ENV !== 'production' && this.options.debug) {
546
+ console.info('correction', measuredItemSize - itemSize)
519
547
  }
520
548
 
521
- const virtualItems: VirtualItem<TItemElement>[] = []
549
+ if (this.destinationOffset === undefined) {
550
+ this.scrollDelta += measuredItemSize - itemSize
522
551
 
523
- const currentMeasureElements: typeof this.measureElementCache = {}
552
+ this._scrollToOffset(this.scrollOffset + this.scrollDelta, {
553
+ canSmooth: false,
554
+ sync: false,
555
+ requested: false,
556
+ })
557
+ }
558
+ }
559
+
560
+ this.pendingMeasuredCacheIndexes.push(index)
561
+ this.itemMeasurementsCache = {
562
+ ...this.itemMeasurementsCache,
563
+ [item.key]: measuredItemSize,
564
+ }
565
+ this.notify()
566
+ }
567
+ }
568
+
569
+ measureElement = (node: TItemElement | null) => {
570
+ if (!node) {
571
+ return
572
+ }
573
+
574
+ this._measureElement(node, true)
575
+ }
576
+
577
+ getVirtualItems = memo(
578
+ () => [this.getIndexes(), this.getMeasurements()],
579
+ (indexes, measurements) => {
580
+ const virtualItems: VirtualItem[] = []
524
581
 
525
582
  for (let k = 0, len = indexes.length; k < len; k++) {
526
583
  const i = indexes[k]!
527
584
  const measurement = measurements[i]!
528
585
 
529
- const item = {
530
- ...measurement,
531
- measureElement: (currentMeasureElements[i] =
532
- this.measureElementCache[i] ?? makeMeasureElement(i)),
533
- }
534
- virtualItems.push(item)
586
+ virtualItems.push(measurement)
535
587
  }
536
588
 
537
- this.measureElementCache = currentMeasureElements
538
-
539
589
  return virtualItems
540
590
  },
541
591
  {
@@ -695,7 +745,7 @@ function calculateRange({
695
745
  outerSize,
696
746
  scrollOffset,
697
747
  }: {
698
- measurements: Item[]
748
+ measurements: VirtualItem[]
699
749
  outerSize: number
700
750
  scrollOffset: number
701
751
  }) {