@pdanpdan/virtual-scroll 0.2.1 → 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.
@@ -117,11 +117,11 @@ describe('useVirtualScroll', () => {
117
117
 
118
118
  it('should recalculate when gaps change', async () => {
119
119
  const { result, props } = setup({ ...defaultProps, gap: 10 });
120
- expect(result.totalHeight.value).toBe(6000); // 100 * (50 + 10)
120
+ expect(result.totalHeight.value).toBe(5990); // 100 * (50 + 10) - 10
121
121
 
122
122
  props.value.gap = 20;
123
123
  await nextTick();
124
- expect(result.totalHeight.value).toBe(7000); // 100 * (50 + 20)
124
+ expect(result.totalHeight.value).toBe(6980); // 100 * (50 + 20) - 20
125
125
  });
126
126
 
127
127
  it('should handle itemSize as a function', async () => {
@@ -130,6 +130,7 @@ describe('useVirtualScroll', () => {
130
130
  itemSize: (_item: { id: number; }, index: number) => 50 + index,
131
131
  });
132
132
  // 50*100 + (0+99)*100/2 = 5000 + 4950 = 9950
133
+ // 9950 - gap(0) = 9950
133
134
  expect(result.totalHeight.value).toBe(9950);
134
135
  });
135
136
 
@@ -140,13 +141,13 @@ describe('useVirtualScroll', () => {
140
141
  columnCount: 10,
141
142
  columnWidth: 100,
142
143
  });
143
- expect(result.totalWidth.value).toBe(1000);
144
- expect(result.totalHeight.value).toBe(5000);
144
+ expect(result.totalWidth.value).toBe(1000); // 10 * 100 - 0
145
+ expect(result.totalHeight.value).toBe(5000); // 100 * 50 - 0
145
146
  });
146
147
 
147
148
  it('should handle horizontal direction', async () => {
148
149
  const { result } = setup({ ...defaultProps, direction: 'horizontal' });
149
- expect(result.totalWidth.value).toBe(5000);
150
+ expect(result.totalWidth.value).toBe(5000); // 100 * 50 - 0
150
151
  expect(result.totalHeight.value).toBe(0);
151
152
  });
152
153
 
@@ -238,46 +239,46 @@ describe('useVirtualScroll', () => {
238
239
  // Horizontal
239
240
  const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
240
241
  await nextTick();
241
- // Estimate is 50. Update with 40.
242
- rH.updateItemSizes([ { index: 0, inlineSize: 40, blockSize: 40 } ]);
242
+ // Estimate is 40 (new DEFAULT_ITEM_SIZE). Update with 30.
243
+ rH.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
243
244
  await nextTick();
244
- expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(40);
245
+ expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(30);
245
246
 
246
- // Subsequent update with smaller size should be ignored
247
- rH.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
247
+ // Subsequent update with smaller size should also be applied now
248
+ rH.updateItemSizes([ { index: 0, inlineSize: 25, blockSize: 25 } ]);
248
249
  await nextTick();
249
- expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(40);
250
+ expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(25);
250
251
 
251
252
  // Vertical
252
253
  const { result: rV } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
253
254
  await nextTick();
254
- rV.updateItemSizes([ { index: 0, inlineSize: 40, blockSize: 40 } ]);
255
+ rV.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
255
256
  await nextTick();
256
- expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(40);
257
+ expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(30);
257
258
 
258
- // Subsequent update with smaller size should be ignored
259
- rV.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
259
+ // Subsequent update with smaller size should be applied
260
+ rV.updateItemSizes([ { index: 0, inlineSize: 20, blockSize: 20 } ]);
260
261
  await nextTick();
261
- expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(40);
262
+ expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(20);
262
263
  });
263
264
 
264
265
  it('should handle updateItemSize and trigger reactivity', async () => {
265
266
  const { result } = setup({ ...defaultProps, itemSize: undefined });
266
- expect(result.totalHeight.value).toBe(5000); // Default estimate
267
+ expect(result.totalHeight.value).toBe(4000); // 100 * 40
267
268
 
268
269
  result.updateItemSize(0, 100, 100);
269
270
  await nextTick();
270
- expect(result.totalHeight.value).toBe(5050);
271
+ expect(result.totalHeight.value).toBe(4060); // 4000 - 40 + 100
271
272
  expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
272
273
  });
273
274
 
274
275
  it('should treat 0, null, undefined as dynamic itemSize', async () => {
275
276
  for (const val of [ 0, null, undefined ]) {
276
277
  const { result } = setup({ ...defaultProps, itemSize: val as unknown as undefined });
277
- expect(result.totalHeight.value).toBe(5000);
278
+ expect(result.totalHeight.value).toBe(4000);
278
279
  result.updateItemSize(0, 100, 100);
279
280
  await nextTick();
280
- expect(result.totalHeight.value).toBe(5050);
281
+ expect(result.totalHeight.value).toBe(4060);
281
282
  }
282
283
  });
283
284
 
