@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.
@@ -4,15 +4,15 @@ import { FenwickTree } from './fenwick-tree';
4
4
 
5
5
  describe('fenwickTree', () => {
6
6
  describe('initialization', () => {
7
- it('should initialize with correct size', () => {
7
+ it('initializes with correct size', () => {
8
8
  const tree = new FenwickTree(5);
9
9
  expect(tree.query(5)).toBe(0);
10
10
  expect(tree.length).toBe(5);
11
11
  });
12
12
  });
13
13
 
14
- describe('query and update', () => {
15
- it('should update and query values', () => {
14
+ describe('updates & queries', () => {
15
+ it('updates and queries values', () => {
16
16
  const tree = new FenwickTree(5);
17
17
  tree.update(0, 10);
18
18
  tree.update(1, 20);
@@ -24,7 +24,7 @@ describe('fenwickTree', () => {
24
24
  expect(tree.query(3)).toBe(60);
25
25
  });
26
26
 
27
- it('should handle updates to existing indices', () => {
27
+ it('handles updates to existing indices', () => {
28
28
  const tree = new FenwickTree(3);
29
29
  tree.update(1, 10);
30
30
  expect(tree.query(2)).toBe(10);
@@ -32,7 +32,7 @@ describe('fenwickTree', () => {
32
32
  expect(tree.query(2)).toBe(15);
33
33
  });
34
34
 
35
- it('should ignore updates for out of bounds indices', () => {
35
+ it('ignores updates for out of bounds indices', () => {
36
36
  const tree = new FenwickTree(5);
37
37
  tree.update(-1, 10);
38
38
  tree.update(5, 10);
@@ -40,22 +40,29 @@ describe('fenwickTree', () => {
40
40
  });
41
41
  });
42
42
 
43
- describe('search and bounds', () => {
44
- it('should find lower bound correctly', () => {
45
- const tree = new FenwickTree(5);
46
- tree.update(0, 10); // sum up to 1: 10
47
- tree.update(1, 10); // sum up to 2: 20
48
- tree.update(2, 10); // sum up to 3: 30
43
+ describe('value access', () => {
44
+ it('returns the individual value at an index', () => {
45
+ const tree = new FenwickTree(3);
46
+ tree.update(0, 10);
47
+ expect(tree.get(0)).toBe(10);
48
+ expect(tree.get(-1)).toBe(0);
49
+ expect(tree.get(10)).toBe(0);
50
+ });
49
51
 
50
- expect(tree.findLowerBound(5)).toBe(0);
51
- expect(tree.findLowerBound(15)).toBe(1);
52
- expect(tree.findLowerBound(25)).toBe(2);
53
- expect(tree.findLowerBound(35)).toBe(5); // Returns size when not found
52
+ it('returns the underlying values array', () => {
53
+ const tree = new FenwickTree(3);
54
+ tree.update(0, 10);
55
+ tree.update(1, 20);
56
+ const values = tree.getValues();
57
+ expect(values).toBeInstanceOf(Float64Array);
58
+ expect(values[ 0 ]).toBe(10);
59
+ expect(values[ 1 ]).toBe(20);
60
+ expect(values[ 2 ]).toBe(0);
54
61
  });
55
62
  });
56
63
 
57
- describe('rebuild and resize', () => {
58
- it('should set and rebuild correctly', () => {
64
+ describe('rebuild & resize', () => {
65
+ it('sets and rebuilds correctly', () => {
59
66
  const tree = new FenwickTree(5);
60
67
  tree.set(0, 10);
61
68
  tree.set(1, 20);
@@ -67,7 +74,7 @@ describe('fenwickTree', () => {
67
74
  expect(tree.query(3)).toBe(60);
68
75
  });
69
76
 
70
- it('should resize and preserve existing values', () => {
77
+ it('resizes and preserves existing values', () => {
71
78
  const tree = new FenwickTree(5);
72
79
  tree.update(0, 10);
73
80
  tree.resize(10);
@@ -77,36 +84,29 @@ describe('fenwickTree', () => {
77
84
  });
78
85
  });
79
86
 
80
- describe('values access', () => {
81
- it('should return the individual value at an index', () => {
82
- const tree = new FenwickTree(3);
83
- tree.update(0, 10);
84
- expect(tree.get(0)).toBe(10);
85
- expect(tree.get(-1)).toBe(0);
86
- expect(tree.get(10)).toBe(0);
87
- });
87
+ describe('search & bounds', () => {
88
+ it('finds lower bound correctly', () => {
89
+ const tree = new FenwickTree(5);
90
+ tree.update(0, 10); // sum up to 1: 10
91
+ tree.update(1, 10); // sum up to 2: 20
92
+ tree.update(2, 10); // sum up to 3: 30
88
93
 
89
- it('should return the underlying values array', () => {
90
- const tree = new FenwickTree(3);
91
- tree.update(0, 10);
92
- tree.update(1, 20);
93
- const values = tree.getValues();
94
- expect(values).toBeInstanceOf(Float64Array);
95
- expect(values[ 0 ]).toBe(10);
96
- expect(values[ 1 ]).toBe(20);
97
- expect(values[ 2 ]).toBe(0);
94
+ expect(tree.findLowerBound(5)).toBe(0);
95
+ expect(tree.findLowerBound(15)).toBe(1);
96
+ expect(tree.findLowerBound(25)).toBe(2);
97
+ expect(tree.findLowerBound(35)).toBe(5); // Returns size when not found
98
98
  });
99
99
  });
100
100
 
101
- describe('shift', () => {
102
- it('should do nothing when offset is 0', () => {
101
+ describe('shift operations', () => {
102
+ it('does nothing when offset is 0', () => {
103
103
  const tree = new FenwickTree(3);
104
104
  tree.update(0, 10);
105
105
  tree.shift(0);
106
106
  expect(tree.get(0)).toBe(10);
107
107
  });
108
108
 
109
- it('should shift values forward when offset is positive', () => {
109
+ it('shifts values forward when offset is positive', () => {
110
110
  const tree = new FenwickTree(5);
111
111
  tree.update(0, 10);
112
112
  tree.update(1, 20);
@@ -119,7 +119,7 @@ describe('fenwickTree', () => {
119
119
  expect(tree.query(4)).toBe(30);
120
120
  });
121
121
 
122
- it('should shift values backward when offset is negative', () => {
122
+ it('shifts values backward when offset is negative', () => {
123
123
  const tree = new FenwickTree(5);
124
124
  tree.update(2, 10);
125
125
  tree.update(3, 20);
@@ -1,148 +1,228 @@
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
- describe('isWindow', () => {
7
- it('should return true for null', () => {
8
- expect(isWindow(null)).toBe(true);
9
- });
6
+ describe('element type guards', () => {
7
+ describe('is window', () => {
8
+ it('returns true for null', () => {
9
+ expect(isWindow(null)).toBe(true);
10
+ });
10
11
 
11
- it('should return true for window object', () => {
12
- expect(isWindow(window)).toBe(true);
13
- });
12
+ it('returns true for window object', () => {
13
+ expect(isWindow(window)).toBe(true);
14
+ });
14
15
 
15
- it('should return false for an element', () => {
16
- const el = document.createElement('div');
17
- expect(isWindow(el)).toBe(false);
18
- });
19
- });
16
+ it('returns true for document.documentelement object', () => {
17
+ expect(isWindow(document.documentElement)).toBe(true);
18
+ });
20
19
 
21
- describe('isBody', () => {
22
- it('should return true for document.body', () => {
23
- expect(isBody(document.body)).toBe(true);
24
- });
20
+ it('returns false for an element', () => {
21
+ const el = document.createElement('div');
22
+ expect(isWindow(el)).toBe(false);
23
+ });
25
24
 
26
- it('should return false for null', () => {
27
- expect(isBody(null)).toBe(false);
25
+ it('returns false for undefined', () => {
26
+ expect(isWindow(undefined)).toBe(false);
27
+ });
28
28
  });
29
29
 
30
- it('should return false for undefined', () => {
31
- expect(isBody(undefined)).toBe(false);
32
- });
30
+ describe('is body', () => {
31
+ it('returns true for document.body', () => {
32
+ expect(isBody(document.body)).toBe(true);
33
+ });
33
34
 
34
- it('should return false for a string', () => {
35
- // @ts-expect-error testing invalid input
36
- expect(isBody('not an object')).toBe(false);
37
- });
35
+ it('returns false for null', () => {
36
+ expect(isBody(null)).toBe(false);
37
+ });
38
38
 
39
- it('should return false for a plain object', () => {
40
- // @ts-expect-error testing invalid input
41
- expect(isBody({})).toBe(false);
42
- });
39
+ it('returns false for undefined', () => {
40
+ expect(isBody(undefined)).toBe(false);
41
+ });
43
42
 
44
- it('should return false for a div', () => {
45
- const el = document.createElement('div');
46
- expect(isBody(el)).toBe(false);
47
- });
43
+ it('returns false for a string', () => {
44
+ // @ts-expect-error testing invalid input
45
+ expect(isBody('not an object')).toBe(false);
46
+ });
48
47
 
49
- it('should return false for window', () => {
50
- expect(isBody(window)).toBe(false);
51
- });
52
- });
48
+ it('returns false for a plain object', () => {
49
+ // @ts-expect-error testing invalid input
50
+ expect(isBody({})).toBe(false);
51
+ });
53
52
 
54
- describe('isWindowLike', () => {
55
- it('should return true for window', () => {
56
- expect(isWindowLike(window)).toBe(true);
57
- });
53
+ it('returns false for a div', () => {
54
+ const el = document.createElement('div');
55
+ expect(isBody(el)).toBe(false);
56
+ });
58
57
 
59
- it('should return true for body', () => {
60
- expect(isWindowLike(document.body)).toBe(true);
61
- });
58
+ it('returns false for window', () => {
59
+ expect(isBody(window)).toBe(false);
60
+ });
62
61
 
63
- it('should return true for null', () => {
64
- expect(isWindowLike(null)).toBe(true);
62
+ it('returns false for document.documentelement', () => {
63
+ expect(isBody(document.documentElement)).toBe(false);
64
+ });
65
65
  });
66
66
 
67
- it('should return false for a div', () => {
68
- const el = document.createElement('div');
69
- expect(isWindowLike(el)).toBe(false);
70
- });
71
- });
67
+ describe('is window like', () => {
68
+ it('returns true for window', () => {
69
+ expect(isWindowLike(window)).toBe(true);
70
+ });
72
71
 
73
- describe('isElement', () => {
74
- it('should return true for a div', () => {
75
- const el = document.createElement('div');
76
- expect(isElement(el)).toBe(true);
72
+ it('returns true for document.documentelement', () => {
73
+ expect(isWindowLike(document.documentElement)).toBe(true);
74
+ });
75
+
76
+ it('returns true for body', () => {
77
+ expect(isWindowLike(document.body)).toBe(true);
78
+ });
79
+
80
+ it('returns true for null', () => {
81
+ expect(isWindowLike(null)).toBe(true);
82
+ });
83
+
84
+ it('returns false for a div', () => {
85
+ const el = document.createElement('div');
86
+ expect(isWindowLike(el)).toBe(false);
87
+ });
77
88
  });
78
89
 
79
- it('should return false for window', () => {
80
- expect(isElement(window)).toBe(false);
90
+ describe('is element', () => {
91
+ it('returns true for a div', () => {
92
+ const el = document.createElement('div');
93
+ expect(isElement(el)).toBe(true);
94
+ });
95
+
96
+ it('returns true for document.documentelement', () => {
97
+ expect(isElement(document.documentElement)).toBe(true);
98
+ });
99
+
100
+ it('returns false for window', () => {
101
+ expect(isElement(window)).toBe(false);
102
+ });
103
+
104
+ it('returns false for null', () => {
105
+ expect(isElement(null)).toBe(false);
106
+ });
81
107
  });
82
108
 
83
- it('should return false for null', () => {
84
- expect(isElement(null)).toBe(false);
109
+ describe('is scrollable element', () => {
110
+ it('returns true for a div', () => {
111
+ const el = document.createElement('div');
112
+ expect(isScrollableElement(el)).toBe(true);
113
+ });
114
+
115
+ it('returns false for null', () => {
116
+ expect(isScrollableElement(null)).toBe(false);
117
+ });
85
118
  });
86
119
  });
87
120
 
88
- describe('isScrollableElement', () => {
89
- it('should return true for a div', () => {
90
- const el = document.createElement('div');
91
- expect(isScrollableElement(el)).toBe(true);
92
- });
121
+ describe('options type guards', () => {
122
+ describe('is scroll to index options', () => {
123
+ it('returns true for valid options', () => {
124
+ expect(isScrollToIndexOptions({ align: 'start' })).toBe(true);
125
+ expect(isScrollToIndexOptions({ behavior: 'smooth' })).toBe(true);
126
+ expect(isScrollToIndexOptions({ isCorrection: true })).toBe(true);
127
+ });
93
128
 
94
- it('should return false for null', () => {
95
- expect(isScrollableElement(null)).toBe(false);
129
+ it('returns false for other values', () => {
130
+ expect(isScrollToIndexOptions(null)).toBe(false);
131
+ expect(isScrollToIndexOptions('start')).toBe(false);
132
+ expect(isScrollToIndexOptions({})).toBe(false);
133
+ });
96
134
  });
97
135
  });
98
136
 
99
- describe('isScrollToIndexOptions', () => {
100
- it('should return true for valid options', () => {
101
- expect(isScrollToIndexOptions({ align: 'start' })).toBe(true);
102
- expect(isScrollToIndexOptions({ behavior: 'smooth' })).toBe(true);
103
- expect(isScrollToIndexOptions({ isCorrection: true })).toBe(true);
137
+ describe('padding utilities', () => {
138
+ describe('get padding x', () => {
139
+ it('handles numeric padding', () => {
140
+ expect(getPaddingX(10, 'horizontal')).toBe(10);
141
+ expect(getPaddingX(10, 'both')).toBe(10);
142
+ expect(getPaddingX(10, 'vertical')).toBe(0);
143
+ expect(getPaddingX(0, 'horizontal')).toBe(0);
144
+ });
145
+
146
+ it('handles object padding', () => {
147
+ expect(getPaddingX({ x: 15 }, 'vertical')).toBe(15);
148
+ expect(getPaddingX({ y: 20 }, 'horizontal')).toBe(0);
149
+ });
150
+
151
+ it('returns 0 for undefined', () => {
152
+ expect(getPaddingX(undefined)).toBe(0);
153
+ });
154
+ });
155
+
156
+ describe('get padding y', () => {
157
+ it('handles numeric padding', () => {
158
+ expect(getPaddingY(10, 'vertical')).toBe(10);
159
+ expect(getPaddingY(10, 'both')).toBe(10);
160
+ expect(getPaddingY(10, 'horizontal')).toBe(0);
161
+ expect(getPaddingY(0, 'vertical')).toBe(0);
162
+ });
163
+
164
+ it('handles object padding', () => {
165
+ expect(getPaddingY({ y: 15 }, 'horizontal')).toBe(15);
166
+ expect(getPaddingY({ x: 20 }, 'vertical')).toBe(0);
167
+ });
168
+
169
+ it('returns 0 for undefined', () => {
170
+ expect(getPaddingY(undefined)).toBe(0);
171
+ });
104
172
  });
173
+ });
105
174
 
106
- it('should return false for other values', () => {
107
- expect(isScrollToIndexOptions(null)).toBe(false);
108
- expect(isScrollToIndexOptions('start')).toBe(false);
109
- expect(isScrollToIndexOptions({})).toBe(false);
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();
110
180
  });
111
- });
112
181
 
113
- describe('getPaddingX', () => {
114
- it('should handle numeric padding', () => {
115
- expect(getPaddingX(10, 'horizontal')).toBe(10);
116
- expect(getPaddingX(10, 'both')).toBe(10);
117
- expect(getPaddingX(10, 'vertical')).toBe(0);
118
- expect(getPaddingX(0, 'horizontal')).toBe(0);
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 });
119
186
  });
120
187
 
121
- it('should handle object padding', () => {
122
- expect(getPaddingX({ x: 15 }, 'vertical')).toBe(15);
123
- expect(getPaddingX({ y: 20 }, 'horizontal')).toBe(0);
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 });
124
192
  });
125
193
 
126
- it('should return 0 for undefined', () => {
127
- expect(getPaddingX(undefined)).toBe(0);
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 });
128
198
  });
129
- });
130
199
 
131
- describe('getPaddingY', () => {
132
- it('should handle numeric padding', () => {
133
- expect(getPaddingY(10, 'vertical')).toBe(10);
134
- expect(getPaddingY(10, 'both')).toBe(10);
135
- expect(getPaddingY(10, 'horizontal')).toBe(0);
136
- expect(getPaddingY(0, 'vertical')).toBe(0);
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 });
137
206
  });
138
207
 
139
- it('should handle object padding', () => {
140
- expect(getPaddingY({ y: 15 }, 'horizontal')).toBe(15);
141
- expect(getPaddingY({ x: 20 }, 'vertical')).toBe(0);
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);
142
215
  });
143
216
 
144
- it('should return 0 for undefined', () => {
145
- expect(getPaddingY(undefined)).toBe(0);
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);
146
226
  });
147
227
  });
148
228
  });
@@ -1,5 +1,20 @@
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
+
11
+ /**
12
+ * Maximum size (in pixels) for an element that most browsers can handle reliably.
13
+ * Beyond this size, we use scaling for the scrollable area.
14
+ * @default 10000000
15
+ */
16
+ export const BROWSER_MAX_SIZE = 10000000;
17
+
3
18
  /**
4
19
  * Checks if the container is the window object.
5
20
  *
@@ -7,7 +22,7 @@ import type { ScrollDirection, ScrollToIndexOptions } from '../types';
7
22
  * @returns `true` if the container is the global window object.
8
23
  */
9
24
  export function isWindow(container: HTMLElement | Window | null | undefined): container is Window {
10
- return container === null || (typeof window !== 'undefined' && container === window);
25
+ return container === null || container === document.documentElement || (typeof window !== 'undefined' && container === window);
11
26
  }
12
27
 
13
28
  /**
@@ -17,7 +32,7 @@ export function isWindow(container: HTMLElement | Window | null | undefined): co
17
32
  * @returns `true` if the container is the `<body>` element.
18
33
  */
19
34
  export function isBody(container: HTMLElement | Window | null | undefined): container is HTMLElement {
20
- return !!container && typeof container === 'object' && 'tagName' in container && container.tagName === 'BODY';
35
+ return container != null && typeof container === 'object' && 'tagName' in container && container.tagName === 'BODY';
21
36
  }
22
37
 
23
38
  /**
@@ -37,7 +52,7 @@ export function isWindowLike(container: HTMLElement | Window | null | undefined)
37
52
  * @returns `true` if the container is an `HTMLElement`.
38
53
  */
39
54
  export function isElement(container: HTMLElement | Window | null | undefined): container is HTMLElement {
40
- return !!container && 'getBoundingClientRect' in container;
55
+ return container != null && 'getBoundingClientRect' in container;
41
56
  }
42
57
 
43
58
  /**
@@ -47,7 +62,30 @@ export function isElement(container: HTMLElement | Window | null | undefined): c
47
62
  * @returns `true` if the target is an `HTMLElement` with scroll properties.
48
63
  */
49
64
  export function isScrollableElement(target: EventTarget | null): target is HTMLElement {
50
- return !!target && 'scrollLeft' in target;
65
+ return target != null && 'scrollLeft' in target;
66
+ }
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
+ }
51
89
  }
52
90
 
53
91
  /**
@@ -57,7 +95,7 @@ export function isScrollableElement(target: EventTarget | null): target is HTMLE
57
95
  * @returns `true` if the options object contains scroll-to-index specific properties.
58
96
  */
59
97
  export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
60
- return typeof options === 'object' && options !== null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
98
+ return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
61
99
  }
62
100
 
63
101
  /**