@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6-beta.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.
@@ -1,45 +1,63 @@
1
1
  <!--
2
- @component
3
- A high-performance virtualized list component that efficiently renders large datasets
4
- by only mounting DOM nodes for visible items and a small buffer. Optimized for handling
5
- lists of 10k+ items through chunked processing and progressive initialization.
6
-
7
- Props:
8
- - `items` - Array of items to render
9
- - `defaultEstimatedItemHeight` - Initial height estimate for items (default: 40px)
10
- - `mode` - Scroll direction: 'topToBottom' or 'bottomToTop' (default: 'topToBottom')
11
- - `debug` - Enable debug logging (default: false)
12
- - `bufferSize` - Number of items to render outside visible area (default: 20)
13
- - `containerClass` - Custom class for container element
14
- - `viewportClass` - Custom class for viewport element
15
- - `contentClass` - Custom class for content wrapper
16
- - `itemsClass` - Custom class for items wrapper
17
- - `debugFunction` - Custom debug logging function
18
- - `testId` - Base test ID for component elements
19
-
20
- Usage:
2
+ @component SvelteVirtualList
3
+
4
+ A high-performance, memory-efficient virtualized list component for Svelte 5.
5
+ Renders only visible items plus a buffer, supporting dynamic item heights,
6
+ bi-directional (top-to-bottom and bottom-to-top) scrolling, and programmatic control.
7
+
8
+ =============================
9
+ == Key Features ==
10
+ =============================
11
+ - Dynamic item height support (no fixed height required)
12
+ - Top-to-bottom and bottom-to-top (chat-style) scrolling
13
+ - Programmatic scrolling with flexible alignment (top, bottom, auto)
14
+ - Smooth scrolling and buffer size configuration
15
+ - SSR compatible and hydration-friendly
16
+ - TypeScript and Svelte 5 runes/snippets support
17
+ - Customizable styling via class props
18
+ - Debug mode for development and testing
19
+ - Optimized for large lists (10k+ items)
20
+ - Comprehensive test coverage (unit and E2E)
21
+
22
+ =============================
23
+ == Usage Example ==
24
+ =============================
21
25
  ```svelte
22
26
  <SvelteVirtualList
23
27
  items={data}
24
- defaultEstimatedItemHeight={40}
25
- mode="topToBottom"
28
+ mode="bottomToTop"
29
+ bind:this={listRef}
26
30
  >
27
- {#snippet renderItem(item, index)}
28
- <div class="item">{item.text}</div>
31
+ {#snippet renderItem(item)}
32
+ <div>{item.text}</div>
29
33
  {/snippet}
30
34
  </SvelteVirtualList>
31
35
  ```
32
36
 
33
- Features:
34
- - Dynamic height calculation
35
- - Bidirectional scrolling
36
- - Configurable buffer size
37
- - Debug mode
38
- - Custom styling
39
- - Progressive initialization for large datasets
40
- - Memory-optimized for 10k+ items
41
- - Chunked processing for smooth performance
42
- - Progress tracking during initialization
37
+ =============================
38
+ == Architecture Notes ==
39
+ =============================
40
+ - Uses a four-layer DOM structure for optimal performance
41
+ - Only visible items + buffer are mounted in the DOM
42
+ - Height caching and estimation for dynamic content
43
+ - Handles resize events and dynamic content changes
44
+ - Supports chunked initialization for very large lists
45
+ - All scrolling logic is centralized in the scroll() method
46
+ - Bi-directional support: mode="topToBottom" or "bottomToTop"
47
+ - Designed for extensibility and easy debugging
48
+
49
+ =============================
50
+ == For Contributors ==
51
+ =============================
52
+ - Please keep all scrolling logic in the scroll() method
53
+ - Add new features behind feature flags or as optional props
54
+ - Write tests for all new features (see /test and /tests/scroll)
55
+ - Use TypeScript and Svelte 5 runes for all new code
56
+ - Document all exported functions and props with JSDoc
57
+ - See README.md for API and usage details
58
+ - For questions, open an issue or discussion on GitHub
59
+
60
+ MIT License © Humanspeak, Inc.
43
61
  -->
44
62
 
45
63
  <script lang="ts">
