@pdanpdan/virtual-scroll 0.2.0 → 0.3.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.
@@ -6,102 +6,129 @@ import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, reactiv
6
6
  import { FenwickTree } from '../utils/fenwick-tree';
7
7
  import { getPaddingX, getPaddingY, isElement, isScrollableElement, isScrollToIndexOptions } from '../utils/scroll';
8
8
 
9
- export const DEFAULT_ITEM_SIZE = 50;
10
- export const DEFAULT_COLUMN_WIDTH = 150;
9
+ export const DEFAULT_ITEM_SIZE = 40;
10
+ export const DEFAULT_COLUMN_WIDTH = 100;
11
11
  export const DEFAULT_BUFFER = 5;
12
12
 
13
13
  export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
14
14
  export type ScrollAlignment = 'start' | 'center' | 'end' | 'auto';
15
15
 
16
+ /** Options for scroll alignment in a single axis or both axes. */
16
17
  export interface ScrollAlignmentOptions {
18
+ /** Alignment on the X axis. */
17
19
  x?: ScrollAlignment;
20
+ /** Alignment on the Y axis. */
18
21
  y?: ScrollAlignment;
19
22
  }
20
23
 
24
+ /** Options for the scrollToIndex method. */
21
25
  export interface ScrollToIndexOptions {
26
+ /** Where to align the item in the viewport. */
22
27
  align?: ScrollAlignment | ScrollAlignmentOptions;
28
+ /** Scroll behavior. */
23
29
  behavior?: 'auto' | 'smooth';
30
+ /** Internal flag for recursive correction calls. */
24
31
  isCorrection?: boolean;
25
32
  }
26
33
 
34
+ /** Configuration properties for the useVirtualScroll composable. */
27
35
  export interface VirtualScrollProps<T = unknown> {
28
- /** Array of items to be virtualized */
36
+ /** Array of items to be virtualized. */
29
37
  items: T[];
30
- /** Fixed size of each item or a function that returns the size of an item */
38
+ /** Fixed size of each item or a function that returns the size of an item. */
31
39
  itemSize?: number | ((item: T, index: number) => number) | undefined;
32
- /** Direction of the scroll: 'vertical', 'horizontal', or 'both' */
40
+ /** Direction of the scroll: 'vertical', 'horizontal', or 'both'. */
33
41
  direction?: ScrollDirection | undefined;
34
- /** Number of items to render before the visible viewport */
42
+ /** Number of items to render before the visible viewport. */
35
43
  bufferBefore?: number | undefined;
36
- /** Number of items to render after the visible viewport */
44
+ /** Number of items to render after the visible viewport. */
37
45
  bufferAfter?: number | undefined;
38
- /** The scrollable container element or window */
46
+ /** The scrollable container element or window. */
39
47
  container?: HTMLElement | Window | null | undefined;
40
- /** The host element that contains the items */
48
+ /** The host element that contains the items. */
41
49
  hostElement?: HTMLElement | null | undefined;
42
- /** Range of items to render for SSR */
50
+ /** Range of items to render for SSR. */
43
51
  ssrRange?: {
44
52
  start: number;
45
53
  end: number;
46
54
  colStart?: number;
47
55
  colEnd?: number;
48
56
  } | undefined;
49
- /** Number of columns for bidirectional scroll */
57
+ /** Number of columns for bidirectional scroll. */
50
58
  columnCount?: number | undefined;
51
- /** Fixed width of columns or an array of widths for alternating columns */
59
+ /** Fixed width of columns or an array/function for column widths. */
52
60
  columnWidth?: number | number[] | ((index: number) => number) | undefined;
53
- /** Padding at the start of the scroll container (e.g. for sticky headers) */
61
+ /** Padding at the start of the scroll container. */
54
62
  scrollPaddingStart?: number | { x?: number; y?: number; } | undefined;
55
- /** Padding at the end of the scroll container */
63
+ /** Padding at the end of the scroll container. */
56
64
  scrollPaddingEnd?: number | { x?: number; y?: number; } | undefined;
57
- /** Gap between items in pixels (vertical) */
65
+ /** Gap between items in pixels (vertical). */
58
66
  gap?: number | undefined;
59
- /** Gap between columns in pixels (horizontal/grid) */
67
+ /** Gap between columns in pixels (horizontal/grid). */
60
68
  columnGap?: number | undefined;
61
- /** Indices of items that should stick to the top/start */
69
+ /** Indices of items that should stick to the top/start. */
62
70
  stickyIndices?: number[] | undefined;
63
- /** Distance from the end of the scrollable area to trigger 'load' event */
71
+ /** Distance from the end of the scrollable area to trigger 'load' event. */
64
72
  loadDistance?: number | undefined;
65
- /** Whether items are currently being loaded */
73
+ /** Whether items are currently being loaded. */
66
74
  loading?: boolean | undefined;
67
- /** Whether to restore scroll position when items are prepended */
75
+ /** Whether to restore scroll position when items are prepended. */
68
76
  restoreScrollOnPrepend?: boolean | undefined;
69
- /** Initial scroll index to jump to on mount */
77
+ /** Initial scroll index to jump to on mount. */
70
78
  initialScrollIndex?: number | undefined;
71
- /** Alignment for the initial scroll index */
79
+ /** Alignment for the initial scroll index. */
72
80
  initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions | undefined;
73
- /** Default size for items before they are measured */
81
+ /** Default size for items before they are measured. */
74
82
  defaultItemSize?: number | undefined;
75
- /** Default width for columns before they are measured */
83
+ /** Default width for columns before they are measured. */
76
84
  defaultColumnWidth?: number | undefined;
77
- /** Whether to enable debug mode (e.g. showing offsets) */
85
+ /** Whether to enable debug mode. */
78
86
  debug?: boolean | undefined;
79
87
  }
