mtrl-addons 0.2.1 → 0.2.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.
@@ -40,7 +40,7 @@ const createSpeedTracker = (): SpeedTracker => ({
40
40
  const updateSpeedTracker = (
41
41
  tracker: SpeedTracker,
42
42
  newPosition: number,
43
- previousPosition: number
43
+ previousPosition: number,
44
44
  ): SpeedTracker => {
45
45
  const now = Date.now();
46
46
  const timeDelta = now - tracker.lastTime;
@@ -112,9 +112,10 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
112
112
  if (viewportState) {
113
113
  totalVirtualSize = viewportState.virtualTotalSize || 0;
114
114
  containerSize = viewportState.containerSize || 0;
115
- // console.log(
116
- // `[Scrolling] Initialized with totalVirtualSize: ${totalVirtualSize}, containerSize: ${containerSize}`
117
- // );
115
+ // Sync scrollPosition from viewportState (important for initialScrollIndex)
116
+ if (viewportState.scrollPosition > 0) {
117
+ scrollPosition = viewportState.scrollPosition;
118
+ }
118
119
  }
119
120
 
120
121
  // Listen for virtual size changes
@@ -123,6 +124,17 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
123
124
  updateScrollBounds(data.totalVirtualSize, containerSize);
124
125
  });
125
126
 
127
+ // Listen for scroll position changes from other features (e.g., virtual.ts after item size detection)
128
+ component.on?.("viewport:scroll-position-sync", (data: any) => {
129
+ if (data.position !== undefined && data.position !== scrollPosition) {
130
+ scrollPosition = data.position;
131
+ // Also update local tracking vars
132
+ totalVirtualSize =
133
+ viewportState?.virtualTotalSize || totalVirtualSize;
134
+ containerSize = viewportState?.containerSize || containerSize;
135
+ }
136
+ });
137
+
126
138
  // Listen for container size changes
127
139
  component.on?.("viewport:container-size-changed", (data: any) => {
128
140
  if (data.containerSize) {
@@ -239,10 +251,6 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
239
251
 
240
252
  newPosition = clamp(newPosition, 0, maxScroll);
241
253
 
242
- // console.log(
243
- // `[Scrolling] Wheel: delta=${delta}, scrollDelta=${scrollDelta}, pos=${scrollPosition} -> ${newPosition}, max=${maxScroll}`
244
- // );
245
-
246
254
  if (newPosition !== scrollPosition) {
247
255
  scrollPosition = newPosition;
248
256
  const now = Date.now();
@@ -260,7 +268,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
260
268
  speedTracker = updateSpeedTracker(
261
269
  speedTracker,
262
270
  scrollPosition,
263
- previousPosition
271
+ previousPosition,
264
272
  );
265
273
 
266
274
  // Update viewport state
@@ -306,7 +314,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
306
314
  speedTracker = updateSpeedTracker(
307
315
  speedTracker,
308
316
  scrollPosition,
309
- previousPosition
317
+ previousPosition,
310
318
  );
311
319
 
312
320
  // Update viewport state
@@ -352,7 +360,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
352
360
  // Scroll to index
353
361
  const scrollToIndex = (
354
362
  index: number,
355
- alignment: "start" | "center" | "end" = "start"
363
+ alignment: "start" | "center" | "end" = "start",
356
364
  ) => {
357
365
  // console.log(
358
366
  // `[Scrolling] scrollToIndex called: index=${index}, alignment=${alignment}`
@@ -405,7 +413,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
405
413
  const scrollToPage = (
406
414
  page: number,
407
415
  limit: number = 20,
408
- alignment: "start" | "center" | "end" = "start"
416
+ alignment: "start" | "center" | "end" = "start",
409
417
  ) => {
410
418
  // Validate alignment parameter
411
419
  if (
@@ -413,7 +421,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
413
421
  !["start", "center", "end"].includes(alignment)
414
422
  ) {
415
423
  console.warn(
416
- `[Scrolling] Invalid alignment "${alignment}", using "start"`
424
+ `[Scrolling] Invalid alignment "${alignment}", using "start"`,
417
425
  );
418
426
  alignment = "start";
419
427
  }
@@ -427,7 +435,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
427
435
  const collection = (component.viewport as any).collection;
428
436
  if (collection) {
429
437
  const highestLoadedPage = Math.floor(
430
- collection.getLoadedRanges().size
438
+ collection.getLoadedRanges().size,
431
439
  );
432
440
 
433
441
  if (page > highestLoadedPage + 1) {
@@ -435,13 +443,13 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
435
443
  const maxPagesToLoad = 10; // Reasonable limit
436
444
  const targetPage = Math.min(
437
445
  page,
438
- highestLoadedPage + maxPagesToLoad
446
+ highestLoadedPage + maxPagesToLoad,
439
447
  );
440
448
 
441
449
  console.warn(
442
450
  `[Scrolling] Cannot jump directly to page ${page} in cursor mode. ` +
443
451
  `Pages must be loaded sequentially. Current highest page: ${highestLoadedPage}. ` +
444
- `Will load up to page ${targetPage}`
452
+ `Will load up to page ${targetPage}`,
445
453
  );
446
454
 
447
455
  // Trigger sequential loading to the target page (limited)
@@ -449,11 +457,11 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
449
457
  const currentOffset = highestLoadedPage * limit;
450
458
 
451
459
  // Load pages sequentially up to the limited target
452
- console.log(
453
- `[Scrolling] Initiating sequential load from page ${
454
- highestLoadedPage + 1
455
- } to ${targetPage}`
456
- );
460
+ // console.log(
461
+ // `[Scrolling] Initiating sequential load from page ${
462
+ // highestLoadedPage + 1
463
+ // } to ${targetPage}`,
464
+ // );
457
465
 
458
466
  // Scroll to the last loaded position first
459
467
  const lastLoadedIndex = highestLoadedPage * limit;
@@ -479,7 +487,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
479
487
  // Update scroll bounds
480
488
  const updateScrollBounds = (
481
489
  newTotalSize: number,
482
- newContainerSize: number
490
+ newContainerSize: number,
483
491
  ) => {
484
492
  totalVirtualSize = newTotalSize;
485
493
  containerSize = newContainerSize;
@@ -489,6 +497,14 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
489
497
  viewportState.containerSize = newContainerSize;
490
498
  }
491
499
 
500
+ // Don't clamp scroll position until we have real data loaded
501
+ // This prevents resetting initialScrollIndex position before data arrives
502
+ // Check totalItems instead of totalVirtualSize since virtualSize can be non-zero from padding
503
+ const totalItems = viewportState?.totalItems || 0;
504
+ if (totalItems <= 0) {
505
+ return;
506
+ }
507
+
492
508
  // Clamp current position to new bounds
493
509
  const maxScroll = Math.max(0, totalVirtualSize - containerSize);
494
510
  if (scrollPosition > maxScroll) {
@@ -500,7 +516,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
500
516
  const originalScrollToIndex = component.viewport.scrollToIndex;
501
517
  component.viewport.scrollToIndex = (
502
518
  index: number,
503
- alignment?: "start" | "center" | "end"
519
+ alignment?: "start" | "center" | "end",
504
520
  ) => {
505
521
  scrollToIndex(index, alignment);
506
522
  originalScrollToIndex?.(index, alignment);
@@ -510,7 +526,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
510
526
  (component.viewport as any).scrollToPage = (
511
527
  page: number,
512
528
  limit?: number,
513
- alignment?: "start" | "center" | "end"
529
+ alignment?: "start" | "center" | "end",
514
530
  ) => {
515
531
  scrollToPage(page, limit, alignment);
516
532
  };
@@ -562,7 +578,7 @@ export const withScrolling = (config: ScrollingConfig = {}) => {
562
578
  speedTracker = updateSpeedTracker(
563
579
  speedTracker,
564
580
  scrollPosition,
565
- previousPosition
581
+ previousPosition,
566
582
  );
567
583
 
568
584
  // Update viewport state
@@ -13,12 +13,18 @@ import type { ViewportContext, ViewportComponent } from "../types";
13
13
  */
14
14
  export function wrapInitialize<T extends ViewportContext & ViewportComponent>(
15
15
  component: T,
16
- featureInit: () => void
16
+ featureInit: () => void,
17
17
  ): void {
18
18
  const originalInitialize = component.viewport.initialize;
19
19
  component.viewport.initialize = () => {
20
- originalInitialize();
21
- featureInit();
20
+ // Check if already initialized (returns false if already done)
21
+ const result = originalInitialize();
22
+ // Only run feature init if base init actually ran (didn't return false)
23
+ if (result !== false) {
24
+ featureInit();
25
+ }
26
+ // Propagate the result through the chain so outer wrappers know to skip
27
+ return result;
22
28
  };
23
29
  }
24
30
 
@@ -28,7 +34,7 @@ export function wrapInitialize<T extends ViewportContext & ViewportComponent>(
28
34
  */
29
35
  export function wrapDestroy<T extends Record<string, any>>(
30
36
  component: T,
31
- cleanup: () => void
37
+ cleanup: () => void,
32
38
  ): void {
33
39
  if ("destroy" in component && typeof component.destroy === "function") {
34
40
  const originalDestroy = component.destroy;
@@ -82,7 +88,7 @@ export function clamp(value: number, min: number, max: number): number {
82
88
  export function storeFeatureFunction<T extends Record<string, any>>(
83
89
  component: T,
84
90
  name: string,
85
- fn: Function
91
+ fn: Function,
86
92
  ): void {
87
93
  (component as any)[name] = fn;
88
94
  }
@@ -13,6 +13,7 @@ export interface VirtualConfig {
13
13
  orientation?: "vertical" | "horizontal";
14
14
  autoDetectItemSize?: boolean;
15
15
  debug?: boolean;
16
+ initialScrollIndex?: number;
16
17
  }
17
18
 
18
19
  /**
@@ -29,6 +30,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
29
30
  ? VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.AUTO_DETECT_ITEM_SIZE
30
31
  : false,
31
32
  debug = false,
33
+ initialScrollIndex = 0,
32
34
  } = config;
33
35
 
34
36
  // Use provided itemSize or default, but mark if we should auto-detect
@@ -38,6 +40,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
38
40
  const MAX_VIRTUAL_SIZE = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
39
41
  let viewportState: any;
40
42
  let hasCalculatedItemSize = false;
43
+ let hasRecalculatedScrollForCompression = false; // Track if we've recalculated scroll position for compression
41
44
 
42
45
  // Initialize using shared wrapper
43
46
  wrapInitialize(component, () => {
@@ -54,7 +57,32 @@ export const withVirtual = (config: VirtualConfig = {}) => {
54
57
  });
55
58
 
56
59
  updateTotalVirtualSize(viewportState.totalItems);
57
- updateVisibleRange(viewportState.scrollPosition || 0);
60
+
61
+ // If we have an initial scroll index, calculate initial scroll position
62
+ // Note: We store the initialScrollIndex to use directly in range calculations
63
+ // because scroll position may be compressed for large lists
64
+ let initialScrollPosition = viewportState.scrollPosition || 0;
65
+
66
+ if (initialScrollIndex > 0) {
67
+ // Store the target index for use in range calculations
68
+ (viewportState as any).targetScrollIndex = initialScrollIndex;
69
+
70
+ // Calculate scroll position (may be compressed for large lists)
71
+ initialScrollPosition =
72
+ initialScrollIndex * (viewportState.itemSize || initialItemSize);
73
+ viewportState.scrollPosition = initialScrollPosition;
74
+
75
+ // Notify scrolling feature to sync its local scroll position
76
+ // Use setTimeout to ensure scrolling feature has initialized
77
+ setTimeout(() => {
78
+ component.emit?.("viewport:scroll-position-sync", {
79
+ position: initialScrollPosition,
80
+ source: "initial-scroll-index",
81
+ });
82
+ }, 0);
83
+ }
84
+
85
+ updateVisibleRange(initialScrollPosition);
58
86
 
59
87
  // Ensure container size is measured after DOM is ready
60
88
  requestAnimationFrame(() => {
@@ -75,7 +103,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
75
103
 
76
104
  // Calculate visible range
77
105
  const calculateVisibleRange = (
78
- scrollPosition: number
106
+ scrollPosition: number,
79
107
  ): { start: number; end: number } => {
80
108
  if (!viewportState) {
81
109
  console.warn("[Virtual] No viewport state, returning empty range");
@@ -91,6 +119,17 @@ export const withVirtual = (config: VirtualConfig = {}) => {
91
119
  !totalItems ||
92
120
  totalItems <= 0
93
121
  ) {
122
+ // If we have an initialScrollIndex, use it to calculate the range
123
+ // even when totalItems is 0 (not yet loaded from API)
124
+ if (initialScrollIndex > 0) {
125
+ const visibleCount = Math.ceil(
126
+ (containerSize || 600) /
127
+ (viewportState.itemSize || initialItemSize),
128
+ );
129
+ const start = Math.max(0, initialScrollIndex - overscan);
130
+ const end = initialScrollIndex + visibleCount + overscan;
131
+ return { start, end };
132
+ }
94
133
  log(`Invalid state: container=${containerSize}, items=${totalItems}`);
95
134
  return { start: 0, end: 0 };
96
135
  }
@@ -102,12 +141,27 @@ export const withVirtual = (config: VirtualConfig = {}) => {
102
141
 
103
142
  let start: number, end: number;
104
143
 
144
+ // Check if we have a target scroll index (for initialScrollIndex with compression)
145
+ const targetScrollIndex = (viewportState as any).targetScrollIndex;
146
+
105
147
  if (compressionRatio < 1) {
106
148
  // Compressed space calculation
107
- const scrollRatio = scrollPosition / virtualSize;
108
- const exactIndex = scrollRatio * totalItems;
109
- start = Math.floor(exactIndex);
110
- end = Math.ceil(exactIndex) + visibleCount;
149
+ // If we have a targetScrollIndex, use it directly instead of calculating from scroll position
150
+ // This ensures we show the correct items even when virtual space is compressed
151
+ if (targetScrollIndex !== undefined && targetScrollIndex > 0) {
152
+ start = Math.max(0, targetScrollIndex - overscan);
153
+ end = Math.min(
154
+ totalItems - 1,
155
+ targetScrollIndex + visibleCount + overscan,
156
+ );
157
+ // Clear targetScrollIndex after first use so normal scrolling works
158
+ delete (viewportState as any).targetScrollIndex;
159
+ } else {
160
+ const scrollRatio = scrollPosition / virtualSize;
161
+ const exactIndex = scrollRatio * totalItems;
162
+ start = Math.floor(exactIndex);
163
+ end = Math.ceil(exactIndex) + visibleCount;
164
+ }
111
165
 
112
166
  // Near-bottom handling
113
167
  const maxScroll = virtualSize - containerSize;
@@ -115,16 +169,16 @@ export const withVirtual = (config: VirtualConfig = {}) => {
115
169
 
116
170
  if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
117
171
  const itemsAtBottom = Math.floor(
118
- containerSize / viewportState.itemSize
172
+ containerSize / viewportState.itemSize,
119
173
  );
120
174
  const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
121
175
  const interpolation = Math.max(
122
176
  0,
123
- Math.min(1, 1 - distanceFromBottom / containerSize)
177
+ Math.min(1, 1 - distanceFromBottom / containerSize),
124
178
  );
125
179
 
126
180
  start = Math.floor(
127
- start + (firstVisibleAtBottom - start) * interpolation
181
+ start + (firstVisibleAtBottom - start) * interpolation,
128
182
  );
129
183
  end =
130
184
  distanceFromBottom <= 1
@@ -148,7 +202,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
148
202
  // Direct calculation
149
203
  start = Math.max(
150
204
  0,
151
- Math.floor(scrollPosition / viewportState.itemSize) - overscan
205
+ Math.floor(scrollPosition / viewportState.itemSize) - overscan,
152
206
  );
153
207
  end = Math.min(totalItems - 1, start + visibleCount + overscan * 2);
154
208
  }
@@ -179,12 +233,54 @@ export const withVirtual = (config: VirtualConfig = {}) => {
179
233
  return { start, end };
180
234
  };
181
235
 
236
+ // Calculate actual visible range (without overscan buffer)
237
+ const calculateActualVisibleRange = (scrollPosition: number) => {
238
+ if (!viewportState) return { start: 0, end: 0 };
239
+
240
+ const { containerSize, totalItems } = viewportState;
241
+ if (!containerSize || !totalItems) return { start: 0, end: 0 };
242
+
243
+ const itemSize = viewportState.itemSize;
244
+ const visibleCount = Math.ceil(containerSize / itemSize);
245
+ const compressionRatio = viewportState.virtualTotalSize
246
+ ? (totalItems * itemSize) / viewportState.virtualTotalSize
247
+ : 1;
248
+
249
+ let start: number, end: number;
250
+
251
+ if (compressionRatio < 1) {
252
+ // Compressed space - calculate based on scroll ratio
253
+ const virtualSize =
254
+ viewportState.virtualTotalSize || totalItems * itemSize;
255
+ const scrollRatio = scrollPosition / virtualSize;
256
+ start = Math.floor(scrollRatio * totalItems);
257
+ end = Math.min(totalItems - 1, start + visibleCount - 1);
258
+ } else {
259
+ // Direct calculation
260
+ start = Math.floor(scrollPosition / itemSize);
261
+ end = Math.min(totalItems - 1, start + visibleCount - 1);
262
+ }
263
+
264
+ // Ensure valid range
265
+ start = Math.max(0, start);
266
+ end = Math.max(start, end);
267
+
268
+ return { start, end };
269
+ };
270
+
182
271
  // Update functions
183
272
  const updateVisibleRange = (scrollPosition: number) => {
184
273
  if (!viewportState) return;
185
274
  viewportState.visibleRange = calculateVisibleRange(scrollPosition);
275
+
276
+ // DEBUG: Log who calls updateVisibleRange with 0 when it should be non-zero
277
+
278
+ // Calculate actual visible range (without overscan) for UI display
279
+ const actualVisibleRange = calculateActualVisibleRange(scrollPosition);
280
+
186
281
  component.emit?.("viewport:range-changed", {
187
282
  range: viewportState.visibleRange,
283
+ visibleRange: actualVisibleRange,
188
284
  scrollPosition,
189
285
  });
190
286
  };
@@ -201,7 +297,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
201
297
  let totalPadding = 0;
202
298
  if (viewportState.itemsContainer) {
203
299
  const computedStyle = window.getComputedStyle(
204
- viewportState.itemsContainer
300
+ viewportState.itemsContainer,
205
301
  );
206
302
  const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
207
303
  const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
@@ -211,7 +307,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
211
307
  // Include padding in the virtual size
212
308
  viewportState.virtualTotalSize = Math.min(
213
309
  actualSize + totalPadding,
214
- MAX_VIRTUAL_SIZE
310
+ MAX_VIRTUAL_SIZE,
215
311
  );
216
312
 
217
313
  // Strategic log for debugging gap issue
@@ -259,7 +355,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
259
355
 
260
356
  // Event listeners
261
357
  component.on?.("viewport:scroll", (data: any) =>
262
- updateVisibleRange(data.position)
358
+ updateVisibleRange(data.position),
263
359
  );
264
360
 
265
361
  component.on?.("viewport:items-changed", (data: any) => {
@@ -270,20 +366,55 @@ export const withVirtual = (config: VirtualConfig = {}) => {
270
366
  });
271
367
 
272
368
  // Listen for total items changes (important for cursor pagination)
369
+ // Note: setTotalItems() updates viewportState.totalItems BEFORE emitting this event,
370
+ // so we can't rely on viewportState.totalItems to detect the first total.
371
+ // Instead we use the hasRecalculatedScrollForCompression flag.
273
372
  component.on?.("viewport:total-items-changed", (data: any) => {
373
+ if (data.total === undefined) return;
374
+
375
+ // FIX: When we first receive totalItems with initialScrollIndex and compression,
376
+ // recalculate scroll position to account for compression.
377
+ // Without this, initialScrollIndex calculates position as index * itemSize,
378
+ // but rendering uses compressed space, causing items to appear off-screen.
379
+ //
380
+ // We check this BEFORE updateTotalVirtualSize because we need to recalculate
381
+ // scroll position based on the new total, and the flag ensures we only do this once.
274
382
  if (
275
- data.total !== undefined &&
276
- data.total !== viewportState?.totalItems
383
+ initialScrollIndex > 0 &&
384
+ !hasRecalculatedScrollForCompression &&
385
+ data.total > 0
277
386
  ) {
278
- log(
279
- `Total items changed from ${viewportState?.totalItems} to ${data.total}`
280
- );
281
- updateTotalVirtualSize(data.total);
282
- updateVisibleRange(viewportState?.scrollPosition || 0);
387
+ const actualTotalSize = data.total * viewportState.itemSize;
388
+ const isCompressed = actualTotalSize > MAX_VIRTUAL_SIZE;
389
+
390
+ if (isCompressed) {
391
+ // Recalculate scroll position using compression-aware formula
392
+ // Same formula used in scrollToIndex
393
+ const ratio = initialScrollIndex / data.total;
394
+ const compressedPosition = ratio * MAX_VIRTUAL_SIZE;
395
+
396
+ viewportState.scrollPosition = compressedPosition;
283
397
 
284
- // Trigger a render to update the view
285
- component.viewport?.renderItems?.();
398
+ // Notify scrolling feature to sync its local scroll position
399
+ component.emit?.("viewport:scroll-position-sync", {
400
+ position: compressedPosition,
401
+ source: "compression-recalculation",
402
+ });
403
+
404
+ // Clear targetScrollIndex since we've now correctly calculated the scroll position
405
+ // The normal compressed scrolling formula will now work correctly
406
+ delete (viewportState as any).targetScrollIndex;
407
+ }
408
+
409
+ // Mark as done even if not compressed - we only want to do this check once
410
+ hasRecalculatedScrollForCompression = true;
286
411
  }
412
+
413
+ updateTotalVirtualSize(data.total);
414
+ updateVisibleRange(viewportState?.scrollPosition || 0);
415
+
416
+ // Trigger a render to update the view
417
+ component.viewport?.renderItems?.();
287
418
  });
288
419
 
289
420
  // Listen for container size changes to recalculate virtual size
@@ -329,14 +460,24 @@ export const withVirtual = (config: VirtualConfig = {}) => {
329
460
 
330
461
  if (sizes.length > 0) {
331
462
  const avgSize = Math.round(
332
- sizes.reduce((sum, size) => sum + size, 0) / sizes.length
463
+ sizes.reduce((sum, size) => sum + size, 0) / sizes.length,
333
464
  );
334
-
335
- // console.log(
336
- // `[Virtual] Auto-detected item size: ${avgSize}px (was ${viewportState.itemSize}px), based on ${sizes.length} items`
337
- // );
465
+ const previousItemSize = viewportState.itemSize;
338
466
  viewportState.itemSize = avgSize;
339
467
 
468
+ // If we have an initialScrollIndex, recalculate scroll position
469
+ // based on the newly detected item size
470
+ if (initialScrollIndex > 0 && avgSize !== previousItemSize) {
471
+ const newScrollPosition = initialScrollIndex * avgSize;
472
+ viewportState.scrollPosition = newScrollPosition;
473
+
474
+ // Notify scrolling feature to sync its local scroll position
475
+ component.emit?.("viewport:scroll-position-sync", {
476
+ position: newScrollPosition,
477
+ source: "item-size-detected",
478
+ });
479
+ }
480
+
340
481
  // Recalculate everything with new size
341
482
  updateTotalVirtualSize(viewportState.totalItems);
342
483
  updateVisibleRange(viewportState.scrollPosition || 0);
@@ -373,7 +514,7 @@ export const withVirtual = (config: VirtualConfig = {}) => {
373
514
  calculateIndexFromPosition: (position: number) =>
374
515
  Math.floor(
375
516
  position /
376
- ((viewportState?.itemSize || initialItemSize) * compressionRatio)
517
+ ((viewportState?.itemSize || initialItemSize) * compressionRatio),
377
518
  ),
378
519
  calculatePositionForIndex: (index: number) =>
379
520
  index * (viewportState?.itemSize || initialItemSize) * compressionRatio,
@@ -49,7 +49,7 @@ export interface ViewportConfig {
49
49
  // Template for rendering items
50
50
  template?: (
51
51
  item: any,
52
- index: number
52
+ index: number,
53
53
  ) => string | HTMLElement | any[] | Record<string, any>;
54
54
 
55
55
  // Collection/data source configuration
@@ -109,7 +109,7 @@ export interface ViewportConfig {
109
109
  export interface ViewportComponent extends ViewportContext {
110
110
  viewport: {
111
111
  // Core API
112
- initialize(): void;
112
+ initialize(): boolean | void;
113
113
  destroy(): void;
114
114
  updateViewport(): void;
115
115