@@ -122,19 +140,27 @@
122
140
  * - Progressive size adjustment system
123
141
  */
124
142
 
125
- import { onMount } from 'svelte'
126
- import { BROWSER } from 'esm-env'
127
- import type { SvelteVirtualListProps } from './types.js'
143
+ import {
144
+ DEFAULT_SCROLL_OPTIONS,
145
+ type SvelteVirtualListPreviousVisibleRange,
146
+ type SvelteVirtualListProps,
147
+ type SvelteVirtualListScrollOptions
148
+ } from './types.js'
149
+ import { calculateAverageHeightDebounced } from './utils/heightCalculation.js'
150
+ import { createRafScheduler } from './utils/raf.js'
128
151
  import {
129
152
  calculateScrollPosition,
130
- calculateVisibleRange,
131
153
  calculateTransformY,
132
- updateHeightAndScroll as utilsUpdateHeightAndScroll,
133
- calculateAverageHeight,
134
- processChunked
154
+ calculateVisibleRange,
155
+ getScrollOffsetForIndex,
156
+ processChunked,
157
+ updateHeightAndScroll as utilsUpdateHeightAndScroll
135
158
  } from './utils/virtualList.js'
136
- import { rafSchedule } from './utils/raf.js'
137
- import { shouldShowDebugInfo, createDebugInfo } from './utils/virtualListDebug.js'
159
+ import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
160
+ import { BROWSER } from 'esm-env'
161
+ import { onMount, tick } from 'svelte'
162
+
163
+ const rafSchedule = createRafScheduler()
138
164
 
139
165
  /**
140
166
  * Core configuration props with default values
@@ -182,90 +208,43 @@
182
208
  */
183
209
  let heightUpdateTimeout: ReturnType<typeof setTimeout> | null = null // Debounce timer for height updates
184
210
  let resizeObserver: ResizeObserver | null = null // Watches for container size changes
211
+ let itemResizeObserver: ResizeObserver | null = null // Watches for individual item size changes
185
212
 
186
213
  /**
187
214
  * Performance Optimization State
188
215
  */
189
216
  let heightCache = $state<Record<number, number>>({}) // Cache of measured item heights
217
+ let dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
190
218
  const chunkSize = $state(50) // Number of items to process in each chunk
191
219
  let processedItems = $state(0) // Number of items processed during initialization
192
220
 
193
- let prevVisibleRange = $state<{ start: number; end: number } | null>(null)
221
+ let prevVisibleRange = $state<SvelteVirtualListPreviousVisibleRange | null>(null)
194
222
  let prevHeight = $state<number>(0)
195
223
 
196
- /**
197
- * Calculates and updates the average height of visible items with debouncing.
198
- *
199
- * This function optimizes performance by:
200
- * - Debouncing calculations to prevent excessive DOM reads
201
- * - Caching item heights to minimize recalculations
202
- * - Only updating when significant changes are detected
203
- *
204
- * Implementation details:
205
- * - Uses a 200ms debounce timeout
206
- * - Tracks calculation state to prevent concurrent updates
207
- * - Caches heights in heightCache for reuse
208
- * - Only updates if height difference > 1px
209
- *
210
- * State interactions:
211
- * - Updates calculatedItemHeight
212
- * - Updates lastMeasuredIndex
213
- * - Modifies heightCache
214
- * - Uses/sets isCalculatingHeight flag
215
- *
216
- * @example
217
- * ```typescript
218
- * // Automatically called when items are rendered
219
- * $effect(() => {
220
- * if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
221
- * calculateAverageHeightDebounced()
222
- * }
223
- * })
224
- * ```
225
- *
226
- * @returns {void}
227
- */
228
- const calculateAverageHeightDebounced = () => {
229
- if (!BROWSER || isCalculatingHeight || heightUpdateTimeout) return
230
- isCalculatingHeight = true
231
-
232
- if (heightUpdateTimeout) {
233
- clearTimeout(heightUpdateTimeout)
234
- }
235
-
236
- heightUpdateTimeout = setTimeout(() => {
237
- const visibleRange = visibleItems()
238
- const currentIndex = visibleRange.start
239
-
240
- if (currentIndex !== lastMeasuredIndex) {
241
- const { newHeight, newLastMeasuredIndex, updatedHeightCache } =
242
- calculateAverageHeight(
243
- itemElements,
244
- visibleRange,
245
- heightCache,
246
- lastMeasuredIndex,
247
- calculatedItemHeight
248
- )
249
-
250
- if (Math.abs(newHeight - calculatedItemHeight) > 1) {
251
- calculatedItemHeight = newHeight
252
- lastMeasuredIndex = newLastMeasuredIndex
253
- heightCache = updatedHeightCache
254
- }
255
- }
256
-
257
- isCalculatingHeight = false
258
- heightUpdateTimeout = null
259
- }, 200)
260
- }
261
-
262
224
  // Trigger height calculation when items are rendered