80
88
 
89
+ /** Represents an item currently rendered in the virtual scroll area. */
81
90
  export interface RenderedItem<T = unknown> {
91
+ /** The original data item. */
82
92
  item: T;
93
+ /** The index of the item in the original array. */
83
94
  index: number;
95
+ /** The calculated offset relative to the host element. */
84
96
  offset: { x: number; y: number; };
97
+ /** The current measured or estimated size. */
85
98
  size: { width: number; height: number; };
99
+ /** The original X offset before sticky adjustments. */
86
100
  originalX: number;
101
+ /** The original Y offset before sticky adjustments. */
87
102
  originalY: number;
103
+ /** Whether this item is configured to be sticky. */
88
104
  isSticky?: boolean;
105
+ /** Whether this item is currently stuck at the threshold. */
89
106
  isStickyActive?: boolean;
107
+ /** The offset applied for the sticky pushing effect. */
90
108
  stickyOffset: { x: number; y: number; };
91
109
  }
92
110
 
111
+ /** Comprehensive state of the virtual scroll system. */
93
112
  export interface ScrollDetails<T = unknown> {
113
+ /** List of items currently rendered. */
94
114
  items: RenderedItem<T>[];
115
+ /** Index of the first item partially or fully visible in the viewport. */
95
116
  currentIndex: number;
117
+ /** Index of the first column partially or fully visible. */
96
118
  currentColIndex: number;
119
+ /** Current scroll position relative to content start. */
97
120
  scrollOffset: { x: number; y: number; };
121
+ /** Dimensions of the visible viewport. */
98
122
  viewportSize: { width: number; height: number; };
123
+ /** Total calculated size of all items and gaps. */
99
124
  totalSize: { width: number; height: number; };
125
+ /** Whether the container is currently being scrolled. */
100
126
  isScrolling: boolean;
127
+ /** Whether the current scroll was initiated by a method call. */
101
128
  isProgrammaticScroll: boolean;
102
- /** Range of items currently being rendered */
129
+ /** Range of items currently being rendered. */
103
130
  range: { start: number; end: number; };
104
- /** Range of columns currently being rendered (for grid mode) */
131
+ /** Range of columns currently being rendered (for grid mode). */
105
132
  columnRange: { start: number; end: number; padStart: number; padEnd: number; };
106
133
  }
107
134
 
@@ -144,9 +171,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
144
171
  options: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
145
172
  } | null>(null);
146
173
 
147
- const maxWidth = ref(0);
148
- const maxHeight = ref(0);
149
-
150
174
  // Track if sizes are initialized
151
175
  const sizesInitialized = ref(false);
152
176
  let lastItems: T[] = [];
@@ -164,7 +188,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
164
188
  (typeof props.value.itemSize === 'number' && props.value.itemSize > 0) ? props.value.itemSize : null,
165
189
  );
166
190
 
