@tanstack/virtual-core 3.3.0 → 3.4.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
@@ -1,4 +1,4 @@
1
- import { approxEqual, memo, notUndefined } from './utils'
1
+ import { approxEqual, memo, notUndefined, debounce } from './utils'
2
2
 
3
3
  export * from './utils'
4
4
 
@@ -98,6 +98,10 @@ export const observeElementRect = <T extends Element>(
98
98
  }
99
99
  }
100
100
 
101
+ const addEventListenerOptions = {
102
+ passive: true,
103
+ }
104
+
101
105
  export const observeWindowRect = (
102
106
  instance: Virtualizer<Window, any>,
103
107
  cb: (rect: Rect) => void,
@@ -112,58 +116,81 @@ export const observeWindowRect = (
112
116
  }
113
117
  handler()
114
118
 
115
- element.addEventListener('resize', handler, {
116
- passive: true,
117
- })
119
+ element.addEventListener('resize', handler, addEventListenerOptions)
118
120
 
119
121
  return () => {
120
122
  element.removeEventListener('resize', handler)
121
123
  }
122
124
  }
123
125
 
126
+ const supportsScrollend =
127
+ typeof window == 'undefined' ? true : 'onscrollend' in window
128
+
124
129
  export const observeElementOffset = <T extends Element>(
125
130
  instance: Virtualizer<T, any>,
126
- cb: (offset: number) => void,
131
+ cb: (offset: number, isScrolling: boolean) => void,
127
132
  ) => {
128
133
  const element = instance.scrollElement
129
134
  if (!element) {
130
135
  return
131
136
  }
132
137
 
133
- const handler = () => {
134
- cb(element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop'])
138
+ let offset = 0
139
+ const fallback = supportsScrollend
140
+ ? () => undefined
141
+ : debounce(() => {
142
+ cb(offset, false)
143
+ }, 150)
144
+
145
+ const createHandler = (isScrolling: boolean) => () => {
146
+ offset = element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop']
147
+ fallback()
148
+ cb(offset, isScrolling)
135
149
  }
136
- handler()
150
+ const handler = createHandler(true)
151
+ const endHandler = createHandler(false)
152
+ endHandler()
137
153
 
138
- element.addEventListener('scroll', handler, {
139
- passive: true,
140
- })
154
+ element.addEventListener('scroll', handler, addEventListenerOptions)
155
+ element.addEventListener('scrollend', endHandler, addEventListenerOptions)
141
156
 
142
157
  return () => {
143
158
  element.removeEventListener('scroll', handler)
159
+ element.removeEventListener('scrollend', endHandler)
144
160
  }
145
161
  }
146
162
 
147
163
  export const observeWindowOffset = (
148
164
  instance: Virtualizer<Window, any>,
149
- cb: (offset: number) => void,
165
+ cb: (offset: number, isScrolling: boolean) => void,
150
166
  ) => {
151
167
  const element = instance.scrollElement
152
168
  if (!element) {
153
169
  return
154
170
  }
155
171
 
156
- const handler = () => {
157
- cb(element[instance.options.horizontal ? 'scrollX' : 'scrollY'])
172
+ let offset = 0
173
+ const fallback = supportsScrollend
174
+ ? () => undefined
175
+ : debounce(() => {
176
+ cb(offset, false)
177
+ }, 150)
178
+
179
+ const createHandler = (isScrolling: boolean) => () => {
180
+ offset = element[instance.options.horizontal ? 'scrollX' : 'scrollY']
181
+ fallback()
182
+ cb(offset, isScrolling)
158
183
  }
159
- handler()
184
+ const handler = createHandler(true)
185
+ const endHandler = createHandler(false)
186
+ endHandler()
160
187
 
161
- element.addEventListener('scroll', handler, {
162
- passive: true,
163
- })
188
+ element.addEventListener('scroll', handler, addEventListenerOptions)
189
+ element.addEventListener('scrollend', endHandler, addEventListenerOptions)
164
190
 
165
191
  return () => {
166
192
  element.removeEventListener('scroll', handler)
193
+ element.removeEventListener('scrollend', endHandler)
167
194
  }
168
195
  }
169
196
 
@@ -241,7 +268,7 @@ export interface VirtualizerOptions<
241
268
  ) => void | (() => void)
242
269
  observeElementOffset: (
243
270
  instance: Virtualizer<TScrollElement, TItemElement>,
244
- cb: (offset: number) => void,
271
+ cb: (offset: number, isScrolling: boolean) => void,
245
272
  ) => void | (() => void)
246
273
 
247
274
  // Optional
@@ -267,7 +294,6 @@ export interface VirtualizerOptions<
267
294
  rangeExtractor?: (range: Range) => number[]
268
295
  scrollMargin?: number
269
296
  gap?: number
270
- scrollingDelay?: number
271
297
  indexAttribute?: string
272
298
  initialMeasurementsCache?: VirtualItem[]
273
299
  lanes?: number
@@ -281,7 +307,6 @@ export class Virtualizer<
281
307
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
282
308
  scrollElement: TScrollElement | null = null
283
309
  isScrolling: boolean = false
284
- private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
285
310
  private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
286
311
  measurementsCache: VirtualItem[] = []
287
312
  private itemSizeCache = new Map<Key, number>()
@@ -336,7 +361,7 @@ export class Virtualizer<
336
361
  this.itemSizeCache.set(item.key, item.size)
337
362
  })
338
363
 
339
- this.maybeNotify()
364
+ this.notify(false, false)
340
365
  }
341
366
 
