@pdanpdan/virtual-scroll 0.5.0 → 0.6.1

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.
package/src/types.ts CHANGED
@@ -1,3 +1,44 @@
1
+ /** Default fallback size for items (VU). */
2
+ export const DEFAULT_ITEM_SIZE = 40;
3
+ /** Default fallback width for columns (VU). */
4
+ export const DEFAULT_COLUMN_WIDTH = 100;
5
+ /** Default number of items to render outside the viewport. */
6
+ export const DEFAULT_BUFFER = 5;
7
+
8
+ /** Represents a point in 2D space. */
9
+ export interface Point {
10
+ /** X coordinate. */
11
+ x: number;
12
+ /** Y coordinate. */
13
+ y: number;
14
+ }
15
+
16
+ /** Represents dimensions in 2D space. */
17
+ export interface Size {
18
+ /** Width dimension. */
19
+ width: number;
20
+ /** Height dimension. */
21
+ height: number;
22
+ }
23
+
24
+ /** Initial empty state for scroll details. */
25
+ export const EMPTY_SCROLL_DETAILS: ScrollDetails<unknown> = {
26
+ items: [],
27
+ currentIndex: 0,
28
+ currentColIndex: 0,
29
+ currentEndIndex: 0,
30
+ currentEndColIndex: 0,
31
+ scrollOffset: { x: 0, y: 0 },
32
+ displayScrollOffset: { x: 0, y: 0 },
33
+ viewportSize: { width: 0, height: 0 },
34
+ displayViewportSize: { width: 0, height: 0 },
35
+ totalSize: { width: 0, height: 0 },
36
+ isScrolling: false,
37
+ isProgrammaticScroll: false,
38
+ range: { start: 0, end: 0 },
39
+ columnRange: { start: 0, end: 0, padStart: 0, padEnd: 0 },
40
+ };
41
+
1
42
  /**
2
43
  * The direction of the virtual scroll.
3
44
  * - 'vertical': Single-column vertical scrolling.
@@ -55,19 +96,9 @@ export interface RenderedItem<T = unknown> {
55
96
  /** The 0-based index of the item in the original array. */
56
97
  index: number;
57
98
  /** The calculated pixel offset relative to the items wrapper in display pixels (DU). */
58
- offset: {
59
- /** Horizontal offset (left) in DU. */
60
- x: number;
61
- /** Vertical offset (top) in DU. */
62
- y: number;
63
- };
99
+ offset: Point;
64
100
  /** The current measured or estimated size of the item in virtual units (VU). */
65
- size: {
66
- /** Pixel width in VU. */
67
- width: number;
68
- /** Pixel height in VU. */
69
- height: number;
70
- };
101
+ size: Size;
71
102
  /** The original horizontal pixel offset before any sticky adjustments in VU. */
72
103
  originalX: number;
73
104
  /** The original vertical pixel offset before any sticky adjustments in VU. */
@@ -76,13 +107,12 @@ export interface RenderedItem<T = unknown> {
76
107
  isSticky?: boolean;
77
108
  /** Whether this item is currently in a stuck state at the viewport edge. */
78
109
  isStickyActive?: boolean;
110
+ /** Whether this item is currently in a stuck state at the horizontal viewport edge. */
111
+ isStickyActiveX?: boolean;
112
+ /** Whether this item is currently in a stuck state at the vertical viewport edge. */
113
+ isStickyActiveY?: boolean;
79
114
  /** The relative translation applied to the item for the sticky pushing effect in DU. */
80
- stickyOffset: {
81
- /** Horizontal translation in DU. */
82
- x: number;
83
- /** Vertical translation in DU. */
84
- y: number;
85
- };
115
+ stickyOffset: Point;
86
116
  }
87
117
 
88
118
  /** Information about the currently visible range of columns and their paddings. */
