@pdanpdan/virtual-scroll 0.5.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.
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,10 +1,10 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { getPaddingX, getPaddingY, isBody, isElement, isScrollableElement, isScrollToIndexOptions, isWindow, isWindowLike } from './scroll';
3
+ import { getPaddingX, getPaddingY, isBody, isElement, isScrollableElement, isScrollToIndexOptions, isWindow, isWindowLike, scrollTo } from './scroll';
4
4
 
5
5
  describe('scroll utils', () => {
6
6
  describe('element type guards', () => {
7
- describe('iswindow', () => {
7
+ describe('is window', () => {
8
8
  it('returns true for null', () => {
9
9
  expect(isWindow(null)).toBe(true);
10
10
  });
@@ -27,7 +27,7 @@ describe('scroll utils', () => {
27
27
  });
28
28
  });
29
29
 
30
- describe('isbody', () => {
30
+ describe('is body', () => {
31
31
  it('returns true for document.body', () => {
32
32
  expect(isBody(document.body)).toBe(true);
33
33
  });
@@ -64,7 +64,7 @@ describe('scroll utils', () => {
64
64
  });
65
65
  });
66
66
 
67
- describe('iswindowlike', () => {
67
+ describe('is window like', () => {
68
68
  it('returns true for window', () => {
69
69
  expect(isWindowLike(window)).toBe(true);
70
70
  });
@@ -87,7 +87,7 @@ describe('scroll utils', () => {
87
87
  });
88
88
  });
89
89
 
90
- describe('iselement', () => {
90
+ describe('is element', () => {
91
91
  it('returns true for a div', () => {
92
92
  const el = document.createElement('div');
93
93
  expect(isElement(el)).toBe(true);
@@ -106,7 +106,7 @@ describe('scroll utils', () => {
106
106
  });
107
107
  });
108
108
 
109
- describe('isscrollableelement', () => {
109
+ describe('is scrollable element', () => {
110
110
  it('returns true for a div', () => {
111
111
  const el = document.createElement('div');
112
112
  expect(isScrollableElement(el)).toBe(true);
@@ -119,7 +119,7 @@ describe('scroll utils', () => {
119
119
  });
120
120
 
121
121
  describe('options type guards', () => {
122
- describe('isscrolltoindexoptions', () => {
122
+ describe('is scroll to index options', () => {
123
123
  it('returns true for valid options', () => {
124
124
  expect(isScrollToIndexOptions({ align: 'start' })).toBe(true);
125
125
  expect(isScrollToIndexOptions({ behavior: 'smooth' })).toBe(true);
@@ -135,7 +135,7 @@ describe('scroll utils', () => {
135
135
  });
136
136
 
137
137
  describe('padding utilities', () => {
138
- describe('getpaddingx', () => {
138
+ describe('get padding x', () => {
139
139
  it('handles numeric padding', () => {
140
140
  expect(getPaddingX(10, 'horizontal')).toBe(10);
141
141
  expect(getPaddingX(10, 'both')).toBe(10);
@@ -153,7 +153,7 @@ describe('scroll utils', () => {
153
153
  });
154
154
  });
155
155
 
156
- describe('getpaddingy', () => {
156
+ describe('get padding y', () => {
157
157
  it('handles numeric padding', () => {
158
158
  expect(getPaddingY(10, 'vertical')).toBe(10);
159
159
  expect(getPaddingY(10, 'both')).toBe(10);
@@ -171,4 +171,58 @@ describe('scroll utils', () => {
171
171
  });
172
172
  });
173
173
  });
174
+
175
+ describe('scrollTo utility', () => {
176
+ it('does nothing if container is undefined', () => {
177
+ const spy = vi.spyOn(window, 'scrollTo');
178
+ scrollTo(undefined, { top: 100 });
179
+ expect(spy).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it('scrolls the window if container is null', () => {
183
+ const spy = vi.spyOn(window, 'scrollTo');
184
+ scrollTo(null, { top: 100 });
185
+ expect(spy).toHaveBeenCalledWith({ top: 100 });
186
+ });
187
+
188
+ it('scrolls the window if container is window', () => {
189
+ const spy = vi.spyOn(window, 'scrollTo');
190
+ scrollTo(window, { top: 100 });
191
+ expect(spy).toHaveBeenCalledWith({ top: 100 });
192
+ });
193
+
194
+ it('scrolls the window if container is document.documentElement', () => {
195
+ const spy = vi.spyOn(window, 'scrollTo');
196
+ scrollTo(document.documentElement, { top: 100 });
197
+ expect(spy).toHaveBeenCalledWith({ top: 100 });
198
+ });
199
+
200
+ it('scrolls an element using scrollTo if available', () => {
201
+ const el = document.createElement('div');
202
+ const spy = vi.fn();
203
+ el.scrollTo = spy;
204
+ scrollTo(el, { left: 50, top: 100 });
205
+ expect(spy).toHaveBeenCalledWith({ left: 50, top: 100 });
206
+ });
207
+
208
+ it('scrolls an element using scrollLeft/scrollTop if scrollTo is missing', () => {
209
+ const el = document.createElement('div');
210
+ // @ts-expect-error forcing missing scrollTo
211
+ el.scrollTo = undefined;
212
+ scrollTo(el, { left: 50, top: 100 });
213
+ expect(el.scrollLeft).toBe(50);
214
+ expect(el.scrollTop).toBe(100);
215
+ });
216
+
217
+ it('does not set undefined values on scrollLeft/scrollTop', () => {
218
+ const el = document.createElement('div');
219
+ el.scrollLeft = 10;
220
+ el.scrollTop = 20;
221
+ // @ts-expect-error forcing missing scrollTo
222
+ el.scrollTo = undefined;
223
+ scrollTo(el, { behavior: 'smooth' });
224
+ expect(el.scrollLeft).toBe(10);
225
+ expect(el.scrollTop).toBe(20);
226
+ });
227
+ });
174
228
  });
@@ -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
  *