342
367
  setOptions = (opts: VirtualizerOptions<TScrollElement, TItemElement>) => {
@@ -360,7 +385,6 @@ export class Virtualizer<
360
385
  initialRect: { width: 0, height: 0 },
361
386
  scrollMargin: 0,
362
387
  gap: 0,
363
- scrollingDelay: 150,
364
388
  indexAttribute: 'data-index',
365
389
  initialMeasurementsCache: [],
366
390
  lanes: 1,
@@ -368,34 +392,22 @@ export class Virtualizer<
368
392
  }
369
393
  }
370
394
 
371
- private notify = (sync: boolean) => {
372
- this.options.onChange?.(this, sync)
395
+ private notify = (force: boolean, sync: boolean) => {
396
+ const { startIndex, endIndex } = this.range ?? {
397
+ startIndex: undefined,
398
+ endIndex: undefined,
399
+ }
400
+ const range = this.calculateRange()
401
+
402
+ if (
403
+ force ||
404
+ startIndex !== range?.startIndex ||
405
+ endIndex !== range?.endIndex
406
+ ) {
407
+ this.options.onChange?.(this, sync)
408
+ }
373
409
  }
374
410
 
375
- private maybeNotify = memo(
376
- () => {
377
- this.calculateRange()
378
-
379
- return [
380
- this.isScrolling,
381
- this.range ? this.range.startIndex : null,
382
- this.range ? this.range.endIndex : null,
383
- ]
384
- },
385
- (isScrolling) => {
386
- this.notify(isScrolling)
387
- },
388
- {
389
- key: process.env.NODE_ENV !== 'production' && 'maybeNotify',
390
- debug: () => this.options.debug,
391
- initialDeps: [
392
- this.isScrolling,
393
- this.range ? this.range.startIndex : null,
394
- this.range ? this.range.endIndex : null,
395
- ] as [boolean, number | null, number | null],
396
- },
397
- )
398
-
399
411
  private cleanup = () => {
400
412
  this.unsubs.filter(Boolean).forEach((d) => d!())
401
413
  this.unsubs = []
@@ -426,37 +438,24 @@ export class Virtualizer<
426
438
  this.unsubs.push(
427
439
  this.options.observeElementRect(this, (rect) => {
428
440
  this.scrollRect = rect
429
- this.maybeNotify()
441
+ this.notify(false, false)
430
442
  }),
431
443
  )
432
444
 
433
445
  this.unsubs.push(
434
- this.options.observeElementOffset(this, (offset) => {
446
+ this.options.observeElementOffset(this, (offset, isScrolling) => {
435
447
  this.scrollAdjustments = 0
436
-
437
- if (this.scrollOffset === offset) {
438
- return
439
- }
440
-
441
- if (this.isScrollingTimeoutId !== null) {
442
- clearTimeout(this.isScrollingTimeoutId)
443
- this.isScrollingTimeoutId = null
444
- }
445
-
446
- this.isScrolling = true
447
- this.scrollDirection =
448
- this.scrollOffset < offset ? 'forward' : 'backward'
448
+ this.scrollDirection = isScrolling
449
+ ? this.scrollOffset < offset
450
+ ? 'forward'
451
+ : 'backward'
452
+ : null
449
453
  this.scrollOffset = offset
450
454
 
451
- this.maybeNotify()
452
-
453
- this.isScrollingTimeoutId = setTimeout(() => {
454
- this.isScrollingTimeoutId = null
455
- this.isScrolling = false
456
- this.scrollDirection = null
455
+ const prevIsScrolling = this.isScrolling
456
+ this.isScrolling = isScrolling
457
457
 
458
- this.maybeNotify()
459
- }, this.options.scrollingDelay)
458
+ this.notify(prevIsScrolling !== isScrolling, isScrolling)
460
459
  }),
461
460
  )
462
461
  }
@@ -466,7 +465,7 @@ export class Virtualizer<
466
465
  return this.scrollRect[this.options.horizontal ? 'width' : 'height']
467
466
  }
468
467
 
469
- private memoOptions = memo(
468
+ private getMeasurementOptions = memo(
470
469
  () => [
471
470
  this.options.count,
472
471
  this.options.paddingStart,
@@ -529,7 +528,7 @@ export class Virtualizer<
529
528
  }
530
529
 
531
530
  private getMeasurements = memo(
532
- () => [this.memoOptions(), this.itemSizeCache],
531
+ () => [this.getMeasurementOptions(), this.itemSizeCache],
533
532
  ({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => {
534
533
  const min =
535
534
  this.pendingMeasuredCacheIndexes.length > 0
@@ -612,7 +611,8 @@ export class Virtualizer<
612
611
  return range === null
613
612
  ? []
614
613
  : rangeExtractor({
615
- ...range,
614
+ startIndex: range.startIndex,
615
+ endIndex: range.endIndex,
616
616
  overscan,
617
617
  count,
618
618
  })
@@ -691,7 +691,7 @@ export class Virtualizer<
691
691
  this.pendingMeasuredCacheIndexes.push(item.index)
692
692
  this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size))
693
693
 
694
- this.notify(false)
694
+ this.notify(true, false)
695
695
  }
696
696
  }
697
697
 
@@ -918,7 +918,7 @@ export class Virtualizer<
918
918
 
919
919
  measure = () => {
920
920
  this.itemSizeCache = new Map()
921
- this.notify(false)
921
+ this.options.onChange?.(this, false)
922
922
  }
923
923
  }
924
924
 
package/src/utils.ts CHANGED
@@ -77,3 +77,11 @@ export function notUndefined<T>(value: T | undefined, msg?: string): T {
77
77
  }
78
78
 
79
79
  export const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1
80
+
81
+ export const debounce = (fn: Function, ms: number) => {
82
+ let timeoutId: ReturnType<typeof setTimeout>
83
+ return function (this: any, ...args: any[]) {
84
+ clearTimeout(timeoutId)
85
+ timeoutId = setTimeout(() => fn.apply(this, args), ms)
86
+ }
87
+ }