@tanstack/virtual-core 3.0.0-beta.17 → 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,6 +343,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
330
343
  this.cleanup()
331
344
 
332
345
  this.scrollElement = scrollElement
346
+ this._scrollToOffset(this.scrollOffset, {
347
+ canSmooth: false,
348
+ sync: true,
349
+ })
333
350
 
334
351
  this.unsubs.push(
335
352
  this.options.observeElementRect(this, (rect) => {
@@ -340,10 +357,30 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
340
357
 
341
358
  this.unsubs.push(
342
359
  this.options.observeElementOffset(this, (offset) => {
343
- 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
+
344
379
  this.calculateRange()
345
380
  }),
346
381
  )
382
+ } else if (!this.isScrolling) {
383
+ this.calculateRange()
347
384
  }
348
385
  }
349
386
 
@@ -390,7 +427,7 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
390
427
  },
391
428
  )
392
429
 
393
- private calculateRange = memo(
430
+ calculateRange = memo(
394
431
  () => [this.getMeasurements(), this.getSize(), this.scrollOffset],
395
432
  (measurements, outerSize, scrollOffset) => {
396
433
  const range = calculateRange({
@@ -460,7 +497,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
460
497
  if (!this.destinationOffset) {
461
498
  this._scrollToOffset(
462
499
  this.scrollOffset + (measuredItemSize - itemSize),
463
- false,
500
+ {
501
+ canSmooth: false,
502
+ sync: false,
503
+ },
464
504
  )
465
505
  }
466
506
  }
@@ -502,7 +542,10 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
502
542
 
503
543
  scrollToOffset = (
504
544
  toOffset: number,
505
- { align = 'start', smoothScroll = this.options.enableSmoothScroll }: ScrollToOffsetOptions = {},
545
+ {
546
+ align = 'start',
547
+ smoothScroll = this.options.enableSmoothScroll,
548
+ }: ScrollToOffsetOptions = {},
506
549
  ) => {
507
550
  const offset = this.scrollOffset
508
551
  const size = this.getSize()
@@ -517,18 +560,22 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
517
560
  }
518
561
  }
519
562
 
563
+ const options = {
564
+ canSmooth: smoothScroll,
565
+ sync: false,
566
+ }
520
567
  if (align === 'start') {
521
- this._scrollToOffset(toOffset, smoothScroll)
522
- } else if (align === 'end') {
523
- this._scrollToOffset(toOffset - size, smoothScroll)
524
- } else if (align === 'center') {
525
- 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)
526
573
  }
527
574
  }
528
575
 
529
576
  scrollToIndex = (
530
577
  index: number,
531
- { align = 'auto', smoothScroll = this.options.enableSmoothScroll, ...rest }: ScrollToIndexOptions = {},
578
+ { align = 'auto', ...rest }: ScrollToIndexOptions = {},
532
579
  ) => {
533
580
  const measurements = this.getMeasurements()
534
581
  const offset = this.scrollOffset
@@ -559,22 +606,21 @@ export class Virtualizer<TScrollElement = unknown, TItemElement = unknown> {
559
606
  ? measurement.end + this.options.scrollPaddingEnd
560
607
  : measurement.start - this.options.scrollPaddingStart
561
608
 
562
- this.scrollToOffset(toOffset, { align, smoothScroll, ...rest })
609
+ this.scrollToOffset(toOffset, { align, ...rest })
563
610
  }
564
611
 
565
612
  getTotalSize = () =>
566
613
  (this.getMeasurements()[this.options.count - 1]?.end ||
567
614
  this.options.paddingStart) + this.options.paddingEnd
568
615
 
569
- private _scrollToOffset = (offset: number, canSmooth: boolean) => {
616
+ private _scrollToOffset = (
617
+ offset: number,
618
+ options: { canSmooth: boolean; sync: boolean },
619
+ ) => {
570
620
  clearTimeout(this.scrollCheckFrame)
571
621
 
572
622
  this.destinationOffset = offset
573
- this.options.scrollToFn(
574
- offset,
575
- canSmooth,
576
- this,
577
- )
623
+ this.options.scrollToFn(offset, options, this)
578
624
 
579
625
  let scrollCheckFrame: ReturnType<typeof setTimeout>
580
626