263
225
  $effect(() => {
264
226
  if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
265
- calculateAverageHeightDebounced()
227
+ updateHeight()
266
228
  }
267
229
  })
268
230
 
231
+ const updateHeight = () => {
232
+ heightUpdateTimeout = calculateAverageHeightDebounced(
233
+ isCalculatingHeight,
234
+ heightUpdateTimeout,
235
+ visibleItems,
236
+ itemElements,
237
+ heightCache,
238
+ lastMeasuredIndex,
239
+ calculatedItemHeight,
240
+ (result) => {
241
+ calculatedItemHeight = result.newHeight
242
+ lastMeasuredIndex = result.newLastMeasuredIndex
243
+ heightCache = result.updatedHeightCache
244
+ }
245
+ )
246
+ }
247
+
269
248
  // Add new effect to handle height changes
270
249
  $effect(() => {
271
250
  if (BROWSER && initialized && mode === 'bottomToTop' && viewportElement) {
@@ -305,7 +284,7 @@
305
284
  const targetScrollTop = Math.max(0, totalHeight - height)
306
285
 
307
286
  // Add delay to ensure layout is complete
308
- setTimeout(() => {
287
+ tick().then(() => {
309
288
  if (viewportElement) {
310
289
  // Start at the bottom for bottom-to-top mode
311
290
  viewportElement.scrollTop = targetScrollTop
@@ -320,7 +299,7 @@
320
299
  initialized = true
321
300
  })
322
301
  }
323
- }, 50)
302
+ })
324
303
  }
325
304
  })
326
305
 
@@ -341,10 +320,10 @@
341
320
  * console.log(`Rendering items from ${range.start} to ${range.end}`)
