@pdanpdan/virtual-scroll 0.3.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.
@@ -0,0 +1,2850 @@
1
+ import type { RenderedItem } from '../types';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import {
6
+ calculateColumnRange,
7
+ calculateItemPosition,
8
+ calculateItemStyle,
9
+ calculateRange,
10
+ calculateScrollTarget,
11
+ calculateStickyItem,
12
+ calculateTotalSize,
13
+ } from './virtual-scroll-logic';
14
+
15
+ describe('virtual-scroll-logic', () => {
16
+ describe('calculatetotalsize', () => {
17
+ it('calculates vertical total size with fixed size', () => {
18
+ const result = calculateTotalSize({
19
+ columnCount: 0,
20
+ columnGap: 0,
21
+ direction: 'vertical',
22
+ fixedSize: 50,
23
+ fixedWidth: null,
24
+ gap: 10,
25
+ itemsLength: 100,
26
+ queryColumn: () => 0,
27
+ queryX: () => 0,
28
+ queryY: () => 0,
29
+ usableHeight: 500,
30
+ usableWidth: 500,
31
+ });
32
+ // 50 * 100 + 10 * 99
33
+ expect(result.height).toBe(5990);
34
+ expect(result.width).toBe(500);
35
+ });
36
+
37
+ it('calculates vertical total size with dynamic sizes', () => {
38
+ const result = calculateTotalSize({
39
+ columnCount: 0,
40
+ columnGap: 0,
41
+ direction: 'vertical',
42
+ fixedSize: null,
43
+ fixedWidth: null,
44
+ gap: 5,
45
+ itemsLength: 10,
46
+ queryColumn: () => 0,
47
+ queryX: () => 0,
48
+ queryY: (idx) => idx * 45,
49
+ usableHeight: 500,
50
+ usableWidth: 500,
51
+ });
52
+ // 45 * 10 - 5 (gap after last item)
53
+ expect(result.height).toBe(445);
54
+ expect(result.width).toBe(500);
55
+ });
56
+
57
+ it('calculates horizontal total size with fixed size', () => {
58
+ const result = calculateTotalSize({
59
+ columnCount: 0,
60
+ columnGap: 10,
61
+ direction: 'horizontal',
62
+ fixedSize: 50,
63
+ fixedWidth: null,
64
+ gap: 0,
65
+ itemsLength: 100,
66
+ queryColumn: () => 0,
67
+ queryX: () => 0,
68
+ queryY: () => 0,
69
+ usableHeight: 500,
70
+ usableWidth: 500,
71
+ });
72
+ expect(result.width).toBe(5990);
73
+ expect(result.height).toBe(500);
74
+ });
75
+
76
+ it('calculates horizontal total size with dynamic sizes', () => {
77
+ const result = calculateTotalSize({
78
+ columnCount: 0,
79
+ columnGap: 10,
80
+ direction: 'horizontal',
81
+ fixedSize: null,
82
+ fixedWidth: null,
83
+ gap: 0,
84
+ itemsLength: 100,
85
+ queryColumn: () => 0,
86
+ queryX: (idx) => idx * 60,
87
+ queryY: () => 0,
88
+ usableHeight: 500,
89
+ usableWidth: 500,
90
+ });
91
+ expect(result.width).toBe(5990);
92
+ expect(result.height).toBe(500);
93
+ });
94
+
95
+ it('calculates grid (both) total size with fixed sizes', () => {
96
+ const result = calculateTotalSize({
97
+ columnCount: 5,
98
+ columnGap: 5,
99
+ direction: 'both',
100
+ fixedSize: 50,
101
+ fixedWidth: 100,
102
+ gap: 10,
103
+ itemsLength: 100,
104
+ queryColumn: () => 0,
105
+ queryX: () => 0,
106
+ queryY: () => 0,
107
+ usableHeight: 500,
108
+ usableWidth: 500,
109
+ });
110
+ expect(result.height).toBe(5990);
111
+ expect(result.width).toBe(520);
112
+ });
113
+
114
+ it('calculates grid (both) total size with fixed row size and dynamic column width', () => {
115
+ const result = calculateTotalSize({
116
+ columnCount: 5,
117
+ columnGap: 5,
118
+ direction: 'both',
119
+ fixedSize: 50,
120
+ fixedWidth: null,
121
+ gap: 10,
122
+ itemsLength: 100,
123
+ queryColumn: (idx) => idx * 105,
124
+ queryX: () => 0,
125
+ queryY: () => 0,
126
+ usableHeight: 500,
127
+ usableWidth: 500,
128
+ });
129
+ expect(result.height).toBe(5990);
130
+ expect(result.width).toBe(520);
131
+ });
132
+
133
+ it('calculates grid (both) total size with dynamic sizes', () => {
134
+ const result = calculateTotalSize({
135
+ columnCount: 5,
136
+ columnGap: 5,
137
+ direction: 'both',
138
+ fixedSize: null,
139
+ fixedWidth: null,
140
+ gap: 10,
141
+ itemsLength: 100,
142
+ queryColumn: (idx) => idx * 105,
143
+ queryX: () => 0,
144
+ queryY: (idx) => idx * 60,
145
+ usableHeight: 500,
146
+ usableWidth: 500,
147
+ });
148
+ expect(result.height).toBe(5990);
149
+ expect(result.width).toBe(520);
150
+ });
151
+
152
+ it('calculates total sizes for single item (both, fixed rows, fixed cols)', () => {
153
+ const result = calculateTotalSize({
154
+ columnCount: 1,
155
+ columnGap: 10,
156
+ direction: 'both',
157
+ fixedSize: 50,
158
+ fixedWidth: 100,
159
+ gap: 10,
160
+ itemsLength: 1,
161
+ queryColumn: () => 0,
162
+ queryX: () => 0,
163
+ queryY: () => 0,
164
+ usableHeight: 500,
165
+ usableWidth: 500,
166
+ });
167
+ expect(result.height).toBe(500);
168
+ expect(result.width).toBe(500);
169
+ });
170
+
171
+ it('calculates total width for single item (horizontal, fixed size)', () => {
172
+ const result = calculateTotalSize({
173
+ columnCount: 0,
174
+ columnGap: 10,
175
+ direction: 'horizontal',
176
+ fixedSize: 50,
177
+ fixedWidth: null,
178
+ gap: 0,
179
+ itemsLength: 1,
180
+ queryColumn: () => 0,
181
+ queryX: () => 0,
182
+ queryY: () => 0,
183
+ usableHeight: 500,
184
+ usableWidth: 500,
185
+ });
186
+ expect(result.width).toBe(50);
187
+ });
188
+
189
+ it('calculates total width for single item (horizontal, dynamic size)', () => {
190
+ const result = calculateTotalSize({
191
+ columnCount: 0,
192
+ columnGap: 10,
193
+ direction: 'horizontal',
194
+ fixedSize: null,
195
+ fixedWidth: null,
196
+ gap: 0,
197
+ itemsLength: 1,
198
+ queryColumn: () => 0,
199
+ queryX: (idx) => idx * 60,
200
+ queryY: () => 0,
201
+ usableHeight: 500,
202
+ usableWidth: 500,
203
+ });
204
+ expect(result.width).toBe(50);
205
+ });
206
+
207
+ it('calculates total height for single item (vertical, dynamic size)', () => {
208
+ const result = calculateTotalSize({
209
+ columnCount: 0,
210
+ columnGap: 0,
211
+ direction: 'vertical',
212
+ fixedSize: null,
213
+ fixedWidth: null,
214
+ gap: 10,
215
+ itemsLength: 1,
216
+ queryColumn: () => 0,
217
+ queryX: () => 0,
218
+ queryY: (idx) => idx * 60,
219
+ usableHeight: 500,
220
+ usableWidth: 500,
221
+ });
222
+ expect(result.height).toBe(50);
223
+ });
224
+
225
+ it('calculates total height for single small item (vertical, dynamic size, itemslength 1)', () => {
226
+ const result = calculateTotalSize({
227
+ columnCount: 0,
228
+ columnGap: 0,
229
+ direction: 'vertical',
230
+ fixedSize: null,
231
+ fixedWidth: null,
232
+ gap: 10,
233
+ itemsLength: 1,
234
+ queryColumn: () => 0,
235
+ queryX: () => 0,
236
+ queryY: (idx) => (idx === 0 ? 0 : 5),
237
+ usableHeight: 500,
238
+ usableWidth: 500,
239
+ });
240
+ expect(result.height).toBe(0);
241
+ });
242
+
243
+ it('calculates total width for single small item (horizontal, dynamic size, itemslength 1)', () => {
244
+ const result = calculateTotalSize({
245
+ columnCount: 0,
246
+ columnGap: 10,
247
+ direction: 'horizontal',
248
+ fixedSize: null,
249
+ fixedWidth: null,
250
+ gap: 0,
251
+ itemsLength: 1,
252
+ queryColumn: () => 0,
253
+ queryX: (idx) => (idx === 0 ? 0 : 5),
254
+ queryY: () => 0,
255
+ usableHeight: 500,
256
+ usableWidth: 500,
257
+ });
258
+ expect(result.width).toBe(0);
259
+ });
260
+
261
+ it('calculates total height for single item (both, dynamic size, queryy)', () => {
262
+ const result = calculateTotalSize({
263
+ columnCount: 1,
264
+ columnGap: 10,
265
+ direction: 'both',
266
+ fixedSize: null,
267
+ fixedWidth: null,
268
+ gap: 10,
269
+ itemsLength: 1,
270
+ queryColumn: (idx) => (idx === 0 ? 0 : 110),
271
+ queryX: () => 0,
272
+ queryY: (idx) => (idx === 0 ? 0 : 60),
273
+ usableHeight: 500,
274
+ usableWidth: 500,
275
+ });
276
+ expect(result.height).toBe(500);
277
+ expect(result.width).toBe(500);
278
+ });
279
+
280
+ it('calculates total height for single item (both, fixed rows, dynamic cols)', () => {
281
+ const result = calculateTotalSize({
282
+ columnCount: 1,
283
+ columnGap: 10,
284
+ direction: 'both',
285
+ fixedSize: 50,
286
+ fixedWidth: null,
287
+ gap: 10,
288
+ itemsLength: 1,
289
+ queryColumn: (idx) => (idx === 0 ? 0 : 110),
290
+ queryX: () => 0,
291
+ queryY: () => 0,
292
+ usableHeight: 500,
293
+ usableWidth: 500,
294
+ });
295
+ expect(result.height).toBe(500);
296
+ expect(result.width).toBe(500);
297
+ });
298
+
299
+ it('returns viewport size for empty items with fixed sizes (both)', () => {
300
+ const result = calculateTotalSize({
301
+ columnCount: 0,
302
+ columnGap: 10,
303
+ direction: 'both',
304
+ fixedSize: 50,
305
+ fixedWidth: 100,
306
+ gap: 10,
307
+ itemsLength: 0,
308
+ queryColumn: () => 0,
309
+ queryX: () => 0,
310
+ queryY: () => 0,
311
+ usableHeight: 500,
312
+ usableWidth: 500,
313
+ });
314
+ expect(result.height).toBe(500);
315
+ expect(result.width).toBe(500);
316
+ });
317
+
318
+ it('returns viewport size for empty items with dynamic sizes (both)', () => {
319
+ const result = calculateTotalSize({
320
+ columnCount: 0,
321
+ columnGap: 10,
322
+ direction: 'both',
323
+ fixedSize: null,
324
+ fixedWidth: null,
325
+ gap: 10,
326
+ itemsLength: 0,
327
+ queryColumn: () => 0,
328
+ queryX: () => 0,
329
+ queryY: () => 0,
330
+ usableHeight: 500,
331
+ usableWidth: 500,
332
+ });
333
+ expect(result.height).toBe(500);
334
+ expect(result.width).toBe(500);
335
+ });
336
+
337
+ it('returns 0 for empty items with fixed sizes (horizontal)', () => {
338
+ const result = calculateTotalSize({
339
+ columnCount: 0,
340
+ columnGap: 10,
341
+ direction: 'horizontal',
342
+ fixedSize: 50,
343
+ fixedWidth: null,
344
+ gap: 0,
345
+ itemsLength: 0,
346
+ queryColumn: () => 0,
347
+ queryX: () => 0,
348
+ queryY: () => 0,
349
+ usableHeight: 500,
350
+ usableWidth: 500,
351
+ });
352
+ expect(result.width).toBe(0);
353
+ });
354
+
355
+ it('returns 0 for empty items with fixed sizes (vertical)', () => {
356
+ const result = calculateTotalSize({
357
+ columnCount: 0,
358
+ columnGap: 0,
359
+ direction: 'vertical',
360
+ fixedSize: 50,
361
+ fixedWidth: null,
362
+ gap: 10,
363
+ itemsLength: 0,
364
+ queryColumn: () => 0,
365
+ queryX: () => 0,
366
+ queryY: () => 0,
367
+ usableHeight: 500,
368
+ usableWidth: 500,
369
+ });
370
+ expect(result.height).toBe(0);
371
+ });
372
+
373
+ it('returns 0 for empty items with dynamic sizes (horizontal)', () => {
374
+ const result = calculateTotalSize({
375
+ columnCount: 0,
376
+ columnGap: 10,
377
+ direction: 'horizontal',
378
+ fixedSize: null,
379
+ fixedWidth: null,
380
+ gap: 0,
381
+ itemsLength: 0,
382
+ queryColumn: () => 0,
383
+ queryX: () => 0,
384
+ queryY: () => 0,
385
+ usableHeight: 500,
386
+ usableWidth: 500,
387
+ });
388
+ expect(result.width).toBe(0);
389
+ });
390
+
391
+ it('returns 0 for empty items with dynamic sizes (vertical)', () => {
392
+ const result = calculateTotalSize({
393
+ columnCount: 0,
394
+ columnGap: 0,
395
+ direction: 'vertical',
396
+ fixedSize: null,
397
+ fixedWidth: null,
398
+ gap: 10,
399
+ itemsLength: 0,
400
+ queryColumn: () => 0,
401
+ queryX: () => 0,
402
+ queryY: () => 0,
403
+ usableHeight: 500,
404
+ usableWidth: 500,
405
+ });
406
+ expect(result.height).toBe(0);
407
+ });
408
+ });
409
+
410
+ describe('calculaterange', () => {
411
+ it('calculates vertical range with dynamic size', () => {
412
+ const result = calculateRange({
413
+ bufferAfter: 0,
414
+ bufferBefore: 0,
415
+ columnGap: 0,
416
+ direction: 'vertical',
417
+ findLowerBoundX: () => 0,
418
+ findLowerBoundY: (offset) => Math.floor(offset / 50),
419
+ fixedSize: null,
420
+ gap: 0,
421
+ itemsLength: 100,
422
+ queryX: () => 0,
423
+ queryY: (idx) => idx * 50,
424
+ relativeScrollX: 0,
425
+ relativeScrollY: 100,
426
+ usableHeight: 200,
427
+ usableWidth: 500,
428
+ });
429
+ expect(result.start).toBe(2);
430
+ expect(result.end).toBe(6);
431
+ });
432
+
433
+ it('calculates horizontal range with fixed size', () => {
434
+ const result = calculateRange({
435
+ bufferAfter: 0,
436
+ bufferBefore: 0,
437
+ columnGap: 10,
438
+ direction: 'horizontal',
439
+ findLowerBoundX: () => 0,
440
+ findLowerBoundY: () => 0,
441
+ fixedSize: 50,
442
+ gap: 0,
443
+ itemsLength: 100,
444
+ queryX: () => 0,
445
+ queryY: () => 0,
446
+ relativeScrollX: 120,
447
+ relativeScrollY: 0,
448
+ usableHeight: 500,
449
+ usableWidth: 100,
450
+ });
451
+ expect(result.start).toBe(2);
452
+ expect(result.end).toBe(4);
453
+ });
454
+
455
+ it('calculates vertical range with fixed size', () => {
456
+ const result = calculateRange({
457
+ bufferAfter: 5,
458
+ bufferBefore: 5,
459
+ columnGap: 0,
460
+ direction: 'vertical',
461
+ findLowerBoundX: () => 0,
462
+ findLowerBoundY: () => 0,
463
+ fixedSize: 50,
464
+ gap: 0,
465
+ itemsLength: 1000,
466
+ queryX: () => 0,
467
+ queryY: () => 0,
468
+ relativeScrollX: 0,
469
+ relativeScrollY: 1000,
470
+ usableHeight: 500,
471
+ usableWidth: 500,
472
+ });
473
+ expect(result.start).toBe(15);
474
+ expect(result.end).toBe(35);
475
+ });
476
+
477
+ it('calculates horizontal range with dynamic size', () => {
478
+ const result = calculateRange({
479
+ bufferAfter: 0,
480
+ bufferBefore: 0,
481
+ columnGap: 0,
482
+ direction: 'horizontal',
483
+ findLowerBoundX: (offset) => Math.floor(offset / 50),
484
+ findLowerBoundY: () => 0,
485
+ fixedSize: null,
486
+ gap: 0,
487
+ itemsLength: 1000,
488
+ queryX: (idx) => idx * 50,
489
+ queryY: () => 0,
490
+ relativeScrollX: 1000,
491
+ relativeScrollY: 0,
492
+ usableHeight: 500,
493
+ usableWidth: 500,
494
+ });
495
+ expect(result.start).toBe(20);
496
+ expect(result.end).toBe(30);
497
+ });
498
+
499
+ it('calculates horizontal range with dynamic size where end item is partially visible (edge case)', () => {
500
+ const result = calculateRange({
501
+ bufferAfter: 0,
502
+ bufferBefore: 0,
503
+ columnGap: 0,
504
+ direction: 'horizontal',
505
+ findLowerBoundX: (val) => val >= 200 ? 2 : (val >= 100 ? 1 : 0),
506
+ findLowerBoundY: () => 0,
507
+ fixedSize: null,
508
+ gap: 0,
509
+ itemsLength: 2,
510
+ queryX: (idx) => idx * 100,
511
+ queryY: () => 0,
512
+ relativeScrollX: 0,
513
+ relativeScrollY: 0,
514
+ usableHeight: 500,
515
+ usableWidth: 150,
516
+ });
517
+ expect(result.start).toBe(0);
518
+ expect(result.end).toBe(2);
519
+ });
520
+ });
521
+
522
+ describe('calculatescrolltarget', () => {
523
+ it('calculates target for horizontal end alignment', () => {
524
+ const result = calculateScrollTarget({
525
+ scaleX: 1,
526
+ scaleY: 1,
527
+ hostOffsetX: 0,
528
+ hostOffsetY: 0,
529
+ colIndex: 10,
530
+ viewportWidth: 500,
531
+ viewportHeight: 500,
532
+ columnGap: 0,
533
+ direction: 'horizontal',
534
+ fixedSize: 50,
535
+ fixedWidth: null,
536
+ gap: 0,
537
+ getColumnQuery: () => 0,
538
+ getColumnSize: () => 0,
539
+ getItemQueryX: (idx) => idx * 50,
540
+ getItemQueryY: () => 0,
541
+ getItemSizeX: () => 50,
542
+ getItemSizeY: () => 0,
543
+ options: 'end',
544
+ relativeScrollX: 0,
545
+ relativeScrollY: 0,
546
+ rowIndex: null,
547
+ totalHeight: 0,
548
+ totalWidth: 5000,
549
+ });
550
+ expect(result.targetX).toBe(50);
551
+ });
552
+
553
+ it('calculates target for grid column start alignment', () => {
554
+ const result = calculateScrollTarget({
555
+ scaleX: 1,
556
+ scaleY: 1,
557
+ hostOffsetX: 0,
558
+ hostOffsetY: 0,
559
+ colIndex: 10,
560
+ viewportWidth: 500,
561
+ viewportHeight: 500,
562
+ columnGap: 10,
563
+ direction: 'both',
564
+ fixedSize: null,
565
+ fixedWidth: null,
566
+ gap: 0,
567
+ getColumnQuery: (idx) => idx * 110,
568
+ getColumnSize: () => 110,
569
+ getItemQueryX: () => 0,
570
+ getItemQueryY: () => 0,
571
+ getItemSizeX: () => 0,
572
+ getItemSizeY: () => 0,
573
+ options: { align: { x: 'start' } },
574
+ relativeScrollX: 0,
575
+ relativeScrollY: 0,
576
+ rowIndex: null,
577
+ totalHeight: 0,
578
+ totalWidth: 5500,
579
+ });
580
+ expect(result.targetX).toBe(1100);
581
+ });
582
+
583
+ it('calculates target for vertical start alignment with partial align in options object', () => {
584
+ const result = calculateScrollTarget({
585
+ scaleX: 1,
586
+ scaleY: 1,
587
+ hostOffsetX: 0,
588
+ hostOffsetY: 0,
589
+ colIndex: null,
590
+ viewportWidth: 500,
591
+ viewportHeight: 500,
592
+ columnGap: 0,
593
+ direction: 'vertical',
594
+ fixedSize: 50,
595
+ fixedWidth: null,
596
+ gap: 0,
597
+ getColumnQuery: () => 0,
598
+ getColumnSize: () => 0,
599
+ getItemQueryX: () => 0,
600
+ getItemQueryY: (idx) => idx * 50,
601
+ getItemSizeX: () => 0,
602
+ getItemSizeY: () => 50,
603
+ options: { align: { y: 'start' } },
604
+ relativeScrollX: 50,
605
+ relativeScrollY: 0,
606
+ rowIndex: 10,
607
+ totalHeight: 5000,
608
+ totalWidth: 5000,
609
+ });
610
+ expect(result.targetY).toBe(500);
611
+ expect(result.targetX).toBe(50);
612
+ });
613
+
614
+ it('calculates target for horizontal start alignment with partial options object', () => {
615
+ const result = calculateScrollTarget({
616
+ scaleX: 1,
617
+ scaleY: 1,
618
+ hostOffsetX: 0,
619
+ hostOffsetY: 0,
620
+ colIndex: 10,
621
+ viewportWidth: 500,
622
+ viewportHeight: 500,
623
+ columnGap: 0,
624
+ direction: 'horizontal',
625
+ fixedSize: 50,
626
+ fixedWidth: null,
627
+ gap: 0,
628
+ getColumnQuery: () => 0,
629
+ getColumnSize: () => 0,
630
+ getItemQueryX: (idx) => idx * 50,
631
+ getItemQueryY: () => 0,
632
+ getItemSizeX: () => 50,
633
+ getItemSizeY: () => 0,
634
+ options: { align: { x: 'start' } },
635
+ relativeScrollX: 0,
636
+ relativeScrollY: 50,
637
+ rowIndex: 10,
638
+ totalHeight: 5000,
639
+ totalWidth: 5000,
640
+ });
641
+ expect(result.targetX).toBe(500);
642
+ expect(result.targetY).toBe(50);
643
+ });
644
+
645
+ it('calculates target for horizontal start alignment with options object', () => {
646
+ const result = calculateScrollTarget({
647
+ scaleX: 1,
648
+ scaleY: 1,
649
+ hostOffsetX: 0,
650
+ hostOffsetY: 0,
651
+ colIndex: 10,
652
+ viewportWidth: 500,
653
+ viewportHeight: 500,
654
+ columnGap: 0,
655
+ direction: 'horizontal',
656
+ fixedSize: 50,
657
+ fixedWidth: null,
658
+ gap: 0,
659
+ getColumnQuery: () => 0,
660
+ getColumnSize: () => 0,
661
+ getItemQueryX: (idx) => idx * 50,
662
+ getItemQueryY: () => 0,
663
+ getItemSizeX: () => 50,
664
+ getItemSizeY: () => 0,
665
+ options: { align: { x: 'start' } },
666
+ relativeScrollX: 0,
667
+ relativeScrollY: 0,
668
+ rowIndex: null,
669
+ totalHeight: 0,
670
+ totalWidth: 5000,
671
+ });
672
+ expect(result.targetX).toBe(500);
673
+ });
674
+
675
+ it('calculates target for vertical start alignment with dynamic size', () => {
676
+ const result = calculateScrollTarget({
677
+ scaleX: 1,
678
+ scaleY: 1,
679
+ hostOffsetX: 0,
680
+ hostOffsetY: 0,
681
+ colIndex: null,
682
+ viewportWidth: 500,
683
+ viewportHeight: 500,
684
+ columnGap: 0,
685
+ direction: 'vertical',
686
+ fixedSize: null,
687
+ fixedWidth: null,
688
+ gap: 10,
689
+ getColumnQuery: () => 0,
690
+ getColumnSize: () => 0,
691
+ getItemQueryX: () => 0,
692
+ getItemQueryY: (idx) => idx * 60,
693
+ getItemSizeX: () => 0,
694
+ getItemSizeY: () => 60,
695
+ options: 'start',
696
+ relativeScrollX: 0,
697
+ relativeScrollY: 0,
698
+ rowIndex: 10,
699
+ totalHeight: 6000,
700
+ totalWidth: 0,
701
+ });
702
+ expect(result.targetY).toBe(600);
703
+ expect(result.itemHeight).toBe(50);
704
+ });
705
+
706
+ it('calculates target for horizontal start alignment with dynamic size', () => {
707
+ const result = calculateScrollTarget({
708
+ scaleX: 1,
709
+ scaleY: 1,
710
+ hostOffsetX: 0,
711
+ hostOffsetY: 0,
712
+ colIndex: 10,
713
+ viewportWidth: 500,
714
+ viewportHeight: 500,
715
+ columnGap: 10,
716
+ direction: 'horizontal',
717
+ fixedSize: null,
718
+ fixedWidth: null,
719
+ gap: 0,
720
+ getColumnQuery: () => 0,
721
+ getColumnSize: () => 0,
722
+ getItemQueryX: (idx) => idx * 60,
723
+ getItemQueryY: () => 0,
724
+ getItemSizeX: () => 60,
725
+ getItemSizeY: () => 0,
726
+ options: 'start',
727
+ relativeScrollX: 0,
728
+ relativeScrollY: 0,
729
+ rowIndex: null,
730
+ totalHeight: 0,
731
+ totalWidth: 6000,
732
+ });
733
+ expect(result.targetX).toBe(600);
734
+ expect(result.itemWidth).toBe(50);
735
+ });
736
+
737
+ it('calculates target for vertical center alignment', () => {
738
+ const result = calculateScrollTarget({
739
+ scaleX: 1,
740
+ scaleY: 1,
741
+ hostOffsetX: 0,
742
+ hostOffsetY: 0,
743
+ colIndex: null,
744
+ viewportWidth: 500,
745
+ viewportHeight: 500,
746
+ columnGap: 0,
747
+ direction: 'vertical',
748
+ fixedSize: 50,
749
+ fixedWidth: null,
750
+ gap: 0,
751
+ getColumnQuery: () => 0,
752
+ getColumnSize: () => 0,
753
+ getItemQueryX: () => 0,
754
+ getItemQueryY: (idx) => idx * 50,
755
+ getItemSizeX: () => 0,
756
+ getItemSizeY: () => 50,
757
+ options: 'center',
758
+ relativeScrollX: 0,
759
+ relativeScrollY: 0,
760
+ rowIndex: 20,
761
+ totalHeight: 5000,
762
+ totalWidth: 0,
763
+ });
764
+ expect(result.targetY).toBe(775);
765
+ });
766
+
767
+ it('calculates target when rowindex is past itemslength', () => {
768
+ const result = calculateScrollTarget({
769
+ scaleX: 1,
770
+ scaleY: 1,
771
+ hostOffsetX: 0,
772
+ hostOffsetY: 0,
773
+ colIndex: null,
774
+ viewportWidth: 500,
775
+ viewportHeight: 500,
776
+ columnGap: 0,
777
+ direction: 'vertical',
778
+ fixedSize: 50,
779
+ fixedWidth: null,
780
+ gap: 0,
781
+ getColumnQuery: () => 0,
782
+ getColumnSize: () => 0,
783
+ getItemQueryX: () => 0,
784
+ getItemQueryY: (idx) => idx * 50,
785
+ getItemSizeX: () => 0,
786
+ getItemSizeY: () => 50,
787
+ options: 'start',
788
+ relativeScrollX: 0,
789
+ relativeScrollY: 0,
790
+ rowIndex: 200,
791
+ totalHeight: 5000,
792
+ totalWidth: 0,
793
+ });
794
+ expect(result.targetY).toBe(4500);
795
+ });
796
+
797
+ it('calculates target for grid bidirectional alignment', () => {
798
+ const result = calculateScrollTarget({
799
+ scaleX: 1,
800
+ scaleY: 1,
801
+ hostOffsetX: 0,
802
+ hostOffsetY: 0,
803
+ colIndex: 10,
804
+ viewportWidth: 500,
805
+ viewportHeight: 500,
806
+ columnGap: 0,
807
+ direction: 'both',
808
+ fixedSize: 50,
809
+ fixedWidth: null,
810
+ gap: 0,
811
+ getColumnQuery: (idx) => idx * 100,
812
+ getColumnSize: () => 100,
813
+ getItemQueryX: () => 0,
814
+ getItemQueryY: (idx) => idx * 50,
815
+ getItemSizeX: () => 0,
816
+ getItemSizeY: () => 50,
817
+ options: { x: 'center', y: 'end' },
818
+ relativeScrollX: 0,
819
+ relativeScrollY: 0,
820
+ rowIndex: 20,
821
+ totalHeight: 5000,
822
+ totalWidth: 5000,
823
+ });
824
+ expect(result.targetY).toBe(550);
825
+ expect(result.targetX).toBe(800);
826
+ });
827
+
828
+ it('calculates target accounting for active sticky item (vertical start alignment)', () => {
829
+ const result = calculateScrollTarget({
830
+ scaleX: 1,
831
+ scaleY: 1,
832
+ hostOffsetX: 0,
833
+ hostOffsetY: 0,
834
+ colIndex: null,
835
+ viewportWidth: 500,
836
+ viewportHeight: 500,
837
+ columnGap: 0,
838
+ direction: 'vertical',
839
+ fixedSize: 50,
840
+ fixedWidth: null,
841
+ gap: 0,
842
+ getColumnQuery: () => 0,
843
+ getColumnSize: () => 0,
844
+ getItemQueryX: () => 0,
845
+ getItemQueryY: (idx) => idx * 50,
846
+ getItemSizeX: () => 0,
847
+ getItemSizeY: () => 50,
848
+ options: 'start',
849
+ relativeScrollX: 0,
850
+ relativeScrollY: 0,
851
+ rowIndex: 150,
852
+ totalHeight: 10000,
853
+ totalWidth: 0,
854
+ stickyIndices: [ 100 ],
855
+ });
856
+ expect(result.targetY).toBe(7450);
857
+ });
858
+
859
+ it('calculates target accounting for active sticky item (horizontal start alignment)', () => {
860
+ const result = calculateScrollTarget({
861
+ scaleX: 1,
862
+ scaleY: 1,
863
+ hostOffsetX: 0,
864
+ hostOffsetY: 0,
865
+ colIndex: 150,
866
+ viewportWidth: 500,
867
+ viewportHeight: 500,
868
+ columnGap: 0,
869
+ direction: 'horizontal',
870
+ fixedSize: 50,
871
+ fixedWidth: null,
872
+ gap: 0,
873
+ getColumnQuery: () => 0,
874
+ getColumnSize: () => 0,
875
+ getItemQueryX: (idx) => idx * 50,
876
+ getItemQueryY: () => 0,
877
+ getItemSizeX: () => 50,
878
+ getItemSizeY: () => 0,
879
+ options: 'start',
880
+ relativeScrollX: 0,
881
+ relativeScrollY: 0,
882
+ rowIndex: null,
883
+ totalHeight: 0,
884
+ totalWidth: 10000,
885
+ stickyIndices: [ 100 ],
886
+ });
887
+ expect(result.targetX).toBe(7450);
888
+ });
889
+
890
+ it('calculates target for vertical start alignment (sticky indices present but none active)', () => {
891
+ const result = calculateScrollTarget({
892
+ scaleX: 1,
893
+ scaleY: 1,
894
+ hostOffsetX: 0,
895
+ hostOffsetY: 0,
896
+ colIndex: null,
897
+ viewportWidth: 500,
898
+ viewportHeight: 500,
899
+ columnGap: 0,
900
+ direction: 'vertical',
901
+ fixedSize: 50,
902
+ fixedWidth: null,
903
+ gap: 0,
904
+ getColumnQuery: () => 0,
905
+ getColumnSize: () => 0,
906
+ getItemQueryX: () => 0,
907
+ getItemQueryY: (idx) => idx * 50,
908
+ getItemSizeX: () => 0,
909
+ getItemSizeY: () => 50,
910
+ options: 'start',
911
+ relativeScrollX: 0,
912
+ relativeScrollY: 0,
913
+ rowIndex: 50,
914
+ totalHeight: 10000,
915
+ totalWidth: 0,
916
+ stickyIndices: [ 100 ],
917
+ });
918
+ expect(result.targetY).toBe(2500);
919
+ });
920
+
921
+ it('calculates target accounting for active sticky item (vertical start alignment, dynamic size)', () => {
922
+ const result = calculateScrollTarget({
923
+ scaleX: 1,
924
+ scaleY: 1,
925
+ hostOffsetX: 0,
926
+ hostOffsetY: 0,
927
+ colIndex: null,
928
+ viewportWidth: 500,
929
+ viewportHeight: 500,
930
+ columnGap: 0,
931
+ direction: 'vertical',
932
+ fixedSize: null,
933
+ fixedWidth: null,
934
+ gap: 0,
935
+ getColumnQuery: () => 0,
936
+ getColumnSize: () => 0,
937
+ getItemQueryX: () => 0,
938
+ getItemQueryY: (idx) => idx * 50,
939
+ getItemSizeX: () => 0,
940
+ getItemSizeY: () => 50,
941
+ options: 'start',
942
+ relativeScrollX: 0,
943
+ relativeScrollY: 0,
944
+ rowIndex: 150,
945
+ totalHeight: 10000,
946
+ totalWidth: 0,
947
+ stickyIndices: [ 100 ],
948
+ });
949
+ expect(result.targetY).toBe(7450);
950
+ });
951
+
952
+ it('calculates target accounting for active sticky item (vertical auto alignment, scrolling up)', () => {
953
+ const result = calculateScrollTarget({
954
+ scaleX: 1,
955
+ scaleY: 1,
956
+ hostOffsetX: 0,
957
+ hostOffsetY: 0,
958
+ colIndex: null,
959
+ viewportWidth: 500,
960
+ viewportHeight: 500,
961
+ columnGap: 0,
962
+ direction: 'vertical',
963
+ fixedSize: 50,
964
+ fixedWidth: null,
965
+ gap: 0,
966
+ getColumnQuery: () => 0,
967
+ getColumnSize: () => 0,
968
+ getItemQueryX: () => 0,
969
+ getItemQueryY: (idx) => idx * 50,
970
+ getItemSizeX: () => 0,
971
+ getItemSizeY: () => 50,
972
+ options: 'auto',
973
+ relativeScrollX: 0,
974
+ relativeScrollY: 8000,
975
+ rowIndex: 120,
976
+ totalHeight: 10000,
977
+ totalWidth: 0,
978
+ stickyIndices: [ 100 ],
979
+ });
980
+ expect(result.targetY).toBe(5950);
981
+ });
982
+
983
+ it('calculates target accounting for active sticky item (grid start alignment, fixed width)', () => {
984
+ const result = calculateScrollTarget({
985
+ scaleX: 1,
986
+ scaleY: 1,
987
+ hostOffsetX: 0,
988
+ hostOffsetY: 0,
989
+ colIndex: 150,
990
+ viewportWidth: 500,
991
+ viewportHeight: 500,
992
+ columnGap: 0,
993
+ direction: 'both',
994
+ fixedSize: 50,
995
+ fixedWidth: 100,
996
+ gap: 0,
997
+ getColumnQuery: (idx) => idx * 100,
998
+ getColumnSize: () => 100,
999
+ getItemQueryX: () => 0,
1000
+ getItemQueryY: () => 0,
1001
+ getItemSizeX: () => 0,
1002
+ getItemSizeY: () => 50,
1003
+ options: { x: 'start' },
1004
+ relativeScrollX: 0,
1005
+ relativeScrollY: 0,
1006
+ rowIndex: null,
1007
+ totalHeight: 10000,
1008
+ totalWidth: 20000,
1009
+ stickyIndices: [ 100 ],
1010
+ });
1011
+ expect(result.targetX).toBe(14900);
1012
+ });
1013
+
1014
+ it('calculates target accounting for active sticky item (horizontal start alignment, dynamic size)', () => {
1015
+ const result = calculateScrollTarget({
1016
+ scaleX: 1,
1017
+ scaleY: 1,
1018
+ hostOffsetX: 0,
1019
+ hostOffsetY: 0,
1020
+ colIndex: 150,
1021
+ viewportWidth: 500,
1022
+ viewportHeight: 500,
1023
+ columnGap: 0,
1024
+ direction: 'horizontal',
1025
+ fixedSize: null,
1026
+ fixedWidth: null,
1027
+ gap: 0,
1028
+ getColumnQuery: () => 0,
1029
+ getColumnSize: () => 0,
1030
+ getItemQueryX: (idx) => idx * 50,
1031
+ getItemQueryY: () => 0,
1032
+ getItemSizeX: () => 50,
1033
+ getItemSizeY: () => 0,
1034
+ options: 'start',
1035
+ relativeScrollX: 0,
1036
+ relativeScrollY: 0,
1037
+ rowIndex: null,
1038
+ totalHeight: 0,
1039
+ totalWidth: 10000,
1040
+ stickyIndices: [ 100 ],
1041
+ });
1042
+ expect(result.targetX).toBe(7450);
1043
+ });
1044
+
1045
+ it('calculates target accounting for active sticky item (grid start alignment, dynamic width)', () => {
1046
+ const result = calculateScrollTarget({
1047
+ scaleX: 1,
1048
+ scaleY: 1,
1049
+ hostOffsetX: 0,
1050
+ hostOffsetY: 0,
1051
+ colIndex: 150,
1052
+ viewportWidth: 500,
1053
+ viewportHeight: 500,
1054
+ columnGap: 0,
1055
+ direction: 'both',
1056
+ fixedSize: null,
1057
+ fixedWidth: null,
1058
+ gap: 0,
1059
+ getColumnQuery: (idx) => idx * 100,
1060
+ getColumnSize: () => 100,
1061
+ getItemQueryX: () => 0,
1062
+ getItemQueryY: () => 0,
1063
+ getItemSizeX: () => 0,
1064
+ getItemSizeY: () => 50,
1065
+ options: { x: 'start' },
1066
+ relativeScrollX: 0,
1067
+ relativeScrollY: 0,
1068
+ rowIndex: null,
1069
+ totalHeight: 10000,
1070
+ totalWidth: 20000,
1071
+ stickyIndices: [ 100 ],
1072
+ });
1073
+ expect(result.targetX).toBe(14900);
1074
+ });
1075
+
1076
+ it('calculates target accounting for active sticky item (vertical auto alignment, dynamic size)', () => {
1077
+ const result = calculateScrollTarget({
1078
+ scaleX: 1,
1079
+ scaleY: 1,
1080
+ hostOffsetX: 0,
1081
+ hostOffsetY: 0,
1082
+ colIndex: null,
1083
+ viewportWidth: 500,
1084
+ viewportHeight: 500,
1085
+ columnGap: 0,
1086
+ direction: 'vertical',
1087
+ fixedSize: null,
1088
+ fixedWidth: null,
1089
+ gap: 0,
1090
+ getColumnQuery: () => 0,
1091
+ getColumnSize: () => 0,
1092
+ getItemQueryX: () => 0,
1093
+ getItemQueryY: (idx) => idx * 50,
1094
+ getItemSizeX: () => 0,
1095
+ getItemSizeY: () => 50,
1096
+ options: 'auto',
1097
+ relativeScrollX: 0,
1098
+ relativeScrollY: 8000,
1099
+ rowIndex: 120,
1100
+ totalHeight: 10000,
1101
+ totalWidth: 0,
1102
+ stickyIndices: [ 100 ],
1103
+ });
1104
+ expect(result.targetY).toBe(5950);
1105
+ });
1106
+
1107
+ it('calculates target for vertical auto alignment (item taller than viewport)', () => {
1108
+ const result = calculateScrollTarget({
1109
+ scaleX: 1,
1110
+ scaleY: 1,
1111
+ hostOffsetX: 0,
1112
+ hostOffsetY: 0,
1113
+ colIndex: null,
1114
+ viewportWidth: 500,
1115
+ viewportHeight: 500,
1116
+ columnGap: 0,
1117
+ direction: 'vertical',
1118
+ fixedSize: 1000,
1119
+ fixedWidth: null,
1120
+ gap: 0,
1121
+ getColumnQuery: () => 0,
1122
+ getColumnSize: () => 0,
1123
+ getItemQueryX: () => 0,
1124
+ getItemQueryY: (idx) => idx * 1000,
1125
+ getItemSizeX: () => 0,
1126
+ getItemSizeY: () => 1000,
1127
+ options: 'auto',
1128
+ relativeScrollX: 0,
1129
+ relativeScrollY: 0,
1130
+ rowIndex: 5,
1131
+ totalHeight: 10000,
1132
+ totalWidth: 0,
1133
+ });
1134
+ expect(result.targetY).toBe(5000);
1135
+ });
1136
+
1137
+ it('calculates target for vertical auto alignment (sticky indices present but none active)', () => {
1138
+ const result = calculateScrollTarget({
1139
+ scaleX: 1,
1140
+ scaleY: 1,
1141
+ hostOffsetX: 0,
1142
+ hostOffsetY: 0,
1143
+ colIndex: null,
1144
+ viewportWidth: 500,
1145
+ viewportHeight: 500,
1146
+ columnGap: 0,
1147
+ direction: 'vertical',
1148
+ fixedSize: 50,
1149
+ fixedWidth: null,
1150
+ gap: 0,
1151
+ getColumnQuery: () => 0,
1152
+ getColumnSize: () => 0,
1153
+ getItemQueryX: () => 0,
1154
+ getItemQueryY: (idx) => idx * 50,
1155
+ getItemSizeX: () => 0,
1156
+ getItemSizeY: () => 50,
1157
+ options: 'auto',
1158
+ relativeScrollX: 0,
1159
+ relativeScrollY: 8000,
1160
+ rowIndex: 50,
1161
+ totalHeight: 10000,
1162
+ totalWidth: 0,
1163
+ stickyIndices: [ 100 ],
1164
+ });
1165
+ expect(result.targetY).toBe(2500);
1166
+ });
1167
+
1168
+ it('calculates target for horizontal start alignment (sticky indices present but none active)', () => {
1169
+ const result = calculateScrollTarget({
1170
+ scaleX: 1,
1171
+ scaleY: 1,
1172
+ hostOffsetX: 0,
1173
+ hostOffsetY: 0,
1174
+ colIndex: 50,
1175
+ viewportWidth: 500,
1176
+ viewportHeight: 500,
1177
+ columnGap: 0,
1178
+ direction: 'horizontal',
1179
+ fixedSize: 50,
1180
+ fixedWidth: null,
1181
+ gap: 0,
1182
+ getColumnQuery: () => 0,
1183
+ getColumnSize: () => 0,
1184
+ getItemQueryX: (idx) => idx * 50,
1185
+ getItemQueryY: () => 0,
1186
+ getItemSizeX: () => 50,
1187
+ getItemSizeY: () => 0,
1188
+ options: 'start',
1189
+ relativeScrollX: 0,
1190
+ relativeScrollY: 0,
1191
+ rowIndex: null,
1192
+ totalHeight: 0,
1193
+ totalWidth: 10000,
1194
+ stickyIndices: [ 100 ],
1195
+ });
1196
+ expect(result.targetX).toBe(2500);
1197
+ });
1198
+
1199
+ it('calculates target for vertical auto alignment (large item already visible)', () => {
1200
+ const result = calculateScrollTarget({
1201
+ scaleX: 1,
1202
+ scaleY: 1,
1203
+ hostOffsetX: 0,
1204
+ hostOffsetY: 0,
1205
+ colIndex: null,
1206
+ viewportWidth: 500,
1207
+ viewportHeight: 500,
1208
+ columnGap: 0,
1209
+ direction: 'vertical',
1210
+ fixedSize: 1000,
1211
+ fixedWidth: null,
1212
+ gap: 0,
1213
+ getColumnQuery: () => 0,
1214
+ getColumnSize: () => 0,
1215
+ getItemQueryX: () => 0,
1216
+ getItemQueryY: (idx) => idx * 1000,
1217
+ getItemSizeX: () => 0,
1218
+ getItemSizeY: () => 1000,
1219
+ options: 'auto',
1220
+ relativeScrollX: 0,
1221
+ relativeScrollY: 200,
1222
+ rowIndex: 0,
1223
+ totalHeight: 10000,
1224
+ totalWidth: 0,
1225
+ });
1226
+ expect(result.targetY).toBe(200);
1227
+ });
1228
+
1229
+ it('detects visibility correctly when under a sticky item (auto alignment)', () => {
1230
+ const params = {
1231
+ scaleX: 1,
1232
+ scaleY: 1,
1233
+ hostOffsetX: 0,
1234
+ hostOffsetY: 0,
1235
+ colIndex: null,
1236
+ viewportWidth: 500,
1237
+ viewportHeight: 500,
1238
+ columnCount: 100,
1239
+ columnGap: 0,
1240
+ direction: 'vertical' as const,
1241
+ fixedSize: 100,
1242
+ fixedWidth: null,
1243
+ gap: 0,
1244
+ getColumnQuery: (idx: number) => idx * 100,
1245
+ getColumnSize: () => 100,
1246
+ getItemQueryX: (idx: number) => idx * 100,
1247
+ getItemQueryY: (index: number) => index * 100,
1248
+ getItemSizeX: () => 100,
1249
+ getItemSizeY: () => 100,
1250
+ itemsLength: 1000,
1251
+ options: 'auto' as const,
1252
+ relativeScrollX: 0,
1253
+ relativeScrollY: 14950,
1254
+ rowIndex: 150,
1255
+ stickyIndices: [ 100 ],
1256
+ totalHeight: 120000,
1257
+ totalWidth: 10000,
1258
+ usableHeight: 800,
1259
+ usableWidth: 1000,
1260
+ };
1261
+
1262
+ const result = calculateScrollTarget(params);
1263
+ expect(result.targetY).toBe(14900);
1264
+ });
1265
+
1266
+ it('aligns correctly under a sticky item (start alignment)', () => {
1267
+ const getItemQueryY = (index: number) => {
1268
+ if (index <= 100) {
1269
+ return index * 120;
1270
+ }
1271
+ let sum = 12000;
1272
+ for (let i = 100; i < index; i++) {
1273
+ sum += (i % 2 === 0 ? 80 : 160);
1274
+ }
1275
+ return sum;
1276
+ };
1277
+
1278
+ const getItemSizeY = (index: number) => (index % 2 === 0 ? 80 : 160);
1279
+
1280
+ const params = {
1281
+ scaleX: 1,
1282
+ scaleY: 1,
1283
+ hostOffsetX: 0,
1284
+ hostOffsetY: 0,
1285
+ colIndex: 50,
1286
+ viewportWidth: 500,
1287
+ viewportHeight: 500,
1288
+ columnCount: 100,
1289
+ columnGap: 0,
1290
+ direction: 'both' as const,
1291
+ fixedSize: null,
1292
+ fixedWidth: null,
1293
+ gap: 0,
1294
+ getColumnQuery: (idx: number) => idx * 100,
1295
+ getColumnSize: () => 100,
1296
+ getItemQueryX: (idx: number) => idx * 100,
1297
+ getItemQueryY,
1298
+ getItemSizeX: () => 100,
1299
+ getItemSizeY,
1300
+ itemsLength: 1000,
1301
+ options: 'start' as const,
1302
+ relativeScrollX: 0,
1303
+ relativeScrollY: 0,
1304
+ rowIndex: 150,
1305
+ stickyIndices: [ 100, 200, 300 ],
1306
+ totalHeight: 120000,
1307
+ totalWidth: 10000,
1308
+ usableHeight: 800,
1309
+ usableWidth: 1000,
1310
+ };
1311
+
1312
+ const result = calculateScrollTarget(params);
1313
+ expect(result.targetY).toBe(17920);
1314
+ });
1315
+
1316
+ it('aligns to end when scrolling forward (vertical)', () => {
1317
+ const params = {
1318
+ scaleX: 1,
1319
+ scaleY: 1,
1320
+ hostOffsetX: 0,
1321
+ hostOffsetY: 0,
1322
+ rowIndex: 150,
1323
+ viewportWidth: 500,
1324
+ viewportHeight: 500,
1325
+ colIndex: null,
1326
+ options: 'auto' as const,
1327
+ itemsLength: 1000,
1328
+ columnCount: 0,
1329
+ direction: 'vertical' as const,
1330
+ usableWidth: 1000,
1331
+ usableHeight: 800,
1332
+ totalWidth: 1000,
1333
+ totalHeight: 100000,
1334
+ gap: 0,
1335
+ columnGap: 0,
1336
+ fixedSize: 100,
1337
+ fixedWidth: null,
1338
+ relativeScrollX: 0,
1339
+ relativeScrollY: 0,
1340
+ getItemSizeY: () => 100,
1341
+ getItemSizeX: () => 1000,
1342
+ getItemQueryY: (idx: number) => idx * 100,
1343
+ getItemQueryX: () => 0,
1344
+ getColumnSize: () => 0,
1345
+ getColumnQuery: () => 0,
1346
+ stickyIndices: [],
1347
+ };
1348
+
1349
+ const result = calculateScrollTarget(params);
1350
+ expect(result.targetY).toBe(14600);
1351
+ expect(result.effectiveAlignY).toBe('end');
1352
+ });
1353
+
1354
+ it('aligns to start when scrolling backward (vertical)', () => {
1355
+ const params = {
1356
+ scaleX: 1,
1357
+ scaleY: 1,
1358
+ hostOffsetX: 0,
1359
+ hostOffsetY: 0,
1360
+ rowIndex: 10,
1361
+ viewportWidth: 500,
1362
+ viewportHeight: 500,
1363
+ colIndex: null,
1364
+ options: 'auto' as const,
1365
+ itemsLength: 1000,
1366
+ columnCount: 0,
1367
+ direction: 'vertical' as const,
1368
+ usableWidth: 1000,
1369
+ usableHeight: 800,
1370
+ totalWidth: 1000,
1371
+ totalHeight: 100000,
1372
+ gap: 0,
1373
+ columnGap: 0,
1374
+ fixedSize: 100,
1375
+ fixedWidth: null,
1376
+ relativeScrollX: 0,
1377
+ relativeScrollY: 15000,
1378
+ getItemSizeY: () => 100,
1379
+ getItemSizeX: () => 1000,
1380
+ getItemQueryY: (idx: number) => idx * 100,
1381
+ getItemQueryX: () => 0,
1382
+ getColumnSize: () => 0,
1383
+ getColumnQuery: () => 0,
1384
+ stickyIndices: [],
1385
+ };
1386
+
1387
+ const result = calculateScrollTarget(params);
1388
+ expect(result.targetY).toBe(1000);
1389
+ expect(result.effectiveAlignY).toBe('start');
1390
+ });
1391
+
1392
+ it('stays put if already visible (vertical)', () => {
1393
+ const params = {
1394
+ scaleX: 1,
1395
+ scaleY: 1,
1396
+ hostOffsetX: 0,
1397
+ hostOffsetY: 0,
1398
+ rowIndex: 150,
1399
+ viewportWidth: 500,
1400
+ viewportHeight: 500,
1401
+ colIndex: null,
1402
+ options: 'auto' as const,
1403
+ itemsLength: 1000,
1404
+ columnCount: 0,
1405
+ direction: 'vertical' as const,
1406
+ usableWidth: 1000,
1407
+ usableHeight: 800,
1408
+ totalWidth: 1000,
1409
+ totalHeight: 100000,
1410
+ gap: 0,
1411
+ columnGap: 0,
1412
+ fixedSize: 100,
1413
+ fixedWidth: null,
1414
+ relativeScrollX: 0,
1415
+ relativeScrollY: 14500,
1416
+ getItemSizeY: () => 100,
1417
+ getItemSizeX: () => 1000,
1418
+ getItemQueryY: (idx: number) => idx * 100,
1419
+ getItemQueryX: () => 0,
1420
+ getColumnSize: () => 0,
1421
+ getColumnQuery: () => 0,
1422
+ stickyIndices: [],
1423
+ };
1424
+
1425
+ const result = calculateScrollTarget(params);
1426
+ expect(result.targetY).toBe(14600);
1427
+ expect(result.effectiveAlignY).toBe('end');
1428
+ });
1429
+
1430
+ it('aligns to start if partially visible at top (backward scroll effect)', () => {
1431
+ const params = {
1432
+ scaleX: 1,
1433
+ scaleY: 1,
1434
+ hostOffsetX: 0,
1435
+ hostOffsetY: 0,
1436
+ rowIndex: 150,
1437
+ viewportWidth: 500,
1438
+ viewportHeight: 500,
1439
+ colIndex: null,
1440
+ options: 'auto' as const,
1441
+ itemsLength: 1000,
1442
+ columnCount: 0,
1443
+ direction: 'vertical' as const,
1444
+ usableWidth: 1000,
1445
+ usableHeight: 800,
1446
+ totalWidth: 1000,
1447
+ totalHeight: 100000,
1448
+ gap: 0,
1449
+ columnGap: 0,
1450
+ fixedSize: 100,
1451
+ fixedWidth: null,
1452
+ relativeScrollX: 0,
1453
+ relativeScrollY: 15050,
1454
+ getItemSizeY: () => 100,
1455
+ getItemSizeX: () => 1000,
1456
+ getItemQueryY: (idx: number) => idx * 100,
1457
+ getItemQueryX: () => 0,
1458
+ getColumnSize: () => 0,
1459
+ getColumnQuery: () => 0,
1460
+ stickyIndices: [],
1461
+ };
1462
+
1463
+ const result = calculateScrollTarget(params);
1464
+ expect(result.targetY).toBe(15000);
1465
+ expect(result.effectiveAlignY).toBe('start');
1466
+ });
1467
+
1468
+ it('aligns to end if partially visible at bottom (forward scroll effect)', () => {
1469
+ const params = {
1470
+ scaleX: 1,
1471
+ scaleY: 1,
1472
+ hostOffsetX: 0,
1473
+ hostOffsetY: 0,
1474
+ rowIndex: 150,
1475
+ viewportWidth: 500,
1476
+ viewportHeight: 500,
1477
+ colIndex: null,
1478
+ options: 'auto' as const,
1479
+ itemsLength: 1000,
1480
+ columnCount: 0,
1481
+ direction: 'vertical' as const,
1482
+ usableWidth: 1000,
1483
+ usableHeight: 800,
1484
+ totalWidth: 1000,
1485
+ totalHeight: 100000,
1486
+ gap: 0,
1487
+ columnGap: 0,
1488
+ fixedSize: 100,
1489
+ fixedWidth: null,
1490
+ relativeScrollX: 0,
1491
+ relativeScrollY: 14250,
1492
+ getItemSizeY: () => 100,
1493
+ getItemSizeX: () => 1000,
1494
+ getItemQueryY: (idx: number) => idx * 100,
1495
+ getItemQueryX: () => 0,
1496
+ getColumnSize: () => 0,
1497
+ getColumnQuery: () => 0,
1498
+ stickyIndices: [],
1499
+ };
1500
+
1501
+ const result = calculateScrollTarget(params);
1502
+ expect(result.targetY).toBe(14600);
1503
+ expect(result.effectiveAlignY).toBe('end');
1504
+ });
1505
+
1506
+ it('does not account for non-sticky footer (flowpaddingendy) in scroll target calculation', () => {
1507
+ const params = {
1508
+ scaleX: 1,
1509
+ scaleY: 1,
1510
+ hostOffsetX: 0,
1511
+ hostOffsetY: 0,
1512
+ rowIndex: 10,
1513
+ viewportWidth: 500,
1514
+ viewportHeight: 500,
1515
+ colIndex: null,
1516
+ options: 'end' as const,
1517
+ itemsLength: 100,
1518
+ columnCount: 0,
1519
+ direction: 'vertical' as const,
1520
+ usableWidth: 1000,
1521
+ usableHeight: 1000,
1522
+ totalWidth: 1000,
1523
+ totalHeight: 10000,
1524
+ gap: 0,
1525
+ columnGap: 0,
1526
+ fixedSize: 100,
1527
+ fixedWidth: null,
1528
+ relativeScrollX: 0,
1529
+ relativeScrollY: 0,
1530
+ getItemSizeY: () => 100,
1531
+ getItemSizeX: () => 1000,
1532
+ getItemQueryY: (idx: number) => idx * 100,
1533
+ getItemQueryX: () => 0,
1534
+ getColumnSize: () => 0,
1535
+ getColumnQuery: () => 0,
1536
+ stickyIndices: [],
1537
+ flowPaddingStartY: 150,
1538
+ flowPaddingEndY: 200,
1539
+ };
1540
+
1541
+ const result = calculateScrollTarget(params);
1542
+ expect(result.targetY).toBe(750);
1543
+ });
1544
+
1545
+ it('aligns large item correctly when scrolling forward (minimal movement)', () => {
1546
+ const params = {
1547
+ scaleX: 1,
1548
+ scaleY: 1,
1549
+ hostOffsetX: 0,
1550
+ hostOffsetY: 0,
1551
+ rowIndex: 150,
1552
+ viewportWidth: 500,
1553
+ viewportHeight: 500,
1554
+ colIndex: null,
1555
+ options: 'auto' as const,
1556
+ itemsLength: 1000,
1557
+ columnCount: 0,
1558
+ direction: 'vertical' as const,
1559
+ usableWidth: 1000,
1560
+ usableHeight: 500,
1561
+ totalWidth: 1000,
1562
+ totalHeight: 1000000,
1563
+ gap: 0,
1564
+ columnGap: 0,
1565
+ fixedSize: 1000,
1566
+ fixedWidth: null,
1567
+ relativeScrollX: 0,
1568
+ relativeScrollY: 0,
1569
+ getItemSizeY: () => 1000,
1570
+ getItemSizeX: () => 1000,
1571
+ getItemQueryY: (idx: number) => idx * 1000,
1572
+ getItemQueryX: () => 0,
1573
+ getColumnSize: () => 0,
1574
+ getColumnQuery: () => 0,
1575
+ stickyIndices: [],
1576
+ };
1577
+
1578
+ const result = calculateScrollTarget(params);
1579
+ expect(result.targetY).toBe(150000);
1580
+ expect(result.effectiveAlignY).toBe('start');
1581
+ });
1582
+
1583
+ it('aligns large item correctly when scrolling backward (minimal movement)', () => {
1584
+ const params = {
1585
+ scaleX: 1,
1586
+ scaleY: 1,
1587
+ hostOffsetX: 0,
1588
+ hostOffsetY: 0,
1589
+ rowIndex: 10,
1590
+ viewportWidth: 500,
1591
+ viewportHeight: 500,
1592
+ colIndex: null,
1593
+ options: 'auto' as const,
1594
+ itemsLength: 1000,
1595
+ columnCount: 0,
1596
+ direction: 'vertical' as const,
1597
+ usableWidth: 1000,
1598
+ usableHeight: 500,
1599
+ totalWidth: 1000,
1600
+ totalHeight: 1000000,
1601
+ gap: 0,
1602
+ columnGap: 0,
1603
+ fixedSize: 1000,
1604
+ fixedWidth: null,
1605
+ relativeScrollX: 0,
1606
+ relativeScrollY: 100000,
1607
+ getItemSizeY: () => 1000,
1608
+ getItemSizeX: () => 1000,
1609
+ getItemQueryY: (idx: number) => idx * 1000,
1610
+ getItemQueryX: () => 0,
1611
+ getColumnSize: () => 0,
1612
+ getColumnQuery: () => 0,
1613
+ stickyIndices: [],
1614
+ };
1615
+
1616
+ const result = calculateScrollTarget(params);
1617
+ expect(result.targetY).toBe(10500);
1618
+ expect(result.effectiveAlignY).toBe('end');
1619
+ });
1620
+
1621
+ it('aligns large item correctly on x axis (minimal movement)', () => {
1622
+ const params = {
1623
+ scaleX: 1,
1624
+ scaleY: 1,
1625
+ hostOffsetX: 0,
1626
+ hostOffsetY: 0,
1627
+ rowIndex: null,
1628
+ viewportWidth: 500,
1629
+ viewportHeight: 500,
1630
+ colIndex: 150,
1631
+ options: 'auto' as const,
1632
+ itemsLength: 0,
1633
+ columnCount: 1000,
1634
+ direction: 'horizontal' as const,
1635
+ usableWidth: 500,
1636
+ usableHeight: 1000,
1637
+ totalWidth: 1000000,
1638
+ totalHeight: 1000,
1639
+ gap: 0,
1640
+ columnGap: 0,
1641
+ fixedSize: 1000,
1642
+ fixedWidth: null,
1643
+ relativeScrollX: 0,
1644
+ relativeScrollY: 0,
1645
+ getItemSizeY: () => 1000,
1646
+ getItemSizeX: () => 1000,
1647
+ getItemQueryY: () => 0,
1648
+ getItemQueryX: (idx: number) => idx * 1000,
1649
+ getColumnSize: () => 1000,
1650
+ getColumnQuery: (idx: number) => idx * 1000,
1651
+ stickyIndices: [],
1652
+ };
1653
+
1654
+ const result = calculateScrollTarget(params);
1655
+ expect(result.targetX).toBe(150000);
1656
+ expect(result.effectiveAlignX).toBe('start');
1657
+ });
1658
+
1659
+ it('aligns large item correctly on x axis scrolling backward (minimal movement)', () => {
1660
+ const params = {
1661
+ scaleX: 1,
1662
+ scaleY: 1,
1663
+ hostOffsetX: 0,
1664
+ hostOffsetY: 0,
1665
+ rowIndex: null,
1666
+ viewportWidth: 500,
1667
+ viewportHeight: 500,
1668
+ colIndex: 10,
1669
+ options: 'auto' as const,
1670
+ itemsLength: 0,
1671
+ columnCount: 1000,
1672
+ direction: 'horizontal' as const,
1673
+ usableWidth: 500,
1674
+ usableHeight: 1000,
1675
+ totalWidth: 1000000,
1676
+ totalHeight: 1000,
1677
+ gap: 0,
1678
+ columnGap: 0,
1679
+ fixedSize: 1000,
1680
+ fixedWidth: null,
1681
+ relativeScrollX: 100000,
1682
+ relativeScrollY: 0,
1683
+ getItemSizeY: () => 1000,
1684
+ getItemSizeX: () => 1000,
1685
+ getItemQueryY: () => 0,
1686
+ getItemQueryX: (idx: number) => idx * 1000,
1687
+ getColumnSize: () => 1000,
1688
+ getColumnQuery: (idx: number) => idx * 1000,
1689
+ stickyIndices: [],
1690
+ };
1691
+
1692
+ const result = calculateScrollTarget(params);
1693
+ expect(result.targetX).toBe(10500);
1694
+ expect(result.effectiveAlignX).toBe('end');
1695
+ });
1696
+
1697
+ it('calculates target when colindex is past columncount', () => {
1698
+ const result = calculateScrollTarget({
1699
+ scaleX: 1,
1700
+ scaleY: 1,
1701
+ hostOffsetX: 0,
1702
+ hostOffsetY: 0,
1703
+ colIndex: 200,
1704
+ viewportWidth: 500,
1705
+ viewportHeight: 500,
1706
+ columnGap: 10,
1707
+ direction: 'horizontal',
1708
+ fixedSize: 50,
1709
+ fixedWidth: null,
1710
+ gap: 0,
1711
+ getColumnQuery: () => 0,
1712
+ getColumnSize: () => 0,
1713
+ getItemQueryX: (idx) => idx * 60,
1714
+ getItemQueryY: () => 0,
1715
+ getItemSizeX: () => 50,
1716
+ getItemSizeY: () => 0,
1717
+ options: 'start',
1718
+ relativeScrollX: 0,
1719
+ relativeScrollY: 0,
1720
+ rowIndex: null,
1721
+ totalHeight: 0,
1722
+ totalWidth: 6000,
1723
+ });
1724
+ expect(result.targetX).toBe(5500);
1725
+ });
1726
+
1727
+ it('aligns to start when scrolling backward on x axis (horizontal)', () => {
1728
+ const params = {
1729
+ scaleX: 1,
1730
+ scaleY: 1,
1731
+ hostOffsetX: 0,
1732
+ hostOffsetY: 0,
1733
+ rowIndex: null,
1734
+ viewportWidth: 500,
1735
+ viewportHeight: 500,
1736
+ colIndex: 10,
1737
+ options: 'auto' as const,
1738
+ itemsLength: 0,
1739
+ columnCount: 1000,
1740
+ direction: 'horizontal' as const,
1741
+ usableWidth: 1000,
1742
+ usableHeight: 800,
1743
+ totalWidth: 100000,
1744
+ totalHeight: 1000,
1745
+ gap: 0,
1746
+ columnGap: 0,
1747
+ fixedSize: 100,
1748
+ fixedWidth: null,
1749
+ relativeScrollX: 15000,
1750
+ relativeScrollY: 0,
1751
+ getItemSizeY: () => 1000,
1752
+ getItemSizeX: () => 100,
1753
+ getItemQueryY: () => 0,
1754
+ getItemQueryX: (idx: number) => idx * 100,
1755
+ getColumnSize: () => 0,
1756
+ getColumnQuery: () => 0,
1757
+ stickyIndices: [],
1758
+ };
1759
+
1760
+ const result = calculateScrollTarget(params);
1761
+ expect(result.targetX).toBe(1000);
1762
+ expect(result.effectiveAlignX).toBe('start');
1763
+ });
1764
+
1765
+ it('aligns to end when scrolling forward on x axis (horizontal)', () => {
1766
+ const params = {
1767
+ scaleX: 1,
1768
+ scaleY: 1,
1769
+ hostOffsetX: 0,
1770
+ hostOffsetY: 0,
1771
+ rowIndex: null,
1772
+ viewportWidth: 500,
1773
+ viewportHeight: 500,
1774
+ colIndex: 150,
1775
+ options: 'auto' as const,
1776
+ itemsLength: 0,
1777
+ columnCount: 1000,
1778
+ direction: 'horizontal' as const,
1779
+ usableWidth: 1000,
1780
+ usableHeight: 800,
1781
+ totalWidth: 100000,
1782
+ totalHeight: 1000,
1783
+ gap: 0,
1784
+ columnGap: 0,
1785
+ fixedSize: 100,
1786
+ fixedWidth: null,
1787
+ relativeScrollX: 0,
1788
+ relativeScrollY: 0,
1789
+ getItemSizeY: () => 1000,
1790
+ getItemSizeX: () => 100,
1791
+ getItemQueryY: () => 0,
1792
+ getItemQueryX: (idx: number) => idx * 100,
1793
+ getColumnSize: () => 0,
1794
+ getColumnQuery: () => 0,
1795
+ stickyIndices: [],
1796
+ };
1797
+
1798
+ const result = calculateScrollTarget(params);
1799
+ expect(result.targetX).toBe(14600);
1800
+ expect(result.effectiveAlignX).toBe('end');
1801
+ });
1802
+
1803
+ it('stays put if colindex already visible (horizontal)', () => {
1804
+ const params = {
1805
+ scaleX: 1,
1806
+ scaleY: 1,
1807
+ hostOffsetX: 0,
1808
+ hostOffsetY: 0,
1809
+ rowIndex: null,
1810
+ viewportWidth: 500,
1811
+ viewportHeight: 500,
1812
+ colIndex: 150,
1813
+ options: 'auto' as const,
1814
+ itemsLength: 0,
1815
+ columnCount: 1000,
1816
+ direction: 'horizontal' as const,
1817
+ usableWidth: 1000,
1818
+ usableHeight: 800,
1819
+ totalWidth: 100000,
1820
+ totalHeight: 1000,
1821
+ gap: 0,
1822
+ columnGap: 0,
1823
+ fixedSize: 100,
1824
+ fixedWidth: null,
1825
+ relativeScrollX: 14500,
1826
+ relativeScrollY: 0,
1827
+ getItemSizeY: () => 1000,
1828
+ getItemSizeX: () => 100,
1829
+ getItemQueryY: () => 0,
1830
+ getItemQueryX: (idx: number) => idx * 100,
1831
+ getColumnSize: () => 0,
1832
+ getColumnQuery: () => 0,
1833
+ stickyIndices: [],
1834
+ };
1835
+
1836
+ const result = calculateScrollTarget(params);
1837
+ expect(result.targetX).toBe(14600);
1838
+ expect(result.effectiveAlignX).toBe('end');
1839
+ });
1840
+
1841
+ it('handles coordinate scaling for x and y axes when content exceeds browser_max_size', () => {
1842
+ const params = {
1843
+ scaleX: 2,
1844
+ scaleY: 2,
1845
+ hostOffsetX: 0,
1846
+ hostOffsetY: 0,
1847
+ rowIndex: 100,
1848
+ colIndex: 100,
1849
+ options: 'start' as const,
1850
+ itemsLength: 1000,
1851
+ columnCount: 1000,
1852
+ direction: 'both' as const,
1853
+ usableWidth: 500,
1854
+ usableHeight: 500,
1855
+ viewportWidth: 500,
1856
+ viewportHeight: 500,
1857
+ totalWidth: 30000000,
1858
+ totalHeight: 30000000,
1859
+ gap: 0,
1860
+ columnGap: 0,
1861
+ fixedSize: 50,
1862
+ fixedWidth: 50,
1863
+ relativeScrollX: 0,
1864
+ relativeScrollY: 0,
1865
+ getItemSizeY: () => 50,
1866
+ getItemSizeX: () => 50,
1867
+ getItemQueryY: (idx: number) => idx * 50,
1868
+ getItemQueryX: (idx: number) => idx * 50,
1869
+ getColumnSize: () => 50,
1870
+ getColumnQuery: (idx: number) => idx * 50,
1871
+ stickyIndices: [],
1872
+ };
1873
+
1874
+ const result = calculateScrollTarget(params);
1875
+ expect(result.targetY).toBe(5000);
1876
+ expect(result.targetX).toBe(5000);
1877
+ });
1878
+
1879
+ it('correctly clamps targets when scaling is active', () => {
1880
+ const params = {
1881
+ scaleX: 2,
1882
+ scaleY: 2,
1883
+ hostOffsetX: 0,
1884
+ hostOffsetY: 0,
1885
+ rowIndex: 1000000,
1886
+ colIndex: 1000000,
1887
+ options: 'start' as const,
1888
+ itemsLength: 1000,
1889
+ columnCount: 1000,
1890
+ direction: 'both' as const,
1891
+ usableWidth: 500,
1892
+ usableHeight: 500,
1893
+ viewportWidth: 500,
1894
+ viewportHeight: 500,
1895
+ totalWidth: 30000000,
1896
+ totalHeight: 30000000,
1897
+ gap: 0,
1898
+ columnGap: 0,
1899
+ fixedSize: 50,
1900
+ fixedWidth: 50,
1901
+ relativeScrollX: 0,
1902
+ relativeScrollY: 0,
1903
+ getItemSizeY: () => 50,
1904
+ getItemSizeX: () => 50,
1905
+ getItemQueryY: (idx: number) => idx * 50,
1906
+ getItemQueryX: (idx: number) => idx * 50,
1907
+ getColumnSize: () => 50,
1908
+ getColumnQuery: (idx: number) => idx * 50,
1909
+ stickyIndices: [],
1910
+ };
1911
+
1912
+ const result = calculateScrollTarget(params);
1913
+ expect(result.targetY).toBe(19999000);
1914
+ expect(result.targetX).toBe(19999000);
1915
+ });
1916
+
1917
+ it('handles mixed coordinate scaling (x scaled, y not scaled)', () => {
1918
+ const params = {
1919
+ scaleX: 2,
1920
+ scaleY: 1,
1921
+ hostOffsetX: 0,
1922
+ hostOffsetY: 0,
1923
+ rowIndex: 100,
1924
+ colIndex: 100,
1925
+ options: 'start' as const,
1926
+ itemsLength: 1000,
1927
+ columnCount: 1000,
1928
+ direction: 'both' as const,
1929
+ usableWidth: 500,
1930
+ usableHeight: 500,
1931
+ viewportWidth: 500,
1932
+ viewportHeight: 500,
1933
+ totalWidth: 30000000,
1934
+ totalHeight: 10000,
1935
+ gap: 0,
1936
+ columnGap: 0,
1937
+ fixedSize: 50,
1938
+ fixedWidth: 50,
1939
+ relativeScrollX: 0,
1940
+ relativeScrollY: 0,
1941
+ getItemSizeY: () => 50,
1942
+ getItemSizeX: () => 50,
1943
+ getItemQueryY: (idx: number) => idx * 50,
1944
+ getItemQueryX: (idx: number) => idx * 50,
1945
+ getColumnSize: () => 50,
1946
+ getColumnQuery: (idx: number) => idx * 50,
1947
+ stickyIndices: [],
1948
+ };
1949
+
1950
+ const result = calculateScrollTarget(params);
1951
+ expect(result.targetX).toBe(5000);
1952
+ expect(result.targetY).toBe(5000);
1953
+ });
1954
+
1955
+ it('handles mixed coordinate scaling (y scaled, x not scaled)', () => {
1956
+ const params = {
1957
+ scaleX: 1,
1958
+ scaleY: 2,
1959
+ hostOffsetX: 0,
1960
+ hostOffsetY: 0,
1961
+ rowIndex: 100,
1962
+ colIndex: 100,
1963
+ options: 'start' as const,
1964
+ itemsLength: 1000,
1965
+ columnCount: 1000,
1966
+ direction: 'both' as const,
1967
+ usableWidth: 500,
1968
+ usableHeight: 500,
1969
+ viewportWidth: 500,
1970
+ viewportHeight: 500,
1971
+ totalWidth: 10000,
1972
+ totalHeight: 30000000,
1973
+ gap: 0,
1974
+ columnGap: 0,
1975
+ fixedSize: 50,
1976
+ fixedWidth: 50,
1977
+ relativeScrollX: 0,
1978
+ relativeScrollY: 0,
1979
+ getItemSizeY: () => 50,
1980
+ getItemSizeX: () => 50,
1981
+ getItemQueryY: (idx: number) => idx * 50,
1982
+ getItemQueryX: (idx: number) => idx * 50,
1983
+ getColumnSize: () => 50,
1984
+ getColumnQuery: (idx: number) => idx * 50,
1985
+ stickyIndices: [],
1986
+ };
1987
+
1988
+ const result = calculateScrollTarget(params);
1989
+ expect(result.targetX).toBe(5000);
1990
+ expect(result.targetY).toBe(5000);
1991
+ });
1992
+ });
1993
+
1994
+ describe('calculatecolumnrange', () => {
1995
+ it('calculates column range with dynamic width and 0 columns', () => {
1996
+ const result = calculateColumnRange({
1997
+ colBuffer: 0,
1998
+ columnCount: 0,
1999
+ columnGap: 10,
2000
+ fixedWidth: null,
2001
+ findLowerBound: () => 0,
2002
+ query: () => 0,
2003
+ relativeScrollX: 0,
2004
+ totalColsQuery: () => 0,
2005
+ usableWidth: 200,
2006
+ });
2007
+ expect(result.padStart).toBe(0);
2008
+ expect(result.padEnd).toBe(0);
2009
+ });
2010
+
2011
+ it('calculates column range with dynamic width', () => {
2012
+ const result = calculateColumnRange({
2013
+ colBuffer: 0,
2014
+ columnCount: 100,
2015
+ columnGap: 10,
2016
+ fixedWidth: null,
2017
+ findLowerBound: (offset) => Math.floor(offset / 110),
2018
+ query: (idx) => idx * 110,
2019
+ relativeScrollX: 220,
2020
+ totalColsQuery: () => 100 * 110,
2021
+ usableWidth: 200,
2022
+ });
2023
+ expect(result.start).toBe(2);
2024
+ expect(result.end).toBe(4);
2025
+ expect(result.padStart).toBe(220);
2026
+ expect(result.padEnd).toBe(10560);
2027
+ });
2028
+
2029
+ it('calculates column range with dynamic width where safeend is 0', () => {
2030
+ const result = calculateColumnRange({
2031
+ colBuffer: 0,
2032
+ columnCount: 10,
2033
+ columnGap: 10,
2034
+ fixedWidth: null,
2035
+ findLowerBound: () => 0,
2036
+ query: () => 0,
2037
+ relativeScrollX: -1000,
2038
+ totalColsQuery: () => 1090,
2039
+ usableWidth: 100,
2040
+ });
2041
+ expect(result.end).toBe(0);
2042
+ expect(result.padEnd).toBe(1080);
2043
+ });
2044
+
2045
+ it('calculates column range with fixed width where safeend is 0', () => {
2046
+ const result = calculateColumnRange({
2047
+ colBuffer: 0,
2048
+ columnCount: 10,
2049
+ columnGap: 10,
2050
+ fixedWidth: 100,
2051
+ findLowerBound: () => 0,
2052
+ query: () => 0,
2053
+ relativeScrollX: 0,
2054
+ totalColsQuery: () => 1090,
2055
+ usableWidth: 0,
2056
+ });
2057
+ expect(result.end).toBe(0);
2058
+ expect(result.padEnd).toBe(1090);
2059
+ });
2060
+
2061
+ it('calculates column range with fixed width and 1 column', () => {
2062
+ const result = calculateColumnRange({
2063
+ colBuffer: 0,
2064
+ columnCount: 1,
2065
+ columnGap: 10,
2066
+ fixedWidth: 100,
2067
+ findLowerBound: () => 0,
2068
+ query: () => 0,
2069
+ relativeScrollX: 0,
2070
+ totalColsQuery: () => 100,
2071
+ usableWidth: 200,
2072
+ });
2073
+ expect(result.padStart).toBe(0);
2074
+ expect(result.padEnd).toBe(0);
2075
+ });
2076
+
2077
+ it('calculates column range with fixed width and 0 columns', () => {
2078
+ const result = calculateColumnRange({
2079
+ colBuffer: 0,
2080
+ columnCount: 0,
2081
+ columnGap: 10,
2082
+ fixedWidth: 100,
2083
+ findLowerBound: () => 0,
2084
+ query: () => 0,
2085
+ relativeScrollX: 0,
2086
+ totalColsQuery: () => 0,
2087
+ usableWidth: 200,
2088
+ });
2089
+ expect(result.padStart).toBe(0);
2090
+ expect(result.padEnd).toBe(0);
2091
+ });
2092
+
2093
+ it('calculates column range with fixed width', () => {
2094
+ const result = calculateColumnRange({
2095
+ colBuffer: 0,
2096
+ columnCount: 100,
2097
+ columnGap: 10,
2098
+ fixedWidth: 100,
2099
+ findLowerBound: () => 0,
2100
+ query: () => 0,
2101
+ relativeScrollX: 220,
2102
+ totalColsQuery: () => 100 * 110,
2103
+ usableWidth: 200,
2104
+ });
2105
+ expect(result.start).toBe(2);
2106
+ expect(result.end).toBe(4);
2107
+ expect(result.padStart).toBe(220);
2108
+ expect(result.padEnd).toBe(10560);
2109
+ });
2110
+
2111
+ it('returns empty range when columncount is 0', () => {
2112
+ const result = calculateColumnRange({
2113
+ colBuffer: 2,
2114
+ columnCount: 0,
2115
+ columnGap: 0,
2116
+ fixedWidth: null,
2117
+ findLowerBound: () => 0,
2118
+ query: () => 0,
2119
+ relativeScrollX: 0,
2120
+ totalColsQuery: () => 0,
2121
+ usableWidth: 500,
2122
+ });
2123
+ expect(result.end).toBe(0);
2124
+ });
2125
+
2126
+ it('calculates column range', () => {
2127
+ const result = calculateColumnRange({
2128
+ colBuffer: 2,
2129
+ columnCount: 100,
2130
+ columnGap: 0,
2131
+ fixedWidth: null,
2132
+ findLowerBound: (offset) => Math.floor(offset / 100),
2133
+ query: (idx) => idx * 100,
2134
+ relativeScrollX: 1000,
2135
+ totalColsQuery: () => 10000,
2136
+ usableWidth: 500,
2137
+ });
2138
+ expect(result.start).toBe(8);
2139
+ expect(result.end).toBe(17);
2140
+ });
2141
+
2142
+ it('calculates column range reaching the end of columns', () => {
2143
+ const result = calculateColumnRange({
2144
+ colBuffer: 0,
2145
+ columnCount: 10,
2146
+ columnGap: 10,
2147
+ fixedWidth: 100,
2148
+ findLowerBound: () => 0,
2149
+ query: () => 0,
2150
+ relativeScrollX: 1000,
2151
+ totalColsQuery: () => 1090,
2152
+ usableWidth: 500,
2153
+ });
2154
+ expect(result.start).toBe(9);
2155
+ expect(result.end).toBe(10);
2156
+ expect(result.padStart).toBe(990);
2157
+ expect(result.padEnd).toBe(0);
2158
+ });
2159
+
2160
+ it('calculates column range with dynamic width reaching the end of columns', () => {
2161
+ const result = calculateColumnRange({
2162
+ colBuffer: 0,
2163
+ columnCount: 10,
2164
+ columnGap: 10,
2165
+ fixedWidth: null,
2166
+ findLowerBound: (offset) => Math.floor(offset / 110),
2167
+ query: (idx) => idx * 110,
2168
+ relativeScrollX: 1000,
2169
+ totalColsQuery: () => 10 * 110,
2170
+ usableWidth: 500,
2171
+ });
2172
+ expect(result.start).toBe(9);
2173
+ expect(result.end).toBe(10);
2174
+ expect(result.padStart).toBe(990);
2175
+ expect(result.padEnd).toBe(0);
2176
+ });
2177
+ });
2178
+
2179
+ describe('calculateitemposition', () => {
2180
+ it('calculates position for vertical item with fixed size', () => {
2181
+ const result = calculateItemPosition({
2182
+ columnGap: 0,
2183
+ direction: 'vertical',
2184
+ fixedSize: 50,
2185
+ gap: 10,
2186
+ getSizeX: () => 0,
2187
+ getSizeY: () => 50,
2188
+ index: 10,
2189
+ queryX: () => 0,
2190
+ queryY: () => 0,
2191
+ totalWidth: 500,
2192
+ usableHeight: 500,
2193
+ usableWidth: 500,
2194
+ });
2195
+ expect(result.y).toBe(600);
2196
+ expect(result.height).toBe(50);
2197
+ expect(result.width).toBe(500);
2198
+ });
2199
+
2200
+ it('calculates position for vertical item with dynamic size', () => {
2201
+ const result = calculateItemPosition({
2202
+ columnGap: 0,
2203
+ direction: 'vertical',
2204
+ fixedSize: null,
2205
+ gap: 10,
2206
+ getSizeX: () => 0,
2207
+ getSizeY: () => 60,
2208
+ index: 10,
2209
+ queryX: () => 0,
2210
+ queryY: (idx) => idx * 60,
2211
+ totalWidth: 500,
2212
+ usableHeight: 500,
2213
+ usableWidth: 500,
2214
+ });
2215
+ expect(result.y).toBe(600);
2216
+ expect(result.height).toBe(50);
2217
+ expect(result.width).toBe(500);
2218
+ });
2219
+
2220
+ it('calculates position for horizontal item with fixed size', () => {
2221
+ const result = calculateItemPosition({
2222
+ columnGap: 10,
2223
+ direction: 'horizontal',
2224
+ fixedSize: 50,
2225
+ gap: 0,
2226
+ getSizeX: () => 50,
2227
+ getSizeY: () => 0,
2228
+ index: 10,
2229
+ queryX: () => 0,
2230
+ queryY: () => 0,
2231
+ totalWidth: 5000,
2232
+ usableHeight: 500,
2233
+ usableWidth: 500,
2234
+ });
2235
+ expect(result.x).toBe(600);
2236
+ expect(result.width).toBe(50);
2237
+ expect(result.height).toBe(500);
2238
+ });
2239
+
2240
+ it('calculates position for horizontal item with dynamic size', () => {
2241
+ const result = calculateItemPosition({
2242
+ columnGap: 10,
2243
+ direction: 'horizontal',
2244
+ fixedSize: null,
2245
+ gap: 0,
2246
+ getSizeX: () => 60,
2247
+ getSizeY: () => 0,
2248
+ index: 10,
2249
+ queryX: (idx) => idx * 60,
2250
+ queryY: () => 0,
2251
+ totalWidth: 5000,
2252
+ usableHeight: 500,
2253
+ usableWidth: 500,
2254
+ });
2255
+ expect(result.x).toBe(600);
2256
+ expect(result.width).toBe(50);
2257
+ expect(result.height).toBe(500);
2258
+ });
2259
+
2260
+ it('calculates position for grid (both) item with dynamic size', () => {
2261
+ const result = calculateItemPosition({
2262
+ columnGap: 10,
2263
+ direction: 'both',
2264
+ fixedSize: null,
2265
+ gap: 10,
2266
+ getSizeX: () => 0,
2267
+ getSizeY: () => 60,
2268
+ index: 10,
2269
+ queryX: () => 0,
2270
+ queryY: (idx) => idx * 60,
2271
+ totalWidth: 5000,
2272
+ usableHeight: 500,
2273
+ usableWidth: 500,
2274
+ });
2275
+ expect(result.y).toBe(600);
2276
+ expect(result.height).toBe(50);
2277
+ expect(result.width).toBe(5000);
2278
+ });
2279
+ });
2280
+
2281
+ describe('calculatestickyitem', () => {
2282
+ it('calculates sticky offset when pushing (vertical, dynamic size)', () => {
2283
+ const result = calculateStickyItem({
2284
+ columnGap: 0,
2285
+ direction: 'vertical',
2286
+ fixedSize: null,
2287
+ fixedWidth: null,
2288
+ gap: 0,
2289
+ getItemQueryX: () => 0,
2290
+ getItemQueryY: (idx) => idx * 50,
2291
+ height: 50,
2292
+ index: 0,
2293
+ isSticky: true,
2294
+ originalX: 0,
2295
+ originalY: 0,
2296
+ relativeScrollX: 0,
2297
+ relativeScrollY: 480,
2298
+ stickyIndices: [ 0, 10 ],
2299
+ width: 500,
2300
+ });
2301
+ expect(result.isStickyActive).toBe(true);
2302
+ expect(result.stickyOffset.y).toBe(-30);
2303
+ });
2304
+
2305
+ it('calculates sticky offset when pushing (horizontal, fixed size)', () => {
2306
+ const result = calculateStickyItem({
2307
+ columnGap: 0,
2308
+ direction: 'horizontal',
2309
+ fixedSize: 50,
2310
+ fixedWidth: null,
2311
+ gap: 0,
2312
+ getItemQueryX: (idx) => idx * 50,
2313
+ getItemQueryY: () => 0,
2314
+ height: 500,
2315
+ index: 0,
2316
+ isSticky: true,
2317
+ originalX: 0,
2318
+ originalY: 0,
2319
+ relativeScrollX: 480,
2320
+ relativeScrollY: 0,
2321
+ stickyIndices: [ 0, 10 ],
2322
+ width: 50,
2323
+ });
2324
+ expect(result.isStickyActive).toBe(true);
2325
+ expect(result.stickyOffset.x).toBe(-30);
2326
+ });
2327
+
2328
+ it('is not sticky if scroll is before original position (horizontal)', () => {
2329
+ const result = calculateStickyItem({
2330
+ columnGap: 0,
2331
+ direction: 'horizontal',
2332
+ fixedSize: 50,
2333
+ fixedWidth: 100,
2334
+ gap: 0,
2335
+ getItemQueryX: (idx) => idx * 100,
2336
+ getItemQueryY: (idx) => idx * 50,
2337
+ height: 50,
2338
+ index: 1,
2339
+ isSticky: true,
2340
+ originalX: 100,
2341
+ originalY: 0,
2342
+ relativeScrollX: 50,
2343
+ relativeScrollY: 0,
2344
+ stickyIndices: [ 1 ],
2345
+ width: 100,
2346
+ });
2347
+ expect(result.isStickyActive).toBe(false);
2348
+ });
2349
+
2350
+ it('does not calculate horizontal sticky if vertical is already active in grid mode', () => {
2351
+ const result = calculateStickyItem({
2352
+ columnGap: 0,
2353
+ direction: 'both',
2354
+ fixedSize: 50,
2355
+ fixedWidth: 100,
2356
+ gap: 0,
2357
+ getItemQueryX: (idx) => idx * 100,
2358
+ getItemQueryY: (idx) => idx * 50,
2359
+ height: 50,
2360
+ index: 0,
2361
+ isSticky: true,
2362
+ originalX: 0,
2363
+ originalY: 0,
2364
+ relativeScrollX: 10,
2365
+ relativeScrollY: 10,
2366
+ stickyIndices: [ 0 ],
2367
+ width: 100,
2368
+ });
2369
+ expect(result.isStickyActive).toBe(true);
2370
+ expect(result.stickyOffset.y).toBe(0);
2371
+ expect(result.stickyOffset.x).toBe(0);
2372
+ });
2373
+
2374
+ it('calculates sticky active state for both directions (horizontal first)', () => {
2375
+ const result = calculateStickyItem({
2376
+ columnGap: 0,
2377
+ direction: 'both',
2378
+ fixedSize: 50,
2379
+ fixedWidth: 100,
2380
+ gap: 0,
2381
+ getItemQueryX: (idx) => idx * 100,
2382
+ getItemQueryY: (idx) => idx * 50,
2383
+ height: 50,
2384
+ index: 0,
2385
+ isSticky: true,
2386
+ originalX: 0,
2387
+ originalY: 0,
2388
+ relativeScrollX: 10,
2389
+ relativeScrollY: 0,
2390
+ stickyIndices: [ 0 ],
2391
+ width: 100,
2392
+ });
2393
+ expect(result.isStickyActive).toBe(true);
2394
+ expect(result.stickyOffset.x).toBe(0);
2395
+ expect(result.stickyOffset.y).toBe(0);
2396
+ });
2397
+
2398
+ it('calculates sticky active state when past next item (grid, fixed size)', () => {
2399
+ const result = calculateStickyItem({
2400
+ columnGap: 10,
2401
+ direction: 'both',
2402
+ fixedSize: 50,
2403
+ fixedWidth: null,
2404
+ gap: 10,
2405
+ getItemQueryX: (idx) => idx * 60,
2406
+ getItemQueryY: (idx) => idx * 60,
2407
+ height: 50,
2408
+ index: 0,
2409
+ isSticky: true,
2410
+ originalX: 0,
2411
+ originalY: 0,
2412
+ relativeScrollX: 60,
2413
+ relativeScrollY: 0,
2414
+ stickyIndices: [ 0, 1 ],
2415
+ width: 50,
2416
+ });
2417
+ expect(result.isStickyActive).toBe(false);
2418
+ });
2419
+
2420
+ it('calculates sticky active state when past next item (grid, fixed width)', () => {
2421
+ const result = calculateStickyItem({
2422
+ columnGap: 10,
2423
+ direction: 'both',
2424
+ fixedSize: 50,
2425
+ fixedWidth: 100,
2426
+ gap: 10,
2427
+ getItemQueryX: (idx) => idx * 110,
2428
+ getItemQueryY: (idx) => idx * 60,
2429
+ height: 50,
2430
+ index: 0,
2431
+ isSticky: true,
2432
+ originalX: 0,
2433
+ originalY: 0,
2434
+ relativeScrollX: 110,
2435
+ relativeScrollY: 0,
2436
+ stickyIndices: [ 0, 1 ],
2437
+ width: 100,
2438
+ });
2439
+ expect(result.isStickyActive).toBe(false);
2440
+ });
2441
+
2442
+ it('calculates sticky active state when past next item (horizontal, fixed width)', () => {
2443
+ const result = calculateStickyItem({
2444
+ columnGap: 0,
2445
+ direction: 'horizontal',
2446
+ fixedSize: null,
2447
+ fixedWidth: 50,
2448
+ gap: 0,
2449
+ getItemQueryX: (idx) => idx * 50,
2450
+ getItemQueryY: () => 0,
2451
+ height: 500,
2452
+ index: 0,
2453
+ isSticky: true,
2454
+ originalX: 0,
2455
+ originalY: 0,
2456
+ relativeScrollX: 600,
2457
+ relativeScrollY: 0,
2458
+ stickyIndices: [ 0, 10 ],
2459
+ width: 50,
2460
+ });
2461
+ expect(result.isStickyActive).toBe(false);
2462
+ });
2463
+
2464
+ it('calculates sticky active state when past next item (horizontal, fixed size)', () => {
2465
+ const result = calculateStickyItem({
2466
+ columnGap: 0,
2467
+ direction: 'horizontal',
2468
+ fixedSize: 50,
2469
+ fixedWidth: null,
2470
+ gap: 0,
2471
+ getItemQueryX: (idx) => idx * 50,
2472
+ getItemQueryY: () => 0,
2473
+ height: 500,
2474
+ index: 0,
2475
+ isSticky: true,
2476
+ originalX: 0,
2477
+ originalY: 0,
2478
+ relativeScrollX: 600,
2479
+ relativeScrollY: 0,
2480
+ stickyIndices: [ 0, 10 ],
2481
+ width: 50,
2482
+ });
2483
+ expect(result.isStickyActive).toBe(false);
2484
+ });
2485
+
2486
+ it('calculates sticky active state when no next sticky item (horizontal)', () => {
2487
+ const result = calculateStickyItem({
2488
+ columnGap: 0,
2489
+ direction: 'horizontal',
2490
+ fixedSize: 50,
2491
+ fixedWidth: null,
2492
+ gap: 0,
2493
+ getItemQueryX: (idx) => idx * 50,
2494
+ getItemQueryY: () => 0,
2495
+ height: 500,
2496
+ index: 10,
2497
+ isSticky: true,
2498
+ originalX: 500,
2499
+ originalY: 0,
2500
+ relativeScrollX: 600,
2501
+ relativeScrollY: 0,
2502
+ stickyIndices: [ 0, 10 ],
2503
+ width: 50,
2504
+ });
2505
+ expect(result.isStickyActive).toBe(true);
2506
+ expect(result.stickyOffset.x).toBe(0);
2507
+ });
2508
+
2509
+ it('calculates sticky active state when no next sticky item (vertical)', () => {
2510
+ const result = calculateStickyItem({
2511
+ columnGap: 0,
2512
+ direction: 'vertical',
2513
+ fixedSize: 50,
2514
+ fixedWidth: null,
2515
+ gap: 0,
2516
+ getItemQueryX: () => 0,
2517
+ getItemQueryY: (idx) => idx * 50,
2518
+ height: 50,
2519
+ index: 10,
2520
+ isSticky: true,
2521
+ originalX: 0,
2522
+ originalY: 500,
2523
+ relativeScrollX: 0,
2524
+ relativeScrollY: 600,
2525
+ stickyIndices: [ 0, 10 ],
2526
+ width: 500,
2527
+ });
2528
+ expect(result.isStickyActive).toBe(true);
2529
+ expect(result.stickyOffset.y).toBe(0);
2530
+ });
2531
+
2532
+ it('ensures only one sticky item is active at a time in a sequence', () => {
2533
+ const stickyIndices = [ 0, 1, 2, 3, 4 ];
2534
+ const scrollY = 75;
2535
+
2536
+ const results = stickyIndices.map((idx) => calculateStickyItem({
2537
+ columnGap: 0,
2538
+ direction: 'vertical',
2539
+ fixedSize: 50,
2540
+ fixedWidth: null,
2541
+ gap: 0,
2542
+ getItemQueryX: () => 0,
2543
+ getItemQueryY: (i) => i * 50,
2544
+ height: 50,
2545
+ index: idx,
2546
+ isSticky: true,
2547
+ originalX: 0,
2548
+ originalY: idx * 50,
2549
+ relativeScrollX: 0,
2550
+ relativeScrollY: scrollY,
2551
+ stickyIndices,
2552
+ width: 500,
2553
+ }));
2554
+
2555
+ const activeIndices = results.map((r, i) => r.isStickyActive ? i : -1).filter((i) => i !== -1);
2556
+ expect(activeIndices).toEqual([ 1 ]);
2557
+ });
2558
+
2559
+ it('does not make non-sticky items active sticky', () => {
2560
+ const result = calculateStickyItem({
2561
+ columnGap: 0,
2562
+ direction: 'vertical',
2563
+ fixedSize: 50,
2564
+ fixedWidth: null,
2565
+ gap: 0,
2566
+ getItemQueryX: () => 0,
2567
+ getItemQueryY: (idx) => idx * 50,
2568
+ height: 50,
2569
+ index: 5,
2570
+ isSticky: false,
2571
+ originalX: 0,
2572
+ originalY: 250,
2573
+ relativeScrollX: 0,
2574
+ relativeScrollY: 300,
2575
+ stickyIndices: [ 0, 10 ],
2576
+ width: 500,
2577
+ });
2578
+ expect(result.isStickyActive).toBe(false);
2579
+ });
2580
+ });
2581
+
2582
+ describe('calculateitemstyle', () => {
2583
+ it('calculates style for table container', () => {
2584
+ const result = calculateItemStyle({
2585
+ containerTag: 'table',
2586
+ direction: 'vertical',
2587
+ isHydrated: true,
2588
+ isRtl: false,
2589
+ item: {
2590
+ index: 10,
2591
+ isStickyActive: false,
2592
+ offset: { x: 0, y: 600 },
2593
+ size: { height: 50, width: 500 },
2594
+ stickyOffset: { x: 0, y: 0 },
2595
+ } as unknown as RenderedItem<unknown>,
2596
+ itemSize: 50,
2597
+ paddingStartX: 0,
2598
+ paddingStartY: 0,
2599
+ });
2600
+ expect(result.minInlineSize).toBe('100%');
2601
+ });
2602
+
2603
+ it('calculates style for dynamic item size', () => {
2604
+ const result = calculateItemStyle({
2605
+ containerTag: 'div',
2606
+ direction: 'vertical',
2607
+ isHydrated: true,
2608
+ isRtl: false,
2609
+ item: {
2610
+ index: 10,
2611
+ isStickyActive: false,
2612
+ offset: { x: 0, y: 600 },
2613
+ size: { height: 50, width: 500 },
2614
+ stickyOffset: { x: 0, y: 0 },
2615
+ } as unknown as RenderedItem<unknown>,
2616
+ itemSize: 0,
2617
+ paddingStartX: 0,
2618
+ paddingStartY: 0,
2619
+ });
2620
+ expect(result.blockSize).toBe('auto');
2621
+ expect(result.minBlockSize).toBe('1px');
2622
+ });
2623
+
2624
+ it('calculates style for dynamic item size (horizontal)', () => {
2625
+ const result = calculateItemStyle({
2626
+ containerTag: 'div',
2627
+ direction: 'horizontal',
2628
+ isHydrated: true,
2629
+ isRtl: false,
2630
+ item: {
2631
+ index: 10,
2632
+ isStickyActive: false,
2633
+ offset: { x: 600, y: 0 },
2634
+ size: { height: 500, width: 50 },
2635
+ stickyOffset: { x: 0, y: 0 },
2636
+ } as unknown as RenderedItem<unknown>,
2637
+ itemSize: 0,
2638
+ paddingStartX: 0,
2639
+ paddingStartY: 0,
2640
+ });
2641
+ expect(result.inlineSize).toBe('auto');
2642
+ expect(result.minInlineSize).toBe('1px');
2643
+ });
2644
+
2645
+ it('calculates style for sticky item (vertical only)', () => {
2646
+ const result = calculateItemStyle({
2647
+ containerTag: 'div',
2648
+ direction: 'vertical',
2649
+ isHydrated: true,
2650
+ isRtl: false,
2651
+ item: {
2652
+ index: 10,
2653
+ isStickyActive: true,
2654
+ offset: { x: 0, y: 600 },
2655
+ size: { height: 50, width: 500 },
2656
+ stickyOffset: { x: 0, y: -10 },
2657
+ } as unknown as RenderedItem<unknown>,
2658
+ itemSize: 50,
2659
+ paddingStartX: 10,
2660
+ paddingStartY: 10,
2661
+ });
2662
+ expect(result.insetBlockStart).toBe('10px');
2663
+ expect(result.insetInlineStart).toBeUndefined();
2664
+ });
2665
+
2666
+ it('calculates style for sticky item (grid both directions)', () => {
2667
+ const result = calculateItemStyle({
2668
+ containerTag: 'div',
2669
+ direction: 'both',
2670
+ isHydrated: true,
2671
+ isRtl: false,
2672
+ item: {
2673
+ index: 10,
2674
+ isStickyActive: true,
2675
+ offset: { x: 600, y: 600 },
2676
+ size: { height: 50, width: 50 },
2677
+ stickyOffset: { x: -10, y: -10 },
2678
+ } as unknown as RenderedItem<unknown>,
2679
+ itemSize: 50,
2680
+ paddingStartX: 10,
2681
+ paddingStartY: 10,
2682
+ });
2683
+ expect(result.insetBlockStart).toBe('10px');
2684
+ expect(result.insetInlineStart).toBe('10px');
2685
+ });
2686
+
2687
+ it('calculates style for sticky item (grid)', () => {
2688
+ const result = calculateItemStyle({
2689
+ containerTag: 'div',
2690
+ direction: 'both',
2691
+ isHydrated: true,
2692
+ isRtl: false,
2693
+ item: {
2694
+ index: 10,
2695
+ isStickyActive: true,
2696
+ offset: { x: 600, y: 600 },
2697
+ size: { height: 50, width: 50 },
2698
+ stickyOffset: { x: -10, y: -10 },
2699
+ } as unknown as RenderedItem<unknown>,
2700
+ itemSize: 50,
2701
+ paddingStartX: 10,
2702
+ paddingStartY: 10,
2703
+ });
2704
+ expect(result.insetBlockStart).toBe('10px');
2705
+ expect(result.insetInlineStart).toBe('10px');
2706
+ expect(result.transform).toBe('translate(-10px, -10px)');
2707
+ });
2708
+
2709
+ it('calculates style for non-hydrated item', () => {
2710
+ const result = calculateItemStyle({
2711
+ containerTag: 'div',
2712
+ direction: 'vertical',
2713
+ isHydrated: false,
2714
+ isRtl: false,
2715
+ item: {
2716
+ index: 10,
2717
+ isStickyActive: false,
2718
+ offset: { x: 0, y: 600 },
2719
+ size: { height: 50, width: 500 },
2720
+ stickyOffset: { x: 0, y: 0 },
2721
+ } as unknown as RenderedItem<unknown>,
2722
+ itemSize: 50,
2723
+ paddingStartX: 0,
2724
+ paddingStartY: 0,
2725
+ });
2726
+ expect(result.transform).toBeUndefined();
2727
+ });
2728
+
2729
+ it('calculates style for sticky item (horizontal)', () => {
2730
+ const result = calculateItemStyle({
2731
+ containerTag: 'div',
2732
+ direction: 'horizontal',
2733
+ isHydrated: true,
2734
+ isRtl: false,
2735
+ item: {
2736
+ index: 10,
2737
+ isStickyActive: true,
2738
+ offset: { x: 600, y: 0 },
2739
+ size: { height: 500, width: 50 },
2740
+ stickyOffset: { x: -10, y: 0 },
2741
+ } as unknown as RenderedItem<unknown>,
2742
+ itemSize: 50,
2743
+ paddingStartX: 10,
2744
+ paddingStartY: 10,
2745
+ });
2746
+ expect(result.insetInlineStart).toBe('10px');
2747
+ expect(result.transform).toBe('translate(-10px, 0px)');
2748
+ });
2749
+
2750
+ it('calculates style for sticky item with padding', () => {
2751
+ const result = calculateItemStyle({
2752
+ containerTag: 'div',
2753
+ direction: 'both',
2754
+ isHydrated: true,
2755
+ isRtl: false,
2756
+ item: {
2757
+ index: 10,
2758
+ isStickyActive: true,
2759
+ offset: { x: 600, y: 600 },
2760
+ size: { height: 50, width: 50 },
2761
+ stickyOffset: { x: -10, y: -20 },
2762
+ } as unknown as RenderedItem<unknown>,
2763
+ itemSize: 50,
2764
+ paddingStartX: 10,
2765
+ paddingStartY: 20,
2766
+ });
2767
+ expect(result.insetBlockStart).toBe('20px');
2768
+ expect(result.insetInlineStart).toBe('10px');
2769
+ expect(result.transform).toBe('translate(-10px, -20px)');
2770
+ });
2771
+
2772
+ it('correctly inverts transform in rtl mode', () => {
2773
+ const item: RenderedItem = {
2774
+ index: 0,
2775
+ item: {},
2776
+ offset: { x: 100, y: 200 },
2777
+ originalX: 100,
2778
+ originalY: 200,
2779
+ size: { height: 50, width: 100 },
2780
+ stickyOffset: { x: 10, y: 20 },
2781
+ };
2782
+
2783
+ // LTR
2784
+ let result = calculateItemStyle({
2785
+ containerTag: 'div',
2786
+ direction: 'vertical',
2787
+ isHydrated: true,
2788
+ isRtl: false,
2789
+ item,
2790
+ itemSize: 50,
2791
+ paddingStartX: 0,
2792
+ paddingStartY: 0,
2793
+ });
2794
+ expect(result.transform).toBe('translate(100px, 200px)');
2795
+
2796
+ // RTL
2797
+ result = calculateItemStyle({
2798
+ containerTag: 'div',
2799
+ direction: 'vertical',
2800
+ isHydrated: true,
2801
+ isRtl: true,
2802
+ item,
2803
+ itemSize: 50,
2804
+ paddingStartX: 0,
2805
+ paddingStartY: 0,
2806
+ });
2807
+ expect(result.transform).toBe('translate(-100px, 200px)');
2808
+
2809
+ // RTL sticky
2810
+ result = calculateItemStyle({
2811
+ containerTag: 'div',
2812
+ direction: 'vertical',
2813
+ isHydrated: true,
2814
+ isRtl: true,
2815
+ item: { ...item, isStickyActive: true },
2816
+ itemSize: 50,
2817
+ paddingStartX: 0,
2818
+ paddingStartY: 0,
2819
+ });
2820
+ expect(result.transform).toBe('translate(-10px, 20px)');
2821
+ });
2822
+
2823
+ it('maintains 1:1 movement even when scale is high', () => {
2824
+ const item: RenderedItem<unknown> = {
2825
+ index: 100,
2826
+ isSticky: false,
2827
+ isStickyActive: false,
2828
+ item: {},
2829
+ offset: { x: 0, y: 600 },
2830
+ originalX: 0,
2831
+ originalY: 5100,
2832
+ size: { height: 50, width: 100 },
2833
+ stickyOffset: { x: 0, y: 0 },
2834
+ };
2835
+
2836
+ const style = calculateItemStyle({
2837
+ containerTag: 'div',
2838
+ direction: 'vertical',
2839
+ isHydrated: true,
2840
+ isRtl: false,
2841
+ item,
2842
+ itemSize: 50,
2843
+ paddingStartX: 0,
2844
+ paddingStartY: 0,
2845
+ });
2846
+
2847
+ expect(style.transform).toBe('translate(0px, 600px)');
2848
+ });
2849
+ });
2850
+ });