167
- const defaultSize = computed(() => fixedItemSize.value || props.value.defaultItemSize || DEFAULT_ITEM_SIZE);
191
+ const defaultSize = computed(() => props.value.defaultItemSize || fixedItemSize.value || DEFAULT_ITEM_SIZE);
168
192
 
169
193
  const sortedStickyIndices = computed(() =>
170
194
  [ ...(props.value.stickyIndices || []) ].sort((a, b) => a - b),
@@ -181,27 +205,42 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
181
205
  if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
182
206
  const { start = 0, end = 0, colStart = 0, colEnd = 0 } = props.value.ssrRange;
183
207
  const colCount = props.value.columnCount || 0;
184
- if (props.value.direction === 'both' && colCount > 0) {
185
- return columnSizes.query(colEnd || colCount) - columnSizes.query(colStart);
208
+ if (props.value.direction === 'both') {
209
+ if (colCount <= 0) {
210
+ return 0;
211
+ }
212
+ const effectiveColEnd = colEnd || colCount;
213
+ const total = columnSizes.query(effectiveColEnd) - columnSizes.query(colStart);
214
+ return Math.max(0, total - (effectiveColEnd > colStart ? (props.value.columnGap || 0) : 0));
186
215
  }
216
+ /* v8 ignore else -- @preserve */
187
217
  if (props.value.direction === 'horizontal') {
188
218
  if (fixedItemSize.value !== null) {
189
- return (end - start) * (fixedItemSize.value + (props.value.columnGap || 0));
219
+ const len = end - start;
220
+ return Math.max(0, len * (fixedItemSize.value + (props.value.columnGap || 0)) - (len > 0 ? (props.value.columnGap || 0) : 0));
190
221
  }
191
- return itemSizesX.query(end) - itemSizesX.query(start);
222
+ const total = itemSizesX.query(end) - itemSizesX.query(start);
223
+ return Math.max(0, total - (end > start ? (props.value.columnGap || 0) : 0));
192
224
  }
193
225
  }
194
226
 
195
- if (props.value.direction === 'both' && props.value.columnCount) {
196
- return columnSizes.query(props.value.columnCount);
227
+ if (props.value.direction === 'both') {
228
+ const colCount = props.value.columnCount || 0;
229
+ if (colCount <= 0) {
230
+ return 0;
231
+ }
232
+ const total = columnSizes.query(colCount);
233
+ return Math.max(0, total - (props.value.columnGap || 0));
197
234
  }
198
235
  if (props.value.direction === 'vertical') {
199
236
  return 0;
200
237
  }
201
238
  if (fixedItemSize.value !== null) {
202
- return props.value.items.length * (fixedItemSize.value + (props.value.columnGap || 0));
239
+ const len = props.value.items.length;
240
+ return Math.max(0, len * (fixedItemSize.value + (props.value.columnGap || 0)) - (len > 0 ? (props.value.columnGap || 0) : 0));
203
241
  }
204
- return itemSizesX.query(props.value.items.length);
242
+ const total = itemSizesX.query(props.value.items.length);
243
+ return Math.max(0, total - (props.value.items.length > 0 ? (props.value.columnGap || 0) : 0));
205
244
  });
206
245
 
207
246
  /**
@@ -213,11 +252,14 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
213
252
 
214
253
  if (!isHydrated.value && props.value.ssrRange && !isMounted.value) {
215
254
  const { start, end } = props.value.ssrRange;
255
+ /* v8 ignore else -- @preserve */
216
256
  if (props.value.direction === 'vertical' || props.value.direction === 'both') {
217
257
  if (fixedItemSize.value !== null) {
218
- return (end - start) * (fixedItemSize.value + (props.value.gap || 0));
258
+ const len = end - start;
259
+ return Math.max(0, len * (fixedItemSize.value + (props.value.gap || 0)) - (len > 0 ? (props.value.gap || 0) : 0));
219
260
  }
220
- return itemSizesY.query(end) - itemSizesY.query(start);
261
+ const total = itemSizesY.query(end) - itemSizesY.query(start);
262
+ return Math.max(0, total - (end > start ? (props.value.gap || 0) : 0));
221
263
  }
222
264
  }
223
265
 
@@ -225,9 +267,11 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
225
267
  return 0;
226
268
  }
227
269
  if (fixedItemSize.value !== null) {
228
- return props.value.items.length * (fixedItemSize.value + (props.value.gap || 0));
270
+ const len = props.value.items.length;
271
+ return Math.max(0, len * (fixedItemSize.value + (props.value.gap || 0)) - (len > 0 ? (props.value.gap || 0) : 0));
229
272
  }
