@pdanpdan/virtual-scroll 0.4.0 → 0.6.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.
@@ -13,7 +13,7 @@ import {
13
13
  } from './virtual-scroll-logic';
14
14
 
15
15
  describe('virtual-scroll-logic', () => {
16
- describe('calculateTotalSize', () => {
16
+ describe('calculate total size', () => {
17
17
  it('calculates vertical total size with fixed size', () => {
18
18
  const result = calculateTotalSize({
19
19
  columnCount: 0,
@@ -29,10 +29,31 @@ describe('virtual-scroll-logic', () => {
29
29
  usableHeight: 500,
30
30
  usableWidth: 500,
31
31
  });
32
+ // 50 * 100 + 10 * 99
32
33
  expect(result.height).toBe(5990);
33
34
  expect(result.width).toBe(500);
34
35
  });
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
+
36
57
  it('calculates horizontal total size with fixed size', () => {
37
58
  const result = calculateTotalSize({
38
59
  columnCount: 0,
@@ -52,23 +73,23 @@ describe('virtual-scroll-logic', () => {
52
73
  expect(result.height).toBe(500);
53
74
  });
54
75
 
55
- it('calculates grid (both) total size with fixed row size and dynamic column width', () => {
76
+ it('calculates horizontal total size with dynamic sizes', () => {
56
77
  const result = calculateTotalSize({
57
- columnCount: 5,
58
- columnGap: 5,
59
- direction: 'both',
60
- fixedSize: 50,
78
+ columnCount: 0,
79
+ columnGap: 10,
80
+ direction: 'horizontal',
81
+ fixedSize: null,
61
82
  fixedWidth: null,
62
- gap: 10,
83
+ gap: 0,
63
84
  itemsLength: 100,
64
- queryColumn: (idx) => idx * 105,
65
- queryX: () => 0,
85
+ queryColumn: () => 0,
86
+ queryX: (idx) => idx * 60,
66
87
  queryY: () => 0,
67
88
  usableHeight: 500,
68
89
  usableWidth: 500,
69
90
  });
70
- expect(result.height).toBe(5990);
71
- expect(result.width).toBe(520);
91
+ expect(result.width).toBe(5990);
92
+ expect(result.height).toBe(500);
72
93
  });
73
94
 
74
95
  it('calculates grid (both) total size with fixed sizes', () => {
@@ -90,18 +111,18 @@ describe('virtual-scroll-logic', () => {
90
111
  expect(result.width).toBe(520);
91
112
  });
92
113
 
93
- it('calculates grid (both) total size with dynamic sizes', () => {
114
+ it('calculates grid (both) total size with fixed row size and dynamic column width', () => {
94
115
  const result = calculateTotalSize({
95
116
  columnCount: 5,
96
117
  columnGap: 5,
97
118
  direction: 'both',
98
- fixedSize: null,
119
+ fixedSize: 50,
99
120
  fixedWidth: null,
100
121
  gap: 10,
101
122
  itemsLength: 100,
102
123
  queryColumn: (idx) => idx * 105,
103
124
  queryX: () => 0,
104
- queryY: (idx) => idx * 60,
125
+ queryY: () => 0,
105
126
  usableHeight: 500,
106
127
  usableWidth: 500,
107
128
  });
@@ -109,40 +130,23 @@ describe('virtual-scroll-logic', () => {
109
130
  expect(result.width).toBe(520);
110
131
  });
111
132
 
112
- it('calculates horizontal total size with dynamic sizes', () => {
133
+ it('calculates grid (both) total size with dynamic sizes', () => {
113
134
  const result = calculateTotalSize({
114
- columnCount: 0,
115
- columnGap: 10,
116
- direction: 'horizontal',
135
+ columnCount: 5,
136
+ columnGap: 5,
137
+ direction: 'both',
117
138
  fixedSize: null,
118
139
  fixedWidth: null,
119
- gap: 0,
140
+ gap: 10,
120
141
  itemsLength: 100,
121
- queryColumn: () => 0,
122
- queryX: (idx) => idx * 60,
123
- queryY: () => 0,
124
- usableHeight: 500,
125
- usableWidth: 500,
126
- });
127
- expect(result.width).toBe(5990);
128
- });
129
-
130
- it('calculates vertical total size with dynamic sizes', () => {
131
- const result = calculateTotalSize({
132
- columnCount: 0,
133
- columnGap: 0,
134
- direction: 'vertical',
135
- fixedSize: null,
136
- fixedWidth: null,
137
- gap: 5,
138
- itemsLength: 10,
139
- queryColumn: () => 0,
142
+ queryColumn: (idx) => idx * 105,
140
143
  queryX: () => 0,
141
- queryY: (idx) => idx * 45,
144
+ queryY: (idx) => idx * 60,
142
145
  usableHeight: 500,
143
146
  usableWidth: 500,
144
147
  });
145
- expect(result.height).toBe(445);
148
+ expect(result.height).toBe(5990);
149
+ expect(result.width).toBe(520);
146
150
  });
147
151
 
148
152
  it('calculates total sizes for single item (both, fixed rows, fixed cols)', () => {
@@ -160,8 +164,6 @@ describe('virtual-scroll-logic', () => {
160
164
  usableHeight: 500,
161
165
  usableWidth: 500,
162
166
  });
163
- // columnCount=1 * (100 + 10) - 10 = 100.
164
- // itemsLength=1 * (50 + 10) - 10 = 50.
165
167
  expect(result.height).toBe(500);
166
168
  expect(result.width).toBe(500);
167
169
  });
@@ -199,7 +201,6 @@ describe('virtual-scroll-logic', () => {
199
201
  usableHeight: 500,
200
202
  usableWidth: 500,
201
203
  });
202
- // queryX(1) = 60. gap = 10. total = 60 - 10 = 50.
203
204
  expect(result.width).toBe(50);
204
205
  });
205
206
 
@@ -218,11 +219,10 @@ describe('virtual-scroll-logic', () => {
218
219
  usableHeight: 500,
219
220
  usableWidth: 500,
220
221
  });
221
- // queryY(1) = 60. gap = 10. total = 60 - 10 = 50.
222
222
  expect(result.height).toBe(50);
223
223
  });
224
224
 
225
- it('calculates total height for single small item (vertical, dynamic size, itemsLength 1)', () => {
225
+ it('calculates total height for single small item (vertical, dynamic size, itemslength 1)', () => {
226
226
  const result = calculateTotalSize({
227
227
  columnCount: 0,
228
228
  columnGap: 0,
@@ -240,7 +240,7 @@ describe('virtual-scroll-logic', () => {
240
240
  expect(result.height).toBe(0);
241
241
  });
242
242
 
243
- it('calculates total width for single small item (horizontal, dynamic size, itemsLength 1)', () => {
243
+ it('calculates total width for single small item (horizontal, dynamic size, itemslength 1)', () => {
244
244
  const result = calculateTotalSize({
245
245
  columnCount: 0,
246
246
  columnGap: 10,
@@ -258,7 +258,7 @@ describe('virtual-scroll-logic', () => {
258
258
  expect(result.width).toBe(0);
259
259
  });
260
260
 
261
- it('calculates total height for single item (both, dynamic size, queryY)', () => {
261
+ it('calculates total height for single item (both, dynamic size, queryy)', () => {
262
262
  const result = calculateTotalSize({
263
263
  columnCount: 1,
264
264
  columnGap: 10,
@@ -315,14 +315,14 @@ describe('virtual-scroll-logic', () => {
315
315
  expect(result.width).toBe(500);
316
316
  });
317
317
 
318
- it('returns 0 for empty items with fixed sizes (horizontal)', () => {
318
+ it('returns viewport size for empty items with dynamic sizes (both)', () => {
319
319
  const result = calculateTotalSize({
320
320
  columnCount: 0,
321
321
  columnGap: 10,
322
- direction: 'horizontal',
323
- fixedSize: 50,
322
+ direction: 'both',
323
+ fixedSize: null,
324
324
  fixedWidth: null,
325
- gap: 0,
325
+ gap: 10,
326
326
  itemsLength: 0,
327
327
  queryColumn: () => 0,
328
328
  queryX: () => 0,
@@ -330,17 +330,18 @@ describe('virtual-scroll-logic', () => {
330
330
  usableHeight: 500,
331
331
  usableWidth: 500,
332
332
  });
333
- expect(result.width).toBe(0);
333
+ expect(result.height).toBe(500);
334
+ expect(result.width).toBe(500);
334
335
  });
335
336
 
336
- it('returns 0 for empty items with fixed sizes (vertical)', () => {
337
+ it('returns 0 for empty items with fixed sizes (horizontal)', () => {
337
338
  const result = calculateTotalSize({
338
339
  columnCount: 0,
339
- columnGap: 0,
340
- direction: 'vertical',
340
+ columnGap: 10,
341
+ direction: 'horizontal',
341
342
  fixedSize: 50,
342
343
  fixedWidth: null,
343
- gap: 10,
344
+ gap: 0,
344
345
  itemsLength: 0,
345
346
  queryColumn: () => 0,
346
347
  queryX: () => 0,
@@ -348,15 +349,15 @@ describe('virtual-scroll-logic', () => {
348
349
  usableHeight: 500,
349
350
  usableWidth: 500,
350
351
  });
351
- expect(result.height).toBe(0);
352
+ expect(result.width).toBe(0);
352
353
  });
353
354
 
354
- it('returns viewport size for empty items with dynamic sizes (both)', () => {
355
+ it('returns 0 for empty items with fixed sizes (vertical)', () => {
355
356
  const result = calculateTotalSize({
356
357
  columnCount: 0,
357
- columnGap: 10,
358
- direction: 'both',
359
- fixedSize: null,
358
+ columnGap: 0,
359
+ direction: 'vertical',
360
+ fixedSize: 50,
360
361
  fixedWidth: null,
361
362
  gap: 10,
362
363
  itemsLength: 0,
@@ -366,8 +367,7 @@ describe('virtual-scroll-logic', () => {
366
367
  usableHeight: 500,
367
368
  usableWidth: 500,
368
369
  });
369
- expect(result.height).toBe(500);
370
- expect(result.width).toBe(500);
370
+ expect(result.height).toBe(0);
371
371
  });
372
372
 
373
373
  it('returns 0 for empty items with dynamic sizes (horizontal)', () => {
@@ -407,7 +407,7 @@ describe('virtual-scroll-logic', () => {
407
407
  });
408
408
  });
409
409
 
410
- describe('calculateRange', () => {
410
+ describe('calculate range', () => {
411
411
  it('calculates vertical range with dynamic size', () => {
412
412
  const result = calculateRange({
413
413
  bufferAfter: 0,
@@ -448,11 +448,6 @@ describe('virtual-scroll-logic', () => {
448
448
  usableHeight: 500,
449
449
  usableWidth: 100,
450
450
  });
451
- // item 0: 0-50, gap 50-60
452
- // item 1: 60-110, gap 110-120
453
- // item 2: 120-170, gap 170-180
454
- // scroll 120 -> item 2.
455
- // viewport 100 -> ends at 220. item 3: 180-230.
456
451
  expect(result.start).toBe(2);
457
452
  expect(result.end).toBe(4);
458
453
  });
@@ -502,14 +497,6 @@ describe('virtual-scroll-logic', () => {
502
497
  });
503
498
 
504
499
  it('calculates horizontal range with dynamic size where end item is partially visible (edge case)', () => {
505
- // Setup:
506
- // Item 0: 0-100
507
- // Item 1: 100-200
508
- // Viewport: 0-150.
509
- // Target end = 150.
510
- // findLowerBoundX(150) -> returns 1 because queryX(1)=100 <= 150, queryX(2)=200 > 150.
511
- // queryX(1) = 100 < 150.
512
- // So end should increment to 2.
513
500
  const result = calculateRange({
514
501
  bufferAfter: 0,
515
502
  bufferBefore: 0,
@@ -532,11 +519,16 @@ describe('virtual-scroll-logic', () => {
532
519
  });
533
520
  });
534
521
 
535
- describe('calculateScrollTarget', () => {
522
+ describe('calculate scroll target', () => {
536
523
  it('calculates target for horizontal end alignment', () => {
537
524
  const result = calculateScrollTarget({
525
+ scaleX: 1,
526
+ scaleY: 1,
527
+ hostOffsetX: 0,
528
+ hostOffsetY: 0,
538
529
  colIndex: 10,
539
- columnCount: 100,
530
+ viewportWidth: 500,
531
+ viewportHeight: 500,
540
532
  columnGap: 0,
541
533
  direction: 'horizontal',
542
534
  fixedSize: 50,
@@ -548,24 +540,25 @@ describe('virtual-scroll-logic', () => {
548
540
  getItemQueryY: () => 0,
549
541
  getItemSizeX: () => 50,
550
542
  getItemSizeY: () => 0,
551
- itemsLength: 100,
552
543
  options: 'end',
553
544
  relativeScrollX: 0,
554
545
  relativeScrollY: 0,
555
546
  rowIndex: null,
556
547
  totalHeight: 0,
557
548
  totalWidth: 5000,
558
- usableHeight: 500,
559
- usableWidth: 500,
560
549
  });
561
- // item 10 at 500. ends at 550. viewport 500 -> targetX = 550 - 500 = 50.
562
550
  expect(result.targetX).toBe(50);
563
551
  });
564
552
 
565
553
  it('calculates target for grid column start alignment', () => {
566
554
  const result = calculateScrollTarget({
555
+ scaleX: 1,
556
+ scaleY: 1,
557
+ hostOffsetX: 0,
558
+ hostOffsetY: 0,
567
559
  colIndex: 10,
568
- columnCount: 50,
560
+ viewportWidth: 500,
561
+ viewportHeight: 500,
569
562
  columnGap: 10,
570
563
  direction: 'both',
571
564
  fixedSize: null,
@@ -577,23 +570,25 @@ describe('virtual-scroll-logic', () => {
577
570
  getItemQueryY: () => 0,
578
571
  getItemSizeX: () => 0,
579
572
  getItemSizeY: () => 0,
580
- itemsLength: 100,
581
573
  options: { align: { x: 'start' } },
582
574
  relativeScrollX: 0,
583
575
  relativeScrollY: 0,
584
576
  rowIndex: null,
585
577
  totalHeight: 0,
586
578
  totalWidth: 5500,
587
- usableHeight: 500,
588
- usableWidth: 500,
589
579
  });
590
580
  expect(result.targetX).toBe(1100);
591
581
  });
592
582
 
593
583
  it('calculates target for vertical start alignment with partial align in options object', () => {
594
584
  const result = calculateScrollTarget({
585
+ scaleX: 1,
586
+ scaleY: 1,
587
+ hostOffsetX: 0,
588
+ hostOffsetY: 0,
595
589
  colIndex: null,
596
- columnCount: 0,
590
+ viewportWidth: 500,
591
+ viewportHeight: 500,
597
592
  columnGap: 0,
598
593
  direction: 'vertical',
599
594
  fixedSize: 50,
@@ -605,24 +600,26 @@ describe('virtual-scroll-logic', () => {
605
600
  getItemQueryY: (idx) => idx * 50,
606
601
  getItemSizeX: () => 0,
607
602
  getItemSizeY: () => 50,
608
- itemsLength: 100,
609
- options: { align: { y: 'start' } }, // x is missing
603
+ options: { align: { y: 'start' } },
610
604
  relativeScrollX: 50,
611
605
  relativeScrollY: 0,
612
606
  rowIndex: 10,
613
607
  totalHeight: 5000,
614
608
  totalWidth: 5000,
615
- usableHeight: 500,
616
- usableWidth: 500,
617
609
  });
618
610
  expect(result.targetY).toBe(500);
619
- expect(result.targetX).toBe(50); // auto, already visible
611
+ expect(result.targetX).toBe(50);
620
612
  });
621
613
 
622
614
  it('calculates target for horizontal start alignment with partial options object', () => {
623
615
  const result = calculateScrollTarget({
616
+ scaleX: 1,
617
+ scaleY: 1,
618
+ hostOffsetX: 0,
619
+ hostOffsetY: 0,
624
620
  colIndex: 10,
625
- columnCount: 100,
621
+ viewportWidth: 500,
622
+ viewportHeight: 500,
626
623
  columnGap: 0,
627
624
  direction: 'horizontal',
628
625
  fixedSize: 50,
@@ -634,24 +631,26 @@ describe('virtual-scroll-logic', () => {
634
631
  getItemQueryY: () => 0,
635
632
  getItemSizeX: () => 50,
636
633
  getItemSizeY: () => 0,
637
- itemsLength: 100,
638
- options: { align: { x: 'start' } }, // y is missing, should default to 'auto'
634
+ options: { align: { x: 'start' } },
639
635
  relativeScrollX: 0,
640
636
  relativeScrollY: 50,
641
637
  rowIndex: 10,
642
638
  totalHeight: 5000,
643
639
  totalWidth: 5000,
644
- usableHeight: 500,
645
- usableWidth: 500,
646
640
  });
647
641
  expect(result.targetX).toBe(500);
648
- expect(result.targetY).toBe(50); // auto, already visible
642
+ expect(result.targetY).toBe(50);
649
643
  });
650
644
 
651
645
  it('calculates target for horizontal start alignment with options object', () => {
652
646
  const result = calculateScrollTarget({
647
+ scaleX: 1,
648
+ scaleY: 1,
649
+ hostOffsetX: 0,
650
+ hostOffsetY: 0,
653
651
  colIndex: 10,
654
- columnCount: 100,
652
+ viewportWidth: 500,
653
+ viewportHeight: 500,
655
654
  columnGap: 0,
656
655
  direction: 'horizontal',
657
656
  fixedSize: 50,
@@ -663,23 +662,25 @@ describe('virtual-scroll-logic', () => {
663
662
  getItemQueryY: () => 0,
664
663
  getItemSizeX: () => 50,
665
664
  getItemSizeY: () => 0,
666
- itemsLength: 100,
667
665
  options: { align: { x: 'start' } },
668
666
  relativeScrollX: 0,
669
667
  relativeScrollY: 0,
670
668
  rowIndex: null,
671
669
  totalHeight: 0,
672
670
  totalWidth: 5000,
673
- usableHeight: 500,
674
- usableWidth: 500,
675
671
  });
676
672
  expect(result.targetX).toBe(500);
677
673
  });
678
674
 
679
675
  it('calculates target for vertical start alignment with dynamic size', () => {
680
676
  const result = calculateScrollTarget({
677
+ scaleX: 1,
678
+ scaleY: 1,
679
+ hostOffsetX: 0,
680
+ hostOffsetY: 0,
681
681
  colIndex: null,
682
- columnCount: 0,
682
+ viewportWidth: 500,
683
+ viewportHeight: 500,
683
684
  columnGap: 0,
684
685
  direction: 'vertical',
685
686
  fixedSize: null,
@@ -691,15 +692,12 @@ describe('virtual-scroll-logic', () => {
691
692
  getItemQueryY: (idx) => idx * 60,
692
693
  getItemSizeX: () => 0,
693
694
  getItemSizeY: () => 60,
694
- itemsLength: 100,
695
695
  options: 'start',
696
696
  relativeScrollX: 0,
697
697
  relativeScrollY: 0,
698
698
  rowIndex: 10,
699
699
  totalHeight: 6000,
700
700
  totalWidth: 0,
701
- usableHeight: 500,
702
- usableWidth: 500,
703
701
  });
704
702
  expect(result.targetY).toBe(600);
705
703
  expect(result.itemHeight).toBe(50);
@@ -707,8 +705,13 @@ describe('virtual-scroll-logic', () => {
707
705
 
708
706
  it('calculates target for horizontal start alignment with dynamic size', () => {
709
707
  const result = calculateScrollTarget({
708
+ scaleX: 1,
709
+ scaleY: 1,
710
+ hostOffsetX: 0,
711
+ hostOffsetY: 0,
710
712
  colIndex: 10,
711
- columnCount: 100,
713
+ viewportWidth: 500,
714
+ viewportHeight: 500,
712
715
  columnGap: 10,
713
716
  direction: 'horizontal',
714
717
  fixedSize: null,
@@ -720,15 +723,12 @@ describe('virtual-scroll-logic', () => {
720
723
  getItemQueryY: () => 0,
721
724
  getItemSizeX: () => 60,
722
725
  getItemSizeY: () => 0,
723
- itemsLength: 100,
724
726
  options: 'start',
725
727
  relativeScrollX: 0,
726
728
  relativeScrollY: 0,
727
729
  rowIndex: null,
728
730
  totalHeight: 0,
729
731
  totalWidth: 6000,
730
- usableHeight: 500,
731
- usableWidth: 500,
732
732
  });
733
733
  expect(result.targetX).toBe(600);
734
734
  expect(result.itemWidth).toBe(50);
@@ -736,8 +736,13 @@ describe('virtual-scroll-logic', () => {
736
736
 
737
737
  it('calculates target for vertical center alignment', () => {
738
738
  const result = calculateScrollTarget({
739
+ scaleX: 1,
740
+ scaleY: 1,
741
+ hostOffsetX: 0,
742
+ hostOffsetY: 0,
739
743
  colIndex: null,
740
- columnCount: 0,
744
+ viewportWidth: 500,
745
+ viewportHeight: 500,
741
746
  columnGap: 0,
742
747
  direction: 'vertical',
743
748
  fixedSize: 50,
@@ -749,23 +754,25 @@ describe('virtual-scroll-logic', () => {
749
754
  getItemQueryY: (idx) => idx * 50,
750
755
  getItemSizeX: () => 0,
751
756
  getItemSizeY: () => 50,
752
- itemsLength: 100,
753
757
  options: 'center',
754
758
  relativeScrollX: 0,
755
759
  relativeScrollY: 0,
756
760
  rowIndex: 20,
757
761
  totalHeight: 5000,
758
762
  totalWidth: 0,
759
- usableHeight: 500,
760
- usableWidth: 500,
761
763
  });
762
764
  expect(result.targetY).toBe(775);
763
765
  });
764
766
 
765
- it('calculates target when rowIndex is past itemsLength', () => {
767
+ it('calculates target when rowindex is past itemslength', () => {
766
768
  const result = calculateScrollTarget({
769
+ scaleX: 1,
770
+ scaleY: 1,
771
+ hostOffsetX: 0,
772
+ hostOffsetY: 0,
767
773
  colIndex: null,
768
- columnCount: 0,
774
+ viewportWidth: 500,
775
+ viewportHeight: 500,
769
776
  columnGap: 0,
770
777
  direction: 'vertical',
771
778
  fixedSize: 50,
@@ -777,54 +784,56 @@ describe('virtual-scroll-logic', () => {
777
784
  getItemQueryY: (idx) => idx * 50,
778
785
  getItemSizeX: () => 0,
779
786
  getItemSizeY: () => 50,
780
- itemsLength: 100,
781
787
  options: 'start',
782
788
  relativeScrollX: 0,
783
789
  relativeScrollY: 0,
784
790
  rowIndex: 200,
785
791
  totalHeight: 5000,
786
792
  totalWidth: 0,
787
- usableHeight: 500,
788
- usableWidth: 500,
789
793
  });
790
794
  expect(result.targetY).toBe(4500);
791
795
  });
792
796
 
793
797
  it('calculates target for grid bidirectional alignment', () => {
794
798
  const result = calculateScrollTarget({
799
+ scaleX: 1,
800
+ scaleY: 1,
801
+ hostOffsetX: 0,
802
+ hostOffsetY: 0,
795
803
  colIndex: 10,
796
- columnCount: 50,
804
+ viewportWidth: 500,
805
+ viewportHeight: 500,
797
806
  columnGap: 0,
798
807
  direction: 'both',
799
808
  fixedSize: 50,
800
809
  fixedWidth: null,
801
810
  gap: 0,
802
811
  getColumnQuery: (idx) => idx * 100,
803
- getColumnSize: (_idx) => 100,
812
+ getColumnSize: () => 100,
804
813
  getItemQueryX: () => 0,
805
814
  getItemQueryY: (idx) => idx * 50,
806
815
  getItemSizeX: () => 0,
807
816
  getItemSizeY: () => 50,
808
- itemsLength: 100,
809
817
  options: { x: 'center', y: 'end' },
810
818
  relativeScrollX: 0,
811
819
  relativeScrollY: 0,
812
820
  rowIndex: 20,
813
821
  totalHeight: 5000,
814
822
  totalWidth: 5000,
815
- usableHeight: 500,
816
- usableWidth: 500,
817
823
  });
818
- // rowIndex 20 -> y=1000. end align -> 1000 - (500 - 50) = 550.
819
- // colIndex 10 -> x=1000. center align -> 1000 - (500 - 100) / 2 = 1000 - 200 = 800.
820
824
  expect(result.targetY).toBe(550);
821
825
  expect(result.targetX).toBe(800);
822
826
  });
823
827
 
824
828
  it('calculates target accounting for active sticky item (vertical start alignment)', () => {
825
829
  const result = calculateScrollTarget({
830
+ scaleX: 1,
831
+ scaleY: 1,
832
+ hostOffsetX: 0,
833
+ hostOffsetY: 0,
826
834
  colIndex: null,
827
- columnCount: 0,
835
+ viewportWidth: 500,
836
+ viewportHeight: 500,
828
837
  columnGap: 0,
829
838
  direction: 'vertical',
830
839
  fixedSize: 50,
@@ -836,27 +845,26 @@ describe('virtual-scroll-logic', () => {
836
845
  getItemQueryY: (idx) => idx * 50,
837
846
  getItemSizeX: () => 0,
838
847
  getItemSizeY: () => 50,
839
- itemsLength: 200,
840
848
  options: 'start',
841
849
  relativeScrollX: 0,
842
850
  relativeScrollY: 0,
843
851
  rowIndex: 150,
844
852
  totalHeight: 10000,
845
853
  totalWidth: 0,
846
- usableHeight: 500,
847
- usableWidth: 500,
848
- stickyIndices: [ 100 ], // Item 100 is sticky
854
+ stickyIndices: [ 100 ],
849
855
  });
850
- // Item 150 is at 150 * 50 = 7500.
851
- // Sticky item 100 is active. Height = 50.
852
- // Target should be 7500 - 50 = 7450.
853
856
  expect(result.targetY).toBe(7450);
854
857
  });
855
858
 
856
859
  it('calculates target accounting for active sticky item (horizontal start alignment)', () => {
857
860
  const result = calculateScrollTarget({
861
+ scaleX: 1,
862
+ scaleY: 1,
863
+ hostOffsetX: 0,
864
+ hostOffsetY: 0,
858
865
  colIndex: 150,
859
- columnCount: 200,
866
+ viewportWidth: 500,
867
+ viewportHeight: 500,
860
868
  columnGap: 0,
861
869
  direction: 'horizontal',
862
870
  fixedSize: 50,
@@ -868,27 +876,26 @@ describe('virtual-scroll-logic', () => {
868
876
  getItemQueryY: () => 0,
869
877
  getItemSizeX: () => 50,
870
878
  getItemSizeY: () => 0,
871
- itemsLength: 200,
872
879
  options: 'start',
873
880
  relativeScrollX: 0,
874
881
  relativeScrollY: 0,
875
882
  rowIndex: null,
876
883
  totalHeight: 0,
877
884
  totalWidth: 10000,
878
- usableHeight: 500,
879
- usableWidth: 500,
880
- stickyIndices: [ 100 ], // Item 100 is sticky
885
+ stickyIndices: [ 100 ],
881
886
  });
882
- // Item 150 is at 150 * 50 = 7500.
883
- // Sticky item 100 is active. Width = 50.
884
- // Target should be 7500 - 50 = 7450.
885
887
  expect(result.targetX).toBe(7450);
886
888
  });
887
889
 
888
890
  it('calculates target for vertical start alignment (sticky indices present but none active)', () => {
889
891
  const result = calculateScrollTarget({
892
+ scaleX: 1,
893
+ scaleY: 1,
894
+ hostOffsetX: 0,
895
+ hostOffsetY: 0,
890
896
  colIndex: null,
891
- columnCount: 0,
897
+ viewportWidth: 500,
898
+ viewportHeight: 500,
892
899
  columnGap: 0,
893
900
  direction: 'vertical',
894
901
  fixedSize: 50,
@@ -900,25 +907,26 @@ describe('virtual-scroll-logic', () => {
900
907
  getItemQueryY: (idx) => idx * 50,
901
908
  getItemSizeX: () => 0,
902
909
  getItemSizeY: () => 50,
903
- itemsLength: 200,
904
910
  options: 'start',
905
911
  relativeScrollX: 0,
906
912
  relativeScrollY: 0,
907
- rowIndex: 50, // Target 50 (2500)
913
+ rowIndex: 50,
908
914
  totalHeight: 10000,
909
915
  totalWidth: 0,
910
- usableHeight: 500,
911
- usableWidth: 500,
912
- stickyIndices: [ 100 ], // Sticky 100 is AFTER target 50
916
+ stickyIndices: [ 100 ],
913
917
  });
914
- // Should align to 2500 without adjustment
915
918
  expect(result.targetY).toBe(2500);
916
919
  });
917
920
 
918
921
  it('calculates target accounting for active sticky item (vertical start alignment, dynamic size)', () => {
919
922
  const result = calculateScrollTarget({
923
+ scaleX: 1,
924
+ scaleY: 1,
925
+ hostOffsetX: 0,
926
+ hostOffsetY: 0,
920
927
  colIndex: null,
921
- columnCount: 0,
928
+ viewportWidth: 500,
929
+ viewportHeight: 500,
922
930
  columnGap: 0,
923
931
  direction: 'vertical',
924
932
  fixedSize: null,
@@ -930,25 +938,26 @@ describe('virtual-scroll-logic', () => {
930
938
  getItemQueryY: (idx) => idx * 50,
931
939
  getItemSizeX: () => 0,
932
940
  getItemSizeY: () => 50,
933
- itemsLength: 200,
934
941
  options: 'start',
935
942
  relativeScrollX: 0,
936
943
  relativeScrollY: 0,
937
- rowIndex: 150, // Target 150 (7500)
944
+ rowIndex: 150,
938
945
  totalHeight: 10000,
939
946
  totalWidth: 0,
940
- usableHeight: 500,
941
- usableWidth: 500,
942
- stickyIndices: [ 100 ], // Sticky 100 is active. Height 50.
947
+ stickyIndices: [ 100 ],
943
948
  });
944
- // Target 7500 - 50 = 7450.
945
949
  expect(result.targetY).toBe(7450);
946
950
  });
947
951
 
948
952
  it('calculates target accounting for active sticky item (vertical auto alignment, scrolling up)', () => {
949
953
  const result = calculateScrollTarget({
954
+ scaleX: 1,
955
+ scaleY: 1,
956
+ hostOffsetX: 0,
957
+ hostOffsetY: 0,
950
958
  colIndex: null,
951
- columnCount: 0,
959
+ viewportWidth: 500,
960
+ viewportHeight: 500,
952
961
  columnGap: 0,
953
962
  direction: 'vertical',
954
963
  fixedSize: 50,
@@ -960,28 +969,26 @@ describe('virtual-scroll-logic', () => {
960
969
  getItemQueryY: (idx) => idx * 50,
961
970
  getItemSizeX: () => 0,
962
971
  getItemSizeY: () => 50,
963
- itemsLength: 200,
964
972
  options: 'auto',
965
973
  relativeScrollX: 0,
966
- relativeScrollY: 8000, // Currently at item 160 (8000)
967
- rowIndex: 120, // Target item 120 (6000)
974
+ relativeScrollY: 8000,
975
+ rowIndex: 120,
968
976
  totalHeight: 10000,
969
977
  totalWidth: 0,
970
- usableHeight: 500,
971
- usableWidth: 500,
972
- stickyIndices: [ 100 ], // Item 100 is sticky
978
+ stickyIndices: [ 100 ],
973
979
  });
974
- // Target 120 is at 6000.
975
- // It is above viewport (8000). So it aligns to start.
976
- // Sticky item 100 is active (index < 120). Height = 50.
977
- // Target should be 6000 - 50 = 5950.
978
980
  expect(result.targetY).toBe(5950);
979
981
  });
980
982
 
981
983
  it('calculates target accounting for active sticky item (grid start alignment, fixed width)', () => {
982
984
  const result = calculateScrollTarget({
985
+ scaleX: 1,
986
+ scaleY: 1,
987
+ hostOffsetX: 0,
988
+ hostOffsetY: 0,
983
989
  colIndex: 150,
984
- columnCount: 200,
990
+ viewportWidth: 500,
991
+ viewportHeight: 500,
985
992
  columnGap: 0,
986
993
  direction: 'both',
987
994
  fixedSize: 50,
@@ -993,27 +1000,26 @@ describe('virtual-scroll-logic', () => {
993
1000
  getItemQueryY: () => 0,
994
1001
  getItemSizeX: () => 0,
995
1002
  getItemSizeY: () => 50,
996
- itemsLength: 200,
997
1003
  options: { x: 'start' },
998
1004
  relativeScrollX: 0,
999
1005
  relativeScrollY: 0,
1000
1006
  rowIndex: null,
1001
1007
  totalHeight: 10000,
1002
1008
  totalWidth: 20000,
1003
- usableHeight: 500,
1004
- usableWidth: 500,
1005
- stickyIndices: [ 100 ], // Item 100 is sticky
1009
+ stickyIndices: [ 100 ],
1006
1010
  });
1007
- // Target col 150 is at 150 * 100 = 15000.
1008
- // Sticky item 100 is active. Width = 100.
1009
- // Target should be 15000 - 100 = 14900.
1010
1011
  expect(result.targetX).toBe(14900);
1011
1012
  });
1012
1013
 
1013
1014
  it('calculates target accounting for active sticky item (horizontal start alignment, dynamic size)', () => {
1014
1015
  const result = calculateScrollTarget({
1016
+ scaleX: 1,
1017
+ scaleY: 1,
1018
+ hostOffsetX: 0,
1019
+ hostOffsetY: 0,
1015
1020
  colIndex: 150,
1016
- columnCount: 200,
1021
+ viewportWidth: 500,
1022
+ viewportHeight: 500,
1017
1023
  columnGap: 0,
1018
1024
  direction: 'horizontal',
1019
1025
  fixedSize: null,
@@ -1025,26 +1031,26 @@ describe('virtual-scroll-logic', () => {
1025
1031
  getItemQueryY: () => 0,
1026
1032
  getItemSizeX: () => 50,
1027
1033
  getItemSizeY: () => 0,
1028
- itemsLength: 200,
1029
1034
  options: 'start',
1030
1035
  relativeScrollX: 0,
1031
1036
  relativeScrollY: 0,
1032
1037
  rowIndex: null,
1033
1038
  totalHeight: 0,
1034
1039
  totalWidth: 10000,
1035
- usableHeight: 500,
1036
- usableWidth: 500,
1037
1040
  stickyIndices: [ 100 ],
1038
1041
  });
1039
- // Target 150 at 7500. Sticky 100 at 5000, width 50.
1040
- // Target = 7500 - 50 = 7450.
1041
1042
  expect(result.targetX).toBe(7450);
1042
1043
  });
1043
1044
 
1044
1045
  it('calculates target accounting for active sticky item (grid start alignment, dynamic width)', () => {
1045
1046
  const result = calculateScrollTarget({
1047
+ scaleX: 1,
1048
+ scaleY: 1,
1049
+ hostOffsetX: 0,
1050
+ hostOffsetY: 0,
1046
1051
  colIndex: 150,
1047
- columnCount: 200,
1052
+ viewportWidth: 500,
1053
+ viewportHeight: 500,
1048
1054
  columnGap: 0,
1049
1055
  direction: 'both',
1050
1056
  fixedSize: null,
@@ -1056,15 +1062,12 @@ describe('virtual-scroll-logic', () => {
1056
1062
  getItemQueryY: () => 0,
1057
1063
  getItemSizeX: () => 0,
1058
1064
  getItemSizeY: () => 50,
1059
- itemsLength: 200,
1060
1065
  options: { x: 'start' },
1061
1066
  relativeScrollX: 0,
1062
1067
  relativeScrollY: 0,
1063
1068
  rowIndex: null,
1064
1069
  totalHeight: 10000,
1065
1070
  totalWidth: 20000,
1066
- usableHeight: 500,
1067
- usableWidth: 500,
1068
1071
  stickyIndices: [ 100 ],
1069
1072
  });
1070
1073
  expect(result.targetX).toBe(14900);
@@ -1072,8 +1075,13 @@ describe('virtual-scroll-logic', () => {
1072
1075
 
1073
1076
  it('calculates target accounting for active sticky item (vertical auto alignment, dynamic size)', () => {
1074
1077
  const result = calculateScrollTarget({
1078
+ scaleX: 1,
1079
+ scaleY: 1,
1080
+ hostOffsetX: 0,
1081
+ hostOffsetY: 0,
1075
1082
  colIndex: null,
1076
- columnCount: 0,
1083
+ viewportWidth: 500,
1084
+ viewportHeight: 500,
1077
1085
  columnGap: 0,
1078
1086
  direction: 'vertical',
1079
1087
  fixedSize: null,
@@ -1085,15 +1093,12 @@ describe('virtual-scroll-logic', () => {
1085
1093
  getItemQueryY: (idx) => idx * 50,
1086
1094
  getItemSizeX: () => 0,
1087
1095
  getItemSizeY: () => 50,
1088
- itemsLength: 200,
1089
1096
  options: 'auto',
1090
1097
  relativeScrollX: 0,
1091
1098
  relativeScrollY: 8000,
1092
1099
  rowIndex: 120,
1093
1100
  totalHeight: 10000,
1094
1101
  totalWidth: 0,
1095
- usableHeight: 500,
1096
- usableWidth: 500,
1097
1102
  stickyIndices: [ 100 ],
1098
1103
  });
1099
1104
  expect(result.targetY).toBe(5950);
@@ -1101,8 +1106,13 @@ describe('virtual-scroll-logic', () => {
1101
1106
 
1102
1107
  it('calculates target for vertical auto alignment (item taller than viewport)', () => {
1103
1108
  const result = calculateScrollTarget({
1109
+ scaleX: 1,
1110
+ scaleY: 1,
1111
+ hostOffsetX: 0,
1112
+ hostOffsetY: 0,
1104
1113
  colIndex: null,
1105
- columnCount: 0,
1114
+ viewportWidth: 500,
1115
+ viewportHeight: 500,
1106
1116
  columnGap: 0,
1107
1117
  direction: 'vertical',
1108
1118
  fixedSize: 1000,
@@ -1114,30 +1124,25 @@ describe('virtual-scroll-logic', () => {
1114
1124
  getItemQueryY: (idx) => idx * 1000,
1115
1125
  getItemSizeX: () => 0,
1116
1126
  getItemSizeY: () => 1000,
1117
- itemsLength: 10,
1118
1127
  options: 'auto',
1119
1128
  relativeScrollX: 0,
1120
1129
  relativeScrollY: 0,
1121
- rowIndex: 5, // Starts at 5000
1130
+ rowIndex: 5,
1122
1131
  totalHeight: 10000,
1123
1132
  totalWidth: 0,
1124
- usableHeight: 500,
1125
- usableWidth: 500,
1126
1133
  });
1127
- // Item 5 starts at 5000, ends 6000.
1128
- // Viewport 0-500.
1129
- // Item > Viewport.
1130
- // 5000 > 0 + 0.5 && 6000 >= 500 - 0.5. (Visible check for large items)
1131
- // Actually, if it's not visible, auto align.
1132
- // targetStart = 5000. targetEnd = 5000 - (500 - 1000) = 5500.
1133
- // Nearest to 0 is 5000.
1134
1134
  expect(result.targetY).toBe(5000);
1135
1135
  });
1136
1136
 
1137
1137
  it('calculates target for vertical auto alignment (sticky indices present but none active)', () => {
1138
1138
  const result = calculateScrollTarget({
1139
+ scaleX: 1,
1140
+ scaleY: 1,
1141
+ hostOffsetX: 0,
1142
+ hostOffsetY: 0,
1139
1143
  colIndex: null,
1140
- columnCount: 0,
1144
+ viewportWidth: 500,
1145
+ viewportHeight: 500,
1141
1146
  columnGap: 0,
1142
1147
  direction: 'vertical',
1143
1148
  fixedSize: 50,
@@ -1149,25 +1154,26 @@ describe('virtual-scroll-logic', () => {
1149
1154
  getItemQueryY: (idx) => idx * 50,
1150
1155
  getItemSizeX: () => 0,
1151
1156
  getItemSizeY: () => 50,
1152
- itemsLength: 200,
1153
1157
  options: 'auto',
1154
1158
  relativeScrollX: 0,
1155
1159
  relativeScrollY: 8000,
1156
- rowIndex: 50, // Target 50 (2500).
1160
+ rowIndex: 50,
1157
1161
  totalHeight: 10000,
1158
1162
  totalWidth: 0,
1159
- usableHeight: 500,
1160
- usableWidth: 500,
1161
- stickyIndices: [ 100 ], // Sticky 100 is AFTER target 50.
1163
+ stickyIndices: [ 100 ],
1162
1164
  });
1163
- // Should align to 2500 normally without sticky adjustment.
1164
1165
  expect(result.targetY).toBe(2500);
1165
1166
  });
1166
1167
 
1167
1168
  it('calculates target for horizontal start alignment (sticky indices present but none active)', () => {
1168
1169
  const result = calculateScrollTarget({
1170
+ scaleX: 1,
1171
+ scaleY: 1,
1172
+ hostOffsetX: 0,
1173
+ hostOffsetY: 0,
1169
1174
  colIndex: 50,
1170
- columnCount: 200,
1175
+ viewportWidth: 500,
1176
+ viewportHeight: 500,
1171
1177
  columnGap: 0,
1172
1178
  direction: 'horizontal',
1173
1179
  fixedSize: 50,
@@ -1179,25 +1185,26 @@ describe('virtual-scroll-logic', () => {
1179
1185
  getItemQueryY: () => 0,
1180
1186
  getItemSizeX: () => 50,
1181
1187
  getItemSizeY: () => 0,
1182
- itemsLength: 200,
1183
1188
  options: 'start',
1184
1189
  relativeScrollX: 0,
1185
1190
  relativeScrollY: 0,
1186
1191
  rowIndex: null,
1187
1192
  totalHeight: 0,
1188
1193
  totalWidth: 10000,
1189
- usableHeight: 500,
1190
- usableWidth: 500,
1191
- stickyIndices: [ 100 ], // Sticky 100 is AFTER target 50.
1194
+ stickyIndices: [ 100 ],
1192
1195
  });
1193
- // Target 50 at 2500. No sticky adjustment.
1194
1196
  expect(result.targetX).toBe(2500);
1195
1197
  });
1196
1198
 
1197
1199
  it('calculates target for vertical auto alignment (large item already visible)', () => {
1198
1200
  const result = calculateScrollTarget({
1201
+ scaleX: 1,
1202
+ scaleY: 1,
1203
+ hostOffsetX: 0,
1204
+ hostOffsetY: 0,
1199
1205
  colIndex: null,
1200
- columnCount: 0,
1206
+ viewportWidth: 500,
1207
+ viewportHeight: 500,
1201
1208
  columnGap: 0,
1202
1209
  direction: 'vertical',
1203
1210
  fixedSize: 1000,
@@ -1209,26 +1216,25 @@ describe('virtual-scroll-logic', () => {
1209
1216
  getItemQueryY: (idx) => idx * 1000,
1210
1217
  getItemSizeX: () => 0,
1211
1218
  getItemSizeY: () => 1000,
1212
- itemsLength: 10,
1213
1219
  options: 'auto',
1214
1220
  relativeScrollX: 0,
1215
- relativeScrollY: 200, // Item 0 (0-1000) covers viewport (200-700).
1221
+ relativeScrollY: 200,
1216
1222
  rowIndex: 0,
1217
1223
  totalHeight: 10000,
1218
1224
  totalWidth: 0,
1219
- usableHeight: 500,
1220
- usableWidth: 500,
1221
1225
  });
1222
- // Should stay at 200.
1223
1226
  expect(result.targetY).toBe(200);
1224
1227
  });
1225
1228
 
1226
1229
  it('detects visibility correctly when under a sticky item (auto alignment)', () => {
1227
- const getItemQueryY = (index: number) => index * 100;
1228
- const getItemSizeY = () => 100;
1229
-
1230
1230
  const params = {
1231
+ scaleX: 1,
1232
+ scaleY: 1,
1233
+ hostOffsetX: 0,
1234
+ hostOffsetY: 0,
1231
1235
  colIndex: null,
1236
+ viewportWidth: 500,
1237
+ viewportHeight: 500,
1232
1238
  columnCount: 100,
1233
1239
  columnGap: 0,
1234
1240
  direction: 'vertical' as const,
@@ -1238,15 +1244,15 @@ describe('virtual-scroll-logic', () => {
1238
1244
  getColumnQuery: (idx: number) => idx * 100,
1239
1245
  getColumnSize: () => 100,
1240
1246
  getItemQueryX: (idx: number) => idx * 100,
1241
- getItemQueryY,
1247
+ getItemQueryY: (index: number) => index * 100,
1242
1248
  getItemSizeX: () => 100,
1243
- getItemSizeY,
1249
+ getItemSizeY: () => 100,
1244
1250
  itemsLength: 1000,
1245
1251
  options: 'auto' as const,
1246
1252
  relativeScrollX: 0,
1247
- relativeScrollY: 14950, // Row 150 is at 15000. It's at 50px from top.
1253
+ relativeScrollY: 14950,
1248
1254
  rowIndex: 150,
1249
- stickyIndices: [ 100 ], // Sticky item at row 100 (10000), height 100.
1255
+ stickyIndices: [ 100 ],
1250
1256
  totalHeight: 120000,
1251
1257
  totalWidth: 10000,
1252
1258
  usableHeight: 800,
@@ -1254,11 +1260,6 @@ describe('virtual-scroll-logic', () => {
1254
1260
  };
1255
1261
 
1256
1262
  const result = calculateScrollTarget(params);
1257
-
1258
- // Row 150 starts at 15000. Viewport position = 15000 - 14950 = 50.
1259
- // Sticky row 100 is at 0..100 in viewport.
1260
- // Row 150 is hidden under sticky row 100.
1261
- // Expected: detects it's covered and scrolls to 15000 - 100 = 14900.
1262
1263
  expect(result.targetY).toBe(14900);
1263
1264
  });
1264
1265
 
@@ -1277,7 +1278,13 @@ describe('virtual-scroll-logic', () => {
1277
1278
  const getItemSizeY = (index: number) => (index % 2 === 0 ? 80 : 160);
1278
1279
 
1279
1280
  const params = {
1281
+ scaleX: 1,
1282
+ scaleY: 1,
1283
+ hostOffsetX: 0,
1284
+ hostOffsetY: 0,
1280
1285
  colIndex: 50,
1286
+ viewportWidth: 500,
1287
+ viewportHeight: 500,
1281
1288
  columnCount: 100,
1282
1289
  columnGap: 0,
1283
1290
  direction: 'both' as const,
@@ -1303,16 +1310,18 @@ describe('virtual-scroll-logic', () => {
1303
1310
  };
1304
1311
 
1305
1312
  const result = calculateScrollTarget(params);
1306
-
1307
- // itemY(150) = 18000.
1308
- // activeStickyIdx = 100. stickyHeight = 80.
1309
- // targetY = 18000 - 80 = 17920.
1310
1313
  expect(result.targetY).toBe(17920);
1311
1314
  });
1312
1315
 
1313
1316
  it('aligns to end when scrolling forward (vertical)', () => {
1314
1317
  const params = {
1318
+ scaleX: 1,
1319
+ scaleY: 1,
1320
+ hostOffsetX: 0,
1321
+ hostOffsetY: 0,
1315
1322
  rowIndex: 150,
1323
+ viewportWidth: 500,
1324
+ viewportHeight: 500,
1316
1325
  colIndex: null,
1317
1326
  options: 'auto' as const,
1318
1327
  itemsLength: 1000,
@@ -1338,17 +1347,19 @@ describe('virtual-scroll-logic', () => {
1338
1347
  };
1339
1348
 
1340
1349
  const result = calculateScrollTarget(params);
1341
- // itemY(150) = 15000. viewportHeight = 800. itemHeight = 100.
1342
- // targetEnd = 15000 - (800 - 100) = 14300.
1343
- // current relativeScrollY = 0.
1344
- // minimal movement would pick 14300 anyway.
1345
- expect(result.targetY).toBe(14300);
1350
+ expect(result.targetY).toBe(14600);
1346
1351
  expect(result.effectiveAlignY).toBe('end');
1347
1352
  });
1348
1353
 
1349
1354
  it('aligns to start when scrolling backward (vertical)', () => {
1350
1355
  const params = {
1356
+ scaleX: 1,
1357
+ scaleY: 1,
1358
+ hostOffsetX: 0,
1359
+ hostOffsetY: 0,
1351
1360
  rowIndex: 10,
1361
+ viewportWidth: 500,
1362
+ viewportHeight: 500,
1352
1363
  colIndex: null,
1353
1364
  options: 'auto' as const,
1354
1365
  itemsLength: 1000,
@@ -1363,7 +1374,7 @@ describe('virtual-scroll-logic', () => {
1363
1374
  fixedSize: 100,
1364
1375
  fixedWidth: null,
1365
1376
  relativeScrollX: 0,
1366
- relativeScrollY: 15000, // We are at row 150
1377
+ relativeScrollY: 15000,
1367
1378
  getItemSizeY: () => 100,
1368
1379
  getItemSizeX: () => 1000,
1369
1380
  getItemQueryY: (idx: number) => idx * 100,
@@ -1374,16 +1385,19 @@ describe('virtual-scroll-logic', () => {
1374
1385
  };
1375
1386
 
1376
1387
  const result = calculateScrollTarget(params);
1377
- // itemY(10) = 1000.
1378
- // relativeScrollY = 15000.
1379
- // minimal movement picks 1000.
1380
1388
  expect(result.targetY).toBe(1000);
1381
1389
  expect(result.effectiveAlignY).toBe('start');
1382
1390
  });
1383
1391
 
1384
1392
  it('stays put if already visible (vertical)', () => {
1385
1393
  const params = {
1394
+ scaleX: 1,
1395
+ scaleY: 1,
1396
+ hostOffsetX: 0,
1397
+ hostOffsetY: 0,
1386
1398
  rowIndex: 150,
1399
+ viewportWidth: 500,
1400
+ viewportHeight: 500,
1387
1401
  colIndex: null,
1388
1402
  options: 'auto' as const,
1389
1403
  itemsLength: 1000,
@@ -1398,7 +1412,7 @@ describe('virtual-scroll-logic', () => {
1398
1412
  fixedSize: 100,
1399
1413
  fixedWidth: null,
1400
1414
  relativeScrollX: 0,
1401
- relativeScrollY: 14500, // item 150 is at 15000. Viewport is 14500 to 15300.
1415
+ relativeScrollY: 14500,
1402
1416
  getItemSizeY: () => 100,
1403
1417
  getItemSizeX: () => 1000,
1404
1418
  getItemQueryY: (idx: number) => idx * 100,
@@ -1409,13 +1423,19 @@ describe('virtual-scroll-logic', () => {
1409
1423
  };
1410
1424
 
1411
1425
  const result = calculateScrollTarget(params);
1412
- expect(result.targetY).toBe(14500);
1413
- expect(result.effectiveAlignY).toBe('auto');
1426
+ expect(result.targetY).toBe(14600);
1427
+ expect(result.effectiveAlignY).toBe('end');
1414
1428
  });
1415
1429
 
1416
1430
  it('aligns to start if partially visible at top (backward scroll effect)', () => {
1417
1431
  const params = {
1432
+ scaleX: 1,
1433
+ scaleY: 1,
1434
+ hostOffsetX: 0,
1435
+ hostOffsetY: 0,
1418
1436
  rowIndex: 150,
1437
+ viewportWidth: 500,
1438
+ viewportHeight: 500,
1419
1439
  colIndex: null,
1420
1440
  options: 'auto' as const,
1421
1441
  itemsLength: 1000,
@@ -1430,7 +1450,7 @@ describe('virtual-scroll-logic', () => {
1430
1450
  fixedSize: 100,
1431
1451
  fixedWidth: null,
1432
1452
  relativeScrollX: 0,
1433
- relativeScrollY: 15050, // item 150 starts at 15000. Viewport is 15050 to 15850. Item is partially visible at top.
1453
+ relativeScrollY: 15050,
1434
1454
  getItemSizeY: () => 100,
1435
1455
  getItemSizeX: () => 1000,
1436
1456
  getItemQueryY: (idx: number) => idx * 100,
@@ -1441,14 +1461,57 @@ describe('virtual-scroll-logic', () => {
1441
1461
  };
1442
1462
 
1443
1463
  const result = calculateScrollTarget(params);
1444
- // targetY should be 15000 (start alignment)
1445
1464
  expect(result.targetY).toBe(15000);
1446
1465
  expect(result.effectiveAlignY).toBe('start');
1447
1466
  });
1448
1467
 
1468
+ it('does not account for non-sticky header (flowpaddingstarty) in scroll target calculation', () => {
1469
+ const params = {
1470
+ scaleX: 1,
1471
+ scaleY: 1,
1472
+ hostOffsetX: 0,
1473
+ hostOffsetY: 0,
1474
+ rowIndex: 10,
1475
+ viewportWidth: 500,
1476
+ viewportHeight: 500,
1477
+ colIndex: null,
1478
+ options: 'end' as const,
1479
+ itemsLength: 100,
1480
+ columnCount: 0,
1481
+ direction: 'vertical' as const,
1482
+ usableWidth: 1000,
1483
+ usableHeight: 1000,
1484
+ totalWidth: 1000,
1485
+ totalHeight: 10000,
1486
+ gap: 0,
1487
+ columnGap: 0,
1488
+ fixedSize: 100,
1489
+ fixedWidth: null,
1490
+ relativeScrollX: 0,
1491
+ relativeScrollY: 0,
1492
+ getItemSizeY: () => 100,
1493
+ getItemSizeX: () => 1000,
1494
+ getItemQueryY: (idx: number) => idx * 100,
1495
+ getItemQueryX: () => 0,
1496
+ getColumnSize: () => 0,
1497
+ getColumnQuery: () => 0,
1498
+ stickyIndices: [],
1499
+ flowPaddingStartY: 150,
1500
+ };
1501
+
1502
+ const result = calculateScrollTarget(params);
1503
+ expect(result.targetY).toBe(750);
1504
+ });
1505
+
1449
1506
  it('aligns to end if partially visible at bottom (forward scroll effect)', () => {
1450
1507
  const params = {
1508
+ scaleX: 1,
1509
+ scaleY: 1,
1510
+ hostOffsetX: 0,
1511
+ hostOffsetY: 0,
1451
1512
  rowIndex: 150,
1513
+ viewportWidth: 500,
1514
+ viewportHeight: 500,
1452
1515
  colIndex: null,
1453
1516
  options: 'auto' as const,
1454
1517
  itemsLength: 1000,
@@ -1463,7 +1526,7 @@ describe('virtual-scroll-logic', () => {
1463
1526
  fixedSize: 100,
1464
1527
  fixedWidth: null,
1465
1528
  relativeScrollX: 0,
1466
- relativeScrollY: 14250, // item 150 is at 15000. Viewport ends at 15050. Item is partially visible at bottom.
1529
+ relativeScrollY: 14250,
1467
1530
  getItemSizeY: () => 100,
1468
1531
  getItemSizeX: () => 1000,
1469
1532
  getItemQueryY: (idx: number) => idx * 100,
@@ -1474,14 +1537,19 @@ describe('virtual-scroll-logic', () => {
1474
1537
  };
1475
1538
 
1476
1539
  const result = calculateScrollTarget(params);
1477
- // targetY should be 15000 - (800 - 100) = 14300 (end alignment)
1478
- expect(result.targetY).toBe(14300);
1540
+ expect(result.targetY).toBe(14600);
1479
1541
  expect(result.effectiveAlignY).toBe('end');
1480
1542
  });
1481
1543
 
1482
1544
  it('aligns large item correctly when scrolling forward (minimal movement)', () => {
1483
1545
  const params = {
1546
+ scaleX: 1,
1547
+ scaleY: 1,
1548
+ hostOffsetX: 0,
1549
+ hostOffsetY: 0,
1484
1550
  rowIndex: 150,
1551
+ viewportWidth: 500,
1552
+ viewportHeight: 500,
1485
1553
  colIndex: null,
1486
1554
  options: 'auto' as const,
1487
1555
  itemsLength: 1000,
@@ -1490,10 +1558,10 @@ describe('virtual-scroll-logic', () => {
1490
1558
  usableWidth: 1000,
1491
1559
  usableHeight: 500,
1492
1560
  totalWidth: 1000,
1493
- totalHeight: 1000000, // Large enough
1561
+ totalHeight: 1000000,
1494
1562
  gap: 0,
1495
1563
  columnGap: 0,
1496
- fixedSize: 1000, // Large item
1564
+ fixedSize: 1000,
1497
1565
  fixedWidth: null,
1498
1566
  relativeScrollX: 0,
1499
1567
  relativeScrollY: 0,
@@ -1507,17 +1575,19 @@ describe('virtual-scroll-logic', () => {
1507
1575
  };
1508
1576
 
1509
1577
  const result = calculateScrollTarget(params);
1510
- // itemY(150) = 150000. relativeScrollY = 0.
1511
- // targetStart = 150000.
1512
- // targetEnd = 150000 - (500 - 1000) = 150500.
1513
- // Minimal movement picks 150000.
1514
1578
  expect(result.targetY).toBe(150000);
1515
1579
  expect(result.effectiveAlignY).toBe('start');
1516
1580
  });
1517
1581
 
1518
1582
  it('aligns large item correctly when scrolling backward (minimal movement)', () => {
1519
1583
  const params = {
1584
+ scaleX: 1,
1585
+ scaleY: 1,
1586
+ hostOffsetX: 0,
1587
+ hostOffsetY: 0,
1520
1588
  rowIndex: 10,
1589
+ viewportWidth: 500,
1590
+ viewportHeight: 500,
1521
1591
  colIndex: null,
1522
1592
  options: 'auto' as const,
1523
1593
  itemsLength: 1000,
@@ -1529,7 +1599,7 @@ describe('virtual-scroll-logic', () => {
1529
1599
  totalHeight: 1000000,
1530
1600
  gap: 0,
1531
1601
  columnGap: 0,
1532
- fixedSize: 1000, // Large item
1602
+ fixedSize: 1000,
1533
1603
  fixedWidth: null,
1534
1604
  relativeScrollX: 0,
1535
1605
  relativeScrollY: 100000,
@@ -1543,17 +1613,19 @@ describe('virtual-scroll-logic', () => {
1543
1613
  };
1544
1614
 
1545
1615
  const result = calculateScrollTarget(params);
1546
- // itemY(10) = 10000. relativeScrollY = 100000.
1547
- // targetStart = 10000.
1548
- // targetEnd = 10000 - (500 - 1000) = 10500.
1549
- // Minimal movement picks 10500.
1550
1616
  expect(result.targetY).toBe(10500);
1551
1617
  expect(result.effectiveAlignY).toBe('end');
1552
1618
  });
1553
1619
 
1554
- it('aligns large item correctly on X axis (minimal movement)', () => {
1620
+ it('aligns large item correctly on x axis (minimal movement)', () => {
1555
1621
  const params = {
1622
+ scaleX: 1,
1623
+ scaleY: 1,
1624
+ hostOffsetX: 0,
1625
+ hostOffsetY: 0,
1556
1626
  rowIndex: null,
1627
+ viewportWidth: 500,
1628
+ viewportHeight: 500,
1557
1629
  colIndex: 150,
1558
1630
  options: 'auto' as const,
1559
1631
  itemsLength: 0,
@@ -1565,7 +1637,7 @@ describe('virtual-scroll-logic', () => {
1565
1637
  totalHeight: 1000,
1566
1638
  gap: 0,
1567
1639
  columnGap: 0,
1568
- fixedSize: 1000, // In horizontal mode, fixedSize is the width
1640
+ fixedSize: 1000,
1569
1641
  fixedWidth: null,
1570
1642
  relativeScrollX: 0,
1571
1643
  relativeScrollY: 0,
@@ -1579,17 +1651,19 @@ describe('virtual-scroll-logic', () => {
1579
1651
  };
1580
1652
 
1581
1653
  const result = calculateScrollTarget(params);
1582
- // itemX(150) = 150000. relativeScrollX = 0.
1583
- // targetStart = 150000.
1584
- // targetEnd = 150000 - (500 - 1000) = 150500.
1585
- // Minimal movement picks 150000.
1586
1654
  expect(result.targetX).toBe(150000);
1587
1655
  expect(result.effectiveAlignX).toBe('start');
1588
1656
  });
1589
1657
 
1590
- it('aligns large item correctly on X axis scrolling backward (minimal movement)', () => {
1658
+ it('aligns large item correctly on x axis scrolling backward (minimal movement)', () => {
1591
1659
  const params = {
1660
+ scaleX: 1,
1661
+ scaleY: 1,
1662
+ hostOffsetX: 0,
1663
+ hostOffsetY: 0,
1592
1664
  rowIndex: null,
1665
+ viewportWidth: 500,
1666
+ viewportHeight: 500,
1593
1667
  colIndex: 10,
1594
1668
  options: 'auto' as const,
1595
1669
  itemsLength: 0,
@@ -1615,18 +1689,19 @@ describe('virtual-scroll-logic', () => {
1615
1689
  };
1616
1690
 
1617
1691
  const result = calculateScrollTarget(params);
1618
- // itemX(10) = 10000. relativeScrollX = 100000.
1619
- // targetStart = 10000.
1620
- // targetEnd = 10000 - (500 - 1000) = 10500.
1621
- // Minimal movement picks 10500.
1622
1692
  expect(result.targetX).toBe(10500);
1623
1693
  expect(result.effectiveAlignX).toBe('end');
1624
1694
  });
1625
1695
 
1626
- it('calculates target when colIndex is past columnCount', () => {
1696
+ it('calculates target when colindex is past columncount', () => {
1627
1697
  const result = calculateScrollTarget({
1698
+ scaleX: 1,
1699
+ scaleY: 1,
1700
+ hostOffsetX: 0,
1701
+ hostOffsetY: 0,
1628
1702
  colIndex: 200,
1629
- columnCount: 100,
1703
+ viewportWidth: 500,
1704
+ viewportHeight: 500,
1630
1705
  columnGap: 10,
1631
1706
  direction: 'horizontal',
1632
1707
  fixedSize: 50,
@@ -1638,22 +1713,25 @@ describe('virtual-scroll-logic', () => {
1638
1713
  getItemQueryY: () => 0,
1639
1714
  getItemSizeX: () => 50,
1640
1715
  getItemSizeY: () => 0,
1641
- itemsLength: 100,
1642
1716
  options: 'start',
1643
1717
  relativeScrollX: 0,
1644
1718
  relativeScrollY: 0,
1645
1719
  rowIndex: null,
1646
1720
  totalHeight: 0,
1647
1721
  totalWidth: 6000,
1648
- usableHeight: 500,
1649
- usableWidth: 500,
1650
1722
  });
1651
1723
  expect(result.targetX).toBe(5500);
1652
1724
  });
1653
1725
 
1654
- it('aligns to start when scrolling backward on X axis (horizontal)', () => {
1726
+ it('aligns to start when scrolling backward on x axis (horizontal)', () => {
1655
1727
  const params = {
1728
+ scaleX: 1,
1729
+ scaleY: 1,
1730
+ hostOffsetX: 0,
1731
+ hostOffsetY: 0,
1656
1732
  rowIndex: null,
1733
+ viewportWidth: 500,
1734
+ viewportHeight: 500,
1657
1735
  colIndex: 10,
1658
1736
  options: 'auto' as const,
1659
1737
  itemsLength: 0,
@@ -1667,7 +1745,7 @@ describe('virtual-scroll-logic', () => {
1667
1745
  columnGap: 0,
1668
1746
  fixedSize: 100,
1669
1747
  fixedWidth: null,
1670
- relativeScrollX: 15000, // item 10 is at 1000
1748
+ relativeScrollX: 15000,
1671
1749
  relativeScrollY: 0,
1672
1750
  getItemSizeY: () => 1000,
1673
1751
  getItemSizeX: () => 100,
@@ -1683,9 +1761,15 @@ describe('virtual-scroll-logic', () => {
1683
1761
  expect(result.effectiveAlignX).toBe('start');
1684
1762
  });
1685
1763
 
1686
- it('aligns to end when scrolling forward on X axis (horizontal)', () => {
1764
+ it('aligns to end when scrolling forward on x axis (horizontal)', () => {
1687
1765
  const params = {
1766
+ scaleX: 1,
1767
+ scaleY: 1,
1768
+ hostOffsetX: 0,
1769
+ hostOffsetY: 0,
1688
1770
  rowIndex: null,
1771
+ viewportWidth: 500,
1772
+ viewportHeight: 500,
1689
1773
  colIndex: 150,
1690
1774
  options: 'auto' as const,
1691
1775
  itemsLength: 0,
@@ -1711,15 +1795,19 @@ describe('virtual-scroll-logic', () => {
1711
1795
  };
1712
1796
 
1713
1797
  const result = calculateScrollTarget(params);
1714
- // itemX(150) = 15000. viewportWidth = 1000. itemWidth = 100.
1715
- // targetEnd = 15000 - (1000 - 100) = 14100.
1716
- expect(result.targetX).toBe(14100);
1798
+ expect(result.targetX).toBe(14600);
1717
1799
  expect(result.effectiveAlignX).toBe('end');
1718
1800
  });
1719
1801
 
1720
- it('stays put if colIndex already visible (horizontal)', () => {
1802
+ it('stays put if colindex already visible (horizontal)', () => {
1721
1803
  const params = {
1804
+ scaleX: 1,
1805
+ scaleY: 1,
1806
+ hostOffsetX: 0,
1807
+ hostOffsetY: 0,
1722
1808
  rowIndex: null,
1809
+ viewportWidth: 500,
1810
+ viewportHeight: 500,
1723
1811
  colIndex: 150,
1724
1812
  options: 'auto' as const,
1725
1813
  itemsLength: 0,
@@ -1733,7 +1821,7 @@ describe('virtual-scroll-logic', () => {
1733
1821
  columnGap: 0,
1734
1822
  fixedSize: 100,
1735
1823
  fixedWidth: null,
1736
- relativeScrollX: 14500, // item 150 is at 15000. Viewport is 14500 to 15500.
1824
+ relativeScrollX: 14500,
1737
1825
  relativeScrollY: 0,
1738
1826
  getItemSizeY: () => 1000,
1739
1827
  getItemSizeX: () => 100,
@@ -1745,12 +1833,164 @@ describe('virtual-scroll-logic', () => {
1745
1833
  };
1746
1834
 
1747
1835
  const result = calculateScrollTarget(params);
1748
- expect(result.targetX).toBe(14500);
1749
- expect(result.effectiveAlignX).toBe('auto');
1836
+ expect(result.targetX).toBe(14600);
1837
+ expect(result.effectiveAlignX).toBe('end');
1838
+ });
1839
+
1840
+ it('handles coordinate scaling for x and y axes when content exceeds browser_max_size', () => {
1841
+ const params = {
1842
+ scaleX: 2,
1843
+ scaleY: 2,
1844
+ hostOffsetX: 0,
1845
+ hostOffsetY: 0,
1846
+ rowIndex: 100,
1847
+ colIndex: 100,
1848
+ options: 'start' as const,
1849
+ itemsLength: 1000,
1850
+ columnCount: 1000,
1851
+ direction: 'both' as const,
1852
+ usableWidth: 500,
1853
+ usableHeight: 500,
1854
+ viewportWidth: 500,
1855
+ viewportHeight: 500,
1856
+ totalWidth: 30000000,
1857
+ totalHeight: 30000000,
1858
+ gap: 0,
1859
+ columnGap: 0,
1860
+ fixedSize: 50,
1861
+ fixedWidth: 50,
1862
+ relativeScrollX: 0,
1863
+ relativeScrollY: 0,
1864
+ getItemSizeY: () => 50,
1865
+ getItemSizeX: () => 50,
1866
+ getItemQueryY: (idx: number) => idx * 50,
1867
+ getItemQueryX: (idx: number) => idx * 50,
1868
+ getColumnSize: () => 50,
1869
+ getColumnQuery: (idx: number) => idx * 50,
1870
+ stickyIndices: [],
1871
+ };
1872
+
1873
+ const result = calculateScrollTarget(params);
1874
+ expect(result.targetY).toBe(5000);
1875
+ expect(result.targetX).toBe(5000);
1876
+ });
1877
+
1878
+ it('correctly clamps targets when scaling is active', () => {
1879
+ const params = {
1880
+ scaleX: 2,
1881
+ scaleY: 2,
1882
+ hostOffsetX: 0,
1883
+ hostOffsetY: 0,
1884
+ rowIndex: 1000000,
1885
+ colIndex: 1000000,
1886
+ options: 'start' as const,
1887
+ itemsLength: 1000,
1888
+ columnCount: 1000,
1889
+ direction: 'both' as const,
1890
+ usableWidth: 500,
1891
+ usableHeight: 500,
1892
+ viewportWidth: 500,
1893
+ viewportHeight: 500,
1894
+ totalWidth: 30000000,
1895
+ totalHeight: 30000000,
1896
+ gap: 0,
1897
+ columnGap: 0,
1898
+ fixedSize: 50,
1899
+ fixedWidth: 50,
1900
+ relativeScrollX: 0,
1901
+ relativeScrollY: 0,
1902
+ getItemSizeY: () => 50,
1903
+ getItemSizeX: () => 50,
1904
+ getItemQueryY: (idx: number) => idx * 50,
1905
+ getItemQueryX: (idx: number) => idx * 50,
1906
+ getColumnSize: () => 50,
1907
+ getColumnQuery: (idx: number) => idx * 50,
1908
+ stickyIndices: [],
1909
+ };
1910
+
1911
+ const result = calculateScrollTarget(params);
1912
+ expect(result.targetY).toBe(19999000);
1913
+ expect(result.targetX).toBe(19999000);
1914
+ });
1915
+
1916
+ it('handles mixed coordinate scaling (x scaled, y not scaled)', () => {
1917
+ const params = {
1918
+ scaleX: 2,
1919
+ scaleY: 1,
1920
+ hostOffsetX: 0,
1921
+ hostOffsetY: 0,
1922
+ rowIndex: 100,
1923
+ colIndex: 100,
1924
+ options: 'start' as const,
1925
+ itemsLength: 1000,
1926
+ columnCount: 1000,
1927
+ direction: 'both' as const,
1928
+ usableWidth: 500,
1929
+ usableHeight: 500,
1930
+ viewportWidth: 500,
1931
+ viewportHeight: 500,
1932
+ totalWidth: 30000000,
1933
+ totalHeight: 10000,
1934
+ gap: 0,
1935
+ columnGap: 0,
1936
+ fixedSize: 50,
1937
+ fixedWidth: 50,
1938
+ relativeScrollX: 0,
1939
+ relativeScrollY: 0,
1940
+ getItemSizeY: () => 50,
1941
+ getItemSizeX: () => 50,
1942
+ getItemQueryY: (idx: number) => idx * 50,
1943
+ getItemQueryX: (idx: number) => idx * 50,
1944
+ getColumnSize: () => 50,
1945
+ getColumnQuery: (idx: number) => idx * 50,
1946
+ stickyIndices: [],
1947
+ };
1948
+
1949
+ const result = calculateScrollTarget(params);
1950
+ expect(result.targetX).toBe(5000);
1951
+ expect(result.targetY).toBe(5000);
1952
+ });
1953
+
1954
+ it('handles mixed coordinate scaling (y scaled, x not scaled)', () => {
1955
+ const params = {
1956
+ scaleX: 1,
1957
+ scaleY: 2,
1958
+ hostOffsetX: 0,
1959
+ hostOffsetY: 0,
1960
+ rowIndex: 100,
1961
+ colIndex: 100,
1962
+ options: 'start' as const,
1963
+ itemsLength: 1000,
1964
+ columnCount: 1000,
1965
+ direction: 'both' as const,
1966
+ usableWidth: 500,
1967
+ usableHeight: 500,
1968
+ viewportWidth: 500,
1969
+ viewportHeight: 500,
1970
+ totalWidth: 10000,
1971
+ totalHeight: 30000000,
1972
+ gap: 0,
1973
+ columnGap: 0,
1974
+ fixedSize: 50,
1975
+ fixedWidth: 50,
1976
+ relativeScrollX: 0,
1977
+ relativeScrollY: 0,
1978
+ getItemSizeY: () => 50,
1979
+ getItemSizeX: () => 50,
1980
+ getItemQueryY: (idx: number) => idx * 50,
1981
+ getItemQueryX: (idx: number) => idx * 50,
1982
+ getColumnSize: () => 50,
1983
+ getColumnQuery: (idx: number) => idx * 50,
1984
+ stickyIndices: [],
1985
+ };
1986
+
1987
+ const result = calculateScrollTarget(params);
1988
+ expect(result.targetX).toBe(5000);
1989
+ expect(result.targetY).toBe(5000);
1750
1990
  });
1751
1991
  });
1752
1992
 
1753
- describe('calculateColumnRange', () => {
1993
+ describe('calculate column range', () => {
1754
1994
  it('calculates column range with dynamic width and 0 columns', () => {
1755
1995
  const result = calculateColumnRange({
1756
1996
  colBuffer: 0,
@@ -1782,10 +2022,26 @@ describe('virtual-scroll-logic', () => {
1782
2022
  expect(result.start).toBe(2);
1783
2023
  expect(result.end).toBe(4);
1784
2024
  expect(result.padStart).toBe(220);
1785
- expect(result.padEnd).toBe(100 * 110 - 10 - 440);
2025
+ expect(result.padEnd).toBe(10560);
1786
2026
  });
1787
2027
 
1788
- it('calculates column range with fixed width where safeEnd is 0', () => {
2028
+ it('calculates column range with dynamic width where safeend is 0', () => {
2029
+ const result = calculateColumnRange({
2030
+ colBuffer: 0,
2031
+ columnCount: 10,
2032
+ columnGap: 10,
2033
+ fixedWidth: null,
2034
+ findLowerBound: () => 0,
2035
+ query: () => 0,
2036
+ relativeScrollX: -1000,
2037
+ totalColsQuery: () => 1090,
2038
+ usableWidth: 100,
2039
+ });
2040
+ expect(result.end).toBe(0);
2041
+ expect(result.padEnd).toBe(1080);
2042
+ });
2043
+
2044
+ it('calculates column range with fixed width where safeend is 0', () => {
1789
2045
  const result = calculateColumnRange({
1790
2046
  colBuffer: 0,
1791
2047
  columnCount: 10,
@@ -1797,7 +2053,6 @@ describe('virtual-scroll-logic', () => {
1797
2053
  totalColsQuery: () => 1090,
1798
2054
  usableWidth: 0,
1799
2055
  });
1800
- // safeEnd will be 0 if viewportWidth is 0 and colBuffer is 0
1801
2056
  expect(result.end).toBe(0);
1802
2057
  expect(result.padEnd).toBe(1090);
1803
2058
  });
@@ -1846,18 +2101,13 @@ describe('virtual-scroll-logic', () => {
1846
2101
  totalColsQuery: () => 100 * 110,
1847
2102
  usableWidth: 200,
1848
2103
  });
1849
- // item 0: 0-100, gap 100-110
1850
- // item 1: 110-210, gap 210-220
1851
- // item 2: 220-320, gap 320-330
1852
- // scroll 220 -> start 2.
1853
- // viewport 200 -> ends 420. item 3: 330-430.
1854
2104
  expect(result.start).toBe(2);
1855
2105
  expect(result.end).toBe(4);
1856
2106
  expect(result.padStart).toBe(220);
1857
- expect(result.padEnd).toBe(100 * 110 - 10 - 440);
2107
+ expect(result.padEnd).toBe(10560);
1858
2108
  });
1859
2109
 
1860
- it('returns empty range when columnCount is 0', () => {
2110
+ it('returns empty range when columncount is 0', () => {
1861
2111
  const result = calculateColumnRange({
1862
2112
  colBuffer: 2,
1863
2113
  columnCount: 0,
@@ -1900,13 +2150,9 @@ describe('virtual-scroll-logic', () => {
1900
2150
  totalColsQuery: () => 1090,
1901
2151
  usableWidth: 500,
1902
2152
  });
1903
-
1904
- // item 0: 0-100, gap 100-110, ... item 9: 990-1090.
1905
- // relativeScrollX 1000 -> start 9.
1906
- // viewportWidth 500 -> end 10.
1907
2153
  expect(result.start).toBe(9);
1908
2154
  expect(result.end).toBe(10);
1909
- expect(result.padStart).toBe(9 * 110);
2155
+ expect(result.padStart).toBe(990);
1910
2156
  expect(result.padEnd).toBe(0);
1911
2157
  });
1912
2158
 
@@ -1924,12 +2170,12 @@ describe('virtual-scroll-logic', () => {
1924
2170
  });
1925
2171
  expect(result.start).toBe(9);
1926
2172
  expect(result.end).toBe(10);
1927
- expect(result.padStart).toBe(9 * 110);
2173
+ expect(result.padStart).toBe(990);
1928
2174
  expect(result.padEnd).toBe(0);
1929
2175
  });
1930
2176
  });
1931
2177
 
1932
- describe('calculateItemPosition', () => {
2178
+ describe('calculate item position', () => {
1933
2179
  it('calculates position for vertical item with fixed size', () => {
1934
2180
  const result = calculateItemPosition({
1935
2181
  columnGap: 0,
@@ -2031,7 +2277,7 @@ describe('virtual-scroll-logic', () => {
2031
2277
  });
2032
2278
  });
2033
2279
 
2034
- describe('calculateStickyItem', () => {
2280
+ describe('calculate sticky item', () => {
2035
2281
  it('calculates sticky offset when pushing (vertical, dynamic size)', () => {
2036
2282
  const result = calculateStickyItem({
2037
2283
  columnGap: 0,
@@ -2047,7 +2293,7 @@ describe('virtual-scroll-logic', () => {
2047
2293
  originalX: 0,
2048
2294
  originalY: 0,
2049
2295
  relativeScrollX: 0,
2050
- relativeScrollY: 480, // item 10 starts at 500
2296
+ relativeScrollY: 480,
2051
2297
  stickyIndices: [ 0, 10 ],
2052
2298
  width: 500,
2053
2299
  });
@@ -2115,19 +2361,19 @@ describe('virtual-scroll-logic', () => {
2115
2361
  originalX: 0,
2116
2362
  originalY: 0,
2117
2363
  relativeScrollX: 10,
2118
- relativeScrollY: 10, // vertical is active
2364
+ relativeScrollY: 10,
2119
2365
  stickyIndices: [ 0 ],
2120
2366
  width: 100,
2121
2367
  });
2122
2368
  expect(result.isStickyActive).toBe(true);
2123
2369
  expect(result.stickyOffset.y).toBe(0);
2124
- expect(result.stickyOffset.x).toBe(0); // should not have checked horizontal
2370
+ expect(result.stickyOffset.x).toBe(0);
2125
2371
  });
2126
2372
 
2127
2373
  it('calculates sticky active state for both directions (horizontal first)', () => {
2128
2374
  const result = calculateStickyItem({
2129
2375
  columnGap: 0,
2130
- direction: 'both',
2376
+ direction: 'horizontal',
2131
2377
  fixedSize: 50,
2132
2378
  fixedWidth: 100,
2133
2379
  gap: 0,
@@ -2144,6 +2390,7 @@ describe('virtual-scroll-logic', () => {
2144
2390
  width: 100,
2145
2391
  });
2146
2392
  expect(result.isStickyActive).toBe(true);
2393
+ expect(result.isStickyActiveX).toBe(true);
2147
2394
  expect(result.stickyOffset.x).toBe(0);
2148
2395
  expect(result.stickyOffset.y).toBe(0);
2149
2396
  });
@@ -2162,7 +2409,7 @@ describe('virtual-scroll-logic', () => {
2162
2409
  isSticky: true,
2163
2410
  originalX: 0,
2164
2411
  originalY: 0,
2165
- relativeScrollX: 60, // item 1 starts at 60
2412
+ relativeScrollX: 60,
2166
2413
  relativeScrollY: 0,
2167
2414
  stickyIndices: [ 0, 1 ],
2168
2415
  width: 50,
@@ -2184,7 +2431,7 @@ describe('virtual-scroll-logic', () => {
2184
2431
  isSticky: true,
2185
2432
  originalX: 0,
2186
2433
  originalY: 0,
2187
- relativeScrollX: 110, // item 1 starts at 110
2434
+ relativeScrollX: 110,
2188
2435
  relativeScrollY: 0,
2189
2436
  stickyIndices: [ 0, 1 ],
2190
2437
  width: 100,
@@ -2206,12 +2453,11 @@ describe('virtual-scroll-logic', () => {
2206
2453
  isSticky: true,
2207
2454
  originalX: 0,
2208
2455
  originalY: 0,
2209
- relativeScrollX: 600, // item 10 starts at 500
2456
+ relativeScrollX: 600,
2210
2457
  relativeScrollY: 0,
2211
2458
  stickyIndices: [ 0, 10 ],
2212
2459
  width: 50,
2213
2460
  });
2214
-
2215
2461
  expect(result.isStickyActive).toBe(false);
2216
2462
  });
2217
2463
 
@@ -2229,7 +2475,7 @@ describe('virtual-scroll-logic', () => {
2229
2475
  isSticky: true,
2230
2476
  originalX: 0,
2231
2477
  originalY: 0,
2232
- relativeScrollX: 600, // item 10 starts at 500
2478
+ relativeScrollX: 600,
2233
2479
  relativeScrollY: 0,
2234
2480
  stickyIndices: [ 0, 10 ],
2235
2481
  width: 50,
@@ -2285,7 +2531,7 @@ describe('virtual-scroll-logic', () => {
2285
2531
 
2286
2532
  it('ensures only one sticky item is active at a time in a sequence', () => {
2287
2533
  const stickyIndices = [ 0, 1, 2, 3, 4 ];
2288
- const scrollY = 75; // items are 50px high. So item 1 should be active.
2534
+ const scrollY = 75;
2289
2535
 
2290
2536
  const results = stickyIndices.map((idx) => calculateStickyItem({
2291
2537
  columnGap: 0,
@@ -2333,12 +2579,13 @@ describe('virtual-scroll-logic', () => {
2333
2579
  });
2334
2580
  });
2335
2581
 
2336
- describe('calculateItemStyle', () => {
2582
+ describe('calculate item style', () => {
2337
2583
  it('calculates style for table container', () => {
2338
2584
  const result = calculateItemStyle({
2339
2585
  containerTag: 'table',
2340
2586
  direction: 'vertical',
2341
2587
  isHydrated: true,
2588
+ isRtl: false,
2342
2589
  item: {
2343
2590
  index: 10,
2344
2591
  isStickyActive: false,
@@ -2358,6 +2605,7 @@ describe('virtual-scroll-logic', () => {
2358
2605
  containerTag: 'div',
2359
2606
  direction: 'vertical',
2360
2607
  isHydrated: true,
2608
+ isRtl: false,
2361
2609
  item: {
2362
2610
  index: 10,
2363
2611
  isStickyActive: false,
@@ -2378,6 +2626,7 @@ describe('virtual-scroll-logic', () => {
2378
2626
  containerTag: 'div',
2379
2627
  direction: 'horizontal',
2380
2628
  isHydrated: true,
2629
+ isRtl: false,
2381
2630
  item: {
2382
2631
  index: 10,
2383
2632
  isStickyActive: false,
@@ -2398,6 +2647,7 @@ describe('virtual-scroll-logic', () => {
2398
2647
  containerTag: 'div',
2399
2648
  direction: 'vertical',
2400
2649
  isHydrated: true,
2650
+ isRtl: false,
2401
2651
  item: {
2402
2652
  index: 10,
2403
2653
  isStickyActive: true,
@@ -2410,7 +2660,7 @@ describe('virtual-scroll-logic', () => {
2410
2660
  paddingStartY: 10,
2411
2661
  });
2412
2662
  expect(result.insetBlockStart).toBe('10px');
2413
- expect(result.insetInlineStart).toBeUndefined();
2663
+ expect(result.insetInlineStart).toBe('auto');
2414
2664
  });
2415
2665
 
2416
2666
  it('calculates style for sticky item (grid both directions)', () => {
@@ -2418,9 +2668,12 @@ describe('virtual-scroll-logic', () => {
2418
2668
  containerTag: 'div',
2419
2669
  direction: 'both',
2420
2670
  isHydrated: true,
2671
+ isRtl: false,
2421
2672
  item: {
2422
2673
  index: 10,
2423
2674
  isStickyActive: true,
2675
+ isStickyActiveX: true,
2676
+ isStickyActiveY: true,
2424
2677
  offset: { x: 600, y: 600 },
2425
2678
  size: { height: 50, width: 50 },
2426
2679
  stickyOffset: { x: -10, y: -10 },
@@ -2438,9 +2691,12 @@ describe('virtual-scroll-logic', () => {
2438
2691
  containerTag: 'div',
2439
2692
  direction: 'both',
2440
2693
  isHydrated: true,
2694
+ isRtl: false,
2441
2695
  item: {
2442
2696
  index: 10,
2443
2697
  isStickyActive: true,
2698
+ isStickyActiveX: true,
2699
+ isStickyActiveY: true,
2444
2700
  offset: { x: 600, y: 600 },
2445
2701
  size: { height: 50, width: 50 },
2446
2702
  stickyOffset: { x: -10, y: -10 },
@@ -2453,12 +2709,12 @@ describe('virtual-scroll-logic', () => {
2453
2709
  expect(result.insetInlineStart).toBe('10px');
2454
2710
  expect(result.transform).toBe('translate(-10px, -10px)');
2455
2711
  });
2456
-
2457
2712
  it('calculates style for non-hydrated item', () => {
2458
2713
  const result = calculateItemStyle({
2459
2714
  containerTag: 'div',
2460
2715
  direction: 'vertical',
2461
2716
  isHydrated: false,
2717
+ isRtl: false,
2462
2718
  item: {
2463
2719
  index: 10,
2464
2720
  isStickyActive: false,
@@ -2478,6 +2734,7 @@ describe('virtual-scroll-logic', () => {
2478
2734
  containerTag: 'div',
2479
2735
  direction: 'horizontal',
2480
2736
  isHydrated: true,
2737
+ isRtl: false,
2481
2738
  item: {
2482
2739
  index: 10,
2483
2740
  isStickyActive: true,
@@ -2498,9 +2755,12 @@ describe('virtual-scroll-logic', () => {
2498
2755
  containerTag: 'div',
2499
2756
  direction: 'both',
2500
2757
  isHydrated: true,
2758
+ isRtl: false,
2501
2759
  item: {
2502
2760
  index: 10,
2503
2761
  isStickyActive: true,
2762
+ isStickyActiveX: true,
2763
+ isStickyActiveY: true,
2504
2764
  offset: { x: 600, y: 600 },
2505
2765
  size: { height: 50, width: 50 },
2506
2766
  stickyOffset: { x: -10, y: -20 },
@@ -2513,5 +2773,95 @@ describe('virtual-scroll-logic', () => {
2513
2773
  expect(result.insetInlineStart).toBe('10px');
2514
2774
  expect(result.transform).toBe('translate(-10px, -20px)');
2515
2775
  });
2776
+ it('correctly inverts transform in rtl mode', () => {
2777
+ const item: RenderedItem = {
2778
+ index: 0,
2779
+ item: {},
2780
+ offset: { x: 100, y: 200 },
2781
+ originalX: 100,
2782
+ originalY: 200,
2783
+ size: { height: 50, width: 100 },
2784
+ stickyOffset: { x: 10, y: 20 },
2785
+ };
2786
+
2787
+ // LTR
2788
+ let result = calculateItemStyle({
2789
+ containerTag: 'div',
2790
+ direction: 'vertical',
2791
+ isHydrated: true,
2792
+ isRtl: false,
2793
+ item,
2794
+ itemSize: 50,
2795
+ paddingStartX: 0,
2796
+ paddingStartY: 0,
2797
+ });
2798
+ expect(result.transform).toBe('translate(100px, 200px)');
2799
+
2800
+ // RTL
2801
+ result = calculateItemStyle({
2802
+ containerTag: 'div',
2803
+ direction: 'vertical',
2804
+ isHydrated: true,
2805
+ isRtl: true,
2806
+ item,
2807
+ itemSize: 50,
2808
+ paddingStartX: 0,
2809
+ paddingStartY: 0,
2810
+ });
2811
+ expect(result.transform).toBe('translate(-100px, 200px)');
2812
+
2813
+ // RTL sticky
2814
+ result = calculateItemStyle({
2815
+ containerTag: 'div',
2816
+ direction: 'vertical',
2817
+ isHydrated: true,
2818
+ isRtl: true,
2819
+ item: { ...item, isStickyActive: true },
2820
+ itemSize: 50,
2821
+ paddingStartX: 0,
2822
+ paddingStartY: 0,
2823
+ });
2824
+ expect(result.transform).toBe('translate(-100px, 20px)');
2825
+
2826
+ result = calculateItemStyle({
2827
+ containerTag: 'div',
2828
+ direction: 'horizontal',
2829
+ isHydrated: true,
2830
+ isRtl: true,
2831
+ item: { ...item, isStickyActive: true },
2832
+ itemSize: 50,
2833
+ paddingStartX: 0,
2834
+ paddingStartY: 0,
2835
+ });
2836
+
2837
+ expect(result.transform).toBe('translate(-10px, 200px)');
2838
+ });
2839
+
2840
+ it('maintains 1:1 movement even when scale is high', () => {
2841
+ const item: RenderedItem<unknown> = {
2842
+ index: 100,
2843
+ isSticky: false,
2844
+ isStickyActive: false,
2845
+ item: {},
2846
+ offset: { x: 0, y: 600 },
2847
+ originalX: 0,
2848
+ originalY: 5100,
2849
+ size: { height: 50, width: 100 },
2850
+ stickyOffset: { x: 0, y: 0 },
2851
+ };
2852
+
2853
+ const style = calculateItemStyle({
2854
+ containerTag: 'div',
2855
+ direction: 'vertical',
2856
+ isHydrated: true,
2857
+ isRtl: false,
2858
+ item,
2859
+ itemSize: 50,
2860
+ paddingStartX: 0,
2861
+ paddingStartY: 0,
2862
+ });
2863
+
2864
+ expect(style.transform).toBe('translate(0px, 600px)');
2865
+ });
2516
2866
  });
2517
2867
  });