@tanstack/virtual-core 3.0.0-beta.18 → 3.0.0-beta.19

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
@@ -84,7 +84,7 @@ export const observeElementRect = (
84
84
  const observer = new ResizeObserver((entries) => {
85
85
  cb({
86
86
  width: entries[0]?.contentRect.width as number,
87
- height: entries[0]?.contentRect.height as number
87
+ height: entries[0]?.contentRect.height as number,
88
88
  })
89
89
  })
90
90
 
@@ -148,7 +148,10 @@ const createOffsetObserver = (mode: ObserverMode) => {
148
148
  let prevY: number = instance.scrollElement[propY]
149
149
 
150
150
  const scroll = () => {
151
- cb(instance.scrollElement[instance.options.horizontal ? propX : propY])
151
+ const offset =
152
+ instance.scrollElement[instance.options.horizontal ? propX : propY]
153
+
154
+ cb(Math.max(0, offset - instance.options.scrollMargin))
152
155
  }
153
156
 
154
157
  scroll()
@@ -191,22 +194,26 @@ export const measureElement = (
191
194
 
192
195
  export const windowScroll = (
193
196
  offset: number,
194
- canSmooth: boolean,
197
+ { canSmooth, sync }: { canSmooth: boolean; sync: boolean },
195
198
  instance: Virtualizer<any, any>,
196
199
  ) => {
200
+ const toOffset = sync ? offset : offset + instance.options.scrollMargin
201
+
197
202
  ;(instance.scrollElement as Window)?.scrollTo?.({
198
- [instance.options.horizontal ? 'left' : 'top']: offset,
203
+ [instance.options.horizontal ? 'left' : 'top']: toOffset,
199
204
  behavior: canSmooth ? 'smooth' : undefined,
200
205
  })
201
206
  }
202
207
 
203
208
  export const elementScroll = (
204
209
  offset: number,
205
- canSmooth: boolean,
210
+ { canSmooth, sync }: { canSmooth: boolean; sync: boolean },
206
211
  instance: Virtualizer<any, any>,
207
212
  ) => {
213
+ const toOffset = sync ? offset : offset + instance.options.scrollMargin
214
+
208
215
  ;(instance.scrollElement as Element)?.scrollTo?.({
209
- [instance.options.horizontal ? 'left' : 'top']: offset,
216
+ [instance.options.horizontal ? 'left' : 'top']: toOffset,
210
217
  behavior: canSmooth ? 'smooth' : undefined,
211
218
  })
212
219
  }
@@ -223,7 +230,7 @@ export interface VirtualizerOptions<
223
230
  // Required from the framework adapter (but can be overridden)
224
231
  scrollToFn: (
225
232
  offset: number,
226
- canSmooth: boolean,
233
+ options: { canSmooth: boolean; sync: boolean },
227
234
  instance: Virtualizer<TScrollElement, TItemElement>,
228
235
  ) => void
229
236
  observeElementRect: (
@@ -253,12 +260,16 @@ export interface VirtualizerOptions<
253
260
  getItemKey?: (index: number) => Key
254
261
  rangeExtractor?: (range: Range) => number[]
255
262
  enableSmoothScroll?: boolean
263
+ scrollMargin?: number
264
+ scrollingDelay?: number
256
265
  }
257
266
 
258
267
  export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
259
268
  private unsubs: (void | (() => void))[] = []
260
269
  options!: Required<VirtualizerOptions<TScrollElement, TItemElement>>
261
270
  scrollElement: TScrollElement | null = null
271
+ isScrolling: boolean = false
272
+ private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null
262
273
  private measurementsCache: Item[] = []
263
274
  private itemMeasurementsCache: Record<Key, number> = {}
264
275
  private pendingMeasuredCacheIndexes: number[] = []
@@ -270,7 +281,7 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
270
281
  number,
271
282
  (measurableItem: TItemElement | null) => void
272
283
  > = {}
273
- private range: { startIndex: number; endIndex: number } = {
284
+ range: { startIndex: number; endIndex: number } = {
274
285
  startIndex: 0,
275
286
  endIndex: 0,
276
287
  }
@@ -303,6 +314,8 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
303
314
  onChange: () => {},
304
315
  measureElement,
305
316
  initialRect: { width: 0, height: 0 },
317
+ scrollMargin: 0,
318
+ scrollingDelay: 150,
306
319
  ...opts,
307
320
  }
308
321
  }
@@ -330,7 +343,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
330
343
  this.cleanup()
331
344
 
332
345
  this.scrollElement = scrollElement
333
- this._scrollToOffset(this.scrollOffset, false)
346
+ this._scrollToOffset(this.scrollOffset, {
347
+ canSmooth: false,
348
+ sync: true,
349
+ })
334
350
 
335
351
  this.unsubs.push(
336
352
  this.options.observeElementRect(this, (rect) => {
@@ -341,10 +357,30 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
341
357
 
342
358
  this.unsubs.push(
343
359
  this.options.observeElementOffset(this, (offset) => {
344
- this.scrollOffset = offset
360
+ if (this.isScrollingTimeoutId !== null) {
361
+ clearTimeout(this.isScrollingTimeoutId)
362
+ this.isScrollingTimeoutId = null
363
+ }
364
+
365
+ if (this.scrollOffset !== offset) {
366
+ this.isScrolling = true
367
+ this.scrollOffset = offset
368
+
369
+ this.isScrollingTimeoutId = setTimeout(() => {
370
+ this.isScrollingTimeoutId = null
371
+ this.isScrolling = false
372
+
373
+ this.notify()
374
+ }, this.options.scrollingDelay)
375
+ } else {
376
+ this.isScrolling = false
377
+ }
378
+
345
379
  this.calculateRange()
346
380
  }),
347
381
  )
382
+ } else if (!this.isScrolling) {
383
+ this.calculateRange()
348
384
  }
349
385
  }
350
386
 
@@ -391,7 +427,7 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
391
427
  },
392
428
  )
393
429
 
394
- private calculateRange = memo(
430
+ calculateRange = memo(
395
431
  () => [this.getMeasurements(), this.getSize(), this.scrollOffset],
396
432
  (measurements, outerSize, scrollOffset) => {
397
433
  const range = calculateRange({
@@ -461,7 +497,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
461
497
  if (!this.destinationOffset) {
462
498
  this._scrollToOffset(
463
499
  this.scrollOffset + (measuredItemSize - itemSize),
464
- false,
500
+ {
501
+ canSmooth: false,
502
+ sync: false,
503
+ },
465
504
  )
466
505
  }
467
506
  }
@@ -503,7 +542,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
503
542
 
504
543
  scrollToOffset = (
505
544
  toOffset: number,
506
- { align = 'start', smoothScroll = this.options.enableSmoothScroll }: ScrollToOffsetOptions = {},
545
+ {
546
+ align = 'start',
547
+ smoothScroll = this.options.enableSmoothScroll,
548
+ }: ScrollToOffsetOptions = {},
507
549
  ) => {
508
550
  const offset = this.scrollOffset
509
551
  const size = this.getSize()
@@ -518,18 +560,22 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
518
560
  }
519
561
  }
520
562
 
563
+ const options = {
564
+ canSmooth: smoothScroll,
565
+ sync: false,
566
+ }
521
567
  if (align === 'start') {
522
- this._scrollToOffset(toOffset, smoothScroll)
523
- } else if (align === 'end') {
524
- this._scrollToOffset(toOffset - size, smoothScroll)
525
- } else if (align === 'center') {
526
- this._scrollToOffset(toOffset - size / 2, smoothScroll)
568
+ this._scrollToOffset(toOffset, options)
569
+ } else if (align === 'end') {
570
+ this._scrollToOffset(toOffset - size, options)
571
+ } else if (align === 'center') {
572
+ this._scrollToOffset(toOffset - size / 2, options)
527
573
  }
528
574
  }
529
575
 
530
576
  scrollToIndex = (
531
577
  index: number,
532
- { align = 'auto', smoothScroll = this.options.enableSmoothScroll, ...rest }: ScrollToIndexOptions = {},
578
+ { align = 'auto', ...rest }: ScrollToIndexOptions = {},
533
579
  ) => {
534
580
  const measurements = this.getMeasurements()
535
581
  const offset = this.scrollOffset
@@ -560,22 +606,21 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
560
606
  ? measurement.end + this.options.scrollPaddingEnd
561
607
  : measurement.start - this.options.scrollPaddingStart
562
608
 
563
- this.scrollToOffset(toOffset, { align, smoothScroll, ...rest })
609
+ this.scrollToOffset(toOffset, { align, ...rest })
564
610
  }
565
611
 
566
612
  getTotalSize = () =>
567
613
  (this.getMeasurements()[this.options.count - 1]?.end ||
568
614
  this.options.paddingStart) + this.options.paddingEnd
569
615
 
570
- private _scrollToOffset = (offset: number, canSmooth: boolean) => {
616
+ private _scrollToOffset = (
617
+ offset: number,
618
+ options: { canSmooth: boolean; sync: boolean },
619
+ ) => {
571
620
  clearTimeout(this.scrollCheckFrame)
572
621
 
573
622
  this.destinationOffset = offset
574
- this.options.scrollToFn(
575
- offset,
576
- canSmooth,
577
- this,
578
- )
623
+ this.options.scrollToFn(offset, options, this)
579
624
 
580
625
  let scrollCheckFrame: ReturnType<typeof setTimeout>
581
626