230
- return itemSizesY.query(props.value.items.length);
273
+ const total = itemSizesY.query(props.value.items.length);
274
+ return Math.max(0, total - (props.value.items.length > 0 ? (props.value.gap || 0) : 0));
231
275
  });
232
276
 
233
277
  const relativeScrollX = computed(() => {
@@ -251,8 +295,10 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
251
295
  return cw;
252
296
  }
253
297
  if (Array.isArray(cw) && cw.length > 0) {
254
- return cw[ index % cw.length ] || DEFAULT_COLUMN_WIDTH;
298
+ const val = cw[ index % cw.length ];
299
+ return (val != null && val > 0) ? val : (props.value.defaultColumnWidth || DEFAULT_COLUMN_WIDTH);
255
300
  }
301
+ /* v8 ignore else -- @preserve */
256
302
  if (typeof cw === 'function') {
257
303
  return cw(index);
258
304
  }
@@ -354,7 +400,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
354
400
  itemWidth = fixedSize !== null ? fixedSize : itemSizesX.get(colIndex) - columnGap;
355
401
  } else {
356
402
  targetX = columnSizes.query(colIndex);
357
- itemWidth = columnSizes.get(colIndex);
403
+ itemWidth = columnSizes.get(colIndex) - columnGap;
358
404
  }
359
405
 
360
406
  // Apply X Alignment