@@ -110,40 +140,15 @@ export interface ScrollDetails<T = unknown> {
110
140
  /** Index of the last column visible before any sticky end column in the viewport (grid mode). */
111
141
  currentEndColIndex: number;
112
142
  /** Current relative pixel scroll position from the content start in VU. */
113
- scrollOffset: {
114
- /** Horizontal position (X) in VU. */
115
- x: number;
116
- /** Vertical position (Y) in VU. */
117
- y: number;
118
- };
143
+ scrollOffset: Point;
119
144
  /** Current display pixel scroll position (before scaling) in DU. */
120
- displayScrollOffset: {
121
- /** Horizontal position (X) in DU. */
122
- x: number;
123
- /** Vertical position (Y) in DU. */
124
- y: number;
125
- };
145
+ displayScrollOffset: Point;
126
146
  /** Current dimensions of the visible viewport area in VU. */
127
- viewportSize: {
128
- /** Pixel width in VU. */
129
- width: number;
130
- /** Pixel height in VU. */
131
- height: number;
132
- };
147
+ viewportSize: Size;
133
148
  /** Current dimensions of the visible viewport area in display pixels (DU). */
134
- displayViewportSize: {
135
- /** Pixel width in DU. */
136
- width: number;
137
- /** Pixel height in DU. */
138
- height: number;
139
- };
149
+ displayViewportSize: Size;
140
150
  /** Total calculated or estimated size of all items and gaps in VU. */
141
- totalSize: {
142
- /** Total pixel width in VU. */
143
- width: number;
144
- /** Total pixel height in VU. */
145
- height: number;
146
- };
151
+ totalSize: Size;
147
152
  /** Whether the container is currently being scrolled by the user or an animation. */
148
153
  isScrolling: boolean;
149
154
  /** Whether the current scroll operation was initiated programmatically. */
@@ -159,18 +164,34 @@ export interface ScrollDetails<T = unknown> {
159
164
  columnRange: ColumnRange;
160
165
  }
161
166
 
162
- /** Configuration properties for the `useVirtualScroll` composable. */
163
- export interface VirtualScrollProps<T = unknown> {
164
- /**
165
- * Array of data items to virtualize.
166
- */
167
+ /**
168
+ * Configuration for Server-Side Rendering.
169
+ * Defines which items are rendered statically on the server.
170
+ */
171
+ export interface SSRRange {
172
+ /** First row index (for list or grid). */
173
+ start: number;
174
+ /** Exclusive last row index (for list or grid). */
175
+ end: number;
176
+ /** First column index (for grid mode). */
177
+ colStart?: number;
178
+ /** Exclusive last column index (for grid mode). */
179
+ colEnd?: number;
180
+ }
181
+
182
+ /** Pixel padding configuration in display pixels (DU). */
183
+ export type PaddingValue = number | { x?: number; y?: number; };
184
+
185
+ /** Base configuration properties shared between the component and the composable. */
186
+ export interface VirtualScrollBaseProps<T = unknown> {
187
+ /** Array of data items to virtualize. */
167
188
  items: T[];
168
189
 
169
190
  /**
170
191
  * Fixed size of each item in virtual units (VU) or a function that returns the size of an item.
171
192
  * Pass `0`, `null` or `undefined` for automatic dynamic size detection via `ResizeObserver`.
172
193
  */
173
- itemSize?: number | ((item: T, index: number) => number) | undefined;
194
+ itemSize?: number | ((item: T, index: number) => number) | null | undefined;
174
195
 
175
196
  /**
176
197
  * Direction of the virtual scroll.
@@ -196,32 +217,11 @@ export interface VirtualScrollProps<T = unknown> {
196
217
  */
197
218
  container?: HTMLElement | Window | null | undefined;
198
219
 
199
- /**
200
- * The host element that directly wraps the absolute-positioned items.
201
- * Used for calculating relative offsets in display pixels (DU).
202
- */
203
- hostElement?: HTMLElement | null | undefined;
204
-
205
- /**
206
- * The root element of the VirtualScroll component.
207
- * Used for calculating relative offsets in display pixels (DU).
208
- */
209
- hostRef?: HTMLElement | null | undefined;
210
-
211
220
  /**
212
221
  * Configuration for Server-Side Rendering.
213
222
  * Defines which items are rendered statically on the server.
214
223
  */
215
- ssrRange?: {
216
- /** First row index. */
217
- start: number;
218
- /** Exclusive last row index. */
219
- end: number;
220
- /** First column index (grid mode). */
221
- colStart?: number;
222
- /** Exclusive last column index (grid mode). */
223
- colEnd?: number;
224
- } | undefined;
224
+ ssrRange?: SSRRange | undefined;
225
225
 
226
226
  /**
227
227
  * Number of columns for bidirectional grid scrolling.
@@ -232,29 +232,17 @@ export interface VirtualScrollProps<T = unknown> {
232
232
  * Fixed width of columns in VU, an array of widths, or a function returning widths.
233
233
  * Pass `0`, `null` or `undefined` for dynamic column detection.
234
234
  */
235
- columnWidth?: number | number[] | ((index: number) => number) | undefined;
235
+ columnWidth?: number | number[] | ((index: number) => number) | null | undefined;
236
236
 
237
237
  /**
238
238
  * Pixel padding at the start of the scroll container in display pixels (DU).
239
239
  */
240
- scrollPaddingStart?: number | { x?: number; y?: number; } | undefined;
240
+ scrollPaddingStart?: PaddingValue | undefined;
241
241
 
242
242
  /**
243
243
  * Pixel padding at the end of the scroll container in DU.
244
244
  */
245
- scrollPaddingEnd?: number | { x?: number; y?: number; } | undefined;
246
-
247
- /**
248
- * Size of sticky elements at the start of the viewport (top or left) in DU.
249
- * Used to adjust the visible range and item positioning without increasing content size.
250
- */
251
- stickyStart?: number | { x?: number; y?: number; } | undefined;
252
-
253
- /**
254
- * Size of sticky elements at the end of the viewport (bottom or right) in DU.
255
- * Used to adjust the visible range without increasing content size.
256
- */
257
- stickyEnd?: number | { x?: number; y?: number; } | undefined;
245
+ scrollPaddingEnd?: PaddingValue | undefined;
258
246
 
259
247
  /**
260
248
  * Gap between items in virtual units (VU).
@@ -273,16 +261,6 @@ export interface VirtualScrollProps<T = unknown> {
273
261
  */
274
262
  stickyIndices?: number[] | undefined;
275
263
 
276
- /**
277
- * Extra padding (display pixels - DU) at the start of the flow (e.g. non-sticky header).
278
- */
279
- flowPaddingStart?: number | { x?: number; y?: number; } | undefined;
280
-
281
- /**
282
- * Extra padding (DU) at the end of the flow (e.g. non-sticky footer).
283
- */
284
- flowPaddingEnd?: number | { x?: number; y?: number; } | undefined;
285
-
286
264
  /**
287
265
  * Threshold distance from the end in display pixels (DU) to emit the 'load' event.
288
266
  * @default 200
@@ -326,6 +304,43 @@ export interface VirtualScrollProps<T = unknown> {
326
304
  debug?: boolean | undefined;
327
305
  }
328
306
 
307
+ /** Configuration properties for the `useVirtualScroll` composable. */
308
+ export interface VirtualScrollProps<T = unknown> extends VirtualScrollBaseProps<T> {
309
+ /**
310
+ * The host element that directly wraps the absolute-positioned items.
311
+ * Used for calculating relative offsets in display pixels (DU).
312
+ */
313
+ hostElement?: HTMLElement | null | undefined;
314
+
315
+ /**
316
+ * The root element of the VirtualScroll component.
317
+ * Used for calculating relative offsets in display pixels (DU).
318
+ */
319
+ hostRef?: HTMLElement | null | undefined;
320
+
321
+ /**
322
+ * Size of sticky elements at the start of the viewport (top or left) in DU.
323
+ * Used to adjust the visible range and item positioning without increasing content size.
324
+ */
325
+ stickyStart?: PaddingValue | undefined;
326
+
327
+ /**
328
+ * Size of sticky elements at the end of the viewport (bottom or right) in DU.
329
+ * Used to adjust the visible range without increasing content size.
330
+ */
331
+ stickyEnd?: PaddingValue | undefined;
332
+
333
+ /**
334
+ * Extra padding (display pixels - DU) at the start of the flow (e.g. non-sticky header).
335
+ */
336
+ flowPaddingStart?: PaddingValue | undefined;
337
+
338
+ /**
339
+ * Extra padding (DU) at the end of the flow (e.g. non-sticky footer).
340
+ */
341
+ flowPaddingEnd?: PaddingValue | undefined;
342
+ }
343
+
329
344
  /** Help provide axis specific information to the scrollbar. */
330
345
  export type ScrollAxis = 'vertical' | 'horizontal';
331
346
 
@@ -374,6 +389,8 @@ export interface VirtualScrollbarProps {
374
389
 
375
390
  /** Properties passed to the 'scrollbar' scoped slot. */
376
391
  export interface ScrollbarSlotProps {
392
+ /** The axis for this scrollbar. */
393
+ axis: ScrollAxis;
377
394
  /** Current scroll position as a percentage (0 to 1). */
378
395
  positionPercent: number;
379
396
  /** Viewport size as a percentage of total size (0 to 1). */
@@ -412,16 +429,7 @@ export interface ItemSlotProps<T = unknown> {
412
429
  /** The 0-based index of the item. */
413
430
  index: number;
414
431
  /** Information about the currently visible range of columns. */
415
- columnRange: {
416
- /** First rendered column. */
417
- start: number;
418
- /** Last rendered column (exclusive). */
419
- end: number;
420
- /** Pixel space before first column. */
421
- padStart: number;
422
- /** Pixel space after last column. */
423
- padEnd: number;
424
- };
432
+ columnRange: ColumnRange;
425
433
  /** Helper to get the current calculated width of any column index. */
426
434
  getColumnWidth: (index: number) => number;
427
435
  /** Vertical gap between items. */
@@ -432,73 +440,31 @@ export interface ItemSlotProps<T = unknown> {
432
440
  isSticky?: boolean | undefined;
433
441
  /** Whether this item is currently in a sticky state at the edge. */
434
442
  isStickyActive?: boolean | undefined;
443
+ /** Whether this item is currently in a sticky state at the horizontal edge. */
444
+ isStickyActiveX?: boolean | undefined;
445
+ /** Whether this item is currently in a sticky state at the vertical edge. */
446
+ isStickyActiveY?: boolean | undefined;
447
+ /** The calculated pixel offset relative to the items wrapper in display pixels (DU). */
448
+ offset: {
449
+ /** Horizontal offset (left) in DU. */
450
+ x: number;
451
+ /** Vertical offset (top) in DU. */
452
+ y: number;
453
+ };
435
454
  }
436
455
 
437
456
  /** Configuration properties for the `VirtualScroll` component. */
438
- export interface VirtualScrollComponentProps<T = unknown> {
439
- /** Array of items to be virtualized. */
440
- items: T[];
441
- /** Fixed size of each item (in pixels) or a function that returns the size of an item. */
442
- itemSize?: number | ((item: T, index: number) => number) | null;
443
- /** Direction of the scroll. */
444
- direction?: ScrollDirection;
445
- /** Number of items to render before the visible viewport. */
446
- bufferBefore?: number;
447
- /** Number of items to render after the visible viewport. */
448
- bufferAfter?: number;
449
- /** The scrollable container element or window. */
450
- container?: HTMLElement | Window | null;
451
- /** Range of items to render during Server-Side Rendering. */
452
- ssrRange?: {
453
- /** First row index to render. */
454
- start: number;
455
- /** Last row index to render (exclusive). */
456
- end: number;
457
- /** First column index to render (for grid mode). */
458
- colStart?: number;
459
- /** Last column index to render (exclusive, for grid mode). */
460
- colEnd?: number;
461
- };
462
- /** Number of columns for bidirectional (grid) scroll. */
463
- columnCount?: number;
464
- /** Fixed width of columns (in pixels), an array of widths, or a function for column widths. */
465
- columnWidth?: number | number[] | ((index: number) => number) | null;
457
+ export interface VirtualScrollComponentProps<T = unknown> extends VirtualScrollBaseProps<T> {
466
458
  /** The HTML tag to use for the root container. */
467
459
  containerTag?: string;
468
460
  /** The HTML tag to use for the items wrapper. */
469
461
  wrapperTag?: string;
470
462
  /** The HTML tag to use for each item. */
471
463
  itemTag?: string;
472
- /** Additional padding at the start of the scroll container (top or left). */
473
- scrollPaddingStart?: number | { x?: number; y?: number; };
474
- /** Additional padding at the end of the scroll container (bottom or right). */
475
- scrollPaddingEnd?: number | { x?: number; y?: number; };
476
464
  /** Whether the content in the 'header' slot is sticky. */
477
465
  stickyHeader?: boolean;
478
466
  /** Whether the content in the 'footer' slot is sticky. */
479
467
  stickyFooter?: boolean;
480
- /** Gap between items in pixels. */
481
- gap?: number;
482
- /** Gap between columns in pixels. */
483
- columnGap?: number;
484
- /** Indices of items that should stick to the top/start of the viewport. */
485
- stickyIndices?: number[];
486
- /** Distance from the end of the scrollable area (in pixels) to trigger the 'load' event. */
487
- loadDistance?: number;
488
- /** Whether items are currently being loaded. */
489
- loading?: boolean;
490
- /** Whether to automatically restore and maintain scroll position when items are prepended to the list. */
491
- restoreScrollOnPrepend?: boolean;
492
- /** Initial scroll index to jump to immediately after mount. */
493
- initialScrollIndex?: number;
494
- /** Alignment for the initial scroll index. */
495
- initialScrollAlign?: ScrollAlignment | ScrollAlignmentOptions;
496
- /** Default size for items before they are measured by ResizeObserver. */
497
- defaultItemSize?: number;
498
- /** Default width for columns before they are measured by ResizeObserver. */
499
- defaultColumnWidth?: number;
500
- /** Whether to show debug information (visible offsets and indices) over items. */
501
- debug?: boolean;
502
468
  /** Whether to use virtual scrollbars for styling purposes. */
503
469
  virtualScrollbar?: boolean;
504
470
  }
@@ -544,7 +510,7 @@ export interface VirtualScrollInstance<T = unknown> extends VirtualScrollCompone
544
510
  /** Physical height of the content in the DOM (clamped to browser limits). */
545
511
  renderedHeight: number;
546
512
  /** Absolute offset of the component within its container. */
547
- componentOffset: { x: number; y: number; };
513
+ componentOffset: Point;
548
514
  /** Properties for the vertical scrollbar. */
549
515
  scrollbarPropsVertical: ScrollbarSlotProps | null;
550
516
  /** Properties for the horizontal scrollbar. */
@@ -615,10 +581,6 @@ export interface ScrollTargetParams {
615
581
  flowPaddingStartX?: number | undefined;
616
582
  /** Flow padding start on Y axis. */
617
583
  flowPaddingStartY?: number | undefined;
618
- /** Flow padding end on X axis. */
619
- flowPaddingEndX?: number | undefined;
620
- /** Flow padding end on Y axis. */
621
- flowPaddingEndY?: number | undefined;
622
584
  /** Scroll padding start on X axis. */
623
585
  paddingStartX?: number | undefined;
624
586
  /** Scroll padding start on Y axis. */
@@ -1,5 +1,13 @@
1
+ /**
2
+ * Utilities for scroll management and element type detection.
3
+ * Provides helper functions for checking Window and Body elements,
4
+ * and a universal scrollTo function.
5
+ */
6
+
1
7
  import type { ScrollDirection, ScrollToIndexOptions } from '../types';
2
8
 
9
+ /* global ScrollToOptions */
10
+
3
11
  /**
4
12
  * Maximum size (in pixels) for an element that most browsers can handle reliably.
5
13
  * Beyond this size, we use scaling for the scrollable area.
@@ -57,6 +65,29 @@ export function isScrollableElement(target: EventTarget | null): target is HTMLE
57
65
  return target != null && 'scrollLeft' in target;
58
66
  }
59
67
 
68
+ /**
69
+ * Universal scroll function that handles both Window and HTMLElements.
70
+ *
71
+ * @param container - The container to scroll.
72
+ * @param options - Scroll options.
73
+ */
74
+ export function scrollTo(container: HTMLElement | Window | null | undefined, options: ScrollToOptions) {
75
+ if (isWindow(container)) {
76
+ window.scrollTo(options);
77
+ } else if (container != null && isScrollableElement(container)) {
78
+ if (typeof container.scrollTo === 'function') {
79
+ container.scrollTo(options);
80
+ } else {
81
+ if (options.left !== undefined) {
82
+ container.scrollLeft = options.left;
83
+ }
84
+ if (options.top !== undefined) {
85
+ container.scrollTop = options.top;
86
+ }
87
+ }
88
+ }
89
+ }
90
+
60
91
  /**
61
92
  * Helper to determine if an options argument is a full `ScrollToIndexOptions` object.
62
93
  *
@@ -293,6 +293,45 @@ function calculateAxisTarget({
293
293
  return { target, itemSize, effectiveAlign };
294
294
  }
295
295
 
296
+ /**
297
+ * Helper to calculate sticky state for a single axis.
298
+ *
299
+ * @param scrollPos - Virtual scroll position.
300
+ * @param originalPos - Original virtual item position.
301
+ * @param size - Virtual item size.
302
+ * @param index - Item index.
303
+ * @param stickyIndices - All sticky indices.
304
+ * @param getNextStickyPos - Resolver for the next sticky item's position.
305
+ * @returns Sticky state for this axis.
306
+ */
307
+ function calculateAxisSticky(
308
+ scrollPos: number,
309
+ originalPos: number,
310
+ size: number,
311
+ index: number,
312
+ stickyIndices: number[],
313
+ getNextStickyPos: (idx: number) => number,
314
+ ) {
315
+ if (scrollPos <= originalPos) {
316
+ return { isActive: false, offset: 0 };
317
+ }
318
+
319
+ const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
320
+ if (nextStickyIdx === undefined) {
321
+ return { isActive: true, offset: 0 };
322
+ }
323
+
324
+ const nextStickyPos = getNextStickyPos(nextStickyIdx);
325
+ if (scrollPos >= nextStickyPos) {
326
+ return { isActive: false, offset: 0 };
327
+ }
328
+
329
+ return {
330
+ isActive: true,
331
+ offset: Math.max(0, Math.min(size, nextStickyPos - scrollPos)) - size,
332
+ };
333
+ }
334
+
296
335
  // --- Exported Functions ---
297
336
 
298
337
  /**
@@ -639,7 +678,6 @@ export function calculateColumnRange({
639
678
  * @param params.height - Virtual item height (VU).
640
679
  * @param params.stickyIndices - All sticky indices.
641
680
  * @param params.fixedSize - Fixed item size (VU).
642
- * @param params.fixedWidth - Fixed column width (VU).
643
681
  * @param params.gap - Item gap (VU).
644
682
  * @param params.columnGap - Column gap (VU).
645
683
  * @param params.getItemQueryY - Resolver for vertical offset (VU).
@@ -659,61 +697,56 @@ export function calculateStickyItem({
659
697
  height,
660
698
  stickyIndices,
661
699
  fixedSize,
662
- fixedWidth,
663
700
  gap,
664
701
  columnGap,
665
702
  getItemQueryY,
666
703
  getItemQueryX,
667
704
  }: StickyParams) {
668
- let isStickyActive = false;
705
+ let isStickyActiveX = false;
706
+ let isStickyActiveY = false;
669
707
  const stickyOffset = { x: 0, y: 0 };
670
708
 
671
709
  if (!isSticky) {
672
- return { isStickyActive, stickyOffset };
710
+ return { isStickyActiveX, isStickyActiveY, isStickyActive: false, stickyOffset };
673
711
  }
674
712
 
675
713
  // Y Axis (Sticky Rows)
676
714
  if (direction === 'vertical' || direction === 'both') {
677
- if (relativeScrollY > originalY) {
678
- const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
679
-
680
- if (nextStickyIdx !== undefined) {
681
- const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : getItemQueryY(nextStickyIdx);
682
- if (relativeScrollY >= nextStickyY) {
683
- isStickyActive = false;
684
- } else {
685
- isStickyActive = true;
686
- stickyOffset.y = Math.max(0, Math.min(height, nextStickyY - relativeScrollY)) - height;
687
- }
688
- } else {
689
- isStickyActive = true;
690
- }
691
- }
715
+ const res = calculateAxisSticky(
716
+ relativeScrollY,
717
+ originalY,
718
+ height,
719
+ index,
720
+ stickyIndices,
721
+ (nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + gap) : getItemQueryY(nextIdx)),
722
+ );
723
+ isStickyActiveY = res.isActive;
724
+ stickyOffset.y = res.offset;
692
725
  }
693
726
 
694
727
  // X Axis (Sticky Columns / Items)
695
- if (direction === 'horizontal' || (direction === 'both' && !isStickyActive)) {
696
- if (relativeScrollX > originalX) {
697
- const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
698
-
699
- if (nextStickyIdx !== undefined) {
700
- const nextStickyX = direction === 'horizontal'
701
- ? (fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : getItemQueryX(nextStickyIdx))
702
- : (fixedWidth !== null ? nextStickyIdx * (fixedWidth + columnGap) : getItemQueryX(nextStickyIdx));
703
-
704
- if (relativeScrollX >= nextStickyX) {
705
- isStickyActive = false;
706
- } else {
707
- isStickyActive = true;
708
- stickyOffset.x = Math.max(0, Math.min(width, nextStickyX - relativeScrollX)) - width;
709
- }
710
- } else {
711
- isStickyActive = true;
712
- }
728
+ if (direction === 'horizontal') {
729
+ const res = calculateAxisSticky(
730
+ relativeScrollX,
731
+ originalX,
732
+ width,
733
+ index,
734
+ stickyIndices,
735
+ (nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + columnGap) : getItemQueryX(nextIdx)),
736
+ );
737
+
738
+ if (res.isActive) {
739
+ isStickyActiveX = true;
740
+ stickyOffset.x = res.offset;
713
741
  }
714
742
  }
715
743
 
716
- return { isStickyActive, stickyOffset };
744
+ return {
745
+ isStickyActiveX,
746
+ isStickyActiveY,
747
+ isStickyActive: isStickyActiveX || isStickyActiveY,
748
+ stickyOffset,
749
+ };
717
750
  }
718
751
 
719
752
  /**
@@ -824,18 +857,18 @@ export function calculateItemStyle<T = unknown>({
824
857
  }
825
858
 
826
859
  if (isHydrated) {
827
- const tx = isRtl
828
- ? -(item.isStickyActive ? item.stickyOffset.x : item.offset.x)
829
- : (item.isStickyActive ? item.stickyOffset.x : item.offset.x);
860
+ const isStickingVertically = item.isStickyActiveY ?? (item.isStickyActive && (isVertical || isBoth));
861
+ const isStickingHorizontally = item.isStickyActiveX ?? (item.isStickyActive && isHorizontal);
830
862
 
831
- if (item.isStickyActive) {
832
- if (isVertical || isBoth) {
833
- style.insetBlockStart = `${ paddingStartY }px`;
834
- }
835
- if (isHorizontal || isBoth) {
836
- style.insetInlineStart = `${ paddingStartX }px`;
837
- }
838
- style.transform = `translate(${ tx }px, ${ item.stickyOffset.y }px)`;
863
+ const tx = isRtl
864
+ ? -(isStickingHorizontally ? item.stickyOffset.x : item.offset.x)
865
+ : (isStickingHorizontally ? item.stickyOffset.x : item.offset.x);
866
+ const ty = isStickingVertically ? item.stickyOffset.y : item.offset.y;
867
+
868
+ if (item.isStickyActive || item.isStickyActiveX || item.isStickyActiveY) {
869
+ style.insetBlockStart = isStickingVertically ? `${ paddingStartY }px` : 'auto';
870
+ style.insetInlineStart = isStickingHorizontally ? `${ paddingStartX }px` : 'auto';
871
+ style.transform = `translate(${ tx }px, ${ ty }px)`;
839
872
  } else {
840
873
  style.transform = `translate(${ tx }px, ${ item.offset.y }px)`;
841
874
  }