342
321
  * ```
343
322
  *
344
- * @returns {{ start: number, end: number }} Object containing start and end indices of visible items
323
+ * @returns {SvelteVirtualListPreviousVisibleRange} Object containing start and end indices of visible items
345
324
  */
346
- const visibleItems = $derived(() => {
347
- if (!items.length) return { start: 0, end: 0 }
325
+ const visibleItems = $derived((): SvelteVirtualListPreviousVisibleRange => {
326
+ if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
348
327
  const viewportHeight = height || 0
349
328
 
350
329
  return calculateVisibleRange(
@@ -406,12 +385,12 @@
406
385
  */
407
386
  const updateHeightAndScroll = (immediate = false) => {
408
387
  if (!initialized && mode === 'bottomToTop') {
409
- setTimeout(() => {
388
+ tick().then(() => {
410
389
  if (containerElement) {
411
390
  const initialHeight = containerElement.getBoundingClientRect().height
412
391
  height = initialHeight
413
392
 
414
- setTimeout(() => {
393
+ tick().then(() => {
415
394
  if (containerElement && viewportElement) {
416
395
  const finalHeight = containerElement.getBoundingClientRect().height
417
396
  height = finalHeight
@@ -438,9 +417,9 @@
438
417
  }
439
418
  })
440
419
  }
441
- }, 100)
420
+ })
442
421
  }
443
- }, 100)
422
+ })
444
423
  return
445
424
  }
446
425
 
@@ -510,6 +489,44 @@
510
489
  }
511
490
  })
512
491
 
492
+ // Create itemResizeObserver immediately when in browser
493
+ if (BROWSER) {
494
+ // Watch for individual item size changes
495
+ itemResizeObserver = new ResizeObserver((entries) => {
496
+ let shouldRecalculate = false
497
+
498
+ if (debug) {
499
+ console.log(`ResizeObserver fired for ${entries.length} entries`)
500
+ }
501
+
502
+ for (const entry of entries) {
503
+ const element = entry.target as HTMLElement
504
+ const elementIndex = itemElements.indexOf(element)
505
+
506
+ if (elementIndex !== -1) {
507
+ const actualIndex = visibleItems().start + elementIndex
508
+
509
+ // ResizeObserver fired = element resized, so add to dirty queue
510
+ dirtyItems.add(actualIndex)
511
+ shouldRecalculate = true
512
+
513
+ if (debug) {
514
+ console.log(
515
+ `Item ${actualIndex} marked dirty (resized), queue size: ${dirtyItems.size}`
516
+ )
517
+ }
518
+ }
519
+ }
520
+
521
+ if (shouldRecalculate) {
522
+ // Trigger virtual list recalculation
523
+ rafSchedule(() => {
524
+ updateHeight()
525
+ })
526
+ }
527
+ })
528
+ }
529
+
513
530
  // Setup and cleanup
514
531
  onMount(() => {
515
532
  if (BROWSER) {
@@ -530,6 +547,9 @@
530
547
  if (resizeObserver) {
531
548
  resizeObserver.disconnect()
532
549
  }
550
+ if (itemResizeObserver) {
551
+ itemResizeObserver.disconnect()
552
+ }
533
553
  }
534
554
  }
535
555
  })
@@ -541,6 +561,279 @@
541
561
  prevHeight = calculatedItemHeight
542
562
  }
543
563
  })
564
+
565
+ /**
566
+ * Scrolls the virtual list to the item at the given index.
567
+ *
568
+ * @deprecated This function is deprecated and will be removed in a future version.
569
+ * Use the new scroll method from the component instance instead.
570
+ *
571
+ * @function scrollToIndex
572
+ * @param index The index of the item to scroll to.
573
+ * @param smoothScroll (default: true) Whether to use smooth scrolling.
574
+ * @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
575
+ *
576
+ * @example
577
+ * // Svelte usage:
578
+ * // In your <script> block:
579
+ * import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
580
+ * let virtualList;
581
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
582
+ *
583
+ * // In your markup:
584
+ * <button onclick={() => virtualList.scrollToIndex(5000)}>
585
+ * Scroll to 5000
586
+ * </button>
587
+ * <SvelteVirtualList {items} bind:this={virtualList}>
588
+ * {#snippet renderItem(item)}
589
+ * <div>{item.text}</div>
590
+ * {/snippet}
591
+ * </SvelteVirtualList>
592
+ *
593
+ * @returns {void}
594
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
595
+ */
596
+ export const scrollToIndex = (
597
+ index: number,
598
+ smoothScroll = true,
599
+ shouldThrowOnBounds = true
600
+ ): void => {
601
+ // Deprecation warning
602
+ console.warn(
603
+ 'SvelteVirtualList: scrollToIndex is deprecated and will be removed in a future version. ' +
604
+ 'Use the new scroll method from the component instance instead.'
605
+ )
606
+
607
+ // Call the new scroll function with the provided parameters
608
+ scroll({ index, smoothScroll, shouldThrowOnBounds })
609
+ }
610
+
611
+ /**
612
+ * Scrolls the virtual list to the item at the given index using a type-based options approach.
613
+ *
614
+ * @function scroll
615
+ * @param options Configuration options for scrolling behavior.
616
+ *
617
+ * @example
618
+ * // Svelte usage:
619
+ * // In your <script> block:
620
+ * import SvelteVirtualList from './index.js';
621
+ * let virtualList;
622
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
623
+ *
624
+ * <button onclick={() => virtualList.scroll({ index: 5000 })}>
625
+ * Scroll to 5000
626
+ * </button>
627
+ * <SvelteVirtualList {items} bind:this={virtualList}>
628
+ * {#snippet renderItem(item)}
629
+ * <div>{item.text}</div>
630
+ * {/snippet}
631
+ * </SvelteVirtualList>
632
+ *
633
+ * @returns {void}
634
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
635
+ */
636
+ export const scroll = (options: SvelteVirtualListScrollOptions): void => {
637
+ const { index, smoothScroll, shouldThrowOnBounds, align } = {
638
+ ...DEFAULT_SCROLL_OPTIONS,
639
+ ...options
640
+ }
641
+
642
+ if (!items.length) return
643
+ if (!viewportElement) {
644
+ tick().then(() => {
645
+ if (!viewportElement) return
646
+ scroll({ index, smoothScroll, shouldThrowOnBounds, align })
647
+ })
648
+ return
649
+ }
650
+
651
+ // Bounds checking
652
+ let targetIndex = index
653
+ if (targetIndex < 0 || targetIndex >= items.length) {
654
+ if (shouldThrowOnBounds) {
655
+ throw new Error(
656
+ `scroll: index ${targetIndex} is out of bounds (0-${items.length - 1})`
657
+ )
658
+ } else {
659
+ targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1))
660
+ }
661
+ }
662
+
663
+ const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
664
+ let scrollTarget: number | null = null
665
+
666
+ if (mode === 'bottomToTop') {
667
+ const totalHeight = items.length * calculatedItemHeight
668
+ const itemOffset = targetIndex * calculatedItemHeight
669
+ const itemHeight = calculatedItemHeight
670
+ if (align === 'auto') {
671
+ // If item is above the viewport, align to top
672
+ if (targetIndex < firstVisibleIndex) {
673
+ scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
674
+ // If item is below the viewport, align to bottom
675
+ } else if (targetIndex > lastVisibleIndex - 1) {
676
+ scrollTarget = Math.max(0, totalHeight - itemOffset - height)
677
+ } else {
678
+ // Item is visible but not aligned: align to nearest edge
679
+ // Calculate the offset of the item relative to the viewport
680
+ const itemTop = totalHeight - (itemOffset + itemHeight)
681
+ const itemBottom = totalHeight - itemOffset
682
+ const distanceToTop = Math.abs(scrollTop - itemTop)
683
+ const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
684
+ if (distanceToTop < distanceToBottom) {
685
+ // Closer to top, align to top
686
+ scrollTarget = itemTop
687
+ } else {
688
+ // Closer to bottom, align to bottom
689
+ scrollTarget = Math.max(0, itemBottom - height)
690
+ }
691
+ }
692
+ } else if (align === 'top') {
693
+ // Align to top
694
+ scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
695
+ } else if (align === 'bottom') {
696
+ // Align to bottom
697
+ scrollTarget = Math.max(0, totalHeight - itemOffset - height)
698
+ } else if (align === 'nearest') {
699
+ // If not visible, align to nearest edge; if visible, do nothing
700
+ const itemTop = totalHeight - (itemOffset + itemHeight)
701
+ const itemBottom = totalHeight - itemOffset
702
+ if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
703
+ // Not visible, align to nearest edge
704
+ const distanceToTop = Math.abs(scrollTop - itemTop)
705
+ const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
706
+ if (distanceToTop < distanceToBottom) {
707
+ scrollTarget = itemTop
708
+ } else {
709
+ scrollTarget = Math.max(0, itemBottom - height)
710
+ }
711
+ } else {
712
+ // Already visible, do nothing
713
+ return
714
+ }
715
+ }
716
+ } else {
717
+ // topToBottom (default)
718
+ if (align === 'auto') {
719
+ // If item is above the viewport, align to top
720
+ if (targetIndex < firstVisibleIndex) {
721
+ scrollTarget = getScrollOffsetForIndex(
722
+ heightCache,
723
+ calculatedItemHeight,
724
+ targetIndex
725
+ )
726
+ // If item is below the viewport, align to bottom
727
+ } else if (targetIndex > lastVisibleIndex - 1) {
728
+ const itemBottom = getScrollOffsetForIndex(
729
+ heightCache,
730
+ calculatedItemHeight,
731
+ targetIndex + 1
732
+ )
733
+ scrollTarget = Math.max(0, itemBottom - height)
734
+ } else {
735
+ // Item is visible but not aligned: align to nearest edge
736
+ const itemTop = getScrollOffsetForIndex(
737
+ heightCache,
738
+ calculatedItemHeight,
739
+ targetIndex
740
+ )
741
+ const itemBottom = getScrollOffsetForIndex(
742
+ heightCache,
743
+ calculatedItemHeight,
744
+ targetIndex + 1
745
+ )
746
+ const distanceToTop = Math.abs(scrollTop - itemTop)
747
+ const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
748
+ if (distanceToTop < distanceToBottom) {
749
+ // Closer to top, align to top
750
+ scrollTarget = itemTop
751
+ } else {
752
+ // Closer to bottom, align to bottom
753
+ scrollTarget = Math.max(0, itemBottom - height)
754
+ }
755
+ }
756
+ } else if (align === 'top') {
757
+ scrollTarget = getScrollOffsetForIndex(
758
+ heightCache,
759
+ calculatedItemHeight,
760
+ targetIndex
761
+ )
762
+ } else if (align === 'bottom') {
763
+ const itemBottom = getScrollOffsetForIndex(
764
+ heightCache,
765
+ calculatedItemHeight,
766
+ targetIndex + 1
767
+ )
768
+ scrollTarget = Math.max(0, itemBottom - height)
769
+ } else if (align === 'nearest') {
770
+ const itemTop = getScrollOffsetForIndex(
771
+ heightCache,
772
+ calculatedItemHeight,
773
+ targetIndex
774
+ )
775
+ const itemBottom = getScrollOffsetForIndex(
776
+ heightCache,
777
+ calculatedItemHeight,
778
+ targetIndex + 1
779
+ )
780
+ if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
781
+ // Not visible, align to nearest edge
782
+ const distanceToTop = Math.abs(scrollTop - itemTop)
783
+ const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
784
+ if (distanceToTop < distanceToBottom) {
785
+ scrollTarget = itemTop
786
+ } else {
787
+ scrollTarget = Math.max(0, itemBottom - height)
788
+ }
789
+ } else {
790
+ // Already visible, do nothing
791
+ return
792
+ }
793
+ }
794
+ }
795
+
796
+ if (scrollTarget !== null) {
797
+ viewportElement.scrollTo({
798
+ top: scrollTarget,
799
+ behavior: smoothScroll ? 'smooth' : 'auto'
800
+ })
801
+ }
802
+ }
803
+
804
+ /**
805
+ * Custom Svelte action to automatically observe item elements for size changes.
806
+ * This action is applied to each item element to detect when its dimensions change.
807
+ *
808
+ * @param element - The HTML element to observe
809
+ * @returns {{ destroy: () => void }} Object with destroy method for cleanup
810
+ */
811
+ function autoObserveItemResize(element: HTMLElement) {
812
+ if (itemResizeObserver) {
813
+ itemResizeObserver.observe(element)
814
+ if (debug) {
815
+ console.log(
816
+ 'Started observing element:',
817
+ element,
818
+ 'Current height:',
819
+ element.getBoundingClientRect().height
820
+ )
821
+ }
822
+ } else if (debug) {
823
+ console.log('itemResizeObserver not available for element:', element)
824
+ }
825
+
826
+ return {
827
+ destroy() {
828
+ if (itemResizeObserver) {
829
+ itemResizeObserver.unobserve(element)
830
+ if (debug) {
831
+ console.log('Stopped observing element:', element)
832
+ }
833
+ }
834
+ }
835
+ }
836
+ }
544
837
  </script>
545
838
 
546
839
  <!--
@@ -597,10 +890,10 @@
597
890
  )}
598
891
  {debugFunction
599
892
  ? debugFunction(debugInfo)
600
- : console.log('Virtual List Debug:', debugInfo)}
893
+ : console.info('Virtual List Debug:', debugInfo)}
601
894
  {/if}
602
895
  <!-- Render each visible item -->
603
- <div bind:this={itemElements[i]}>
896
+ <div bind:this={itemElements[i]} use:autoObserveItemResize>
604
897
  {@render renderItem(
605
898
  currentItem,
606
899
  mode === 'bottomToTop'
@@ -648,4 +941,10 @@
648
941
  left: 0;
649
942
  top: 0;
650
943
  }
944
+
945
+ /* Item wrapper divs should size to their content */
946
+ .virtual-list-items > div {
947
+ width: 100%;
948
+ display: block;
949
+ }
651
950
  </style>