@@ -367,6 +413,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
367
413
  } else {
368
414
  const isVisibleX = targetX >= relativeScrollX.value && (targetX + itemWidth) <= (relativeScrollX.value + usableWidth);
369
415
  if (!isVisibleX) {
416
+ /* v8 ignore if -- @preserve */
370
417
  if (targetX < relativeScrollX.value) {
371
418
  // keep targetX at start
372
419
  } else {
@@ -396,6 +443,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
396
443
  let viewW = 0;
397
444
  let viewH = 0;
398
445
 
446
+ /* v8 ignore else -- @preserve */
399
447
  if (typeof window !== 'undefined') {
400
448
  if (container === window) {
401
449
  curX = window.scrollX;
@@ -416,6 +464,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
416
464
  if (!reachedX && colIndex !== null && colIndex !== undefined) {
417
465
  const atLeft = curX <= tolerance && finalX <= tolerance;
418
466
  const atRight = curX >= maxW - viewW - tolerance && finalX >= maxW - viewW - tolerance;
467
+ /* v8 ignore else -- @preserve */
419
468
  if (atLeft || atRight) {
420
469
  reachedX = true;
421
470
  }
@@ -495,12 +544,24 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
495
544
 
496
545
  const paddingStartX = getPaddingX(props.value.scrollPaddingStart, props.value.direction);
497
546
  const paddingStartY = getPaddingY(props.value.scrollPaddingStart, props.value.direction);
547
+ const paddingEndX = getPaddingX(props.value.scrollPaddingEnd, props.value.direction);
548
+ const paddingEndY = getPaddingY(props.value.scrollPaddingEnd, props.value.direction);
549
+
550
+ const usableWidth = viewportWidth.value - (isHorizontal ? (paddingStartX + paddingEndX) : 0);
551
+ const usableHeight = viewportHeight.value - (isVertical ? (paddingStartY + paddingEndY) : 0);
552
+
553
+ const clampedX = (x !== null && x !== undefined)
554
+ ? (isHorizontal ? Math.max(0, Math.min(x, Math.max(0, totalWidth.value - usableWidth))) : Math.max(0, x))
555
+ : null;
556
+ const clampedY = (y !== null && y !== undefined)
557
+ ? (isVertical ? Math.max(0, Math.min(y, Math.max(0, totalHeight.value - usableHeight))) : Math.max(0, y))
558
+ : null;
498
559
 
499
560
  const currentX = (typeof window !== 'undefined' && container === window ? window.scrollX : (container as HTMLElement).scrollLeft);
500
561
  const currentY = (typeof window !== 'undefined' && container === window ? window.scrollY : (container as HTMLElement).scrollTop);
501
562
 
502
- const targetX = (x !== null && x !== undefined) ? x + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
503
- const targetY = (y !== null && y !== undefined) ? y + hostOffset.y - (isVertical ? paddingStartY : 0) : currentY;
563
+ const targetX = (clampedX !== null) ? clampedX + hostOffset.x - (isHorizontal ? paddingStartX : 0) : currentX;
564
+ const targetY = (clampedY !== null) ? clampedY + hostOffset.y - (isVertical ? paddingStartY : 0) : currentY;
504
565
 
505
566
  if (typeof window !== 'undefined' && container === window) {
506
567
  window.scrollTo({
@@ -571,6 +632,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
571
632
  let prependCount = 0;
572
633
  if (props.value.restoreScrollOnPrepend && lastItems.length > 0 && len > lastItems.length) {
573
634
  const oldFirstItem = lastItems[ 0 ];
635
+ /* v8 ignore else -- @preserve */
574
636
  if (oldFirstItem !== undefined) {
575
637
  for (let i = 1; i <= len - lastItems.length; i++) {
576
638
  if (newItems[ i ] === oldFirstItem) {
@@ -614,6 +676,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
614
676
  }
615
677
  }
616
678
 
679
+ /* v8 ignore else -- @preserve */
617
680
  if (addedX > 0 || addedY > 0) {
618
681
  nextTick(() => {
619
682
  scrollToOffset(
@@ -632,14 +695,17 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
632
695
  for (let i = 0; i < colCount; i++) {
633
696
  const width = getColumnWidth(i);
634
697
  const currentW = columnSizes.get(i);
698
+ const isMeasured = measuredColumns[ i ] === 1;
635
699
 
636
- // Only initialize from getColumnWidth if it's not dynamic,
637
- // OR if it's dynamic but we don't have a measurement yet.
638
- if (!isDynamicColumnWidth.value || currentW === 0) {
700
+ // If fixed/function, or if dynamic but not measured yet
701
+ if (!isDynamicColumnWidth.value || !isMeasured || currentW === 0) {
639
702
  const targetW = width + columnGap;
640
703
  if (Math.abs(currentW - targetW) > 0.5) {
641
704
  columnSizes.set(i, targetW);
705
+ measuredColumns[ i ] = isDynamicColumnWidth.value ? 0 : 1;
642
706
  colNeedsRebuild = true;
707
+ } else if (!isDynamicColumnWidth.value) {
708
+ measuredColumns[ i ] = 1;
643
709
  }
644
710
  }
645
711
  }
@@ -657,11 +723,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
657
723
  const currentX = itemSizesX.get(i);
658
724
  const currentY = itemSizesY.get(i);
659
725
 
660
- // If it's dynamic and already has a measurement, keep it.
661
- if (isDynamicItemSize.value && (currentX > 0 || currentY > 0)) {
662
- continue;
663
- }
664
-
665
726
  const size = typeof props.value.itemSize === 'function'
666
727
  ? props.value.itemSize(item as T, i)
667
728
  : defaultSize.value;
@@ -673,24 +734,42 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
673
734
  const targetX = isHorizontal ? size + columnGap : 0;
674
735
  const targetY = (isVertical || isBoth) ? size + gap : 0;
675
736
 
676
- if (Math.abs(currentX - targetX) > 0.5) {
677
- itemSizesX.set(i, targetX);
678
- itemsNeedRebuild = true;
679
- }
680
- if (Math.abs(currentY - targetY) > 0.5) {
681
- itemSizesY.set(i, targetY);
737
+ const isMeasuredX = measuredItemsX[ i ] === 1;
738
+ const isMeasuredY = measuredItemsY[ i ] === 1;
739
+
740
+ // Logic for X
741
+ if (isHorizontal) {
742
+ // If fixed/function, or if dynamic but not measured yet
743
+ if (!isDynamicItemSize.value || !isMeasuredX || currentX === 0) {
744
+ if (Math.abs(currentX - targetX) > 0.5) {
745
+ itemSizesX.set(i, targetX);
746
+ measuredItemsX[ i ] = isDynamicItemSize.value ? 0 : 1;
747
+ itemsNeedRebuild = true;
748
+ } else if (!isDynamicItemSize.value) {
749
+ measuredItemsX[ i ] = 1;
750
+ }
751
+ }
752
+ } else if (currentX !== 0) {
753
+ itemSizesX.set(i, 0);
754
+ measuredItemsX[ i ] = 0;
682
755
  itemsNeedRebuild = true;
683
756
  }
684
757
 
685
- // Max dimension tracking: determines scrollable area size
686
- const w = isHorizontal ? size : (isBoth ? Math.max(size, viewportWidth.value) : 0);
687
- const h = (isVertical || isBoth) ? size : 0;
688
-
689
- if (w > maxWidth.value) {
690
- maxWidth.value = w;
691
- }
692
- if (h > maxHeight.value) {
693
- maxHeight.value = h;
758
+ // Logic for Y
759
+ if (isVertical || isBoth) {
760
+ if (!isDynamicItemSize.value || !isMeasuredY || currentY === 0) {
761
+ if (Math.abs(currentY - targetY) > 0.5) {
762
+ itemSizesY.set(i, targetY);
763
+ measuredItemsY[ i ] = isDynamicItemSize.value ? 0 : 1;
764
+ itemsNeedRebuild = true;
765
+ } else if (!isDynamicItemSize.value) {
766
+ measuredItemsY[ i ] = 1;
767
+ }
768
+ }
769
+ } else if (currentY !== 0) {
770
+ itemSizesY.set(i, 0);
771
+ measuredItemsY[ i ] = 0;
772
+ itemsNeedRebuild = true;
694
773
  }
695
774
  }
696
775
 
@@ -735,12 +814,15 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
735
814
  };
736
815
 
737
816
  watch([
738
- () => props.value.items.length,
817
+ () => props.value.items,
818
+ () => props.value.direction,
739
819
  () => props.value.columnCount,
740
820
  () => props.value.columnWidth,
741
821
  () => props.value.itemSize,
742
822
  () => props.value.gap,
743
823
  () => props.value.columnGap,
824
+ () => props.value.defaultItemSize,
825
+ () => props.value.defaultColumnWidth,
744
826
  ], initializeSizes, { immediate: true });
745
827
 
746
828
  watch(() => [ props.value.container, props.value.hostElement ], () => {
@@ -955,6 +1037,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
955
1037
  if (nextStickyIdx !== undefined) {
956
1038
  const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : itemSizesY.query(nextStickyIdx);
957
1039
  const distance = nextStickyY - relativeScrollY.value;
1040
+ /* v8 ignore else -- @preserve */
958
1041
  if (distance < height) {
959
1042
  stickyOffset.y = -(height - distance);
960
1043
  }
@@ -980,6 +1063,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
980
1063
  if (nextStickyIdx !== undefined) {
981
1064
  const nextStickyX = fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : itemSizesX.query(nextStickyIdx);
982
1065
  const distance = nextStickyX - relativeScrollX.value;
1066
+ /* v8 ignore else -- @preserve */
983
1067
  if (distance < width) {
984
1068
  stickyOffset.x = -(width - distance);
985
1069
  }
@@ -1039,10 +1123,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1039
1123
  const safeStart = Math.max(0, start - colBuffer);
1040
1124
  const safeEnd = Math.min(totalCols, end + colBuffer);
1041
1125
 
1126
+ const padStart = columnSizes.query(safeStart);
1127
+
1042
1128
  return {
1043
1129
  start: safeStart,
1044
1130
  end: safeEnd,
1045
- padStart: columnSizes.query(safeStart),
1131
+ padStart,
1046
1132
  padEnd: columnSizes.query(totalCols) - columnSizes.query(safeEnd),
1047
1133
  };
1048
1134
  });
@@ -1130,18 +1216,16 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1130
1216
  const columnGap = props.value.columnGap || 0;
1131
1217
 
1132
1218
  for (const { index, inlineSize, blockSize, element } of updates) {
1133
- if (isDynamicItemSize.value) {
1134
- if (inlineSize > maxWidth.value) {
1135
- maxWidth.value = inlineSize;
1136
- }
1137
- if (blockSize > maxHeight.value) {
1138
- maxHeight.value = blockSize;
1139
- }
1140
-
1219
+ const isMeasurable = isDynamicItemSize.value || typeof props.value.itemSize === 'function';
1220
+ if (isMeasurable && index >= 0) {
1141
1221
  if (props.value.direction === 'horizontal') {
1142
1222
  const oldWidth = itemSizesX.get(index);
1143
1223
  const targetWidth = inlineSize + columnGap;
1144
- if (Math.abs(oldWidth - targetWidth) > 0.5 && (targetWidth > oldWidth || !measuredItemsX[ index ])) {
1224
+ // Apply if:
1225
+ // 1. It's the first measurement (measuredItemsX[index] is 0)
1226
+ // 2. It's a significant change (> 0.5px)
1227
+ /* v8 ignore else -- @preserve */
1228
+ if (!measuredItemsX[ index ] || Math.abs(targetWidth - oldWidth) > 0.5) {
1145
1229
  itemSizesX.update(index, targetWidth - oldWidth);
1146
1230
  measuredItemsX[ index ] = 1;
1147
1231
  needUpdate = true;
@@ -1150,14 +1234,16 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1150
1234
  if (props.value.direction === 'vertical' || props.value.direction === 'both') {
1151
1235
  const oldHeight = itemSizesY.get(index);
1152
1236
  const targetHeight = blockSize + gap;
1153
- // For grid, keep max height encountered to avoid shrinking on horizontal scroll
1237
+
1154
1238
  if (props.value.direction === 'both') {
1155
- if (targetHeight > oldHeight || !measuredItemsY[ index ]) {
1239
+ // For grid, we should be careful with decreases because a row height is the max of all its cells.
1240
+ /* v8 ignore else -- @preserve */
1241
+ if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
1156
1242
  itemSizesY.update(index, targetHeight - oldHeight);
1157
1243
  measuredItemsY[ index ] = 1;
1158
1244
  needUpdate = true;
1159
1245
  }
1160
- } else if (Math.abs(oldHeight - targetHeight) > 0.5 && (targetHeight > oldHeight || !measuredItemsY[ index ])) {
1246
+ } else if (!measuredItemsY[ index ] || Math.abs(targetHeight - oldHeight) > 0.5) {
1161
1247
  itemSizesY.update(index, targetHeight - oldHeight);
1162
1248
  measuredItemsY[ index ] = 1;
1163
1249
  needUpdate = true;
@@ -1166,11 +1252,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1166
1252
  }
1167
1253
 
1168
1254
  // Dynamic column width measurement
1255
+ const isColMeasurable = isDynamicColumnWidth.value || typeof props.value.columnWidth === 'function';
1169
1256
  if (
1170
1257
  props.value.direction === 'both'
1171
1258
  && element
1172
1259
  && props.value.columnCount
1173
- && isDynamicColumnWidth.value
1260
+ && isColMeasurable
1174
1261
  ) {
1175
1262
  const cells = element.dataset.colIndex !== undefined
1176
1263
  ? [ element ]
@@ -1179,11 +1266,12 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1179
1266
  for (const child of cells) {
1180
1267
  const colIndex = Number.parseInt(child.dataset.colIndex!, 10);
1181
1268
 
1269
+ /* v8 ignore else -- @preserve */
1182
1270
  if (colIndex >= 0 && colIndex < (props.value.columnCount || 0)) {
1183
1271
  const w = child.offsetWidth;
1184
1272
  const oldW = columnSizes.get(colIndex);
1185
1273
  const targetW = w + columnGap;
1186
- if (targetW > oldW || !measuredColumns[ colIndex ]) {
1274
+ if (Math.abs(oldW - targetW) > 0.5) {
1187
1275
  columnSizes.update(colIndex, targetW - oldW);
1188
1276
  measuredColumns[ colIndex ] = 1;
1189
1277
  needUpdate = true;
@@ -1262,6 +1350,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1262
1350
 
1263
1351
  resizeObserver = new ResizeObserver((entries) => {
1264
1352
  for (const entry of entries) {
1353
+ /* v8 ignore else -- @preserve */
1265
1354
  if (entry.target === container) {
1266
1355
  viewportWidth.value = (container as HTMLElement).clientWidth;
1267
1356
  viewportHeight.value = (container as HTMLElement).clientHeight;
@@ -1298,6 +1387,7 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1298
1387
  : props.value.ssrRange?.start;
1299
1388
  const initialAlign = props.value.initialScrollAlign || 'start';
1300
1389
 
1390
+ /* v8 ignore else -- @preserve */
1301
1391
  if (initialIndex !== undefined && initialIndex !== null) {
1302
1392
  scrollToIndex(initialIndex, props.value.ssrRange?.colStart, { align: initialAlign, behavior: 'auto' });
1303
1393
  }
@@ -1328,8 +1418,6 @@ export function useVirtualScroll<T = unknown>(props: Ref<VirtualScrollProps<T>>)
1328
1418
  measuredColumns.fill(0);
1329
1419
  measuredItemsX.fill(0);
1330
1420
  measuredItemsY.fill(0);
1331
- maxWidth.value = 0;
1332
- maxHeight.value = 0;
1333
1421
  initializeSizes();
1334
1422
  };
1335
1423