@@ -289,7 +290,7 @@ describe('useVirtualScroll', () => {
289
290
  columnCount: 2,
290
291
  columnWidth: val as unknown as undefined,
291
292
  });
292
- expect(result.getColumnWidth(0)).toBe(150);
293
+ expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
293
294
  const parent = document.createElement('div');
294
295
  const col0 = document.createElement('div');
295
296
  Object.defineProperty(col0, 'offsetWidth', { value: 200, configurable: true });
@@ -297,7 +298,7 @@ describe('useVirtualScroll', () => {
297
298
  parent.appendChild(col0);
298
299
  result.updateItemSize(0, 200, 50, parent);
299
300
  await nextTick();
300
- expect(result.totalWidth.value).toBe(350);
301
+ expect(result.totalWidth.value).toBe(300); // 200 + 100
301
302
  }
302
303
  });
303
304
 
@@ -345,14 +346,21 @@ describe('useVirtualScroll', () => {
345
346
  expect(result.totalWidth.value).toBe(10 * 200); // 10 columns * 200 defaultColumnWidth
346
347
  });
347
348
 
348
- it('should ignore small delta updates in updateItemSize', async () => {
349
+ it('should ignore small delta updates in updateItemSize only after first measurement', async () => {
349
350
  const { result } = setup({ ...defaultProps, itemSize: undefined });
350
- result.updateItemSize(0, 50.1, 50.1);
351
+ // Default is 40. 40.1 is < 0.5 delta.
352
+ // First measurement should be accepted even if small delta from estimate.
353
+ result.updateItemSize(0, 40.1, 40.1);
351
354
  await nextTick();
352
- expect(result.totalHeight.value).toBe(5000);
355
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
356
+
357
+ // Second measurement with small delta from first should be ignored.
358
+ result.updateItemSize(0, 40.2, 40.2);
359
+ await nextTick();
360
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
353
361
  });
354
362
 
355
- it('should not shrink item height in both mode encountered so far', async () => {
363
+ it('should update item height in both mode now (allow decreases)', async () => {
356
364
  const { result } = setup({ ...defaultProps, direction: 'both', itemSize: undefined, columnCount: 2 });
357
365
  result.updateItemSize(0, 100, 100);
358
366
  await nextTick();
@@ -360,7 +368,7 @@ describe('useVirtualScroll', () => {
360
368
 
361
369
  result.updateItemSize(0, 100, 80);
362
370
  await nextTick();
363
- expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
371
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(80);
364
372
  });
365
373
 
366
374
  it('should update item height in vertical mode', async () => {
@@ -368,26 +376,30 @@ describe('useVirtualScroll', () => {
368
376
  result.updateItemSize(0, 100, 100);
369
377
  await nextTick();
370
378
  expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
379
+
380
+ result.updateItemSize(0, 100, 70);
381
+ await nextTick();
382
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(70);
371
383
  });
372
384
 
373
385
  it('should handle updateItemSize for horizontal direction', async () => {
374
386
  const { result } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
375
387
  result.updateItemSize(0, 100, 50);
376
388
  await nextTick();
377
- expect(result.totalWidth.value).toBe(5050);
389
+ expect(result.totalWidth.value).toBe(4060); // 4000 - 40 + 100
378
390
  });
379
391
 
380
392
  it('should preserve measurements in initializeSizes when dynamic', async () => {
381
393
  const { result, props } = setup({ ...defaultProps, itemSize: undefined });
382
394
  result.updateItemSize(0, 100, 100);
383
395
  await nextTick();
384
- expect(result.totalHeight.value).toBe(5050);
396
+ expect(result.totalHeight.value).toBe(4060);
385
397
 
386
398
  // Trigger initializeSizes by changing length
387
399
  props.value.items = Array.from({ length: 101 }, (_, i) => ({ id: i }));
388
400
  await nextTick();
389
- // Should still be 100 for index 0, not reset to default 50
390
- expect(result.totalHeight.value).toBe(5050 + 50);
401
+ // Should still be 100 for index 0, not reset to default 40
402
+ expect(result.totalHeight.value).toBe(4060 + 40);
391
403
  });
392
404
  });
393
405
 
@@ -398,10 +410,10 @@ describe('useVirtualScroll', () => {
398
410
  const { result } = setup({ ...defaultProps, container, direction: 'horizontal', itemSize: undefined });
399
411
  await nextTick();
400
412
 
401
- // index 10. itemSize is 50 by default. totalWidth = 5000.
413
+ // index 10. itemSize is 40 by default. totalWidth = 4000.
402
414
  result.scrollToIndex(null, 10, { align: 'start', behavior: 'auto' });
403
415
  await nextTick();
404
- expect(result.scrollDetails.value.scrollOffset.x).toBe(500);
416
+ expect(result.scrollDetails.value.scrollOffset.x).toBe(400);
405
417
  });
406
418
 
407
419
  it('should handle scrollToIndex with window fallback when container is missing', async () => {
@@ -858,7 +870,7 @@ describe('useVirtualScroll', () => {
858
870
  columnCount: 2,
859
871
  columnWidth: [ 0 ] as unknown as number[],
860
872
  });
861
- expect(result.getColumnWidth(0)).toBe(150); // DEFAULT_COLUMN_WIDTH
873
+ expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
862
874
  });
863
875
 
864
876
  it('should handle columnWidth as a function', async () => {
@@ -869,7 +881,7 @@ describe('useVirtualScroll', () => {
869
881
  columnWidth: (index: number) => (index % 2 === 0 ? 100 : 200),
870
882
  });
871
883
  expect(result.getColumnWidth(0)).toBe(100);
872
- expect(result.totalWidth.value).toBe(1500);
884
+ expect(result.totalWidth.value).toBe(1500); // 5*100 + 5*200 - 0
873
885
  });
874
886
 
875
887
  it('should handle getColumnWidth fallback when dynamic', async () => {
@@ -879,7 +891,7 @@ describe('useVirtualScroll', () => {
879
891
  columnCount: 2,
880
892
  columnWidth: undefined,
881
893
  });
882
- expect(result.getColumnWidth(0)).toBe(150);
894
+ expect(result.getColumnWidth(0)).toBe(100);
883
895
  });
884
896
 
885
897
  it('should handle columnRange while loop coverage', async () => {
@@ -961,7 +973,6 @@ describe('useVirtualScroll', () => {
961
973
  const { result } = setup({ ...defaultProps, container, stickyIndices: [ 0, 10 ], itemSize: 50 });
962
974
  // We need to trigger scroll to update scrollY
963
975
  container.dispatchEvent(new Event('scroll'));
964
- await nextTick();
965
976
 
966
977
  const item0 = result.renderedItems.value.find((i) => i.index === 0);
967
978
  expect(item0!.offset.y).toBeLessThanOrEqual(450);
@@ -981,7 +992,6 @@ describe('useVirtualScroll', () => {
981
992
  columnGap: 0,
982
993
  });
983
994
  container.dispatchEvent(new Event('scroll'));
984
- await nextTick();
985
995
 
986
996
  const item0 = result.renderedItems.value.find((i) => i.index === 0);
987
997
  expect(item0!.offset.x).toBeLessThanOrEqual(450);
@@ -990,7 +1000,7 @@ describe('useVirtualScroll', () => {
990
1000
  it('should handle dynamic sticky item pushing in vertical mode', async () => {
991
1001
  const container = document.createElement('div');
992
1002
  Object.defineProperty(container, 'clientHeight', { value: 500 });
993
- Object.defineProperty(container, 'scrollTop', { value: 460, writable: true });
1003
+ Object.defineProperty(container, 'scrollTop', { value: 380, writable: true });
994
1004
 
995
1005
  const { result } = setup({
996
1006
  ...defaultProps,
@@ -998,22 +1008,21 @@ describe('useVirtualScroll', () => {
998
1008
  itemSize: undefined, // dynamic
999
1009
  stickyIndices: [ 0, 10 ],
1000
1010
  });
1001
- await nextTick();
1002
1011
 
1003
1012
  // Item 0 is sticky. Item 10 is next sticky.
1004
- // Default size = 50.
1005
- // nextStickyY = itemSizesY.query(10) = 500.
1006
- // distance = 500 - 460 = 40.
1007
- // 40 < 50 (item 0 height), so it should be pushed.
1008
- // stickyOffset.y = -(50 - 40) = -10.
1013
+ // Default size = 40.
1014
+ // nextStickyY = itemSizesY.query(10) = 400.
1015
+ // distance = 400 - 380 = 20.
1016
+ // 20 < 40 (item 0 height), so it should be pushed.
1017
+ // stickyOffset.y = -(40 - 20) = -20.
1009
1018
  const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
1010
- expect(stickyItem?.stickyOffset.y).toBe(-10);
1019
+ expect(stickyItem?.stickyOffset.y).toBe(-20);
1011
1020
  });
1012
1021
 
1013
1022
  it('should handle dynamic sticky item pushing in horizontal mode', async () => {
1014
1023
  const container = document.createElement('div');
1015
1024
  Object.defineProperty(container, 'clientWidth', { value: 500 });
1016
- Object.defineProperty(container, 'scrollLeft', { value: 460, writable: true });
1025
+ Object.defineProperty(container, 'scrollLeft', { value: 380, writable: true });
1017
1026
 
1018
1027
  const { result } = setup({
1019
1028
  ...defaultProps,
@@ -1022,13 +1031,12 @@ describe('useVirtualScroll', () => {
1022
1031
  itemSize: undefined, // dynamic
1023
1032
  stickyIndices: [ 0, 10 ],
1024
1033
  });
1025
- await nextTick();
1026
1034
 
1027
- // nextStickyX = itemSizesX.query(10) = 500.
1028
- // distance = 500 - 460 = 40.
1029
- // 40 < 50, so stickyOffset.x = -10.
1035
+ // nextStickyX = itemSizesX.query(10) = 400.
1036
+ // distance = 400 - 380 = 20.
1037
+ // 20 < 40, so stickyOffset.x = -20.
1030
1038
  const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
1031
- expect(stickyItem?.stickyOffset.x).toBe(-10);
1039
+ expect(stickyItem?.stickyOffset.x).toBe(-20);
1032
1040
  });
1033
1041
  });
1034
1042
 
@@ -1150,7 +1158,7 @@ describe('useVirtualScroll', () => {
1150
1158
  const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: true });
1151
1159
  await nextTick();
1152
1160
 
1153
- const newItems = [ { id: -1 }, { id: 9999 } ]; // completely different
1161
+ const newItems = [ { id: -1 }, { id: 9999 } ];
1154
1162
  props.value.items = newItems;
1155
1163
  await nextTick();
1156
1164
  await nextTick();
@@ -1162,9 +1170,9 @@ describe('useVirtualScroll', () => {
1162
1170
  Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
1163
1171
  Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
1164
1172
  const { result, props } = setup({ ...defaultProps, container, restoreScrollOnPrepend: true });
1165
- result.scrollToIndex(10, null, { behavior: 'smooth' });
1166
- // pendingScroll should be set because it's not reached yet
1173
+ await nextTick();
1167
1174
 
1175
+ result.scrollToIndex(10, null, { behavior: 'smooth' });
1168
1176
  props.value.items = [ { id: -1 }, ...props.value.items ];
1169
1177
  await nextTick();
1170
1178
  });
@@ -1234,11 +1242,11 @@ describe('useVirtualScroll', () => {
1234
1242
  const { result } = setup({ ...defaultProps, itemSize: 0 });
1235
1243
  result.updateItemSize(0, 100, 100);
1236
1244
  await nextTick();
1237
- expect(result.totalHeight.value).toBe(5050);
1245
+ expect(result.totalHeight.value).toBe(4060);
1238
1246
 
1239
1247
  result.refresh();
1240
1248
  await nextTick();
1241
- expect(result.totalHeight.value).toBe(5000);
1249
+ expect(result.totalHeight.value).toBe(4000);
1242
1250
  });
1243
1251
 
1244
1252
  it('should trigger scroll correction on tree update with string alignment', async () => {
@@ -1286,6 +1294,78 @@ describe('useVirtualScroll', () => {
1286
1294
  expect(result.scrollDetails.value.isScrolling).toBe(false);
1287
1295
  vi.useRealTimers();
1288
1296
  });
1297
+
1298
+ it('should update totals when function-based itemSize dependencies change and refresh is called', async () => {
1299
+ const defaultHeight = ref(50);
1300
+ const getRowHeight = () => defaultHeight.value;
1301
+
1302
+ const propsValue = ref({
1303
+ items: mockItems,
1304
+ direction: 'vertical' as const,
1305
+ itemSize: getRowHeight,
1306
+ }) as Ref<VirtualScrollProps<unknown>>;
1307
+
1308
+ const result = useVirtualScroll(propsValue);
1309
+ expect(result.totalHeight.value).toBe(5000); // 100 * 50
1310
+
1311
+ defaultHeight.value = 60;
1312
+ // Total height should still be 5000 because getRowHeight reference didn't change
1313
+ // and initializeSizes hasn't been called automatically.
1314
+ expect(result.totalHeight.value).toBe(5000);
1315
+
1316
+ result.refresh();
1317
+ await nextTick();
1318
+ expect(result.totalHeight.value).toBe(6000);
1319
+ });
1320
+
1321
+ it('should update totals via measurements even if itemSize is a function', async () => {
1322
+ const getRowHeight = () => 50;
1323
+
1324
+ const propsValue = ref({
1325
+ items: mockItems,
1326
+ direction: 'vertical' as const,
1327
+ itemSize: getRowHeight,
1328
+ }) as Ref<VirtualScrollProps<unknown>>;
1329
+
1330
+ const result = useVirtualScroll(propsValue);
1331
+ expect(result.totalHeight.value).toBe(5000);
1332
+
1333
+ // Simulate ResizeObserver measurement
1334
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 70 } ]);
1335
+ await nextTick();
1336
+
1337
+ // Item 0 is now 70 instead of 50. Total: 50 * 99 + 70 = 4950 + 70 = 5020.
1338
+ expect(result.totalHeight.value).toBe(5020);
1339
+ });
1340
+
1341
+ it('should update totals via measurements even if columnWidth is a function', async () => {
1342
+ const getColWidth = () => 100;
1343
+
1344
+ const propsValue = ref({
1345
+ items: mockItems,
1346
+ direction: 'both' as const,
1347
+ columnCount: 5,
1348
+ columnWidth: getColWidth,
1349
+ itemSize: 50,
1350
+ }) as Ref<VirtualScrollProps<unknown>>;
1351
+
1352
+ const result = useVirtualScroll(propsValue);
1353
+ expect(result.totalWidth.value).toBe(500);
1354
+
1355
+ // Simulate ResizeObserver measurement on a cell
1356
+ // We need to provide an element with data-col-index
1357
+ const element = document.createElement('div');
1358
+ const cell = document.createElement('div');
1359
+ cell.dataset.colIndex = '0';
1360
+ Object.defineProperty(cell, 'offsetWidth', { value: 120 });
1361
+ element.appendChild(cell);
1362
+
1363
+ result.updateItemSizes([ { index: 0, inlineSize: 120, blockSize: 50, element } ]);
1364
+ await nextTick();
1365
+
1366
+ // Column 0 is now 120 instead of 100. Total: 100 * 4 + 120 = 520.
1367
+ expect(result.totalWidth.value).toBe(520);
1368
+ });
1289
1369
  });
1290
1370
 
1291
1371
  // eslint-disable-next-line test/prefer-lowercase-title
@@ -1307,7 +1387,7 @@ describe('useVirtualScroll', () => {
1307
1387
  direction: 'both',
1308
1388
  columnCount: 20,
1309
1389
  columnWidth: 100,
1310
- ssrRange: { start: 0, end: 10, colStart: 1, colEnd: 2 }, // SSR values
1390
+ ssrRange: { start: 0, end: 10, colStart: 1, colEnd: 2 },
1311
1391
  initialScrollIndex: 0,
1312
1392
  });
1313
1393
 
@@ -1416,8 +1496,8 @@ describe('useVirtualScroll', () => {
1416
1496
  ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
1417
1497
  }) as Ref<VirtualScrollProps<unknown>>;
1418
1498
  const result = useVirtualScroll(props);
1419
- expect(result.totalWidth.value).toBe(300); // (5-2) * 100
1420
- expect(result.totalHeight.value).toBe(500); // (20-10) * 50
1499
+ expect(result.totalWidth.value).toBe(300); // (5-2) * 100 - gap(0)
1500
+ expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - gap(0)
1421
1501
  });
1422
1502
 
1423
1503
  it('should handle SSR range with horizontal direction for total sizes', () => {
@@ -1428,7 +1508,7 @@ describe('useVirtualScroll', () => {
1428
1508
  ssrRange: { start: 10, end: 20 },
1429
1509
  }) as Ref<VirtualScrollProps<unknown>>;
1430
1510
  const result = useVirtualScroll(props);
1431
- expect(result.totalWidth.value).toBe(500); // (20-10) * 50
1511
+ expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - gap(0)
1432
1512
  });
1433
1513
 
1434
1514
  it('should handle SSR range with vertical offset in renderedItems', () => {
@@ -1450,8 +1530,8 @@ describe('useVirtualScroll', () => {
1450
1530
  ssrRange: { start: 10, end: 20 },
1451
1531
  }) as Ref<VirtualScrollProps<unknown>>;
1452
1532
  const result = useVirtualScroll(props);
1453
- // ssrOffsetX = itemSizesX.query(10) = 10 * 50 = 500
1454
- expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(500);
1533
+ // ssrOffsetX = itemSizesX.query(10) = 10 * 40 = 400
1534
+ expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(400);
1455
1535
  });
1456
1536
 
1457
1537
  it('should handle SSR range with dynamic sizes for total sizes', () => {
@@ -1462,7 +1542,7 @@ describe('useVirtualScroll', () => {
1462
1542
  ssrRange: { start: 10, end: 20 },
1463
1543
  }) as Ref<VirtualScrollProps<unknown>>;
1464
1544
  const result = useVirtualScroll(props);
1465
- expect(result.totalHeight.value).toBe(500);
1545
+ expect(result.totalHeight.value).toBe(400); // (20-10) * 40 - gap(0)
1466
1546
  });
1467
1547
 
1468
1548
  it('should handle SSR range with dynamic horizontal sizes for total sizes', () => {
@@ -1473,7 +1553,7 @@ describe('useVirtualScroll', () => {
1473
1553
  ssrRange: { start: 10, end: 20 },
1474
1554
  }) as Ref<VirtualScrollProps<unknown>>;
1475
1555
  const result = useVirtualScroll(props);
1476
- expect(result.totalWidth.value).toBe(500);
1556
+ expect(result.totalWidth.value).toBe(400); // (20-10) * 40 - 0 gap
1477
1557
  });
1478
1558
 
1479
1559
  it('should handle SSR range with both directions and dynamic offsets', () => {
@@ -1485,8 +1565,8 @@ describe('useVirtualScroll', () => {
1485
1565
  ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
1486
1566
  }) as Ref<VirtualScrollProps<unknown>>;
1487
1567
  const result = useVirtualScroll(props);
1488
- expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
1489
- expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-300);
1568
+ expect(result.totalWidth.value).toBe(300); // (5-2) * 100
1569
+ expect(result.totalHeight.value).toBe(400); // (20-10) * 40
1490
1570
  });
1491
1571
 
1492
1572
  it('should scroll to ssrRange on mount', async () => {
@@ -1547,9 +1627,362 @@ describe('useVirtualScroll', () => {
1547
1627
  });
1548
1628
 
1549
1629
  describe('helpers', () => {
1550
- it('should cover object padding branches in helpers', () => {
1630
+ it('should handle zero column count in totalWidth', async () => {
1631
+ const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 0 });
1632
+ expect(result.totalWidth.value).toBe(0);
1633
+ });
1634
+
1635
+ it('should handle vertical direction in totalWidth', () => {
1636
+ const { result } = setup({ ...defaultProps, direction: 'vertical' });
1637
+ expect(result.totalWidth.value).toBe(0);
1638
+ });
1639
+
1640
+ it('should handle horizontal direction in totalHeight', () => {
1641
+ const { result } = setup({ ...defaultProps, direction: 'horizontal' });
1642
+ expect(result.totalHeight.value).toBe(0);
1643
+ });
1644
+
1645
+ it('should handle zero items in totalWidth/totalHeight', async () => {
1646
+ const { result } = setup({ ...defaultProps, items: [] });
1647
+ expect(result.totalHeight.value).toBe(0);
1648
+
1649
+ const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', items: [] });
1650
+ expect(rH.totalWidth.value).toBe(0);
1651
+ });
1652
+
1653
+ it('should cover SSR with zero items/columns', () => {
1654
+ const props = ref({
1655
+ items: [],
1656
+ direction: 'both',
1657
+ columnCount: 0,
1658
+ ssrRange: { start: 0, end: 0, colStart: 0, colEnd: 0 },
1659
+ }) as Ref<VirtualScrollProps<unknown>>;
1660
+ const result = useVirtualScroll(props);
1661
+ expect(result.totalWidth.value).toBe(0);
1662
+ expect(result.totalHeight.value).toBe(0);
1663
+ });
1664
+
1665
+ it('should handle SSR range with both directions and no columns', () => {
1666
+ const props = ref({
1667
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1668
+ direction: 'both',
1669
+ columnCount: 0,
1670
+ ssrRange: { start: 10, end: 20, colStart: 0, colEnd: 0 },
1671
+ }) as Ref<VirtualScrollProps<unknown>>;
1672
+ const result = useVirtualScroll(props);
1673
+ expect(result.totalWidth.value).toBe(0);
1674
+ });
1675
+
1676
+ it('should handle SSR range with direction both and colCount > 0 but colEnd <= colStart', () => {
1677
+ const props = ref({
1678
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1679
+ direction: 'both',
1680
+ columnCount: 10,
1681
+ ssrRange: { start: 0, end: 10, colStart: 5, colEnd: 5 },
1682
+ }) as Ref<VirtualScrollProps<unknown>>;
1683
+ const result = useVirtualScroll(props);
1684
+ expect(result.totalWidth.value).toBe(0);
1685
+ });
1686
+
1687
+ it('should handle SSR range with vertical/both and end <= start', () => {
1688
+ const props = ref({
1689
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1690
+ direction: 'vertical',
1691
+ ssrRange: { start: 10, end: 10 },
1692
+ }) as Ref<VirtualScrollProps<unknown>>;
1693
+ const result = useVirtualScroll(props);
1694
+ expect(result.totalHeight.value).toBe(0);
1695
+ });
1696
+
1697
+ it('should handle SSR range with dynamic horizontal sizes for total sizes', () => {
1698
+ const props = ref({
1699
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1700
+ direction: 'horizontal',
1701
+ itemSize: 0,
1702
+ ssrRange: { start: 10, end: 20 },
1703
+ }) as Ref<VirtualScrollProps<unknown>>;
1704
+ const result = useVirtualScroll(props);
1705
+ expect(result.totalWidth.value).toBe(400); // (20-10) * 40
1706
+ });
1707
+
1708
+ it('should handle SSR range with both directions and dynamic offsets for total width', () => {
1709
+ const props = ref({
1710
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1711
+ direction: 'both',
1712
+ columnCount: 10,
1713
+ itemSize: 0,
1714
+ ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
1715
+ }) as Ref<VirtualScrollProps<unknown>>;
1716
+ const result = useVirtualScroll(props);
1717
+ expect(result.totalWidth.value).toBe(300);
1718
+ });
1719
+
1720
+ it('should handle updateItemSizes with index < 0', async () => {
1721
+ const { result } = setup({ ...defaultProps, itemSize: undefined });
1722
+ result.updateItemSizes([ { index: -1, inlineSize: 100, blockSize: 100 } ]);
1723
+ await nextTick();
1724
+ // Should not change total height
1725
+ expect(result.totalHeight.value).toBe(4000);
1726
+ });
1727
+
1728
+ it('should handle updateItemSizes with direction vertical and dynamic itemSize for X', async () => {
1729
+ const { result } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
1730
+ // Measured Items X should not be updated if direction is vertical
1731
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
1732
+ await nextTick();
1733
+ expect(result.totalWidth.value).toBe(0);
1734
+ });
1735
+
1736
+ it('should handle SSR with horizontal direction and fixedItemSize', () => {
1737
+ const propsValue = ref({
1738
+ direction: 'horizontal' as const,
1739
+ itemSize: 50,
1740
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1741
+ ssrRange: { end: 20, start: 10 },
1742
+ }) as Ref<VirtualScrollProps<unknown>>;
1743
+ const result = useVirtualScroll(propsValue);
1744
+ expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - 0 gap
1745
+ });
1746
+
1747
+ it('should handle SSR with vertical direction and fixedItemSize', () => {
1748
+ const propsValue = ref({
1749
+ direction: 'vertical' as const,
1750
+ itemSize: 50,
1751
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1752
+ ssrRange: { end: 20, start: 10 },
1753
+ }) as Ref<VirtualScrollProps<unknown>>;
1754
+ const result = useVirtualScroll(propsValue);
1755
+ expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - 0 gap
1756
+ });
1757
+
1758
+ it('should handle SSR with direction both and fixedItemSize for totalHeight', () => {
1759
+ const propsValue = ref({
1760
+ direction: 'both' as const,
1761
+ columnCount: 10,
1762
+ itemSize: 50,
1763
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1764
+ ssrRange: { end: 20, start: 10 },
1765
+ }) as Ref<VirtualScrollProps<unknown>>;
1766
+ const result = useVirtualScroll(propsValue);
1767
+ expect(result.totalHeight.value).toBe(500);
1768
+ });
1769
+
1770
+ it('should handle SSR range with direction both and colEnd falsy', () => {
1771
+ const propsValue = ref({
1772
+ columnCount: 10,
1773
+ columnWidth: 100,
1774
+ direction: 'both' as const,
1775
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
1776
+ ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
1777
+ }) as Ref<VirtualScrollProps<unknown>>;
1778
+ const result = useVirtualScroll(propsValue);
1779
+ // colEnd is 0, so it should use colCount (10)
1780
+ // totalWidth = (10 - 5) * 100 = 500
1781
+ expect(result.totalWidth.value).toBe(500);
1782
+ });
1783
+
1784
+ it('should handle updateItemSizes with direction both and dynamic itemSize for Y', async () => {
1785
+ const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 2, itemSize: undefined });
1786
+ // First measurement
1787
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
1788
+ await nextTick();
1789
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
1790
+
1791
+ // Increase
1792
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 120 } ]);
1793
+ await nextTick();
1794
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(120);
1795
+
1796
+ // Significant decrease
1797
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
1798
+ await nextTick();
1799
+ expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
1800
+ });
1801
+
1802
+ it('should handle object padding branches in helpers', () => {
1551
1803
  expect(getPaddingX({ x: 10 }, 'horizontal')).toBe(10);
1552
1804
  expect(getPaddingY({ y: 20 }, 'vertical')).toBe(20);
1553
1805
  });
1806
+
1807
+ it('should cover totalWidth SSR len <= 0', () => {
1808
+ const propsValue = ref({
1809
+ items: mockItems,
1810
+ direction: 'horizontal' as const,
1811
+ itemSize: 50,
1812
+ ssrRange: { start: 10, end: 10 },
1813
+ }) as Ref<VirtualScrollProps<unknown>>;
1814
+ const result = useVirtualScroll(propsValue);
1815
+ expect(result.totalWidth.value).toBe(0);
1816
+ });
1817
+
1818
+ it('should cover totalWidth SSR end <= start for dynamic sizes', () => {
1819
+ const propsValue = ref({
1820
+ items: mockItems,
1821
+ direction: 'horizontal' as const,
1822
+ itemSize: undefined,
1823
+ ssrRange: { start: 10, end: 10 },
1824
+ }) as Ref<VirtualScrollProps<unknown>>;
1825
+ const result = useVirtualScroll(propsValue);
1826
+ expect(result.totalWidth.value).toBe(0);
1827
+ });
1828
+
1829
+ it('should cover totalWidth non-SSR items.length <= 0 for dynamic sizes', () => {
1830
+ const propsValue = ref({
1831
+ items: [],
1832
+ direction: 'horizontal' as const,
1833
+ itemSize: undefined,
1834
+ }) as Ref<VirtualScrollProps<unknown>>;
1835
+ const result = useVirtualScroll(propsValue);
1836
+ expect(result.totalWidth.value).toBe(0);
1837
+ });
1838
+
1839
+ it('should cover totalHeight SSR len <= 0', () => {
1840
+ const propsValue = ref({
1841
+ items: mockItems,
1842
+ direction: 'vertical' as const,
1843
+ itemSize: 50,
1844
+ ssrRange: { start: 10, end: 10 },
1845
+ }) as Ref<VirtualScrollProps<unknown>>;
1846
+ const result = useVirtualScroll(propsValue);
1847
+ expect(result.totalHeight.value).toBe(0);
1848
+ });
1849
+
1850
+ it('should cover totalHeight SSR end <= start for dynamic sizes', () => {
1851
+ const propsValue = ref({
1852
+ items: mockItems,
1853
+ direction: 'vertical' as const,
1854
+ itemSize: undefined,
1855
+ ssrRange: { start: 10, end: 10 },
1856
+ }) as Ref<VirtualScrollProps<unknown>>;
1857
+ const result = useVirtualScroll(propsValue);
1858
+ expect(result.totalHeight.value).toBe(0);
1859
+ });
1860
+
1861
+ it('should cover totalHeight non-SSR items.length <= 0 for dynamic sizes', () => {
1862
+ const propsValue = ref({
1863
+ items: [],
1864
+ direction: 'vertical' as const,
1865
+ itemSize: undefined,
1866
+ }) as Ref<VirtualScrollProps<unknown>>;
1867
+ const result = useVirtualScroll(propsValue);
1868
+ expect(result.totalHeight.value).toBe(0);
1869
+ });
1870
+
1871
+ describe('internal sizing stabilization and edge cases', () => {
1872
+ const mockItems = Array.from({ length: 10 }, (_, i) => ({ id: i }));
1873
+
1874
+ it('should skip re-initializing sizes for already measured dynamic items', async () => {
1875
+ const props = ref({
1876
+ items: mockItems,
1877
+ direction: 'both' as const,
1878
+ columnCount: 2,
1879
+ }) as Ref<VirtualScrollProps<{ id: number; }>>;
1880
+
1881
+ const result = useVirtualScroll(props);
1882
+ await nextTick();
1883
+
1884
+ // Measure item 0 and col 0
1885
+ const parent = document.createElement('div');
1886
+ const col0 = document.createElement('div');
1887
+ col0.dataset.colIndex = '0';
1888
+ Object.defineProperty(col0, 'offsetWidth', { value: 200 });
1889
+ parent.appendChild(col0);
1890
+
1891
+ result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 150, element: parent } ]);
1892
+ await nextTick();
1893
+
1894
+ expect(result.getColumnWidth(0)).toBe(200);
1895
+ expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
1896
+
1897
+ // Trigger initializeSizes by changing items length
1898
+ props.value.items = Array.from({ length: 11 }, (_, i) => ({ id: i }));
1899
+ await nextTick();
1900
+
1901
+ // Should NOT reset already measured item 0
1902
+ expect(result.getColumnWidth(0)).toBe(200);
1903
+ expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
1904
+ });
1905
+
1906
+ it('should mark items as measured when fixed size matches current size within tolerance', async () => {
1907
+ const props = ref({
1908
+ items: mockItems,
1909
+ direction: 'horizontal' as const,
1910
+ itemSize: 50,
1911
+ }) as Ref<VirtualScrollProps<{ id: number; }>>;
1912
+
1913
+ useVirtualScroll(props);
1914
+ await nextTick();
1915
+
1916
+ // Trigger initializeSizes again with same prop
1917
+ props.value.columnGap = 0;
1918
+ await nextTick();
1919
+ // Hits the branch where Math.abs(current - target) <= 0.5
1920
+ });
1921
+
1922
+ it('should mark columns as measured when fixed width matches current width within tolerance', async () => {
1923
+ const props = ref({
1924
+ items: mockItems,
1925
+ direction: 'both' as const,
1926
+ columnCount: 2,
1927
+ columnWidth: 100,
1928
+ }) as Ref<VirtualScrollProps<{ id: number; }>>;
1929
+
1930
+ useVirtualScroll(props);
1931
+ await nextTick();
1932
+
1933
+ props.value.columnGap = 0;
1934
+ await nextTick();
1935
+ });
1936
+
1937
+ it('should reset item sizes when switching between horizontal and vertical directions', async () => {
1938
+ const props = ref({
1939
+ items: mockItems,
1940
+ direction: 'horizontal' as const,
1941
+ itemSize: 50,
1942
+ }) as Ref<VirtualScrollProps<{ id: number; }>>;
1943
+
1944
+ const result = useVirtualScroll(props);
1945
+ await nextTick();
1946
+ expect(result.totalWidth.value).toBe(500);
1947
+
1948
+ // Switch to vertical (resets X)
1949
+ props.value.direction = 'vertical';
1950
+ await nextTick();
1951
+ expect(result.totalWidth.value).toBe(0);
1952
+
1953
+ // Switch to both
1954
+ props.value.direction = 'both';
1955
+ props.value.columnCount = 10;
1956
+ props.value.columnWidth = 100;
1957
+ await nextTick();
1958
+ expect(result.totalHeight.value).toBe(500);
1959
+ expect(result.totalWidth.value).toBe(1000);
1960
+
1961
+ // Switch to horizontal (resets Y)
1962
+ props.value.direction = 'horizontal';
1963
+ await nextTick();
1964
+ await nextTick();
1965
+ expect(result.totalHeight.value).toBe(0);
1966
+ });
1967
+
1968
+ it('should skip re-initialization if dynamic size is already measured and non-zero', async () => {
1969
+ const props = ref({
1970
+ items: mockItems,
1971
+ direction: 'horizontal' as const,
1972
+ itemSize: undefined, // dynamic
1973
+ }) as Ref<VirtualScrollProps<{ id: number; }>>;
1974
+
1975
+ const result = useVirtualScroll(props);
1976
+ await nextTick();
1977
+
1978
+ result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 50, element: document.createElement('div') } ]);
1979
+ await nextTick();
1980
+
1981
+ props.value.gap = 1;
1982
+ await nextTick();
1983
+
1984
+ expect(result.totalWidth.value).toBeGreaterThan(0);
1985
+ });
1986
+ });
1554
1987
  });
1555
1988
  });