@humanspeak/svelte-virtual-list 0.3.1-beta.1 → 0.3.2
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/dist/SvelteVirtualList.svelte +262 -191
- package/dist/SvelteVirtualList.svelte.d.ts +5 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/{reactive-height-manager → reactive-list-manager}/INTEGRATION_EXAMPLE.md +10 -10
- package/dist/{reactive-height-manager → reactive-list-manager}/README.md +17 -17
- package/dist/reactive-list-manager/ReactiveListManager.svelte.d.ts +221 -0
- package/dist/reactive-list-manager/ReactiveListManager.svelte.js +635 -0
- package/dist/reactive-list-manager/RecomputeScheduler.d.ts +12 -0
- package/dist/reactive-list-manager/RecomputeScheduler.js +54 -0
- package/dist/reactive-list-manager/benchmark.d.ts +5 -0
- package/dist/{reactive-height-manager → reactive-list-manager}/benchmark.js +3 -3
- package/dist/{reactive-height-manager → reactive-list-manager}/index.d.ts +8 -12
- package/dist/{reactive-height-manager → reactive-list-manager}/index.js +10 -13
- package/dist/{reactive-height-manager → reactive-list-manager}/test/TestComponent.svelte +9 -9
- package/dist/{reactive-height-manager → reactive-list-manager}/test/TestComponent.svelte.d.ts +5 -5
- package/dist/{reactive-height-manager → reactive-list-manager}/types.d.ts +9 -3
- package/dist/utils/virtualList.d.ts +2 -2
- package/dist/utils/virtualList.js +44 -17
- package/package.json +134 -133
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.d.ts +0 -116
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.js +0 -200
- package/dist/reactive-height-manager/benchmark.d.ts +0 -5
- package/dist/utils/resizeObserver.d.ts +0 -89
- package/dist/utils/resizeObserver.js +0 -119
- /package/dist/{reactive-height-manager → reactive-list-manager}/types.js +0 -0
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
MIT License © Humanspeak, Inc.
|
|
62
62
|
-->
|
|
63
63
|
|
|
64
|
-
<script lang="ts" generics="TItem =
|
|
64
|
+
<script lang="ts" generics="TItem = unknown">
|
|
65
65
|
/**
|
|
66
66
|
* SvelteVirtualList Implementation Journey
|
|
67
67
|
*
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
|
|
172
172
|
import { calculateScrollTarget } from './utils/scrollCalculation.js'
|
|
173
173
|
import { createAdvancedThrottledCallback } from './utils/throttle.js'
|
|
174
|
-
import {
|
|
174
|
+
import { ReactiveListManager } from './index.js'
|
|
175
175
|
import { BROWSER } from 'esm-env'
|
|
176
176
|
import { onMount, tick, untrack } from 'svelte'
|
|
177
177
|
|
|
@@ -202,22 +202,18 @@
|
|
|
202
202
|
/**
|
|
203
203
|
* DOM References and Core State
|
|
204
204
|
*/
|
|
205
|
-
let containerElement: HTMLElement // Reference to the main container element
|
|
206
|
-
let viewportElement: HTMLElement // Reference to the scrollable viewport element
|
|
207
205
|
const itemElements = $state<HTMLElement[]>([]) // Array of rendered item element references
|
|
208
206
|
|
|
209
207
|
/**
|
|
210
208
|
* Scroll and Height Management
|
|
211
209
|
*/
|
|
212
|
-
let scrollTop = $state(0) // Current scroll position
|
|
213
210
|
let height = $state(0) // Container height
|
|
214
|
-
let calculatedItemHeight = $state(defaultEstimatedItemHeight) // Current average item height
|
|
215
211
|
|
|
216
212
|
/**
|
|
217
213
|
* State Flags and Control
|
|
218
214
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
|
|
216
|
+
const isCalculatingHeight = $state(false) // Prevents concurrent height calculations
|
|
221
217
|
let isScrolling = $state(false) // Tracks active scrolling state
|
|
222
218
|
let lastMeasuredIndex = $state(-1) // Index of last measured item
|
|
223
219
|
let lastScrollTopSnapshot = $state(0) // Previous scroll position snapshot
|
|
@@ -232,37 +228,45 @@
|
|
|
232
228
|
/**
|
|
233
229
|
* Performance Optimization State
|
|
234
230
|
*/
|
|
235
|
-
|
|
236
|
-
let dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
|
|
231
|
+
const dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
|
|
237
232
|
let dirtyItemsCount = $state(0) // Reactive count of dirty items
|
|
233
|
+
// Fallback measurement used only when height has not been established yet
|
|
234
|
+
let measuredFallbackHeight = $state(0)
|
|
238
235
|
|
|
239
236
|
let prevVisibleRange = $state<SvelteVirtualListPreviousVisibleRange | null>(null)
|
|
240
237
|
let prevHeight = $state<number>(0)
|
|
238
|
+
let prevTotalHeightForScrollCorrection = $state<number>(0)
|
|
239
|
+
let lastBottomDistance = $state<number | null>(null)
|
|
241
240
|
|
|
242
241
|
/**
|
|
243
242
|
* Reactive Height Manager - O(1) height calculation system
|
|
244
243
|
* Replaces O(n) totalHeight loop with incremental updates
|
|
245
244
|
*/
|
|
246
|
-
|
|
245
|
+
const heightManager = new ReactiveListManager({
|
|
247
246
|
itemLength: items.length,
|
|
248
|
-
itemHeight: defaultEstimatedItemHeight
|
|
247
|
+
itemHeight: defaultEstimatedItemHeight,
|
|
248
|
+
internalDebug: INTERNAL_DEBUG
|
|
249
249
|
})
|
|
250
250
|
|
|
251
251
|
// Dynamic update coordination to avoid UA scroll anchoring interference
|
|
252
|
-
let dynamicUpdateInProgress = $state(false)
|
|
253
252
|
let suppressBottomAnchoringUntilMs = $state(0)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
253
|
+
|
|
254
|
+
const displayItems = $derived(() => {
|
|
255
|
+
const visibleRange = visibleItems()
|
|
256
|
+
const slice =
|
|
257
|
+
mode === 'bottomToTop'
|
|
258
|
+
? items.slice(visibleRange.start, visibleRange.end).reverse()
|
|
259
|
+
: items.slice(visibleRange.start, visibleRange.end)
|
|
260
|
+
|
|
261
|
+
return slice.map((item, sliceIndex) => ({
|
|
262
|
+
item,
|
|
263
|
+
originalIndex:
|
|
264
|
+
mode === 'bottomToTop'
|
|
265
|
+
? visibleRange.end - 1 - sliceIndex
|
|
266
|
+
: visibleRange.start + sliceIndex,
|
|
267
|
+
sliceIndex
|
|
268
|
+
}))
|
|
269
|
+
})
|
|
266
270
|
|
|
267
271
|
/**
|
|
268
272
|
* Handles scroll position corrections when item heights change, ensuring proper positioning
|
|
@@ -280,7 +284,7 @@
|
|
|
280
284
|
const handleHeightChangesScrollCorrection = (
|
|
281
285
|
heightChanges: Array<{ index: number; oldHeight: number; newHeight: number; delta: number }>
|
|
282
286
|
) => {
|
|
283
|
-
if (!viewportElement || !initialized || userHasScrolledAway) {
|
|
287
|
+
if (!heightManager.viewportElement || !heightManager.initialized || userHasScrolledAway) {
|
|
284
288
|
return
|
|
285
289
|
}
|
|
286
290
|
|
|
@@ -332,16 +336,18 @@
|
|
|
332
336
|
wasAtBottomBeforeHeightChange &&
|
|
333
337
|
!programmaticScrollInProgress &&
|
|
334
338
|
performance.now() >= suppressBottomAnchoringUntilMs &&
|
|
335
|
-
!
|
|
339
|
+
!heightManager.isDynamicUpdateInProgress
|
|
336
340
|
) {
|
|
337
341
|
// Step 1: Scroll to approximate position to ensure Item 0 gets rendered in virtual viewport
|
|
338
342
|
const approximateScrollTop = Math.max(0, totalHeight() - height)
|
|
339
|
-
|
|
340
|
-
scrollTop = approximateScrollTop
|
|
343
|
+
heightManager.viewport.scrollTop = approximateScrollTop
|
|
344
|
+
heightManager.scrollTop = approximateScrollTop
|
|
341
345
|
|
|
342
346
|
// Step 2: Use native scrollIntoView for precise bottom-edge positioning after DOM updates
|
|
343
347
|
tick().then(() => {
|
|
344
|
-
const item0Element =
|
|
348
|
+
const item0Element = heightManager.viewport.querySelector(
|
|
349
|
+
'[data-original-index="0"]'
|
|
350
|
+
)
|
|
345
351
|
if (item0Element) {
|
|
346
352
|
// Native browser API handles all positioning edge cases perfectly
|
|
347
353
|
item0Element.scrollIntoView({
|
|
@@ -351,14 +357,14 @@
|
|
|
351
357
|
})
|
|
352
358
|
|
|
353
359
|
// Sync our internal scroll state with actual DOM position
|
|
354
|
-
scrollTop =
|
|
360
|
+
heightManager.scrollTop = heightManager.viewport.scrollTop
|
|
355
361
|
}
|
|
356
362
|
})
|
|
357
363
|
|
|
358
364
|
return // Skip remaining scroll correction logic - we've handled bottomToTop case
|
|
359
365
|
}
|
|
360
366
|
|
|
361
|
-
const currentScrollTop =
|
|
367
|
+
const currentScrollTop = heightManager.viewport.scrollTop
|
|
362
368
|
const maxScrollTop = Math.max(0, totalHeight() - height)
|
|
363
369
|
|
|
364
370
|
// Calculate total height change impact above current visible area
|
|
@@ -379,8 +385,8 @@
|
|
|
379
385
|
Math.max(0, currentScrollTop + heightChangeAboveViewport)
|
|
380
386
|
)
|
|
381
387
|
|
|
382
|
-
|
|
383
|
-
scrollTop = newScrollTop
|
|
388
|
+
heightManager.viewport.scrollTop = newScrollTop
|
|
389
|
+
heightManager.scrollTop = newScrollTop
|
|
384
390
|
}
|
|
385
391
|
}
|
|
386
392
|
|
|
@@ -391,7 +397,7 @@
|
|
|
391
397
|
if (BROWSER && dirtyItemsCount > 0) {
|
|
392
398
|
// Capture bottom state before any height processing to prevent cascading corrections
|
|
393
399
|
wasAtBottomBeforeHeightChange = atBottom
|
|
394
|
-
|
|
400
|
+
heightManager.startDynamicUpdate()
|
|
395
401
|
updateHeight()
|
|
396
402
|
}
|
|
397
403
|
},
|
|
@@ -413,32 +419,56 @@
|
|
|
413
419
|
})
|
|
414
420
|
|
|
415
421
|
const updateHeight = () => {
|
|
422
|
+
// Capture previous total height for scroll correction (topToBottom anchoring)
|
|
423
|
+
prevTotalHeightForScrollCorrection = heightManager.totalHeight
|
|
416
424
|
heightUpdateTimeout = calculateAverageHeightDebounced(
|
|
417
425
|
isCalculatingHeight,
|
|
418
426
|
heightUpdateTimeout,
|
|
419
427
|
visibleItems,
|
|
420
428
|
itemElements,
|
|
421
|
-
|
|
429
|
+
heightManager.getHeightCache(),
|
|
422
430
|
lastMeasuredIndex,
|
|
423
431
|
heightManager.averageHeight,
|
|
424
432
|
(result) => {
|
|
425
433
|
// Critical updates that must trigger reactive effects immediately
|
|
426
434
|
heightManager.itemHeight = result.newHeight
|
|
427
435
|
lastMeasuredIndex = result.newLastMeasuredIndex
|
|
428
|
-
heightCache = result.updatedHeightCache
|
|
429
436
|
|
|
430
|
-
//
|
|
437
|
+
// Update manager totals/cache before any scroll correction logic relies on them
|
|
438
|
+
if (result.heightChanges.length > 0) {
|
|
439
|
+
heightManager.processDirtyHeights(result.heightChanges)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Handle height changes for scroll correction (manager totals already updated)
|
|
431
443
|
if (result.heightChanges.length > 0 && mode === 'bottomToTop') {
|
|
432
444
|
handleHeightChangesScrollCorrection(result.heightChanges)
|
|
433
445
|
}
|
|
434
446
|
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
447
|
+
// TopToBottom: maintain bottom anchoring when total height changes
|
|
448
|
+
if (mode === 'topToBottom' && heightManager.isReady && heightManager.initialized) {
|
|
449
|
+
const oldTotal = prevTotalHeightForScrollCorrection
|
|
450
|
+
const newTotal = heightManager.totalHeight
|
|
451
|
+
const deltaTotal = newTotal - oldTotal
|
|
452
|
+
// Ignore micro deltas to prevent oscillation
|
|
453
|
+
if (Math.abs(deltaTotal) > 1) {
|
|
454
|
+
const maxScrollTop = Math.max(0, newTotal - (height || 0))
|
|
455
|
+
const tolerance = Math.max(heightManager.averageHeight, 10)
|
|
456
|
+
const currentScrollTop = heightManager.viewport.scrollTop
|
|
457
|
+
const isAtBottom = Math.abs(currentScrollTop - maxScrollTop) <= tolerance
|
|
458
|
+
if (isAtBottom) {
|
|
459
|
+
// Adjust scrollTop by total height delta to hold bottom anchor
|
|
460
|
+
const adjusted = Math.min(
|
|
461
|
+
maxScrollTop,
|
|
462
|
+
Math.max(0, currentScrollTop + deltaTotal)
|
|
463
|
+
)
|
|
464
|
+
heightManager.viewport.scrollTop = adjusted
|
|
465
|
+
heightManager.scrollTop = adjusted
|
|
466
|
+
}
|
|
440
467
|
}
|
|
468
|
+
}
|
|
441
469
|
|
|
470
|
+
// Non-critical updates wrapped in untrack to prevent reactive cascades
|
|
471
|
+
untrack(() => {
|
|
442
472
|
// Clear processed dirty items (all dirty items were processed)
|
|
443
473
|
dirtyItems.clear()
|
|
444
474
|
dirtyItemsCount = 0
|
|
@@ -446,12 +476,12 @@
|
|
|
446
476
|
// Reset bottom state flag
|
|
447
477
|
wasAtBottomBeforeHeightChange = false
|
|
448
478
|
})
|
|
449
|
-
endDynamicUpdate()
|
|
479
|
+
heightManager.endDynamicUpdate()
|
|
450
480
|
},
|
|
451
|
-
100, // debounceTime
|
|
481
|
+
lastMeasuredIndex < 0 ? 0 : 100, // debounceTime (no debounce on first pass)
|
|
452
482
|
dirtyItems, // Pass dirty items for processing
|
|
453
|
-
0, // Don't pass
|
|
454
|
-
0, // Don't pass
|
|
483
|
+
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
484
|
+
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
455
485
|
mode // Pass mode for correct element indexing
|
|
456
486
|
)
|
|
457
487
|
}
|
|
@@ -467,7 +497,7 @@
|
|
|
467
497
|
* CRITICAL: O(1) Reactive Total Height Calculation
|
|
468
498
|
* ===============================================
|
|
469
499
|
*
|
|
470
|
-
* Uses
|
|
500
|
+
* Uses ReactiveListManager for O(1) height calculations instead of O(n) loops.
|
|
471
501
|
* This fixes the root cause of massive scroll jumps in bottomToTop mode.
|
|
472
502
|
*
|
|
473
503
|
* Problem with Previous O(n) Approach:
|
|
@@ -478,7 +508,7 @@
|
|
|
478
508
|
* - Total height jumps from 200,000px to 223,500px (+23,500px!)
|
|
479
509
|
* - This 23,500px error caused massive scroll position overshoots
|
|
480
510
|
*
|
|
481
|
-
* Solution with
|
|
511
|
+
* Solution with ReactiveListManager:
|
|
482
512
|
* - O(1) reactive calculations using incremental updates
|
|
483
513
|
* - Uses actual measured heights from heightCache where available
|
|
484
514
|
* - Only estimates heights for items that haven't been measured yet
|
|
@@ -494,20 +524,40 @@
|
|
|
494
524
|
* This getter is reactive and updates whenever heightManager's internal state changes.
|
|
495
525
|
* Used by: atBottom calculation, scroll corrections, maxScrollTop calculations
|
|
496
526
|
*/
|
|
497
|
-
|
|
527
|
+
const totalHeight = $derived(() => heightManager.totalHeight)
|
|
498
528
|
|
|
499
|
-
|
|
500
|
-
let atBottom = $derived(scrollTop >= totalHeight() - height - 1)
|
|
529
|
+
const atBottom = $derived(heightManager.scrollTop >= totalHeight() - height - 1)
|
|
501
530
|
let wasAtBottomBeforeHeightChange = false
|
|
502
531
|
let lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null = null
|
|
503
532
|
|
|
504
|
-
|
|
505
|
-
|
|
533
|
+
function updateDebugTailDistance() {
|
|
534
|
+
if (!heightManager.viewportElement) return
|
|
535
|
+
const last = heightManager.viewport.querySelector(
|
|
536
|
+
'[data-original-index="999"]'
|
|
537
|
+
) as HTMLElement | null
|
|
538
|
+
if (!last) return
|
|
539
|
+
const v = heightManager.viewport.getBoundingClientRect()
|
|
540
|
+
const r = last.getBoundingClientRect()
|
|
541
|
+
lastBottomDistance = Math.round(Math.abs(r.bottom - v.bottom))
|
|
542
|
+
if (INTERNAL_DEBUG) {
|
|
543
|
+
console.info('[SVL] bottomDistance(px):', lastBottomDistance)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// no UI export; rely on console logs when debug=true
|
|
548
|
+
|
|
549
|
+
// $inspect('scrollState: atTop', atTop)
|
|
550
|
+
// $inspect('scrollState: atBottom', atBottom)
|
|
506
551
|
|
|
507
552
|
$effect(() => {
|
|
508
|
-
if (
|
|
553
|
+
if (
|
|
554
|
+
BROWSER &&
|
|
555
|
+
heightManager.initialized &&
|
|
556
|
+
mode === 'bottomToTop' &&
|
|
557
|
+
heightManager.viewportElement
|
|
558
|
+
) {
|
|
509
559
|
const targetScrollTop = Math.max(0, totalHeight() - height)
|
|
510
|
-
const currentScrollTop =
|
|
560
|
+
const currentScrollTop = heightManager.viewport.scrollTop
|
|
511
561
|
const scrollDifference = Math.abs(currentScrollTop - targetScrollTop)
|
|
512
562
|
|
|
513
563
|
// Only correct scroll if:
|
|
@@ -515,33 +565,34 @@
|
|
|
515
565
|
// 2. User hasn't intentionally scrolled away from bottom
|
|
516
566
|
// 3. We're significantly off target
|
|
517
567
|
// 4. We're not at the bottom (where height changes should be handled more carefully)
|
|
518
|
-
const heightChanged = Math.abs(
|
|
568
|
+
const heightChanged = Math.abs(heightManager.averageHeight - lastCalculatedHeight) > 1
|
|
519
569
|
const maxScrollTop = Math.max(0, totalHeight() - height)
|
|
520
570
|
|
|
521
571
|
// In bottomToTop mode, we're "at bottom" when scroll is at max position
|
|
522
|
-
const isAtBottom =
|
|
572
|
+
const isAtBottom =
|
|
573
|
+
Math.abs(currentScrollTop - maxScrollTop) < heightManager.averageHeight
|
|
523
574
|
const shouldCorrect =
|
|
524
575
|
heightChanged &&
|
|
525
576
|
!userHasScrolledAway &&
|
|
526
577
|
!isAtBottom && // Don't apply aggressive correction when at bottom
|
|
527
578
|
!programmaticScrollInProgress && // Don't interfere with programmatic scrolls
|
|
528
579
|
performance.now() >= suppressBottomAnchoringUntilMs &&
|
|
529
|
-
!
|
|
530
|
-
scrollDifference >
|
|
580
|
+
!heightManager.isDynamicUpdateInProgress &&
|
|
581
|
+
scrollDifference > heightManager.averageHeight * 3
|
|
531
582
|
|
|
532
583
|
if (shouldCorrect) {
|
|
533
584
|
// Round to avoid subpixel positioning issues in bottomToTop mode
|
|
534
585
|
const roundedTargetScrollTop = Math.round(targetScrollTop)
|
|
535
|
-
|
|
536
|
-
scrollTop = roundedTargetScrollTop
|
|
586
|
+
heightManager.viewport.scrollTop = roundedTargetScrollTop
|
|
587
|
+
heightManager.scrollTop = roundedTargetScrollTop
|
|
537
588
|
}
|
|
538
589
|
|
|
539
590
|
// Track if user has scrolled significantly away from bottom
|
|
540
|
-
if (scrollDifference >
|
|
591
|
+
if (scrollDifference > heightManager.averageHeight * 5) {
|
|
541
592
|
userHasScrolledAway = true
|
|
542
593
|
}
|
|
543
594
|
|
|
544
|
-
lastCalculatedHeight =
|
|
595
|
+
lastCalculatedHeight = heightManager.averageHeight
|
|
545
596
|
}
|
|
546
597
|
})
|
|
547
598
|
|
|
@@ -552,17 +603,17 @@
|
|
|
552
603
|
|
|
553
604
|
if (
|
|
554
605
|
BROWSER &&
|
|
555
|
-
initialized &&
|
|
606
|
+
heightManager.initialized &&
|
|
556
607
|
mode === 'bottomToTop' &&
|
|
557
|
-
|
|
608
|
+
heightManager.isReady &&
|
|
558
609
|
lastItemsLength > 0
|
|
559
610
|
) {
|
|
560
611
|
const itemsAdded = currentItemsLength - lastItemsLength
|
|
561
612
|
|
|
562
613
|
if (itemsAdded !== 0) {
|
|
563
614
|
// Capture all reactive values immediately to prevent re-triggering
|
|
564
|
-
const currentScrollTop =
|
|
565
|
-
const currentCalculatedItemHeight =
|
|
615
|
+
const currentScrollTop = heightManager.viewport.scrollTop
|
|
616
|
+
const currentCalculatedItemHeight = heightManager.averageHeight
|
|
566
617
|
const currentHeight = height
|
|
567
618
|
const currentTotalHeight = totalHeight()
|
|
568
619
|
const maxScrollTop = Math.max(0, currentTotalHeight - currentHeight)
|
|
@@ -580,14 +631,14 @@
|
|
|
580
631
|
|
|
581
632
|
if (wasNearBottom || currentScrollTop === 0) {
|
|
582
633
|
// User was at bottom, keep them at bottom after new items are added
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
634
|
+
void heightManager.runDynamicUpdate(() => {
|
|
635
|
+
const newScrollTop = maxScrollTop
|
|
636
|
+
heightManager.viewport.scrollTop = newScrollTop
|
|
637
|
+
heightManager.scrollTop = newScrollTop
|
|
638
|
+
|
|
639
|
+
// Reset the "scrolled away" flag since we're actively managing position
|
|
640
|
+
userHasScrolledAway = false
|
|
641
|
+
})
|
|
591
642
|
}
|
|
592
643
|
}
|
|
593
644
|
}
|
|
@@ -595,42 +646,23 @@
|
|
|
595
646
|
lastItemsLength = currentItemsLength
|
|
596
647
|
})
|
|
597
648
|
|
|
598
|
-
// Update container height
|
|
649
|
+
// Update container height continuously to reflect layout changes that
|
|
650
|
+
// may occur outside ResizeObserver timing (keeps buffers correct across engines)
|
|
599
651
|
$effect(() => {
|
|
600
|
-
if (BROWSER &&
|
|
601
|
-
|
|
652
|
+
if (BROWSER && heightManager.isReady) {
|
|
653
|
+
const h = heightManager.container.getBoundingClientRect().height
|
|
654
|
+
if (Number.isFinite(h) && h > 0) height = h
|
|
602
655
|
}
|
|
603
656
|
})
|
|
604
657
|
|
|
605
|
-
//
|
|
606
|
-
$effect(() => {
|
|
607
|
-
if (
|
|
608
|
-
BROWSER &&
|
|
609
|
-
mode === 'bottomToTop' &&
|
|
610
|
-
viewportElement &&
|
|
611
|
-
height > 0 &&
|
|
612
|
-
items.length &&
|
|
613
|
-
!initialized
|
|
614
|
-
) {
|
|
615
|
-
const targetScrollTop = Math.max(0, totalHeight() - height)
|
|
658
|
+
// One-time fallback measurement when height hasn't been established yet
|
|
616
659
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// Double-check the scroll position after a frame
|
|
625
|
-
requestAnimationFrame(() => {
|
|
626
|
-
if (viewportElement && viewportElement.scrollTop !== targetScrollTop) {
|
|
627
|
-
viewportElement.scrollTop = targetScrollTop
|
|
628
|
-
scrollTop = targetScrollTop
|
|
629
|
-
}
|
|
630
|
-
initialized = true
|
|
631
|
-
})
|
|
632
|
-
}
|
|
633
|
-
})
|
|
660
|
+
// Provide a one-time synchronous measurement only when height is still 0,
|
|
661
|
+
// to avoid DOM reads inside render-time expressions.
|
|
662
|
+
$effect(() => {
|
|
663
|
+
if (BROWSER && height === 0 && heightManager.isReady) {
|
|
664
|
+
const h = heightManager.container.getBoundingClientRect().height
|
|
665
|
+
if (Number.isFinite(h) && h > 0) measuredFallbackHeight = h
|
|
634
666
|
}
|
|
635
667
|
})
|
|
636
668
|
|
|
@@ -648,7 +680,7 @@
|
|
|
648
680
|
* @example
|
|
649
681
|
* ```typescript
|
|
650
682
|
* const range = visibleItems()
|
|
651
|
-
* console.
|
|
683
|
+
* console.info(`Rendering items from ${range.start} to ${range.end}`)
|
|
652
684
|
* ```
|
|
653
685
|
*
|
|
654
686
|
* @returns {SvelteVirtualListPreviousVisibleRange} Object containing start and end indices of visible items
|
|
@@ -659,7 +691,12 @@
|
|
|
659
691
|
|
|
660
692
|
// For bottomToTop mode, don't calculate visible range until properly initialized
|
|
661
693
|
// This prevents showing wrong items when scrollTop starts at 0
|
|
662
|
-
if (
|
|
694
|
+
if (
|
|
695
|
+
mode === 'bottomToTop' &&
|
|
696
|
+
!heightManager.initialized &&
|
|
697
|
+
heightManager.scrollTop === 0 &&
|
|
698
|
+
viewportHeight > 0
|
|
699
|
+
) {
|
|
663
700
|
// Calculate what the correct scroll position should be
|
|
664
701
|
const targetScrollTop = Math.max(0, totalHeight() - viewportHeight)
|
|
665
702
|
|
|
@@ -674,14 +711,15 @@
|
|
|
674
711
|
atBottom,
|
|
675
712
|
wasAtBottomBeforeHeightChange,
|
|
676
713
|
lastVisibleRange,
|
|
677
|
-
totalHeight()
|
|
714
|
+
totalHeight(),
|
|
715
|
+
heightManager.getHeightCache()
|
|
678
716
|
)
|
|
679
717
|
|
|
680
718
|
return lastVisibleRange
|
|
681
719
|
}
|
|
682
720
|
|
|
683
721
|
lastVisibleRange = calculateVisibleRange(
|
|
684
|
-
scrollTop,
|
|
722
|
+
heightManager.scrollTop,
|
|
685
723
|
viewportHeight,
|
|
686
724
|
heightManager.averageHeight,
|
|
687
725
|
items.length,
|
|
@@ -690,7 +728,8 @@
|
|
|
690
728
|
atBottom,
|
|
691
729
|
wasAtBottomBeforeHeightChange,
|
|
692
730
|
lastVisibleRange,
|
|
693
|
-
totalHeight()
|
|
731
|
+
totalHeight(),
|
|
732
|
+
heightManager.getHeightCache()
|
|
694
733
|
)
|
|
695
734
|
|
|
696
735
|
return lastVisibleRange
|
|
@@ -718,12 +757,12 @@
|
|
|
718
757
|
* @returns {void}
|
|
719
758
|
*/
|
|
720
759
|
const handleScroll = () => {
|
|
721
|
-
if (!BROWSER || !viewportElement) return
|
|
760
|
+
if (!BROWSER || !heightManager.viewportElement) return
|
|
722
761
|
|
|
723
762
|
if (!isScrolling) {
|
|
724
763
|
isScrolling = true
|
|
725
764
|
rafSchedule(() => {
|
|
726
|
-
const current =
|
|
765
|
+
const current = heightManager.viewport.scrollTop
|
|
727
766
|
if (mode === 'bottomToTop') {
|
|
728
767
|
const delta = lastScrollTopSnapshot - current
|
|
729
768
|
if (delta > 0.5) {
|
|
@@ -732,7 +771,19 @@
|
|
|
732
771
|
}
|
|
733
772
|
}
|
|
734
773
|
lastScrollTopSnapshot = current
|
|
735
|
-
scrollTop = current
|
|
774
|
+
heightManager.scrollTop = current
|
|
775
|
+
updateDebugTailDistance()
|
|
776
|
+
if (INTERNAL_DEBUG) {
|
|
777
|
+
const vr = visibleItems()
|
|
778
|
+
console.info('[SVL] onscroll', {
|
|
779
|
+
mode,
|
|
780
|
+
scrollTop: heightManager.scrollTop,
|
|
781
|
+
height,
|
|
782
|
+
totalHeight: totalHeight(),
|
|
783
|
+
averageItemHeight: heightManager.averageHeight,
|
|
784
|
+
visibleRange: vr
|
|
785
|
+
})
|
|
786
|
+
}
|
|
736
787
|
isScrolling = false
|
|
737
788
|
})
|
|
738
789
|
}
|
|
@@ -753,37 +804,36 @@
|
|
|
753
804
|
* @param immediate - Whether to skip the delay (used for resize events)
|
|
754
805
|
*/
|
|
755
806
|
const updateHeightAndScroll = (immediate = false) => {
|
|
756
|
-
if (!initialized && mode === 'bottomToTop') {
|
|
807
|
+
if (!heightManager.initialized && mode === 'bottomToTop') {
|
|
757
808
|
tick().then(() => {
|
|
758
|
-
if (
|
|
759
|
-
const initialHeight =
|
|
809
|
+
if (heightManager.isReady) {
|
|
810
|
+
const initialHeight = heightManager.container.getBoundingClientRect().height
|
|
760
811
|
height = initialHeight
|
|
761
812
|
|
|
762
813
|
tick().then(() => {
|
|
763
|
-
if (
|
|
764
|
-
const finalHeight =
|
|
814
|
+
if (heightManager.isReady) {
|
|
815
|
+
const finalHeight =
|
|
816
|
+
heightManager.container.getBoundingClientRect().height
|
|
765
817
|
height = finalHeight
|
|
766
818
|
|
|
767
819
|
const targetScrollTop = calculateScrollPosition(
|
|
768
820
|
items.length,
|
|
769
|
-
|
|
821
|
+
heightManager.averageHeight,
|
|
770
822
|
finalHeight
|
|
771
823
|
)
|
|
772
824
|
|
|
773
|
-
void
|
|
825
|
+
void heightManager.container.offsetHeight
|
|
774
826
|
|
|
775
|
-
|
|
776
|
-
scrollTop = targetScrollTop
|
|
827
|
+
heightManager.viewport.scrollTop = targetScrollTop
|
|
828
|
+
heightManager.scrollTop = targetScrollTop
|
|
777
829
|
|
|
778
830
|
requestAnimationFrame(() => {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
scrollTop = targetScrollTop
|
|
784
|
-
}
|
|
785
|
-
initialized = true
|
|
831
|
+
const currentScroll = heightManager.viewport.scrollTop
|
|
832
|
+
if (currentScroll !== heightManager.scrollTop) {
|
|
833
|
+
heightManager.viewport.scrollTop = targetScrollTop
|
|
834
|
+
heightManager.scrollTop = targetScrollTop
|
|
786
835
|
}
|
|
836
|
+
heightManager.initialized = true
|
|
787
837
|
})
|
|
788
838
|
}
|
|
789
839
|
})
|
|
@@ -794,18 +844,18 @@
|
|
|
794
844
|
|
|
795
845
|
utilsUpdateHeightAndScroll(
|
|
796
846
|
{
|
|
797
|
-
initialized,
|
|
847
|
+
initialized: heightManager.initialized,
|
|
798
848
|
mode,
|
|
799
|
-
containerElement,
|
|
800
|
-
viewportElement,
|
|
801
|
-
calculatedItemHeight,
|
|
849
|
+
containerElement: heightManager.containerElement,
|
|
850
|
+
viewportElement: heightManager.viewportElement,
|
|
851
|
+
calculatedItemHeight: heightManager.averageHeight,
|
|
802
852
|
height,
|
|
803
|
-
scrollTop
|
|
853
|
+
scrollTop: heightManager.scrollTop
|
|
804
854
|
},
|
|
805
855
|
{
|
|
806
856
|
setHeight: (h) => (height = h),
|
|
807
|
-
setScrollTop: (st) => (scrollTop = st),
|
|
808
|
-
setInitialized: (i) => (initialized = i)
|
|
857
|
+
setScrollTop: (st) => (heightManager.scrollTop = st),
|
|
858
|
+
setInitialized: (i) => (heightManager.initialized = i)
|
|
809
859
|
},
|
|
810
860
|
immediate
|
|
811
861
|
)
|
|
@@ -817,7 +867,7 @@
|
|
|
817
867
|
itemResizeObserver = new ResizeObserver((entries) => {
|
|
818
868
|
tick().then(() => {
|
|
819
869
|
let shouldRecalculate = false
|
|
820
|
-
|
|
870
|
+
void visibleItems() // Cache once to avoid reactive loops
|
|
821
871
|
|
|
822
872
|
for (const entry of entries) {
|
|
823
873
|
const element = entry.target as HTMLElement
|
|
@@ -830,7 +880,7 @@
|
|
|
830
880
|
const isSignificant = isSignificantHeightChange(
|
|
831
881
|
actualIndex,
|
|
832
882
|
currentHeight,
|
|
833
|
-
|
|
883
|
+
heightManager.getHeightCache()
|
|
834
884
|
)
|
|
835
885
|
|
|
836
886
|
// Only mark as dirty if height change is significant
|
|
@@ -862,14 +912,18 @@
|
|
|
862
912
|
if (BROWSER) {
|
|
863
913
|
// Initial setup of heights and scroll position
|
|
864
914
|
updateHeightAndScroll()
|
|
915
|
+
// Ensure one initial measurement pass even if no ResizeObserver fires
|
|
916
|
+
tick().then(() =>
|
|
917
|
+
requestAnimationFrame(() => requestAnimationFrame(() => updateHeight()))
|
|
918
|
+
)
|
|
865
919
|
|
|
866
920
|
// Watch for container size changes
|
|
867
921
|
resizeObserver = new ResizeObserver(() => {
|
|
868
922
|
updateHeightAndScroll(true)
|
|
869
923
|
})
|
|
870
924
|
|
|
871
|
-
if (
|
|
872
|
-
resizeObserver.observe(
|
|
925
|
+
if (heightManager.isReady) {
|
|
926
|
+
resizeObserver.observe(heightManager.container)
|
|
873
927
|
}
|
|
874
928
|
|
|
875
929
|
// Cleanup on component destruction
|
|
@@ -888,7 +942,7 @@
|
|
|
888
942
|
$effect(() => {
|
|
889
943
|
if (INTERNAL_DEBUG) {
|
|
890
944
|
prevVisibleRange = visibleItems()
|
|
891
|
-
prevHeight =
|
|
945
|
+
prevHeight = heightManager.averageHeight
|
|
892
946
|
}
|
|
893
947
|
})
|
|
894
948
|
|
|
@@ -970,9 +1024,9 @@
|
|
|
970
1024
|
}
|
|
971
1025
|
|
|
972
1026
|
if (!items.length) return
|
|
973
|
-
if (!viewportElement) {
|
|
1027
|
+
if (!heightManager.viewportElement) {
|
|
974
1028
|
tick().then(() => {
|
|
975
|
-
if (!viewportElement) return
|
|
1029
|
+
if (!heightManager.viewportElement) return
|
|
976
1030
|
scroll({ index, smoothScroll, shouldThrowOnBounds, align })
|
|
977
1031
|
})
|
|
978
1032
|
return
|
|
@@ -998,12 +1052,12 @@
|
|
|
998
1052
|
align: align || 'auto',
|
|
999
1053
|
targetIndex,
|
|
1000
1054
|
itemsLength: items.length,
|
|
1001
|
-
calculatedItemHeight: heightManager.averageHeight, // Use dynamic average from
|
|
1055
|
+
calculatedItemHeight: heightManager.averageHeight, // Use dynamic average from ReactiveListManager
|
|
1002
1056
|
height,
|
|
1003
|
-
scrollTop,
|
|
1057
|
+
scrollTop: heightManager.scrollTop,
|
|
1004
1058
|
firstVisibleIndex,
|
|
1005
1059
|
lastVisibleIndex,
|
|
1006
|
-
heightCache
|
|
1060
|
+
heightCache: heightManager.getHeightCache()
|
|
1007
1061
|
})
|
|
1008
1062
|
|
|
1009
1063
|
// Handle early return for 'nearest' alignment when item is already visible
|
|
@@ -1014,6 +1068,22 @@
|
|
|
1014
1068
|
// Prevent bottom-anchoring logic from interfering with programmatic scroll
|
|
1015
1069
|
programmaticScrollInProgress = true
|
|
1016
1070
|
|
|
1071
|
+
if (INTERNAL_DEBUG && heightManager.viewportElement) {
|
|
1072
|
+
const domMax = Math.max(
|
|
1073
|
+
0,
|
|
1074
|
+
heightManager.viewport.scrollHeight - heightManager.viewport.clientHeight
|
|
1075
|
+
)
|
|
1076
|
+
console.info('[SVL] scroll-intent', {
|
|
1077
|
+
targetIndex,
|
|
1078
|
+
align: align || 'auto',
|
|
1079
|
+
firstVisibleIndex,
|
|
1080
|
+
lastVisibleIndex,
|
|
1081
|
+
currentScrollTop: heightManager.scrollTop,
|
|
1082
|
+
scrollTarget,
|
|
1083
|
+
domMaxScrollTop: domMax
|
|
1084
|
+
})
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1017
1087
|
// CROSS-BROWSER COMPATIBILITY FIX:
|
|
1018
1088
|
// All major browsers (Chrome, Firefox, Safari) have inconsistent behavior with scrollTo()
|
|
1019
1089
|
// in bottomToTop mode when using smooth scrolling. Using scrollIntoView() on the highest
|
|
@@ -1021,7 +1091,7 @@
|
|
|
1021
1091
|
// This approach works universally and maintains the user's expected smooth scroll experience.
|
|
1022
1092
|
if (mode === 'bottomToTop' && smoothScroll) {
|
|
1023
1093
|
// Find the element with the highest original-index in the current viewport
|
|
1024
|
-
const visibleElements =
|
|
1094
|
+
const visibleElements = heightManager.viewport.querySelectorAll('[data-original-index]')
|
|
1025
1095
|
let maxIndex = -1
|
|
1026
1096
|
let maxElement: HTMLElement | null = null
|
|
1027
1097
|
for (const el of visibleElements) {
|
|
@@ -1040,16 +1110,28 @@
|
|
|
1040
1110
|
await tick()
|
|
1041
1111
|
}
|
|
1042
1112
|
|
|
1043
|
-
|
|
1113
|
+
heightManager.viewport.scrollTo({
|
|
1044
1114
|
top: scrollTarget,
|
|
1045
1115
|
behavior: smoothScroll ? 'smooth' : 'auto'
|
|
1046
1116
|
})
|
|
1047
1117
|
|
|
1048
1118
|
// Update scrollTop state in next frame to avoid synchronous re-renders
|
|
1049
1119
|
requestAnimationFrame(() => {
|
|
1050
|
-
scrollTop = scrollTarget
|
|
1120
|
+
heightManager.scrollTop = scrollTarget
|
|
1121
|
+
if (INTERNAL_DEBUG && heightManager.viewportElement) {
|
|
1122
|
+
const domMax = Math.max(
|
|
1123
|
+
0,
|
|
1124
|
+
heightManager.viewport.scrollHeight - heightManager.viewport.clientHeight
|
|
1125
|
+
)
|
|
1126
|
+
console.info('[SVL] scroll-after-call', {
|
|
1127
|
+
scrollTop: heightManager.scrollTop,
|
|
1128
|
+
domMaxScrollTop: domMax
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1051
1131
|
})
|
|
1052
1132
|
|
|
1133
|
+
// No extra alignment step here; allow native smooth scroll to reach DOM max scrollTop
|
|
1134
|
+
|
|
1053
1135
|
// Clear the flag after scroll completes
|
|
1054
1136
|
setTimeout(
|
|
1055
1137
|
() => {
|
|
@@ -1092,14 +1174,14 @@
|
|
|
1092
1174
|
id="virtual-list-container"
|
|
1093
1175
|
{...testId ? { 'data-testid': `${testId}-container` } : {}}
|
|
1094
1176
|
class={containerClass ?? 'virtual-list-container'}
|
|
1095
|
-
bind:this={containerElement}
|
|
1177
|
+
bind:this={heightManager.containerElement}
|
|
1096
1178
|
>
|
|
1097
1179
|
<!-- Viewport handles scrolling -->
|
|
1098
1180
|
<div
|
|
1099
1181
|
id="virtual-list-viewport"
|
|
1100
1182
|
{...testId ? { 'data-testid': `${testId}-viewport` } : {}}
|
|
1101
1183
|
class={viewportClass ?? 'virtual-list-viewport'}
|
|
1102
|
-
bind:this={viewportElement}
|
|
1184
|
+
bind:this={heightManager.viewportElement}
|
|
1103
1185
|
onscroll={handleScroll}
|
|
1104
1186
|
>
|
|
1105
1187
|
<!-- Content provides full scrollable height -->
|
|
@@ -1107,10 +1189,7 @@
|
|
|
1107
1189
|
id="virtual-list-content"
|
|
1108
1190
|
{...testId ? { 'data-testid': `${testId}-content` } : {}}
|
|
1109
1191
|
class={contentClass ?? 'virtual-list-content'}
|
|
1110
|
-
style:height="{(() =>
|
|
1111
|
-
// Use ReactiveHeightManager's accurate total height for better cross-browser compatibility
|
|
1112
|
-
return Math.max(height, totalHeight())
|
|
1113
|
-
})()}px"
|
|
1192
|
+
style:height="{(() => Math.max(height, totalHeight()))()}px"
|
|
1114
1193
|
>
|
|
1115
1194
|
<!-- Items container is translated to show correct items -->
|
|
1116
1195
|
<div
|
|
@@ -1119,52 +1198,39 @@
|
|
|
1119
1198
|
class={itemsClass ?? 'virtual-list-items'}
|
|
1120
1199
|
style:visibility={height === 0 && mode === 'bottomToTop' ? 'hidden' : 'visible'}
|
|
1121
1200
|
style:transform="translateY({(() => {
|
|
1122
|
-
const viewportHeight = height || 0
|
|
1201
|
+
const viewportHeight = height || measuredFallbackHeight || 0
|
|
1123
1202
|
const visibleRange = visibleItems()
|
|
1124
1203
|
|
|
1125
|
-
//
|
|
1126
|
-
|
|
1127
|
-
let effectiveHeight = viewportHeight
|
|
1128
|
-
if (mode === 'bottomToTop' && viewportHeight === 0 && containerElement) {
|
|
1129
|
-
// Measure height synchronously if available
|
|
1130
|
-
effectiveHeight = containerElement.getBoundingClientRect().height || 400
|
|
1131
|
-
} else if (mode === 'bottomToTop' && viewportHeight === 0) {
|
|
1132
|
-
// Fallback to reasonable default height estimate for initial positioning
|
|
1133
|
-
effectiveHeight = 400
|
|
1134
|
-
}
|
|
1204
|
+
// Avoid synchronous DOM reads here; fall back once if height is 0
|
|
1205
|
+
const effectiveHeight = viewportHeight === 0 ? 400 : viewportHeight
|
|
1135
1206
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1207
|
+
// Use precise offset for topToBottom using measured heights when available
|
|
1208
|
+
const transform = Math.round(
|
|
1209
|
+
calculateTransformY(
|
|
1210
|
+
mode,
|
|
1211
|
+
items.length,
|
|
1212
|
+
visibleRange.end,
|
|
1213
|
+
visibleRange.start,
|
|
1214
|
+
heightManager.averageHeight,
|
|
1215
|
+
effectiveHeight,
|
|
1216
|
+
totalHeight(),
|
|
1217
|
+
heightManager.getHeightCache(),
|
|
1218
|
+
measuredFallbackHeight
|
|
1219
|
+
)
|
|
1144
1220
|
)
|
|
1145
1221
|
|
|
1146
1222
|
return transform
|
|
1147
1223
|
})()}px)"
|
|
1148
1224
|
>
|
|
1149
|
-
{#each (
|
|
1150
|
-
const visibleRange = visibleItems()
|
|
1151
|
-
const slice = mode === 'bottomToTop' ? items
|
|
1152
|
-
.slice(visibleRange.start, visibleRange.end)
|
|
1153
|
-
.reverse() : items.slice(visibleRange.start, visibleRange.end)
|
|
1154
|
-
|
|
1155
|
-
// Map each item with its original index for proper DOM element tracking
|
|
1156
|
-
const itemsWithOriginalIndex = slice.map( (item, sliceIndex) => ({ item, originalIndex: mode === 'bottomToTop' ? visibleRange.end - 1 - sliceIndex : visibleRange.start + sliceIndex, sliceIndex }) )
|
|
1157
|
-
|
|
1158
|
-
return itemsWithOriginalIndex
|
|
1159
|
-
})() as currentItemWithIndex, i (currentItemWithIndex.originalIndex)}
|
|
1225
|
+
{#each displayItems() as currentItemWithIndex, i (currentItemWithIndex.originalIndex)}
|
|
1160
1226
|
<!-- Only debug when visible range or average height changes -->
|
|
1161
|
-
{#if debug && i === 0 && shouldShowDebugInfo(prevVisibleRange, visibleItems(), prevHeight,
|
|
1227
|
+
{#if debug && i === 0 && shouldShowDebugInfo(prevVisibleRange, visibleItems(), prevHeight, heightManager.averageHeight)}
|
|
1162
1228
|
{@const debugInfo = createDebugInfo(
|
|
1163
1229
|
visibleItems(),
|
|
1164
1230
|
items.length,
|
|
1165
|
-
Object.keys(
|
|
1166
|
-
|
|
1167
|
-
scrollTop,
|
|
1231
|
+
Object.keys(heightManager.getHeightCache()).length,
|
|
1232
|
+
heightManager.averageHeight,
|
|
1233
|
+
heightManager.scrollTop,
|
|
1168
1234
|
height || 0,
|
|
1169
1235
|
totalHeight()
|
|
1170
1236
|
)}
|
|
@@ -1177,6 +1243,11 @@
|
|
|
1177
1243
|
bind:this={itemElements[currentItemWithIndex.sliceIndex]}
|
|
1178
1244
|
use:autoObserveItemResize
|
|
1179
1245
|
data-original-index={currentItemWithIndex.originalIndex}
|
|
1246
|
+
{...testId
|
|
1247
|
+
? {
|
|
1248
|
+
'data-testid': `${testId}-item-${currentItemWithIndex.originalIndex}`
|
|
1249
|
+
}
|
|
1250
|
+
: {}}
|
|
1180
1251
|
>
|
|
1181
1252
|
{@render renderItem(
|
|
1182
1253
|
currentItemWithIndex.item,
|