@pdanpdan/virtual-scroll 0.4.0 → 0.5.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.
@@ -12,43 +12,422 @@ import type {
12
12
  TotalSizeParams,
13
13
  } from '../types';
14
14
 
15
- import { isScrollToIndexOptions } from './scroll';
15
+ import { BROWSER_MAX_SIZE, isScrollToIndexOptions } from './scroll';
16
+
17
+ // --- Internal Helper Types ---
18
+
19
+ interface GenericRangeParams {
20
+ scrollPos: number;
21
+ containerSize: number;
22
+ count: number;
23
+ bufferBefore: number;
24
+ bufferAfter: number;
25
+ gap: number;
26
+ fixedSize: number | null;
27
+ findLowerBound: (offset: number) => number;
28
+ query: (index: number) => number;
29
+ }
30
+
31
+ interface AxisAlignmentParams {
32
+ align: ScrollAlignment;
33
+ targetPos: number;
34
+ itemSize: number;
35
+ scrollPos: number;
36
+ viewSize: number;
37
+ stickyOffsetStart: number;
38
+ stickyOffsetEnd: number;
39
+ }
40
+
41
+ // --- Internal Helpers ---
42
+
43
+ /**
44
+ * Generic range calculation for a single axis (row or column).
45
+ *
46
+ * @param params - Range parameters.
47
+ * @param params.scrollPos - Virtual scroll position.
48
+ * @param params.containerSize - Usable viewport size.
49
+ * @param params.count - Total item count.
50
+ * @param params.bufferBefore - Buffer items before.
51
+ * @param params.bufferAfter - Buffer items after.
52
+ * @param params.gap - Item gap.
53
+ * @param params.fixedSize - Fixed item size.
54
+ * @param params.findLowerBound - Binary search for index.
55
+ * @param params.query - Prefix sum for index.
56
+ * @returns Start and end indices.
57
+ */
58
+ function calculateGenericRange({
59
+ scrollPos,
60
+ containerSize,
61
+ count,
62
+ bufferBefore,
63
+ bufferAfter,
64
+ gap,
65
+ fixedSize,
66
+ findLowerBound,
67
+ query,
68
+ }: GenericRangeParams) {
69
+ let start = 0;
70
+ let end = count;
71
+ const endOffset = scrollPos + containerSize;
72
+
73
+ if (fixedSize !== null) {
74
+ const step = fixedSize + gap;
75
+ start = Math.floor(scrollPos / step);
76
+ end = Math.ceil(endOffset / step);
77
+ } else {
78
+ start = findLowerBound(scrollPos);
79
+ end = findLowerBound(endOffset);
80
+ if (end < count && query(end) < endOffset) {
81
+ end++;
82
+ }
83
+ }
84
+
85
+ return {
86
+ start: Math.max(0, start - bufferBefore),
87
+ end: Math.min(count, end + bufferAfter),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Binary search for the next sticky index after the current index.
93
+ *
94
+ * @param stickyIndices - Sorted array of sticky indices.
95
+ * @param index - Current index.
96
+ * @returns Next sticky index or undefined.
97
+ */
98
+ function findNextStickyIndex(stickyIndices: number[], index: number): number | undefined {
99
+ let low = 0;
100
+ let high = stickyIndices.length - 1;
101
+ let nextStickyIdx: number | undefined;
102
+
103
+ while (low <= high) {
104
+ const mid = (low + high) >>> 1;
105
+ if (stickyIndices[ mid ]! > index) {
106
+ nextStickyIdx = stickyIndices[ mid ];
107
+ high = mid - 1;
108
+ } else {
109
+ low = mid + 1;
110
+ }
111
+ }
112
+ return nextStickyIdx;
113
+ }
114
+
115
+ /**
116
+ * Binary search for the previous sticky index before the current index.
117
+ *
118
+ * @param stickyIndices - Sorted array of sticky indices.
119
+ * @param index - Current index.
120
+ * @returns Previous sticky index or undefined.
121
+ */
122
+ export function findPrevStickyIndex(stickyIndices: number[], index: number): number | undefined {
123
+ let low = 0;
124
+ let high = stickyIndices.length - 1;
125
+ let prevStickyIdx: number | undefined;
126
+
127
+ while (low <= high) {
128
+ const mid = (low + high) >>> 1;
129
+ if (stickyIndices[ mid ]! < index) {
130
+ prevStickyIdx = stickyIndices[ mid ];
131
+ low = mid + 1;
132
+ } else {
133
+ high = mid - 1;
134
+ }
135
+ }
136
+ return prevStickyIdx;
137
+ }
138
+
139
+ /**
140
+ * Generic alignment calculation for a single axis.
141
+ *
142
+ * @param params - Alignment parameters.
143
+ * @param params.align - Desired alignment.
144
+ * @param params.targetPos - Virtual item position.
145
+ * @param params.itemSize - Virtual item size.
146
+ * @param params.scrollPos - Virtual scroll position.
147
+ * @param params.viewSize - Full viewport size.
148
+ * @param params.stickyOffsetStart - Dynamic sticky offset at start.
149
+ * @param params.stickyOffsetEnd - Sticky offset at end.
150
+ * @returns Target scroll position and effective alignment.
151
+ */
152
+ function calculateAxisAlignment({
153
+ align,
154
+ targetPos,
155
+ itemSize,
156
+ scrollPos,
157
+ viewSize,
158
+ stickyOffsetStart,
159
+ stickyOffsetEnd,
160
+ }: AxisAlignmentParams) {
161
+ const targetStart = targetPos - stickyOffsetStart;
162
+ const targetEnd = targetPos - (viewSize - stickyOffsetEnd - itemSize);
163
+
164
+ if (align === 'start') {
165
+ return { target: targetStart, effectiveAlign: 'start' as const };
166
+ }
167
+ if (align === 'center') {
168
+ return {
169
+ target: targetPos - stickyOffsetStart - (viewSize - stickyOffsetStart - stickyOffsetEnd - itemSize) / 2,
170
+ effectiveAlign: 'center' as const,
171
+ };
172
+ }
173
+ if (align === 'end') {
174
+ return { target: targetEnd, effectiveAlign: 'end' as const };
175
+ }
176
+
177
+ if (isItemVisible(targetPos, itemSize, scrollPos, viewSize, stickyOffsetStart, stickyOffsetEnd)) {
178
+ return { target: scrollPos, effectiveAlign: 'auto' as const };
179
+ }
180
+
181
+ const usableSize = viewSize - stickyOffsetStart - stickyOffsetEnd;
182
+
183
+ if (itemSize <= usableSize) {
184
+ return targetPos < scrollPos + stickyOffsetStart
185
+ ? {
186
+ target: targetStart,
187
+ effectiveAlign: 'start' as const,
188
+ }
189
+ : {
190
+ target: targetEnd,
191
+ effectiveAlign: 'end' as const,
192
+ };
193
+ }
194
+
195
+ return Math.abs(targetStart - scrollPos) < Math.abs(targetEnd - scrollPos)
196
+ ? {
197
+ target: targetStart,
198
+ effectiveAlign: 'start' as const,
199
+ }
200
+ : {
201
+ target: targetEnd,
202
+ effectiveAlign: 'end' as const,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Helper to calculate total size for a single axis.
208
+ *
209
+ * @param count - Item count.
210
+ * @param fixedSize - Fixed size if any.
211
+ * @param gap - Gap size.
212
+ * @param query - Prefix sum resolver.
213
+ * @returns Total size.
214
+ */
215
+ function calculateAxisSize(
216
+ count: number,
217
+ fixedSize: number | null,
218
+ gap: number,
219
+ query: (index: number) => number,
220
+ ): number {
221
+ if (count <= 0) {
222
+ return 0;
223
+ }
224
+ if (fixedSize !== null) {
225
+ return Math.max(0, count * (fixedSize + gap) - gap);
226
+ }
227
+ return Math.max(0, query(count) - gap);
228
+ }
229
+
230
+ /**
231
+ * Helper to calculate target scroll position for a single axis.
232
+ *
233
+ * @param params - Axis target parameters.
234
+ * @param params.index - Row/column index.
235
+ * @param params.align - Desired alignment.
236
+ * @param params.viewSize - Full viewport size.
237
+ * @param params.scrollPos - Virtual scroll position.
238
+ * @param params.fixedSize - Fixed item size.
239
+ * @param params.gap - Item gap.
240
+ * @param params.query - Prefix sum resolver.
241
+ * @param params.getSize - Item size resolver.
242
+ * @param params.stickyIndices - Sticky indices.
243
+ * @param params.stickyStart - Sticky start element size.
244
+ * @param params.stickyEnd - Sticky end element size.
245
+ * @returns Target position, item size and effective alignment.
246
+ */
247
+ function calculateAxisTarget({
248
+ index,
249
+ align,
250
+ viewSize,
251
+ scrollPos,
252
+ fixedSize,
253
+ gap,
254
+ query,
255
+ getSize,
256
+ stickyIndices,
257
+ stickyStart,
258
+ stickyEnd = 0,
259
+ }: {
260
+ index: number;
261
+ align: ScrollAlignment;
262
+ viewSize: number;
263
+ scrollPos: number;
264
+ fixedSize: number | null;
265
+ gap: number;
266
+ query: (idx: number) => number;
267
+ getSize: (idx: number) => number;
268
+ stickyIndices?: number[] | undefined;
269
+ stickyStart: number;
270
+ stickyEnd?: number;
271
+ }) {
272
+ let stickyOffsetStart = stickyStart;
273
+ if (stickyIndices && stickyIndices.length > 0) {
274
+ const activeStickyIdx = findPrevStickyIndex(stickyIndices, index);
275
+ if (activeStickyIdx !== undefined) {
276
+ stickyOffsetStart += calculateAxisSize(1, fixedSize, 0, () => getSize(activeStickyIdx));
277
+ }
278
+ }
279
+
280
+ const itemPos = (fixedSize !== null ? index * (fixedSize + gap) : query(index));
281
+ const itemSize = fixedSize !== null ? fixedSize : getSize(index) - gap;
282
+
283
+ const { target, effectiveAlign } = calculateAxisAlignment({
284
+ align,
285
+ targetPos: itemPos,
286
+ itemSize,
287
+ scrollPos,
288
+ viewSize,
289
+ stickyOffsetStart,
290
+ stickyOffsetEnd: stickyEnd,
291
+ });
292
+
293
+ return { target, itemSize, effectiveAlign };
294
+ }
295
+
296
+ // --- Exported Functions ---
297
+
298
+ /**
299
+ * Determines if an item is visible within the usable viewport.
300
+ *
301
+ * @param itemPos - Virtual start position of the item (VU).
302
+ * @param itemSize - Virtual size of the item (VU).
303
+ * @param scrollPos - Virtual scroll position (VU).
304
+ * @param viewSize - Full size of the viewport (VU).
305
+ * @param stickyOffsetStart - Dynamic offset from sticky items at start (VU).
306
+ * @param stickyOffsetEnd - Offset from sticky items at end (VU).
307
+ * @returns True if visible.
308
+ */
309
+ export function isItemVisible(
310
+ itemPos: number,
311
+ itemSize: number,
312
+ scrollPos: number,
313
+ viewSize: number,
314
+ stickyOffsetStart: number = 0,
315
+ stickyOffsetEnd: number = 0,
316
+ ): boolean {
317
+ const usableStart = scrollPos + stickyOffsetStart;
318
+ const usableEnd = scrollPos + viewSize - stickyOffsetEnd;
319
+ const usableSize = viewSize - stickyOffsetStart - stickyOffsetEnd;
320
+
321
+ if (itemSize <= usableSize) {
322
+ return itemPos >= usableStart - 0.5 && (itemPos + itemSize) <= usableEnd + 0.5;
323
+ }
324
+ return itemPos <= usableStart + 0.5 && (itemPos + itemSize) >= usableEnd - 0.5;
325
+ }
326
+
327
+ /**
328
+ * Maps a display scroll position to a virtual content position.
329
+ *
330
+ * @param displayPos - Display pixel position (DU).
331
+ * @param hostOffset - Offset of the host element in display pixels (DU).
332
+ * @param scale - Coordinate scaling factor (VU/DU).
333
+ * @returns Virtual content position (VU).
334
+ */
335
+ export function displayToVirtual(displayPos: number, hostOffset: number, scale: number): number {
336
+ return (displayPos - hostOffset) * scale;
337
+ }
338
+
339
+ /**
340
+ * Maps a virtual content position to a display scroll position.
341
+ *
342
+ * @param virtualPos - Virtual content position (VU).
343
+ * @param hostOffset - Offset of the host element in display pixels (DU).
344
+ * @param scale - Coordinate scaling factor (VU/DU).
345
+ * @returns Display pixel position (DU).
346
+ */
347
+ export function virtualToDisplay(virtualPos: number, hostOffset: number, scale: number): number {
348
+ return virtualPos / scale + hostOffset;
349
+ }
16
350
 
17
351
  /**
18
352
  * Calculates the target scroll position (relative to content) for a given row/column index and alignment.
19
353
  *
20
- * @param params - The parameters for calculation.
21
- * @returns The target X and Y positions and item dimensions.
354
+ * @param params - Scroll target parameters.
355
+ * @param params.rowIndex - Row index to target.
356
+ * @param params.colIndex - Column index to target.
357
+ * @param params.options - Scroll options including alignment.
358
+ * @param params.direction - Current scroll direction.
359
+ * @param params.viewportWidth - Full viewport width (DU).
360
+ * @param params.viewportHeight - Full viewport height (DU).
361
+ * @param params.totalWidth - Total estimated width (VU).
362
+ * @param params.totalHeight - Total estimated height (VU).
363
+ * @param params.gap - Item gap (VU).
364
+ * @param params.columnGap - Column gap (VU).
365
+ * @param params.fixedSize - Fixed item size (VU).
366
+ * @param params.fixedWidth - Fixed column width (VU).
367
+ * @param params.relativeScrollX - Current relative X scroll (VU).
368
+ * @param params.relativeScrollY - Current relative Y scroll (VU).
369
+ * @param params.getItemSizeY - Resolver for item height (VU).
370
+ * @param params.getItemSizeX - Resolver for item width (VU).
371
+ * @param params.getItemQueryY - Prefix sum resolver for item height (VU).
372
+ * @param params.getItemQueryX - Prefix sum resolver for item width (VU).
373
+ * @param params.getColumnSize - Resolver for column size (VU).
374
+ * @param params.getColumnQuery - Prefix sum resolver for column width (VU).
375
+ * @param params.scaleX - Coordinate scaling factor for X axis.
376
+ * @param params.scaleY - Coordinate scaling factor for Y axis.
377
+ * @param params.hostOffsetX - Display pixels offset of items wrapper on X axis (DU).
378
+ * @param params.hostOffsetY - Display pixels offset of items wrapper on Y axis (DU).
379
+ * @param params.flowPaddingStartX - Display pixels padding at flow start on X axis (DU).
380
+ * @param params.flowPaddingStartY - Display pixels padding at flow start on Y axis (DU).
381
+ * @param params.paddingStartX - Display pixels padding at scroll start on X axis (DU).
382
+ * @param params.paddingStartY - Display pixels padding at scroll start on Y axis (DU).
383
+ * @param params.paddingEndX - Display pixels padding at scroll end on X axis (DU).
384
+ * @param params.paddingEndY - Display pixels padding at scroll end on Y axis (DU).
385
+ * @param params.stickyIndices - List of sticky indices.
386
+ * @param params.stickyStartX - Sticky start offset on X axis (DU).
387
+ * @param params.stickyStartY - Sticky start offset on Y axis (DU).
388
+ * @param params.stickyEndX - Sticky end offset on X axis (DU).
389
+ * @param params.stickyEndY - Sticky end offset on Y axis (DU).
390
+ * @returns The target X and Y positions (VU) and item dimensions (VU).
22
391
  * @see ScrollTargetParams
23
392
  * @see ScrollTargetResult
24
393
  */
25
- export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetResult {
26
- const {
27
- rowIndex,
28
- colIndex,
29
- options,
30
- itemsLength,
31
- columnCount,
32
- direction,
33
- usableWidth,
34
- usableHeight,
35
- totalWidth,
36
- totalHeight,
37
- gap,
38
- columnGap,
39
- fixedSize,
40
- fixedWidth,
41
- relativeScrollX,
42
- relativeScrollY,
43
- getItemSizeY,
44
- getItemSizeX,
45
- getItemQueryY,
46
- getItemQueryX,
47
- getColumnSize,
48
- getColumnQuery,
49
- stickyIndices,
50
- } = params;
51
-
394
+ export function calculateScrollTarget({
395
+ rowIndex,
396
+ colIndex,
397
+ options,
398
+ direction,
399
+ viewportWidth,
400
+ viewportHeight,
401
+ totalWidth,
402
+ totalHeight,
403
+ gap,
404
+ columnGap,
405
+ fixedSize,
406
+ fixedWidth,
407
+ relativeScrollX,
408
+ relativeScrollY,
409
+ getItemSizeY,
410
+ getItemSizeX,
411
+ getItemQueryY,
412
+ getItemQueryX,
413
+ getColumnSize,
414
+ getColumnQuery,
415
+ scaleX,
416
+ scaleY,
417
+ hostOffsetX,
418
+ hostOffsetY,
419
+ stickyIndices,
420
+ stickyStartX = 0,
421
+ stickyStartY = 0,
422
+ stickyEndX = 0,
423
+ stickyEndY = 0,
424
+ flowPaddingStartX = 0,
425
+ flowPaddingStartY = 0,
426
+ paddingStartX = 0,
427
+ paddingStartY = 0,
428
+ paddingEndX = 0,
429
+ paddingEndY = 0,
430
+ }: ScrollTargetParams): ScrollTargetResult {
52
431
  let align: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
53
432
 
54
433
  if (isScrollToIndexOptions(options)) {
@@ -57,167 +436,74 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
57
436
  align = options as ScrollAlignment | ScrollAlignmentOptions;
58
437
  }
59
438
 
60
- const alignX = (typeof align === 'object' ? align.x : align) || 'auto';
61
- const alignY = (typeof align === 'object' ? align.y : align) || 'auto';
62
-
63
- const isVertical = direction === 'vertical' || direction === 'both';
64
- const isHorizontal = direction === 'horizontal' || direction === 'both';
439
+ const alignX = (align && typeof align === 'object' ? align.x : align) || 'auto';
440
+ const alignY = (align && typeof align === 'object' ? align.y : align) || 'auto';
65
441
 
66
442
  let targetX = relativeScrollX;
67
443
  let targetY = relativeScrollY;
68
444
  let itemWidth = 0;
69
445
  let itemHeight = 0;
70
- let effectiveAlignX: ScrollAlignment = alignX === 'auto' ? 'auto' : alignX;
71
- let effectiveAlignY: ScrollAlignment = alignY === 'auto' ? 'auto' : alignY;
446
+ let effectiveAlignX: ScrollAlignment = 'auto';
447
+ let effectiveAlignY: ScrollAlignment = 'auto';
72
448
 
73
- // Y calculation
74
- if (rowIndex != null) {
75
- let stickyOffsetY = 0;
76
- if (isVertical && stickyIndices && stickyIndices.length > 0) {
77
- let activeStickyIdx: number | undefined;
78
- let low = 0;
79
- let high = stickyIndices.length - 1;
80
- while (low <= high) {
81
- const mid = (low + high) >>> 1;
82
- if (stickyIndices[ mid ]! < rowIndex) {
83
- activeStickyIdx = stickyIndices[ mid ];
84
- low = mid + 1;
85
- } else {
86
- high = mid - 1;
87
- }
88
- }
449
+ // Clamp to valid range
450
+ const rWidth = scaleX === 1 ? totalWidth : BROWSER_MAX_SIZE;
451
+ const rHeight = scaleY === 1 ? totalHeight : BROWSER_MAX_SIZE;
89
452
 
90
- if (activeStickyIdx !== undefined) {
91
- stickyOffsetY = fixedSize !== null ? fixedSize : getItemSizeY(activeStickyIdx) - gap;
92
- }
93
- }
453
+ const maxDisplayX = Math.max(0, hostOffsetX + rWidth - viewportWidth);
454
+ const maxDisplayY = Math.max(0, hostOffsetY + rHeight - viewportHeight);
94
455
 
95
- let itemY = 0;
96
- if (rowIndex >= itemsLength) {
97
- itemY = totalHeight;
98
- itemHeight = 0;
99
- } else {
100
- itemY = fixedSize !== null ? rowIndex * (fixedSize + gap) : getItemQueryY(rowIndex);
101
- itemHeight = fixedSize !== null ? fixedSize : getItemSizeY(rowIndex) - gap;
102
- }
456
+ // maxTarget should be in virtual internalScroll coordinates
457
+ const maxTargetX = (maxDisplayX - hostOffsetX) * scaleX;
458
+ const maxTargetY = (maxDisplayY - hostOffsetY) * scaleY;
103
459
 
104
- // Apply Y Alignment
105
- if (alignY === 'start') {
106
- targetY = itemY - stickyOffsetY;
107
- } else if (alignY === 'center') {
108
- targetY = itemY - (usableHeight - itemHeight) / 2;
109
- } else if (alignY === 'end') {
110
- targetY = itemY - (usableHeight - itemHeight);
111
- } else {
112
- // Auto alignment: stay if visible, otherwise align to nearest edge (minimal movement)
113
- const isVisibleY = itemHeight <= (usableHeight - stickyOffsetY)
114
- ? (itemY >= relativeScrollY + stickyOffsetY - 0.5 && (itemY + itemHeight) <= (relativeScrollY + usableHeight + 0.5))
115
- : (itemY <= relativeScrollY + stickyOffsetY + 0.5 && (itemY + itemHeight) >= (relativeScrollY + usableHeight - 0.5));
116
-
117
- if (!isVisibleY) {
118
- const targetStart = itemY - stickyOffsetY;
119
- const targetEnd = itemY - (usableHeight - itemHeight);
120
-
121
- if (itemHeight <= usableHeight - stickyOffsetY) {
122
- if (itemY < relativeScrollY + stickyOffsetY) {
123
- targetY = targetStart;
124
- effectiveAlignY = 'start';
125
- } else {
126
- targetY = targetEnd;
127
- effectiveAlignY = 'end';
128
- }
129
- } else {
130
- // Large item: minimal movement
131
- if (Math.abs(targetStart - relativeScrollY) < Math.abs(targetEnd - relativeScrollY)) {
132
- targetY = targetStart;
133
- effectiveAlignY = 'start';
134
- } else {
135
- targetY = targetEnd;
136
- effectiveAlignY = 'end';
137
- }
138
- }
139
- }
140
- }
460
+ const itemsStartVirtualX = flowPaddingStartX + stickyStartX + paddingStartX;
461
+ const itemsStartVirtualY = flowPaddingStartY + stickyStartY + paddingStartY;
462
+
463
+ // Y calculation
464
+ if (rowIndex != null) {
465
+ const res = calculateAxisTarget({
466
+ index: rowIndex,
467
+ align: alignY as ScrollAlignment,
468
+ viewSize: viewportHeight,
469
+ scrollPos: relativeScrollY,
470
+ fixedSize,
471
+ gap,
472
+ query: getItemQueryY,
473
+ getSize: getItemSizeY,
474
+ stickyIndices,
475
+ stickyStart: stickyStartY + paddingStartY,
476
+ stickyEnd: stickyEndY + paddingEndY,
477
+ });
478
+ targetY = res.target + itemsStartVirtualY;
479
+ itemHeight = res.itemSize;
480
+ effectiveAlignY = res.effectiveAlign;
141
481
  }
142
482
 
143
483
  // X calculation
144
484
  if (colIndex != null) {
145
- let stickyOffsetX = 0;
146
- if (isHorizontal && stickyIndices && stickyIndices.length > 0 && (direction === 'horizontal' || direction === 'both')) {
147
- let activeStickyIdx: number | undefined;
148
- let low = 0;
149
- let high = stickyIndices.length - 1;
150
- while (low <= high) {
151
- const mid = (low + high) >>> 1;
152
- if (stickyIndices[ mid ]! < colIndex) {
153
- activeStickyIdx = stickyIndices[ mid ];
154
- low = mid + 1;
155
- } else {
156
- high = mid - 1;
157
- }
158
- }
159
-
160
- if (activeStickyIdx !== undefined) {
161
- stickyOffsetX = direction === 'horizontal'
162
- ? (fixedSize !== null ? fixedSize : getItemSizeX(activeStickyIdx) - columnGap)
163
- : (fixedWidth !== null ? fixedWidth : getColumnSize(activeStickyIdx) - columnGap);
164
- }
165
- }
166
-
167
- let itemX = 0;
168
- if (colIndex >= columnCount && columnCount > 0) {
169
- itemX = totalWidth;
170
- itemWidth = 0;
171
- } else if (direction === 'horizontal') {
172
- itemX = fixedSize !== null ? colIndex * (fixedSize + columnGap) : getItemQueryX(colIndex);
173
- itemWidth = fixedSize !== null ? fixedSize : getItemSizeX(colIndex) - columnGap;
174
- } else {
175
- itemX = getColumnQuery(colIndex);
176
- itemWidth = getColumnSize(colIndex) - columnGap;
177
- }
178
-
179
- // Apply X Alignment
180
- if (alignX === 'start') {
181
- targetX = itemX - stickyOffsetX;
182
- } else if (alignX === 'center') {
183
- targetX = itemX - (usableWidth - itemWidth) / 2;
184
- } else if (alignX === 'end') {
185
- targetX = itemX - (usableWidth - itemWidth);
186
- } else {
187
- // Auto alignment: stay if visible, otherwise align to nearest edge (minimal movement)
188
- const isVisibleX = itemWidth <= (usableWidth - stickyOffsetX)
189
- ? (itemX >= relativeScrollX + stickyOffsetX - 0.5 && (itemX + itemWidth) <= (relativeScrollX + usableWidth + 0.5))
190
- : (itemX <= relativeScrollX + stickyOffsetX + 0.5 && (itemX + itemWidth) >= (relativeScrollX + usableWidth - 0.5));
191
-
192
- if (!isVisibleX) {
193
- const targetStart = itemX - stickyOffsetX;
194
- const targetEnd = itemX - (usableWidth - itemWidth);
195
-
196
- if (itemWidth <= usableWidth - stickyOffsetX) {
197
- if (itemX < relativeScrollX + stickyOffsetX) {
198
- targetX = targetStart;
199
- effectiveAlignX = 'start';
200
- } else {
201
- targetX = targetEnd;
202
- effectiveAlignX = 'end';
203
- }
204
- } else {
205
- // Large item: minimal movement
206
- if (Math.abs(targetStart - relativeScrollX) < Math.abs(targetEnd - relativeScrollX)) {
207
- targetX = targetStart;
208
- effectiveAlignX = 'start';
209
- } else {
210
- targetX = targetEnd;
211
- effectiveAlignX = 'end';
212
- }
213
- }
214
- }
215
- }
485
+ const isGrid = direction === 'both';
486
+ const isHorizontal = direction === 'horizontal';
487
+ const res = calculateAxisTarget({
488
+ index: colIndex,
489
+ align: alignX as ScrollAlignment,
490
+ viewSize: viewportWidth,
491
+ scrollPos: relativeScrollX,
492
+ fixedSize: isGrid ? fixedWidth : fixedSize,
493
+ gap: (isGrid || isHorizontal) ? columnGap : gap,
494
+ query: isGrid ? getColumnQuery : getItemQueryX,
495
+ getSize: isGrid ? getColumnSize : getItemSizeX,
496
+ stickyIndices,
497
+ stickyStart: stickyStartX + paddingStartX,
498
+ stickyEnd: stickyEndX + paddingEndX,
499
+ });
500
+ targetX = res.target + itemsStartVirtualX;
501
+ itemWidth = res.itemSize;
502
+ effectiveAlignX = res.effectiveAlign;
216
503
  }
217
504
 
218
- // Clamp to valid range
219
- targetX = Math.max(0, Math.min(targetX, Math.max(0, totalWidth - usableWidth)));
220
- targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight - usableHeight)));
505
+ targetX = Math.max(0, Math.min(targetX, maxTargetX));
506
+ targetY = Math.max(0, Math.min(targetY, maxTargetY));
221
507
 
222
508
  return { targetX, targetY, itemWidth, itemHeight, effectiveAlignX, effectiveAlignY };
223
509
  }
@@ -225,153 +511,160 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
225
511
  /**
226
512
  * Calculates the range of items to render based on scroll position and viewport size.
227
513
  *
228
- * @param params - The parameters for calculation.
514
+ * @param params - Range parameters.
515
+ * @param params.direction - Scroll direction.
516
+ * @param params.relativeScrollX - Virtual horizontal position (VU).
517
+ * @param params.relativeScrollY - Virtual vertical position (VU).
518
+ * @param params.usableWidth - Usable viewport width (VU).
519
+ * @param params.usableHeight - Usable viewport height (VU).
520
+ * @param params.itemsLength - Total item count.
521
+ * @param params.bufferBefore - Buffer items before.
522
+ * @param params.bufferAfter - Buffer items after.
523
+ * @param params.gap - Item gap (VU).
524
+ * @param params.columnGap - Column gap (VU).
525
+ * @param params.fixedSize - Fixed item size (VU).
526
+ * @param params.findLowerBoundY - Resolver for vertical index.
527
+ * @param params.findLowerBoundX - Resolver for horizontal index.
528
+ * @param params.queryY - Resolver for vertical offset (VU).
529
+ * @param params.queryX - Resolver for horizontal offset (VU).
229
530
  * @returns The start and end indices of the items to render.
230
531
  * @see RangeParams
231
532
  */
232
- export function calculateRange(params: RangeParams) {
233
- const {
234
- direction,
235
- relativeScrollX,
236
- relativeScrollY,
237
- usableWidth,
238
- usableHeight,
239
- itemsLength,
533
+ export function calculateRange({
534
+ direction,
535
+ relativeScrollX,
536
+ relativeScrollY,
537
+ usableWidth,
538
+ usableHeight,
539
+ itemsLength,
540
+ bufferBefore,
541
+ bufferAfter,
542
+ gap,
543
+ columnGap,
544
+ fixedSize,
545
+ findLowerBoundY,
546
+ findLowerBoundX,
547
+ queryY,
548
+ queryX,
549
+ }: RangeParams) {
550
+ const isVertical = direction === 'vertical' || direction === 'both';
551
+
552
+ return calculateGenericRange({
553
+ scrollPos: isVertical ? relativeScrollY : relativeScrollX,
554
+ containerSize: isVertical ? usableHeight : usableWidth,
555
+ count: itemsLength,
240
556
  bufferBefore,
241
557
  bufferAfter,
242
- gap,
243
- columnGap,
558
+ gap: isVertical ? gap : columnGap,
244
559
  fixedSize,
245
- findLowerBoundY,
246
- findLowerBoundX,
247
- queryY,
248
- queryX,
249
- } = params;
250
-
251
- const isVertical = direction === 'vertical' || direction === 'both';
252
-
253
- let start = 0;
254
- let end = itemsLength;
255
-
256
- if (isVertical) {
257
- if (fixedSize !== null) {
258
- start = Math.floor(relativeScrollY / (fixedSize + gap));
259
- end = Math.ceil((relativeScrollY + usableHeight) / (fixedSize + gap));
260
- } else {
261
- start = findLowerBoundY(relativeScrollY);
262
- const targetY = relativeScrollY + usableHeight;
263
- end = findLowerBoundY(targetY);
264
- if (end < itemsLength && queryY(end) < targetY) {
265
- end++;
266
- }
267
- }
268
- } else {
269
- if (fixedSize !== null) {
270
- start = Math.floor(relativeScrollX / (fixedSize + columnGap));
271
- end = Math.ceil((relativeScrollX + usableWidth) / (fixedSize + columnGap));
272
- } else {
273
- start = findLowerBoundX(relativeScrollX);
274
- const targetX = relativeScrollX + usableWidth;
275
- end = findLowerBoundX(targetX);
276
- if (end < itemsLength && queryX(end) < targetX) {
277
- end++;
278
- }
279
- }
280
- }
281
-
282
- return {
283
- start: Math.max(0, start - bufferBefore),
284
- end: Math.min(itemsLength, end + bufferAfter),
285
- };
560
+ findLowerBound: isVertical ? findLowerBoundY : findLowerBoundX,
561
+ query: isVertical ? queryY : queryX,
562
+ });
286
563
  }
287
564
 
288
565
  /**
289
566
  * Calculates the range of columns to render for bidirectional scroll.
290
567
  *
291
- * @param params - The parameters for calculation.
292
- * @returns The start and end indices and paddings for columns.
568
+ * @param params - Column range parameters.
569
+ * @param params.columnCount - Total column count.
570
+ * @param params.relativeScrollX - Virtual horizontal position (VU).
571
+ * @param params.usableWidth - Usable viewport width (VU).
572
+ * @param params.colBuffer - Column buffer size.
573
+ * @param params.fixedWidth - Fixed column width (VU).
574
+ * @param params.columnGap - Column gap (VU).
575
+ * @param params.findLowerBound - Resolver for column index.
576
+ * @param params.query - Resolver for column offset (VU).
577
+ * @param params.totalColsQuery - Resolver for total width (VU).
578
+ * @returns The start and end indices and paddings for columns (VU).
293
579
  * @see ColumnRangeParams
294
580
  * @see ColumnRange
295
581
  */
296
- export function calculateColumnRange(params: ColumnRangeParams) {
297
- const {
298
- columnCount,
299
- relativeScrollX,
300
- usableWidth,
301
- colBuffer,
302
- fixedWidth,
303
- columnGap,
304
- findLowerBound,
305
- query,
306
- totalColsQuery,
307
- } = params;
308
-
582
+ export function calculateColumnRange({
583
+ columnCount,
584
+ relativeScrollX,
585
+ usableWidth,
586
+ colBuffer,
587
+ fixedWidth,
588
+ columnGap,
589
+ findLowerBound,
590
+ query,
591
+ totalColsQuery,
592
+ }: ColumnRangeParams) {
309
593
  if (!columnCount) {
310
594
  return { start: 0, end: 0, padStart: 0, padEnd: 0 };
311
595
  }
312
596
 
313
- let start = 0;
314
- let end = columnCount;
315
-
316
- if (fixedWidth !== null) {
317
- start = Math.floor(relativeScrollX / (fixedWidth + columnGap));
318
- end = Math.ceil((relativeScrollX + usableWidth) / (fixedWidth + columnGap));
319
- } else {
320
- start = findLowerBound(relativeScrollX);
321
- let currentX = query(start);
322
- let i = start;
323
- while (i < columnCount && currentX < relativeScrollX + usableWidth) {
324
- currentX = query(++i);
325
- }
326
- end = i;
327
- }
597
+ const { start, end } = calculateGenericRange({
598
+ scrollPos: relativeScrollX,
599
+ containerSize: usableWidth,
600
+ count: columnCount,
601
+ bufferBefore: colBuffer,
602
+ bufferAfter: colBuffer,
603
+ gap: columnGap,
604
+ fixedSize: fixedWidth,
605
+ findLowerBound,
606
+ query,
607
+ });
328
608
 
329
- // Add buffer of columns
330
- const safeStart = Math.max(0, start - colBuffer);
331
- const safeEnd = Math.min(columnCount, end + colBuffer);
609
+ const safeStart = start;
610
+ const safeEnd = end;
332
611
 
333
612
  const padStart = fixedWidth !== null ? safeStart * (fixedWidth + columnGap) : query(safeStart);
334
613
  const totalWidth = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, totalColsQuery() - columnGap);
335
614
 
336
- const renderedEnd = fixedWidth !== null
337
- ? (safeEnd * (fixedWidth + columnGap) - (safeEnd >= columnCount ? columnGap : 0))
338
- : (query(safeEnd) - (safeEnd >= columnCount ? columnGap : 0));
615
+ const contentEnd = fixedWidth !== null
616
+ ? (safeEnd * (fixedWidth + columnGap) - (safeEnd > 0 ? columnGap : 0))
617
+ : (query(safeEnd) - (safeEnd > 0 ? columnGap : 0));
339
618
 
340
619
  return {
341
620
  start: safeStart,
342
621
  end: safeEnd,
343
622
  padStart,
344
- padEnd: Math.max(0, totalWidth - renderedEnd),
623
+ padEnd: Math.max(0, totalWidth - contentEnd),
345
624
  };
346
625
  }
347
626
 
348
627
  /**
349
628
  * Calculates the sticky state and offset for a single item.
350
629
  *
351
- * @param params - The parameters for calculation.
352
- * @returns Sticky state and offset.
630
+ * @param params - Sticky item parameters.
631
+ * @param params.index - Item index.
632
+ * @param params.isSticky - If configured as sticky.
633
+ * @param params.direction - Scroll direction.
634
+ * @param params.relativeScrollX - Virtual horizontal position (VU).
635
+ * @param params.relativeScrollY - Virtual vertical position (VU).
636
+ * @param params.originalX - Virtual original X position (VU).
637
+ * @param params.originalY - Virtual original Y position (VU).
638
+ * @param params.width - Virtual item width (VU).
639
+ * @param params.height - Virtual item height (VU).
640
+ * @param params.stickyIndices - All sticky indices.
641
+ * @param params.fixedSize - Fixed item size (VU).
642
+ * @param params.fixedWidth - Fixed column width (VU).
643
+ * @param params.gap - Item gap (VU).
644
+ * @param params.columnGap - Column gap (VU).
645
+ * @param params.getItemQueryY - Resolver for vertical offset (VU).
646
+ * @param params.getItemQueryX - Resolver for horizontal offset (VU).
647
+ * @returns Sticky state and offset (VU).
353
648
  * @see StickyParams
354
649
  */
355
- export function calculateStickyItem(params: StickyParams) {
356
- const {
357
- index,
358
- isSticky,
359
- direction,
360
- relativeScrollX,
361
- relativeScrollY,
362
- originalX,
363
- originalY,
364
- width,
365
- height,
366
- stickyIndices,
367
- fixedSize,
368
- fixedWidth,
369
- gap,
370
- columnGap,
371
- getItemQueryY,
372
- getItemQueryX,
373
- } = params;
374
-
650
+ export function calculateStickyItem({
651
+ index,
652
+ isSticky,
653
+ direction,
654
+ relativeScrollX,
655
+ relativeScrollY,
656
+ originalX,
657
+ originalY,
658
+ width,
659
+ height,
660
+ stickyIndices,
661
+ fixedSize,
662
+ fixedWidth,
663
+ gap,
664
+ columnGap,
665
+ getItemQueryY,
666
+ getItemQueryX,
667
+ }: StickyParams) {
375
668
  let isStickyActive = false;
376
669
  const stickyOffset = { x: 0, y: 0 };
377
670
 
@@ -379,21 +672,10 @@ export function calculateStickyItem(params: StickyParams) {
379
672
  return { isStickyActive, stickyOffset };
380
673
  }
381
674
 
675
+ // Y Axis (Sticky Rows)
382
676
  if (direction === 'vertical' || direction === 'both') {
383
677
  if (relativeScrollY > originalY) {
384
- // Check if next sticky item pushes this one
385
- let nextStickyIdx: number | undefined;
386
- let low = 0;
387
- let high = stickyIndices.length - 1;
388
- while (low <= high) {
389
- const mid = (low + high) >>> 1;
390
- if (stickyIndices[ mid ]! > index) {
391
- nextStickyIdx = stickyIndices[ mid ];
392
- high = mid - 1;
393
- } else {
394
- low = mid + 1;
395
- }
396
- }
678
+ const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
397
679
 
398
680
  if (nextStickyIdx !== undefined) {
399
681
  const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : getItemQueryY(nextStickyIdx);
@@ -409,20 +691,10 @@ export function calculateStickyItem(params: StickyParams) {
409
691
  }
410
692
  }
411
693
 
694
+ // X Axis (Sticky Columns / Items)
412
695
  if (direction === 'horizontal' || (direction === 'both' && !isStickyActive)) {
413
696
  if (relativeScrollX > originalX) {
414
- let nextStickyIdx: number | undefined;
415
- let low = 0;
416
- let high = stickyIndices.length - 1;
417
- while (low <= high) {
418
- const mid = (low + high) >>> 1;
419
- if (stickyIndices[ mid ]! > index) {
420
- nextStickyIdx = stickyIndices[ mid ];
421
- high = mid - 1;
422
- } else {
423
- low = mid + 1;
424
- }
425
- }
697
+ const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
426
698
 
427
699
  if (nextStickyIdx !== undefined) {
428
700
  const nextStickyX = direction === 'horizontal'
@@ -447,26 +719,38 @@ export function calculateStickyItem(params: StickyParams) {
447
719
  /**
448
720
  * Calculates the position and size of a single item.
449
721
  *
450
- * @param params - The parameters for calculation.
451
- * @returns Item position and size.
722
+ * @param params - Item position parameters.
723
+ * @param params.index - Item index.
724
+ * @param params.direction - Scroll direction.
725
+ * @param params.fixedSize - Fixed item size (VU).
726
+ * @param params.gap - Item gap (VU).
727
+ * @param params.columnGap - Column gap (VU).
728
+ * @param params.usableWidth - Usable viewport width (VU).
729
+ * @param params.usableHeight - Usable viewport height (VU).
730
+ * @param params.totalWidth - Total estimated width (VU).
731
+ * @param params.queryY - Resolver for vertical offset (VU).
732
+ * @param params.queryX - Resolver for horizontal offset (VU).
733
+ * @param params.getSizeY - Resolver for height (VU).
734
+ * @param params.getSizeX - Resolver for width (VU).
735
+ * @param params.columnRange - Current column range (for grid mode).
736
+ * @returns Item position and size (VU).
452
737
  * @see ItemPositionParams
453
738
  */
454
- export function calculateItemPosition(params: ItemPositionParams) {
455
- const {
456
- index,
457
- direction,
458
- fixedSize,
459
- gap,
460
- columnGap,
461
- usableWidth,
462
- usableHeight,
463
- totalWidth,
464
- queryY,
465
- queryX,
466
- getSizeY,
467
- getSizeX,
468
- } = params;
469
-
739
+ export function calculateItemPosition({
740
+ index,
741
+ direction,
742
+ fixedSize,
743
+ gap,
744
+ columnGap,
745
+ usableWidth,
746
+ usableHeight,
747
+ totalWidth,
748
+ queryY,
749
+ queryX,
750
+ getSizeY,
751
+ getSizeX,
752
+ columnRange,
753
+ }: ItemPositionParams) {
470
754
  let x = 0;
471
755
  let y = 0;
472
756
  let width = 0;
@@ -476,9 +760,13 @@ export function calculateItemPosition(params: ItemPositionParams) {
476
760
  x = fixedSize !== null ? index * (fixedSize + columnGap) : queryX(index);
477
761
  width = fixedSize !== null ? fixedSize : getSizeX(index) - columnGap;
478
762
  height = usableHeight;
763
+ } else if (direction === 'both' && columnRange) {
764
+ y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
765
+ height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
766
+ x = columnRange.padStart;
767
+ width = Math.max(0, totalWidth - columnRange.padStart - columnRange.padEnd);
479
768
  } else {
480
- // vertical or both
481
- y = (direction === 'vertical' || direction === 'both') && fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
769
+ y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
482
770
  height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
483
771
  width = direction === 'both' ? totalWidth : usableWidth;
484
772
  }
@@ -489,21 +777,28 @@ export function calculateItemPosition(params: ItemPositionParams) {
489
777
  /**
490
778
  * Calculates the style object for a rendered item.
491
779
  *
492
- * @param params - The parameters for calculation.
780
+ * @param params - Item style parameters.
781
+ * @param params.item - Rendered item state.
782
+ * @param params.direction - Scroll direction.
783
+ * @param params.itemSize - Virtual item size (VU).
784
+ * @param params.containerTag - Container HTML tag.
785
+ * @param params.paddingStartX - Horizontal virtual padding (DU).
786
+ * @param params.paddingStartY - Vertical virtual padding (DU).
787
+ * @param params.isHydrated - If mounted and hydrated.
788
+ * @param params.isRtl - If in RTL mode.
493
789
  * @returns Style object.
494
790
  * @see ItemStyleParams
495
791
  */
496
- export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
497
- const {
498
- item,
499
- direction,
500
- itemSize,
501
- containerTag,
502
- paddingStartX,
503
- paddingStartY,
504
- isHydrated,
505
- } = params;
506
-
792
+ export function calculateItemStyle<T = unknown>({
793
+ item,
794
+ direction,
795
+ itemSize,
796
+ containerTag,
797
+ paddingStartX,
798
+ paddingStartY,
799
+ isHydrated,
800
+ isRtl,
801
+ }: ItemStyleParams<T>) {
507
802
  const isVertical = direction === 'vertical';
508
803
  const isHorizontal = direction === 'horizontal';
509
804
  const isBoth = direction === 'both';
@@ -529,18 +824,20 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
529
824
  }
530
825
 
531
826
  if (isHydrated) {
827
+ const tx = isRtl
828
+ ? -(item.isStickyActive ? item.stickyOffset.x : item.offset.x)
829
+ : (item.isStickyActive ? item.stickyOffset.x : item.offset.x);
830
+
532
831
  if (item.isStickyActive) {
533
832
  if (isVertical || isBoth) {
534
833
  style.insetBlockStart = `${ paddingStartY }px`;
535
834
  }
536
-
537
835
  if (isHorizontal || isBoth) {
538
836
  style.insetInlineStart = `${ paddingStartX }px`;
539
837
  }
540
-
541
- style.transform = `translate(${ item.stickyOffset.x }px, ${ item.stickyOffset.y }px)`;
838
+ style.transform = `translate(${ tx }px, ${ item.stickyOffset.y }px)`;
542
839
  } else {
543
- style.transform = `translate(${ item.offset.x }px, ${ item.offset.y }px)`;
840
+ style.transform = `translate(${ tx }px, ${ item.offset.y }px)`;
544
841
  }
545
842
  }
546
843
 
@@ -550,56 +847,55 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
550
847
  /**
551
848
  * Calculates the total width and height of the virtualized content.
552
849
  *
553
- * @param params - The parameters for calculation.
554
- * @returns Total width and height.
850
+ * @param params - Total size parameters.
851
+ * @param params.direction - Scroll direction.
852
+ * @param params.itemsLength - Total item count.
853
+ * @param params.columnCount - Column count.
854
+ * @param params.fixedSize - Fixed item size (VU).
855
+ * @param params.fixedWidth - Fixed column width (VU).
856
+ * @param params.gap - Item gap (VU).
857
+ * @param params.columnGap - Column gap (VU).
858
+ * @param params.usableWidth - Usable viewport width (VU).
859
+ * @param params.usableHeight - Usable viewport height (VU).
860
+ * @param params.queryY - Resolver for vertical offset (VU).
861
+ * @param params.queryX - Resolver for horizontal offset (VU).
862
+ * @param params.queryColumn - Resolver for column offset (VU).
863
+ * @returns Total width and height (VU).
555
864
  * @see TotalSizeParams
556
865
  */
557
- export function calculateTotalSize(params: TotalSizeParams) {
558
- const {
559
- direction,
560
- itemsLength,
561
- columnCount,
562
- fixedSize,
563
- fixedWidth,
564
- gap,
565
- columnGap,
566
- usableWidth,
567
- usableHeight,
568
- queryY,
569
- queryX,
570
- queryColumn,
571
- } = params;
866
+ export function calculateTotalSize({
867
+ direction,
868
+ itemsLength,
869
+ columnCount,
870
+ fixedSize,
871
+ fixedWidth,
872
+ gap,
873
+ columnGap,
874
+ usableWidth,
875
+ usableHeight,
876
+ queryY,
877
+ queryX,
878
+ queryColumn,
879
+ }: TotalSizeParams) {
880
+ const isBoth = direction === 'both';
881
+ const isHorizontal = direction === 'horizontal';
572
882
 
573
883
  let width = 0;
574
884
  let height = 0;
575
885
 
576
- if (direction === 'both') {
577
- if (columnCount > 0) {
578
- width = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, queryColumn(columnCount) - columnGap);
579
- }
580
- if (fixedSize !== null) {
581
- height = Math.max(0, itemsLength * (fixedSize + gap) - (itemsLength > 0 ? gap : 0));
582
- } else {
583
- height = Math.max(0, queryY(itemsLength) - (itemsLength > 0 ? gap : 0));
584
- }
585
- width = Math.max(width, usableWidth);
586
- height = Math.max(height, usableHeight);
587
- } else if (direction === 'horizontal') {
588
- if (fixedSize !== null) {
589
- width = Math.max(0, itemsLength * (fixedSize + columnGap) - (itemsLength > 0 ? columnGap : 0));
590
- } else {
591
- width = Math.max(0, queryX(itemsLength) - (itemsLength > 0 ? columnGap : 0));
592
- }
886
+ if (isBoth) {
887
+ width = calculateAxisSize(columnCount, fixedWidth, columnGap, queryColumn);
888
+ height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
889
+ } else if (isHorizontal) {
890
+ width = calculateAxisSize(itemsLength, fixedSize, columnGap, queryX);
593
891
  height = usableHeight;
594
892
  } else {
595
- // vertical
596
893
  width = usableWidth;
597
- if (fixedSize !== null) {
598
- height = Math.max(0, itemsLength * (fixedSize + gap) - (itemsLength > 0 ? gap : 0));
599
- } else {
600
- height = Math.max(0, queryY(itemsLength) - (itemsLength > 0 ? gap : 0));
601
- }
894
+ height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
602
895
  }
603
896
 
604
- return { width, height };
897
+ return {
898
+ width: isBoth ? Math.max(width, usableWidth) : width,
899
+ height: isBoth ? Math.max(height, usableHeight) : height,
900
+ };
605
901
  }