@tanstack/virtual-core 3.3.0 → 3.5.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
+ }, instance.options.isScrollingResetDelay)
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
+ }, instance.options.isScrollingResetDelay)
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,10 +294,10 @@ 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
300
+ isScrollingResetDelay?: number
274
301
  }
275
302
 
276
303
  export class Virtualizer<
@@ -281,7 +308,6 @@ export class Virtualizer<
281
308
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
282
309
  scrollElement: TScrollElement | null = null
283
310
  isScrolling: boolean = false
284
- private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
285
311
  private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null
286
312
  measurementsCache: VirtualItem[] = []
287
313
  private itemSizeCache = new Map<Key, number>()
@@ -336,7 +362,7 @@ export class Virtualizer<
336
362
  this.itemSizeCache.set(item.key, item.size)
337
363
  })
338
364
 
339
- this.maybeNotify()
365
+ this.notify(false, false)
340
366
  }
341
367
 
342
368
  setOptions = (opts: VirtualizerOptions<TScrollElement, TItemElement>) => {
@@ -360,42 +386,30 @@ export class Virtualizer<
360
386
  initialRect: { width: 0, height: 0 },
361
387
  scrollMargin: 0,
362
388
  gap: 0,
363
- scrollingDelay: 150,
364
389
  indexAttribute: 'data-index',
365
390
  initialMeasurementsCache: [],
366
391
  lanes: 1,
392
+ isScrollingResetDelay: 150,
367
393
  ...opts,
368
394
  }
369
395
  }
370
396
 
371
- private notify = (sync: boolean) => {
372
- this.options.onChange?.(this, sync)
397
+ private notify = (force: boolean, sync: boolean) => {
398
+ const { startIndex, endIndex } = this.range ?? {
399
+ startIndex: undefined,
400
+ endIndex: undefined,
401
+ }
402
+ const range = this.calculateRange()
403
+
404
+ if (
405
+ force ||
406
+ startIndex !== range?.startIndex ||
407
+ endIndex !== range?.endIndex
408
+ ) {
409
+ this.options.onChange?.(this, sync)
410
+ }
373
411
  }
374
412
 
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
413
  private cleanup = () => {
400
414
  this.unsubs.filter(Boolean).forEach((d) => d!())
401
415
  this.unsubs = []
@@ -426,37 +440,24 @@ export class Virtualizer<
426
440
  this.unsubs.push(
427
441
  this.options.observeElementRect(this, (rect) => {
428
442
  this.scrollRect = rect
429
- this.maybeNotify()
443
+ this.notify(false, false)
430
444
  }),
431
445
  )
432
446
 
433
447
  this.unsubs.push(
434
- this.options.observeElementOffset(this, (offset) => {
448
+ this.options.observeElementOffset(this, (offset, isScrolling) => {
435
449
  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'
450
+ this.scrollDirection = isScrolling
451
+ ? this.scrollOffset < offset
452
+ ? 'forward'
453
+ : 'backward'
454
+ : null
449
455
  this.scrollOffset = offset
450
456
 
451
- this.maybeNotify()
452
-
453
- this.isScrollingTimeoutId = setTimeout(() => {
454
- this.isScrollingTimeoutId = null
455
- this.isScrolling = false
456
- this.scrollDirection = null
457
+ const prevIsScrolling = this.isScrolling
458
+ this.isScrolling = isScrolling
457
459
 
458
- this.maybeNotify()
459
- }, this.options.scrollingDelay)
460
+ this.notify(prevIsScrolling !== isScrolling, isScrolling)
460
461
  }),
461
462
  )
462
463
  }
@@ -466,7 +467,7 @@ export class Virtualizer<
466
467
  return this.scrollRect[this.options.horizontal ? 'width' : 'height']
467
468
  }
468
469
 
469
- private memoOptions = memo(
470
+ private getMeasurementOptions = memo(
470
471
  () => [
471
472
  this.options.count,
472
473
  this.options.paddingStart,
@@ -529,7 +530,7 @@ export class Virtualizer<
529
530
  }
530
531
 
531
532
  private getMeasurements = memo(
532
- () => [this.memoOptions(), this.itemSizeCache],
533
+ () => [this.getMeasurementOptions(), this.itemSizeCache],
533
534
  ({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => {
534
535
  const min =
535
536
  this.pendingMeasuredCacheIndexes.length > 0
@@ -612,7 +613,8 @@ export class Virtualizer<
612
613
  return range === null
613
614
  ? []
614
615
  : rangeExtractor({
615
- ...range,
616
+ startIndex: range.startIndex,
617
+ endIndex: range.endIndex,
616
618
  overscan,
617
619
  count,
618
620
  })
@@ -691,7 +693,7 @@ export class Virtualizer<
691
693
  this.pendingMeasuredCacheIndexes.push(item.index)
692
694
  this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size))
693
695
 
694
- this.notify(false)
696
+ this.notify(true, false)
695
697
  }
696
698
  }
697
699
 
@@ -918,7 +920,7 @@ export class Virtualizer<
918
920
 
919
921
  measure = () => {
920
922
  this.itemSizeCache = new Map()
921
- this.notify(false)
923
+ this.options.onChange?.(this, false)
922
924
  }
923
925
  }
924
926
 
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
+ }