@pdanpdan/virtual-scroll 0.3.0 → 0.4.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/README.md +160 -116
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +834 -145
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +639 -416
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +1 -1
- package/src/components/VirtualScroll.test.ts +523 -731
- package/src/components/VirtualScroll.vue +343 -214
- package/src/composables/useVirtualScroll.test.ts +240 -1879
- package/src/composables/useVirtualScroll.ts +482 -554
- package/src/index.ts +2 -0
- package/src/types.ts +535 -0
- package/src/utils/fenwick-tree.ts +38 -18
- package/src/utils/scroll.test.ts +148 -0
- package/src/utils/scroll.ts +40 -10
- package/src/utils/virtual-scroll-logic.test.ts +2517 -0
- package/src/utils/virtual-scroll-logic.ts +605 -0
|
@@ -1,53 +1,39 @@
|
|
|
1
1
|
/* global ScrollToOptions */
|
|
2
|
-
import type { VirtualScrollProps } from '
|
|
2
|
+
import type { VirtualScrollProps } from '../types';
|
|
3
3
|
import type { Ref } from 'vue';
|
|
4
4
|
|
|
5
5
|
import { mount } from '@vue/test-utils';
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import { defineComponent, nextTick, ref } from 'vue';
|
|
8
8
|
|
|
9
|
-
import { getPaddingX, getPaddingY } from '../utils/scroll';
|
|
10
9
|
import { useVirtualScroll } from './useVirtualScroll';
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
// --- Mocks ---
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
globalThis.ResizeObserver = class {
|
|
22
|
-
callback: ResizeObserverCallback;
|
|
23
|
-
static instances: ResizeObserverMock[] = [];
|
|
24
|
-
targets: Set<Element> = new Set();
|
|
25
|
-
|
|
26
|
-
constructor(callback: ResizeObserverCallback) {
|
|
27
|
-
this.callback = callback;
|
|
28
|
-
(this.constructor as unknown as { instances: ResizeObserverMock[]; }).instances.push(this as unknown as ResizeObserverMock);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
observe(target: Element) {
|
|
32
|
-
this.targets.add(target);
|
|
33
|
-
}
|
|
13
|
+
globalThis.ResizeObserver = class ResizeObserver {
|
|
14
|
+
observe = vi.fn();
|
|
15
|
+
unobserve = vi.fn();
|
|
16
|
+
disconnect = vi.fn();
|
|
17
|
+
};
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 500 });
|
|
20
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 500 });
|
|
21
|
+
Object.defineProperty(document.documentElement, 'clientHeight', { configurable: true, value: 500 });
|
|
22
|
+
Object.defineProperty(document.documentElement, 'clientWidth', { configurable: true, value: 500 });
|
|
23
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 500 });
|
|
24
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 500 });
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
globalThis.window.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
27
|
+
if (options.left !== undefined) {
|
|
28
|
+
Object.defineProperty(window, 'scrollX', { configurable: true, value: options.left, writable: true });
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.callback(entries as ResizeObserverEntry[], this as unknown as ResizeObserver);
|
|
30
|
+
if (options.top !== undefined) {
|
|
31
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: options.top, writable: true });
|
|
45
32
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
globalThis.window.scrollTo = vi.fn();
|
|
33
|
+
document.dispatchEvent(new Event('scroll'));
|
|
34
|
+
});
|
|
49
35
|
|
|
50
|
-
// Helper to test composable
|
|
36
|
+
// Helper to test composable
|
|
51
37
|
function setup<T>(propsValue: VirtualScrollProps<T>) {
|
|
52
38
|
const props = ref(propsValue) as Ref<VirtualScrollProps<T>>;
|
|
53
39
|
let result: ReturnType<typeof useVirtualScroll<T>>;
|
|
@@ -59,1930 +45,305 @@ function setup<T>(propsValue: VirtualScrollProps<T>) {
|
|
|
59
45
|
},
|
|
60
46
|
});
|
|
61
47
|
const wrapper = mount(TestComponent);
|
|
62
|
-
return { result: result!,
|
|
48
|
+
return { props, result: result!, wrapper };
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
const mockItems = Array.from({ length: 100 }, (_, i) => ({ id: i }));
|
|
66
|
-
const defaultProps: VirtualScrollProps<{ id: number; }> = {
|
|
67
|
-
items: mockItems,
|
|
68
|
-
itemSize: 50,
|
|
69
|
-
direction: 'vertical' as const,
|
|
70
|
-
bufferBefore: 2,
|
|
71
|
-
bufferAfter: 2,
|
|
72
|
-
container: window,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
51
|
describe('useVirtualScroll', () => {
|
|
52
|
+
const mockItems = Array.from({ length: 100 }, (_, i) => ({ id: i }));
|
|
53
|
+
|
|
76
54
|
beforeEach(() => {
|
|
77
|
-
window.scrollX = 0;
|
|
78
|
-
window.scrollY = 0;
|
|
79
|
-
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 500 });
|
|
80
|
-
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 500 });
|
|
81
|
-
window.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
82
|
-
if (options.left !== undefined) {
|
|
83
|
-
window.scrollX = options.left;
|
|
84
|
-
}
|
|
85
|
-
if (options.top !== undefined) {
|
|
86
|
-
window.scrollY = options.top;
|
|
87
|
-
}
|
|
88
|
-
window.dispatchEvent(new Event('scroll'));
|
|
89
|
-
});
|
|
90
55
|
vi.clearAllMocks();
|
|
91
|
-
|
|
56
|
+
Object.defineProperty(window, 'scrollX', { configurable: true, value: 0, writable: true });
|
|
57
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 0, writable: true });
|
|
92
58
|
});
|
|
93
59
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
it('calculates total dimensions correctly', async () => {
|
|
61
|
+
const { result } = setup({
|
|
62
|
+
container: window,
|
|
63
|
+
direction: 'vertical',
|
|
64
|
+
itemSize: 50,
|
|
65
|
+
items: mockItems,
|
|
98
66
|
});
|
|
99
67
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
103
|
-
|
|
104
|
-
props.value.items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
105
|
-
await nextTick();
|
|
106
|
-
expect(result.totalHeight.value).toBe(2500);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should recalculate totalHeight when itemSize changes', async () => {
|
|
110
|
-
const { result, props } = setup({ ...defaultProps });
|
|
111
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
112
|
-
|
|
113
|
-
props.value.itemSize = 100;
|
|
114
|
-
await nextTick();
|
|
115
|
-
expect(result.totalHeight.value).toBe(10000);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should recalculate when gaps change', async () => {
|
|
119
|
-
const { result, props } = setup({ ...defaultProps, gap: 10 });
|
|
120
|
-
expect(result.totalHeight.value).toBe(5990); // 100 * (50 + 10) - 10
|
|
121
|
-
|
|
122
|
-
props.value.gap = 20;
|
|
123
|
-
await nextTick();
|
|
124
|
-
expect(result.totalHeight.value).toBe(6980); // 100 * (50 + 20) - 20
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should handle itemSize as a function', async () => {
|
|
128
|
-
const { result } = setup({
|
|
129
|
-
...defaultProps,
|
|
130
|
-
itemSize: (_item: { id: number; }, index: number) => 50 + index,
|
|
131
|
-
});
|
|
132
|
-
// 50*100 + (0+99)*100/2 = 5000 + 4950 = 9950
|
|
133
|
-
// 9950 - gap(0) = 9950
|
|
134
|
-
expect(result.totalHeight.value).toBe(9950);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should handle direction both (grid mode)', async () => {
|
|
138
|
-
const { result } = setup({
|
|
139
|
-
...defaultProps,
|
|
140
|
-
direction: 'both',
|
|
141
|
-
columnCount: 10,
|
|
142
|
-
columnWidth: 100,
|
|
143
|
-
});
|
|
144
|
-
expect(result.totalWidth.value).toBe(1000); // 10 * 100 - 0
|
|
145
|
-
expect(result.totalHeight.value).toBe(5000); // 100 * 50 - 0
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should handle horizontal direction', async () => {
|
|
149
|
-
const { result } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
150
|
-
expect(result.totalWidth.value).toBe(5000); // 100 * 50 - 0
|
|
151
|
-
expect(result.totalHeight.value).toBe(0);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should cover default values for buffer and gap', async () => {
|
|
155
|
-
const { result } = setup({
|
|
156
|
-
items: mockItems,
|
|
157
|
-
itemSize: 50,
|
|
158
|
-
} as unknown as VirtualScrollProps<{ id: number; }>);
|
|
159
|
-
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
160
|
-
});
|
|
68
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
69
|
+
expect(result.totalWidth.value).toBe(500);
|
|
161
70
|
});
|
|
162
71
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
72
|
+
it('provides rendered items for the visible range', async () => {
|
|
73
|
+
const { result } = setup({
|
|
74
|
+
container: window,
|
|
75
|
+
direction: 'vertical',
|
|
76
|
+
itemSize: 50,
|
|
77
|
+
items: mockItems,
|
|
168
78
|
});
|
|
169
79
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
173
|
-
expect(result.scrollDetails.value.currentIndex).toBe(0);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should handle horizontal non-fixed size range', async () => {
|
|
177
|
-
const container = document.createElement('div');
|
|
178
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
179
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
180
|
-
const { result } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined, container });
|
|
181
|
-
for (let i = 0; i < 20; i++) {
|
|
182
|
-
result.updateItemSize(i, 50, 50);
|
|
183
|
-
}
|
|
184
|
-
await nextTick();
|
|
80
|
+
await nextTick();
|
|
81
|
+
await nextTick();
|
|
185
82
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(result.scrollDetails.value.currentIndex).toBeGreaterThan(0);
|
|
190
|
-
});
|
|
83
|
+
// viewport 500, item 50 => 10 items + buffer 5 = 15 items
|
|
84
|
+
expect(result.renderedItems.value.length).toBe(15);
|
|
85
|
+
expect(result.renderedItems.value[ 0 ]!.index).toBe(0);
|
|
191
86
|
});
|
|
192
87
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
columnWidth: undefined,
|
|
200
|
-
});
|
|
201
|
-
await nextTick();
|
|
202
|
-
|
|
203
|
-
const cell = document.createElement('div');
|
|
204
|
-
cell.dataset.colIndex = '0';
|
|
205
|
-
|
|
206
|
-
// Getter that returns 10 first time (for guard) and null second time (for fallback)
|
|
207
|
-
let count = 0;
|
|
208
|
-
Object.defineProperty(props.value, 'columnCount', {
|
|
209
|
-
get() {
|
|
210
|
-
count++;
|
|
211
|
-
return count === 1 ? 10 : null;
|
|
212
|
-
},
|
|
213
|
-
configurable: true,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
217
|
-
await nextTick();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should handle updateItemSizes with direct cell element', async () => {
|
|
221
|
-
const { result } = setup({
|
|
222
|
-
...defaultProps,
|
|
223
|
-
direction: 'both',
|
|
224
|
-
columnCount: 2,
|
|
225
|
-
columnWidth: undefined,
|
|
226
|
-
});
|
|
227
|
-
await nextTick();
|
|
228
|
-
|
|
229
|
-
const cell = document.createElement('div');
|
|
230
|
-
Object.defineProperty(cell, 'offsetWidth', { value: 200 });
|
|
231
|
-
cell.dataset.colIndex = '0';
|
|
232
|
-
|
|
233
|
-
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
234
|
-
await nextTick();
|
|
235
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should handle updateItemSizes initial measurement even if smaller than estimate', async () => {
|
|
239
|
-
// Horizontal
|
|
240
|
-
const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
|
|
241
|
-
await nextTick();
|
|
242
|
-
// Estimate is 40 (new DEFAULT_ITEM_SIZE). Update with 30.
|
|
243
|
-
rH.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
244
|
-
await nextTick();
|
|
245
|
-
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(30);
|
|
246
|
-
|
|
247
|
-
// Subsequent update with smaller size should also be applied now
|
|
248
|
-
rH.updateItemSizes([ { index: 0, inlineSize: 25, blockSize: 25 } ]);
|
|
249
|
-
await nextTick();
|
|
250
|
-
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(25);
|
|
251
|
-
|
|
252
|
-
// Vertical
|
|
253
|
-
const { result: rV } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
254
|
-
await nextTick();
|
|
255
|
-
rV.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
256
|
-
await nextTick();
|
|
257
|
-
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(30);
|
|
258
|
-
|
|
259
|
-
// Subsequent update with smaller size should be applied
|
|
260
|
-
rV.updateItemSizes([ { index: 0, inlineSize: 20, blockSize: 20 } ]);
|
|
261
|
-
await nextTick();
|
|
262
|
-
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(20);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('should handle updateItemSize and trigger reactivity', async () => {
|
|
266
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
267
|
-
expect(result.totalHeight.value).toBe(4000); // 100 * 40
|
|
268
|
-
|
|
269
|
-
result.updateItemSize(0, 100, 100);
|
|
270
|
-
await nextTick();
|
|
271
|
-
expect(result.totalHeight.value).toBe(4060); // 4000 - 40 + 100
|
|
272
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should treat 0, null, undefined as dynamic itemSize', async () => {
|
|
276
|
-
for (const val of [ 0, null, undefined ]) {
|
|
277
|
-
const { result } = setup({ ...defaultProps, itemSize: val as unknown as undefined });
|
|
278
|
-
expect(result.totalHeight.value).toBe(4000);
|
|
279
|
-
result.updateItemSize(0, 100, 100);
|
|
280
|
-
await nextTick();
|
|
281
|
-
expect(result.totalHeight.value).toBe(4060);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('should treat 0, null, undefined as dynamic columnWidth', async () => {
|
|
286
|
-
for (const val of [ 0, null, undefined ]) {
|
|
287
|
-
const { result } = setup({
|
|
288
|
-
...defaultProps,
|
|
289
|
-
direction: 'both',
|
|
290
|
-
columnCount: 2,
|
|
291
|
-
columnWidth: val as unknown as undefined,
|
|
292
|
-
});
|
|
293
|
-
expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
|
|
294
|
-
const parent = document.createElement('div');
|
|
295
|
-
const col0 = document.createElement('div');
|
|
296
|
-
Object.defineProperty(col0, 'offsetWidth', { value: 200, configurable: true });
|
|
297
|
-
col0.dataset.colIndex = '0';
|
|
298
|
-
parent.appendChild(col0);
|
|
299
|
-
result.updateItemSize(0, 200, 50, parent);
|
|
300
|
-
await nextTick();
|
|
301
|
-
expect(result.totalWidth.value).toBe(300); // 200 + 100
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should handle dynamic column width with data-col-index', async () => {
|
|
306
|
-
const { result } = setup({
|
|
307
|
-
...defaultProps,
|
|
308
|
-
direction: 'both',
|
|
309
|
-
columnCount: 2,
|
|
310
|
-
columnWidth: undefined,
|
|
311
|
-
});
|
|
312
|
-
const parent = document.createElement('div');
|
|
313
|
-
const child1 = document.createElement('div');
|
|
314
|
-
Object.defineProperty(child1, 'offsetWidth', { value: 200 });
|
|
315
|
-
child1.dataset.colIndex = '0';
|
|
316
|
-
const child2 = document.createElement('div');
|
|
317
|
-
Object.defineProperty(child2, 'offsetWidth', { value: 300 });
|
|
318
|
-
child2.dataset.colIndex = '1';
|
|
319
|
-
parent.appendChild(child1);
|
|
320
|
-
parent.appendChild(child2);
|
|
321
|
-
|
|
322
|
-
result.updateItemSize(0, 500, 50, parent);
|
|
323
|
-
await nextTick();
|
|
324
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
325
|
-
expect(result.getColumnWidth(1)).toBe(300);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should return early in updateItemSize if itemSize is fixed', async () => {
|
|
329
|
-
const { result } = setup({ ...defaultProps, itemSize: 50 });
|
|
330
|
-
result.updateItemSize(0, 100, 100);
|
|
331
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should use defaultItemSize and defaultColumnWidth when provided', () => {
|
|
335
|
-
const { result } = setup({
|
|
336
|
-
...defaultProps,
|
|
337
|
-
itemSize: undefined,
|
|
338
|
-
columnWidth: undefined,
|
|
339
|
-
defaultItemSize: 100,
|
|
340
|
-
defaultColumnWidth: 200,
|
|
341
|
-
direction: 'both',
|
|
342
|
-
columnCount: 10,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
expect(result.totalHeight.value).toBe(100 * 100); // 100 items * 100 defaultItemSize
|
|
346
|
-
expect(result.totalWidth.value).toBe(10 * 200); // 10 columns * 200 defaultColumnWidth
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('should ignore small delta updates in updateItemSize only after first measurement', async () => {
|
|
350
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
351
|
-
// Default is 40. 40.1 is < 0.5 delta.
|
|
352
|
-
// First measurement should be accepted even if small delta from estimate.
|
|
353
|
-
result.updateItemSize(0, 40.1, 40.1);
|
|
354
|
-
await nextTick();
|
|
355
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
|
|
356
|
-
|
|
357
|
-
// Second measurement with small delta from first should be ignored.
|
|
358
|
-
result.updateItemSize(0, 40.2, 40.2);
|
|
359
|
-
await nextTick();
|
|
360
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('should update item height in both mode now (allow decreases)', async () => {
|
|
364
|
-
const { result } = setup({ ...defaultProps, direction: 'both', itemSize: undefined, columnCount: 2 });
|
|
365
|
-
result.updateItemSize(0, 100, 100);
|
|
366
|
-
await nextTick();
|
|
367
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
368
|
-
|
|
369
|
-
result.updateItemSize(0, 100, 80);
|
|
370
|
-
await nextTick();
|
|
371
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(80);
|
|
88
|
+
it('updates when scroll position changes', async () => {
|
|
89
|
+
const { result } = setup({
|
|
90
|
+
container: window,
|
|
91
|
+
direction: 'vertical',
|
|
92
|
+
itemSize: 50,
|
|
93
|
+
items: mockItems,
|
|
372
94
|
});
|
|
373
95
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
result.updateItemSize(0, 100, 100);
|
|
377
|
-
await nextTick();
|
|
378
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
96
|
+
await nextTick();
|
|
97
|
+
await nextTick();
|
|
379
98
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(70);
|
|
383
|
-
});
|
|
99
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 500, writable: true });
|
|
100
|
+
document.dispatchEvent(new Event('scroll'));
|
|
384
101
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
result.updateItemSize(0, 100, 50);
|
|
388
|
-
await nextTick();
|
|
389
|
-
expect(result.totalWidth.value).toBe(4060); // 4000 - 40 + 100
|
|
390
|
-
});
|
|
102
|
+
await nextTick();
|
|
103
|
+
await nextTick();
|
|
391
104
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
await nextTick();
|
|
396
|
-
expect(result.totalHeight.value).toBe(4060);
|
|
397
|
-
|
|
398
|
-
// Trigger initializeSizes by changing length
|
|
399
|
-
props.value.items = Array.from({ length: 101 }, (_, i) => ({ id: i }));
|
|
400
|
-
await nextTick();
|
|
401
|
-
// Should still be 100 for index 0, not reset to default 40
|
|
402
|
-
expect(result.totalHeight.value).toBe(4060 + 40);
|
|
403
|
-
});
|
|
105
|
+
// At 500px, start index is 500/50 = 10. With buffer 5, start is 5.
|
|
106
|
+
expect(result.scrollDetails.value.currentIndex).toBe(10);
|
|
107
|
+
expect(result.renderedItems.value[ 0 ]!.index).toBe(5);
|
|
404
108
|
});
|
|
405
109
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// index 10. itemSize is 40 by default. totalWidth = 4000.
|
|
414
|
-
result.scrollToIndex(null, 10, { align: 'start', behavior: 'auto' });
|
|
415
|
-
await nextTick();
|
|
416
|
-
expect(result.scrollDetails.value.scrollOffset.x).toBe(400);
|
|
110
|
+
it('supports programmatic scrolling', async () => {
|
|
111
|
+
const { result } = setup({
|
|
112
|
+
container: window,
|
|
113
|
+
direction: 'vertical',
|
|
114
|
+
itemSize: 50,
|
|
115
|
+
items: mockItems,
|
|
417
116
|
});
|
|
418
117
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
await nextTick();
|
|
422
|
-
result.scrollToIndex(10, 0);
|
|
423
|
-
await nextTick();
|
|
424
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
425
|
-
});
|
|
118
|
+
await nextTick();
|
|
119
|
+
await nextTick();
|
|
426
120
|
|
|
427
|
-
|
|
428
|
-
const { result } = setup({ ...defaultProps });
|
|
429
|
-
// Row past end
|
|
430
|
-
result.scrollToIndex(mockItems.length + 10, 0);
|
|
431
|
-
await nextTick();
|
|
432
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
433
|
-
|
|
434
|
-
// Col past end (in grid mode)
|
|
435
|
-
const { result: r_grid } = setup({ ...defaultProps, direction: 'both', columnCount: 5, columnWidth: 100 });
|
|
436
|
-
r_grid.scrollToIndex(0, 10);
|
|
437
|
-
await nextTick();
|
|
438
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
439
|
-
|
|
440
|
-
// Column past end in horizontal mode
|
|
441
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
442
|
-
r_horiz.scrollToIndex(0, 200);
|
|
443
|
-
await nextTick();
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('should handle scrollToIndex auto alignment with padding', async () => {
|
|
447
|
-
const container = document.createElement('div');
|
|
448
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
449
|
-
Object.defineProperty(container, 'scrollTop', { value: 200, writable: true, configurable: true });
|
|
450
|
-
|
|
451
|
-
const { result } = setup({ ...defaultProps, container, itemSize: 50, scrollPaddingStart: 100 });
|
|
452
|
-
await nextTick();
|
|
453
|
-
|
|
454
|
-
// Current visible range: [scrollTop + paddingStart, scrollTop + viewport - paddingEnd] = [300, 700]
|
|
455
|
-
// Scroll to item at y=250. 250 < 300, so not visible.
|
|
456
|
-
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
457
|
-
result.scrollToIndex(5, null, 'auto');
|
|
458
|
-
await nextTick();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('should hit scrollToIndex X calculation branches', async () => {
|
|
462
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal', itemSize: 100 });
|
|
463
|
-
await nextTick();
|
|
464
|
-
// colIndex null
|
|
465
|
-
r_horiz.scrollToIndex(0, null);
|
|
466
|
-
// rowIndex null
|
|
467
|
-
r_horiz.scrollToIndex(null, 5);
|
|
468
|
-
await nextTick();
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it('should handle scrollToOffset with element container and scrollTo method', async () => {
|
|
472
|
-
const container = document.createElement('div');
|
|
473
|
-
container.scrollTo = vi.fn();
|
|
474
|
-
const { result } = setup({ ...defaultProps, container });
|
|
475
|
-
result.scrollToOffset(100, 200);
|
|
476
|
-
expect(container.scrollTo).toHaveBeenCalled();
|
|
477
|
-
});
|
|
121
|
+
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
478
122
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
Object.defineProperty(container, 'scrollLeft', { value: 50, writable: true });
|
|
482
|
-
Object.defineProperty(container, 'scrollTop', { value: 60, writable: true });
|
|
123
|
+
await nextTick();
|
|
124
|
+
await nextTick();
|
|
483
125
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// Pass null to x and y to trigger fallbacks to currentX and currentY
|
|
488
|
-
result.scrollToOffset(null, null);
|
|
489
|
-
await nextTick();
|
|
490
|
-
|
|
491
|
-
// scrollOffset.x = targetX - hostOffset.x + (isHorizontal ? paddingStartX : 0)
|
|
492
|
-
// targetX = currentX = 50. hostOffset.x = 0. isHorizontal = false.
|
|
493
|
-
// So scrollOffset.x = 50.
|
|
494
|
-
expect(result.scrollDetails.value.scrollOffset.x).toBe(50);
|
|
495
|
-
expect(result.scrollDetails.value.scrollOffset.y).toBe(60);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it('should handle scrollToOffset with restricted direction for padding fallback', async () => {
|
|
499
|
-
const container = document.createElement('div');
|
|
500
|
-
container.scrollTo = vi.fn();
|
|
501
|
-
|
|
502
|
-
// Horizontal direction: isVertical will be false, so targetY padding fallback will be 0
|
|
503
|
-
const { result } = setup({ ...defaultProps, container, direction: 'horizontal', scrollPaddingStart: 10 });
|
|
504
|
-
await nextTick();
|
|
505
|
-
|
|
506
|
-
result.scrollToOffset(100, 100);
|
|
507
|
-
await nextTick();
|
|
508
|
-
// targetY = 100 + hostOffset.y - (isVertical ? paddingStartY : 0)
|
|
509
|
-
// Since isVertical is false, it uses 0. hostOffset.y is 0 here.
|
|
510
|
-
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
511
|
-
top: 100,
|
|
512
|
-
}));
|
|
513
|
-
|
|
514
|
-
// Vertical direction: isHorizontal will be false, so targetX padding fallback will be 0
|
|
515
|
-
const { result: r2 } = setup({ ...defaultProps, container, direction: 'vertical', scrollPaddingStart: 10 });
|
|
516
|
-
await nextTick();
|
|
517
|
-
r2.scrollToOffset(100, 100);
|
|
518
|
-
await nextTick();
|
|
519
|
-
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
520
|
-
left: 100,
|
|
521
|
-
}));
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it('should handle scrollToOffset with window fallback when container is missing', async () => {
|
|
525
|
-
const { result } = setup({ ...defaultProps, container: undefined });
|
|
526
|
-
await nextTick();
|
|
527
|
-
result.scrollToOffset(100, 200);
|
|
528
|
-
await nextTick();
|
|
529
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('should handle scrollToIndex with null indices', async () => {
|
|
533
|
-
const { result } = setup({ ...defaultProps });
|
|
534
|
-
result.scrollToIndex(null, null);
|
|
535
|
-
await nextTick();
|
|
536
|
-
result.scrollToIndex(10, null);
|
|
537
|
-
await nextTick();
|
|
538
|
-
result.scrollToIndex(null, 10);
|
|
539
|
-
await nextTick();
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
it('should handle scrollToIndex auto alignment', async () => {
|
|
543
|
-
const container = document.createElement('div');
|
|
544
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
545
|
-
Object.defineProperty(container, 'clientWidth', { value: 500, configurable: true });
|
|
546
|
-
|
|
547
|
-
const { result } = setup({ ...defaultProps, container, itemSize: 50 });
|
|
548
|
-
await nextTick();
|
|
549
|
-
|
|
550
|
-
// Scroll down so some items are above
|
|
551
|
-
result.scrollToIndex(20, 0, 'start');
|
|
552
|
-
await nextTick();
|
|
553
|
-
|
|
554
|
-
// Auto align: already visible
|
|
555
|
-
result.scrollToIndex(20, null, 'auto');
|
|
556
|
-
await nextTick();
|
|
557
|
-
|
|
558
|
-
// Auto align: above viewport (scroll up)
|
|
559
|
-
result.scrollToIndex(5, null, 'auto');
|
|
560
|
-
await nextTick();
|
|
561
|
-
|
|
562
|
-
// Auto align: below viewport (scroll down)
|
|
563
|
-
result.scrollToIndex(50, null, 'auto');
|
|
564
|
-
await nextTick();
|
|
565
|
-
|
|
566
|
-
// Horizontal auto align
|
|
567
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal', container, itemSize: 100 });
|
|
568
|
-
await nextTick();
|
|
569
|
-
|
|
570
|
-
r_horiz.scrollToIndex(0, 20, 'start');
|
|
571
|
-
await nextTick();
|
|
572
|
-
|
|
573
|
-
r_horiz.scrollToIndex(null, 5, 'auto');
|
|
574
|
-
await nextTick();
|
|
575
|
-
|
|
576
|
-
r_horiz.scrollToIndex(null, 50, 'auto');
|
|
577
|
-
await nextTick();
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it('should handle scrollToIndex with various alignments', async () => {
|
|
581
|
-
const { result } = setup({ ...defaultProps });
|
|
582
|
-
result.scrollToIndex(50, 0, 'center');
|
|
583
|
-
await nextTick();
|
|
584
|
-
result.scrollToIndex(50, 0, 'end');
|
|
585
|
-
await nextTick();
|
|
586
|
-
result.scrollToIndex(50, 0, { x: 'center', y: 'end' });
|
|
587
|
-
await nextTick();
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it('should handle scrollToOffset with window container', async () => {
|
|
591
|
-
const { result } = setup({ ...defaultProps, container: window });
|
|
592
|
-
result.scrollToOffset(null, 100);
|
|
593
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 100 }));
|
|
594
|
-
|
|
595
|
-
result.scrollToOffset(null, 200, { behavior: 'smooth' });
|
|
596
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 200, behavior: 'smooth' }));
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
it('should handle scrollToIndex with auto alignment and axis preservation', async () => {
|
|
600
|
-
const { result } = setup({ ...defaultProps });
|
|
601
|
-
// Axis preservation (null index)
|
|
602
|
-
result.scrollToIndex(10, null, 'auto');
|
|
603
|
-
await nextTick();
|
|
604
|
-
result.scrollToIndex(null, 5, 'auto');
|
|
605
|
-
await nextTick();
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('should handle scrollToOffset with nulls to keep current position', async () => {
|
|
609
|
-
const { result } = setup({ ...defaultProps, container: window });
|
|
610
|
-
window.scrollX = 50;
|
|
611
|
-
window.scrollY = 60;
|
|
612
|
-
|
|
613
|
-
// Pass null to keep current Y while updating X
|
|
614
|
-
result.scrollToOffset(100, null);
|
|
615
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ left: 100 }));
|
|
616
|
-
|
|
617
|
-
// Pass null to keep current X while updating Y
|
|
618
|
-
result.scrollToOffset(null, 200);
|
|
619
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 200 }));
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it('should handle scrollToOffset with both axes', async () => {
|
|
623
|
-
const { result } = setup({ ...defaultProps });
|
|
624
|
-
result.scrollToOffset(100, 200);
|
|
625
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ left: 100, top: 200 }));
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it('should handle scrollToOffset fallback when scrollTo is missing', async () => {
|
|
629
|
-
const container = document.createElement('div');
|
|
630
|
-
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
631
|
-
const { result } = setup({ ...defaultProps, container });
|
|
632
|
-
|
|
633
|
-
result.scrollToOffset(100, 200);
|
|
634
|
-
expect(container.scrollTop).toBe(200);
|
|
635
|
-
expect(container.scrollLeft).toBe(100);
|
|
636
|
-
|
|
637
|
-
// X only
|
|
638
|
-
result.scrollToOffset(300, null);
|
|
639
|
-
expect(container.scrollLeft).toBe(300);
|
|
640
|
-
|
|
641
|
-
// Y only
|
|
642
|
-
result.scrollToOffset(null, 400);
|
|
643
|
-
expect(container.scrollTop).toBe(400);
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it('should stop programmatic scroll', async () => {
|
|
647
|
-
const { result } = setup(defaultProps);
|
|
648
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
649
|
-
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(true);
|
|
650
|
-
|
|
651
|
-
result.stopProgrammaticScroll();
|
|
652
|
-
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(false);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it('should handle scrollToIndex with element container having scrollTo', async () => {
|
|
656
|
-
const container = document.createElement('div');
|
|
657
|
-
container.scrollTo = vi.fn();
|
|
658
|
-
const { result } = setup({ ...defaultProps, container });
|
|
659
|
-
await nextTick();
|
|
660
|
-
|
|
661
|
-
result.scrollToIndex(10, 0, { behavior: 'auto' });
|
|
662
|
-
await nextTick();
|
|
663
|
-
expect(container.scrollTo).toHaveBeenCalled();
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it('should handle scrollToIndex fallback when scrollTo is missing', async () => {
|
|
667
|
-
const container = document.createElement('div');
|
|
668
|
-
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
669
|
-
const { result } = setup({ ...defaultProps, container });
|
|
670
|
-
await nextTick();
|
|
671
|
-
|
|
672
|
-
// row only
|
|
673
|
-
result.scrollToIndex(10, null, { behavior: 'auto' });
|
|
674
|
-
await nextTick();
|
|
675
|
-
expect(container.scrollTop).toBeGreaterThan(0);
|
|
676
|
-
|
|
677
|
-
// col only
|
|
678
|
-
const { result: resH } = setup({ ...defaultProps, container, direction: 'horizontal' });
|
|
679
|
-
await nextTick();
|
|
680
|
-
resH.scrollToIndex(null, 10, { behavior: 'auto' });
|
|
681
|
-
await nextTick();
|
|
682
|
-
expect(container.scrollLeft).toBeGreaterThan(0);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('should skip undefined items in renderedItems', async () => {
|
|
686
|
-
const items = Array.from({ length: 10 }) as unknown[];
|
|
687
|
-
items[ 0 ] = { id: 0 };
|
|
688
|
-
// other indices are undefined
|
|
689
|
-
const { result } = setup({ ...defaultProps, items, itemSize: 50 });
|
|
690
|
-
await nextTick();
|
|
691
|
-
// only index 0 should be rendered
|
|
692
|
-
expect(result.renderedItems.value.length).toBe(1);
|
|
693
|
-
expect(result.renderedItems.value[ 0 ]?.index).toBe(0);
|
|
694
|
-
});
|
|
126
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
127
|
+
expect(result.scrollDetails.value.currentIndex).toBe(20);
|
|
695
128
|
});
|
|
696
129
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
await nextTick();
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it('should cover fallback branches for unknown targets and directions', async () => {
|
|
707
|
-
// 1. Unknown container type (hits 408, 445, 513, 718 else branches)
|
|
708
|
-
const unknownContainer = {
|
|
709
|
-
addEventListener: vi.fn(),
|
|
710
|
-
removeEventListener: vi.fn(),
|
|
711
|
-
} as unknown as HTMLElement;
|
|
712
|
-
|
|
713
|
-
const { result } = setup({
|
|
714
|
-
...defaultProps,
|
|
715
|
-
container: unknownContainer,
|
|
716
|
-
hostElement: document.createElement('div'),
|
|
717
|
-
});
|
|
718
|
-
await nextTick();
|
|
719
|
-
|
|
720
|
-
result.scrollToIndex(10, 0);
|
|
721
|
-
result.scrollToOffset(100, 100);
|
|
722
|
-
result.updateHostOffset();
|
|
723
|
-
|
|
724
|
-
// 2. Invalid direction (hits 958 else branch)
|
|
725
|
-
const { result: r2 } = setup({
|
|
726
|
-
...defaultProps,
|
|
727
|
-
direction: undefined as unknown as 'vertical',
|
|
728
|
-
stickyIndices: [ 0 ],
|
|
729
|
-
});
|
|
730
|
-
await nextTick();
|
|
731
|
-
window.dispatchEvent(new Event('scroll'));
|
|
732
|
-
await nextTick();
|
|
733
|
-
expect(r2.renderedItems.value.find((i) => i.index === 0)).toBeDefined();
|
|
734
|
-
|
|
735
|
-
// 3. Unknown target in handleScroll (hits 1100 else branch)
|
|
736
|
-
const container = document.createElement('div');
|
|
737
|
-
setup({ ...defaultProps, container });
|
|
738
|
-
const event = new Event('scroll');
|
|
739
|
-
Object.defineProperty(event, 'target', { value: { } });
|
|
740
|
-
container.dispatchEvent(event);
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
it('should cleanup events and observers when container changes', async () => {
|
|
744
|
-
const container = document.createElement('div');
|
|
745
|
-
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
746
|
-
const { props } = setup({ ...defaultProps, container });
|
|
747
|
-
await nextTick();
|
|
748
|
-
|
|
749
|
-
// Change container to trigger cleanup of old one
|
|
750
|
-
props.value.container = document.createElement('div');
|
|
751
|
-
await nextTick();
|
|
752
|
-
|
|
753
|
-
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
it('should cleanup when unmounted and container is window', async () => {
|
|
757
|
-
const { wrapper } = setup({ ...defaultProps, container: window });
|
|
758
|
-
await nextTick();
|
|
759
|
-
wrapper.unmount();
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
it('should cleanup when unmounted', async () => {
|
|
763
|
-
const container = document.createElement('div');
|
|
764
|
-
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
765
|
-
const { wrapper } = setup({ ...defaultProps, container });
|
|
766
|
-
await nextTick();
|
|
767
|
-
|
|
768
|
-
wrapper.unmount();
|
|
769
|
-
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
130
|
+
it('handles dynamic item sizes', async () => {
|
|
131
|
+
const { result } = setup({
|
|
132
|
+
container: window,
|
|
133
|
+
direction: 'vertical',
|
|
134
|
+
itemSize: 0, // dynamic
|
|
135
|
+
items: mockItems,
|
|
770
136
|
});
|
|
771
137
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
document.dispatchEvent(new Event('scroll'));
|
|
775
|
-
await nextTick();
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
it('should handle scroll events on container element', async () => {
|
|
779
|
-
const container = document.createElement('div');
|
|
780
|
-
setup({ ...defaultProps, container });
|
|
781
|
-
Object.defineProperty(container, 'scrollTop', { value: 100 });
|
|
782
|
-
container.dispatchEvent(new Event('scroll'));
|
|
783
|
-
await nextTick();
|
|
784
|
-
});
|
|
138
|
+
await nextTick();
|
|
139
|
+
await nextTick();
|
|
785
140
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
Object.defineProperty(container, 'clientWidth', { value: 500, writable: true });
|
|
789
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, writable: true });
|
|
790
|
-
const { result } = setup({ ...defaultProps, container });
|
|
791
|
-
await nextTick();
|
|
792
|
-
expect(result.scrollDetails.value.viewportSize.width).toBe(500);
|
|
793
|
-
|
|
794
|
-
Object.defineProperty(container, 'clientWidth', { value: 800 });
|
|
795
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.targets.has(container));
|
|
796
|
-
if (observer) {
|
|
797
|
-
observer.trigger([ { target: container } ]);
|
|
798
|
-
}
|
|
799
|
-
await nextTick();
|
|
800
|
-
expect(result.scrollDetails.value.viewportSize.width).toBe(800);
|
|
801
|
-
});
|
|
141
|
+
// Initial estimate 100 * 40 = 4000
|
|
142
|
+
expect(result.totalHeight.value).toBe(4000);
|
|
802
143
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const container = document.createElement('div');
|
|
806
|
-
const { result } = setup({ ...defaultProps, container });
|
|
807
|
-
container.dispatchEvent(new Event('scroll'));
|
|
808
|
-
await nextTick();
|
|
809
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
810
|
-
vi.advanceTimersByTime(250);
|
|
811
|
-
await nextTick();
|
|
812
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
813
|
-
vi.useRealTimers();
|
|
814
|
-
});
|
|
144
|
+
result.updateItemSize(0, 100, 100);
|
|
145
|
+
await nextTick();
|
|
815
146
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
await nextTick();
|
|
819
|
-
props.value.container = null;
|
|
820
|
-
await nextTick();
|
|
821
|
-
props.value.container = window;
|
|
822
|
-
await nextTick();
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it('should handle window resize events', async () => {
|
|
826
|
-
setup({ ...defaultProps, container: window });
|
|
827
|
-
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1200 });
|
|
828
|
-
window.dispatchEvent(new Event('resize'));
|
|
829
|
-
await nextTick();
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
it('should cover handleScroll with document target', async () => {
|
|
833
|
-
setup({ ...defaultProps, container: window });
|
|
834
|
-
document.dispatchEvent(new Event('scroll'));
|
|
835
|
-
await nextTick();
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
it('should handle undefined window in handleScroll', async () => {
|
|
839
|
-
const originalWindow = globalThis.window;
|
|
840
|
-
const container = document.createElement('div');
|
|
841
|
-
setup({ ...defaultProps, container });
|
|
842
|
-
|
|
843
|
-
try {
|
|
844
|
-
(globalThis as unknown as { window: unknown; }).window = undefined;
|
|
845
|
-
container.dispatchEvent(new Event('scroll'));
|
|
846
|
-
await nextTick();
|
|
847
|
-
} finally {
|
|
848
|
-
globalThis.window = originalWindow;
|
|
849
|
-
}
|
|
850
|
-
});
|
|
147
|
+
// Now 1*100 + 99*40 = 100 + 3960 = 4060
|
|
148
|
+
expect(result.totalHeight.value).toBe(4060);
|
|
851
149
|
});
|
|
852
150
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
expect(result.getColumnWidth(0)).toBe(100);
|
|
862
|
-
expect(result.getColumnWidth(1)).toBe(200);
|
|
863
|
-
expect(result.totalWidth.value).toBe(600);
|
|
151
|
+
it('restores scroll position when items are prepended', async () => {
|
|
152
|
+
const items = Array.from({ length: 20 }, (_, i) => ({ id: i }));
|
|
153
|
+
const { props, result } = setup({
|
|
154
|
+
container: window,
|
|
155
|
+
direction: 'vertical',
|
|
156
|
+
itemSize: 50,
|
|
157
|
+
items,
|
|
158
|
+
restoreScrollOnPrepend: true,
|
|
864
159
|
});
|
|
865
160
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
...defaultProps,
|
|
869
|
-
direction: 'both',
|
|
870
|
-
columnCount: 2,
|
|
871
|
-
columnWidth: [ 0 ] as unknown as number[],
|
|
872
|
-
});
|
|
873
|
-
expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
|
|
874
|
-
});
|
|
161
|
+
await nextTick();
|
|
162
|
+
await nextTick();
|
|
875
163
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
columnCount: 10,
|
|
881
|
-
columnWidth: (index: number) => (index % 2 === 0 ? 100 : 200),
|
|
882
|
-
});
|
|
883
|
-
expect(result.getColumnWidth(0)).toBe(100);
|
|
884
|
-
expect(result.totalWidth.value).toBe(1500); // 5*100 + 5*200 - 0
|
|
885
|
-
});
|
|
164
|
+
// Scroll to index 5 (250px)
|
|
165
|
+
result.scrollToOffset(0, 250, { behavior: 'auto' });
|
|
166
|
+
await nextTick();
|
|
167
|
+
await nextTick();
|
|
886
168
|
|
|
887
|
-
|
|
888
|
-
const { result } = setup({
|
|
889
|
-
...defaultProps,
|
|
890
|
-
direction: 'both',
|
|
891
|
-
columnCount: 2,
|
|
892
|
-
columnWidth: undefined,
|
|
893
|
-
});
|
|
894
|
-
expect(result.getColumnWidth(0)).toBe(100);
|
|
895
|
-
});
|
|
169
|
+
expect(window.scrollY).toBe(250);
|
|
896
170
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const { result } = setup({
|
|
900
|
-
...defaultProps,
|
|
901
|
-
direction: 'both',
|
|
902
|
-
columnCount: 50,
|
|
903
|
-
columnWidth: undefined,
|
|
904
|
-
container,
|
|
905
|
-
});
|
|
906
|
-
// Initialize some column widths
|
|
907
|
-
for (let i = 0; i < 20; i++) {
|
|
908
|
-
const parent = document.createElement('div');
|
|
909
|
-
const child = document.createElement('div');
|
|
910
|
-
Object.defineProperty(child, 'offsetWidth', { value: 100 });
|
|
911
|
-
child.dataset.colIndex = String(i);
|
|
912
|
-
parent.appendChild(child);
|
|
913
|
-
result.updateItemSize(0, 100, 50, parent);
|
|
914
|
-
}
|
|
915
|
-
await nextTick();
|
|
916
|
-
expect(result.columnRange.value.end).toBeGreaterThan(result.columnRange.value.start);
|
|
917
|
-
});
|
|
171
|
+
// Prepend 2 items (100px)
|
|
172
|
+
props.value.items = [ { id: -1 }, { id: -2 }, ...items ];
|
|
918
173
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
expect(result.columnRange.value.end).toBe(0);
|
|
923
|
-
});
|
|
174
|
+
await nextTick();
|
|
175
|
+
await nextTick();
|
|
176
|
+
await nextTick();
|
|
924
177
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
await nextTick();
|
|
928
|
-
expect(result.columnRange.value.start).toBe(0);
|
|
929
|
-
});
|
|
178
|
+
// Scroll should be adjusted to 350
|
|
179
|
+
expect(window.scrollY).toBe(350);
|
|
930
180
|
});
|
|
931
181
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const item0 = items.find((i) => i.index === 0);
|
|
939
|
-
const item10 = items.find((i) => i.index === 10);
|
|
940
|
-
expect(item0?.isSticky).toBe(true);
|
|
941
|
-
expect(item10?.isSticky).toBe(true);
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it('should make sticky items active when scrolled past', async () => {
|
|
945
|
-
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ] });
|
|
946
|
-
await nextTick();
|
|
947
|
-
|
|
948
|
-
result.scrollToOffset(0, 100);
|
|
949
|
-
await nextTick();
|
|
950
|
-
|
|
951
|
-
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
952
|
-
expect(item0?.isStickyActive).toBe(true);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it('should include current sticky item in rendered items even if range is ahead', async () => {
|
|
956
|
-
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ], bufferBefore: 0 });
|
|
957
|
-
await nextTick();
|
|
958
|
-
|
|
959
|
-
// Scroll to index 20. Range starts at 20.
|
|
960
|
-
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
961
|
-
await nextTick();
|
|
962
|
-
|
|
963
|
-
expect(result.scrollDetails.value.range.start).toBe(20);
|
|
964
|
-
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
965
|
-
expect(item0).toBeDefined();
|
|
966
|
-
expect(item0?.isStickyActive).toBe(true);
|
|
182
|
+
it('triggers correction when viewport dimensions change', async () => {
|
|
183
|
+
const { result } = setup({
|
|
184
|
+
container: window,
|
|
185
|
+
direction: 'vertical',
|
|
186
|
+
itemSize: 50,
|
|
187
|
+
items: mockItems,
|
|
967
188
|
});
|
|
968
189
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
972
|
-
Object.defineProperty(container, 'scrollTop', { value: 480, writable: true });
|
|
973
|
-
const { result } = setup({ ...defaultProps, container, stickyIndices: [ 0, 10 ], itemSize: 50 });
|
|
974
|
-
// We need to trigger scroll to update scrollY
|
|
975
|
-
container.dispatchEvent(new Event('scroll'));
|
|
190
|
+
await nextTick();
|
|
191
|
+
await nextTick();
|
|
976
192
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
193
|
+
// Scroll to item 50 auto
|
|
194
|
+
result.scrollToIndex(50, null, { align: 'auto', behavior: 'auto' });
|
|
195
|
+
await nextTick();
|
|
980
196
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const { result } = setup({
|
|
987
|
-
...defaultProps,
|
|
988
|
-
direction: 'horizontal',
|
|
989
|
-
container,
|
|
990
|
-
stickyIndices: [ 0, 10 ],
|
|
991
|
-
itemSize: 50,
|
|
992
|
-
columnGap: 0,
|
|
993
|
-
});
|
|
994
|
-
container.dispatchEvent(new Event('scroll'));
|
|
197
|
+
const initialScrollY = window.scrollY;
|
|
198
|
+
// item 50 at 2500. viewport 500. item 50 high.
|
|
199
|
+
// targetEnd = 2500 - (500 - 50) = 2050.
|
|
200
|
+
expect(initialScrollY).toBe(2050);
|
|
995
201
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
202
|
+
// Simulate viewport height decreasing
|
|
203
|
+
Object.defineProperty(document.documentElement, 'clientHeight', { configurable: true, value: 485 });
|
|
204
|
+
window.dispatchEvent(new Event('resize'));
|
|
999
205
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
1003
|
-
Object.defineProperty(container, 'scrollTop', { value: 380, writable: true });
|
|
1004
|
-
|
|
1005
|
-
const { result } = setup({
|
|
1006
|
-
...defaultProps,
|
|
1007
|
-
container,
|
|
1008
|
-
itemSize: undefined, // dynamic
|
|
1009
|
-
stickyIndices: [ 0, 10 ],
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
// Item 0 is sticky. Item 10 is next sticky.
|
|
1013
|
-
// Default size = 40.
|
|
1014
|
-
// nextStickyY = itemSizesY.query(10) = 400.
|
|
1015
|
-
// distance = 400 - 380 = 20.
|
|
1016
|
-
// 20 < 40 (item 0 height), so it should be pushed.
|
|
1017
|
-
// stickyOffset.y = -(40 - 20) = -20.
|
|
1018
|
-
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1019
|
-
expect(stickyItem?.stickyOffset.y).toBe(-20);
|
|
1020
|
-
});
|
|
206
|
+
await nextTick();
|
|
207
|
+
await nextTick();
|
|
1021
208
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
1025
|
-
Object.defineProperty(container, 'scrollLeft', { value: 380, writable: true });
|
|
1026
|
-
|
|
1027
|
-
const { result } = setup({
|
|
1028
|
-
...defaultProps,
|
|
1029
|
-
container,
|
|
1030
|
-
direction: 'horizontal',
|
|
1031
|
-
itemSize: undefined, // dynamic
|
|
1032
|
-
stickyIndices: [ 0, 10 ],
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
// nextStickyX = itemSizesX.query(10) = 400.
|
|
1036
|
-
// distance = 400 - 380 = 20.
|
|
1037
|
-
// 20 < 40, so stickyOffset.x = -20.
|
|
1038
|
-
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1039
|
-
expect(stickyItem?.stickyOffset.x).toBe(-20);
|
|
1040
|
-
});
|
|
209
|
+
// It should have corrected to: 2500 - (485 - 50) = 2500 - 435 = 2065.
|
|
210
|
+
expect(window.scrollY).toBe(2065);
|
|
1041
211
|
});
|
|
1042
212
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1054
|
-
const { result, props } = setup({
|
|
1055
|
-
...defaultProps,
|
|
1056
|
-
items,
|
|
1057
|
-
container,
|
|
1058
|
-
itemSize: 50,
|
|
1059
|
-
restoreScrollOnPrepend: true,
|
|
1060
|
-
});
|
|
1061
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1062
|
-
await nextTick();
|
|
1063
|
-
|
|
1064
|
-
expect(result.scrollDetails.value.scrollOffset.y).toBe(100);
|
|
1065
|
-
|
|
1066
|
-
// Prepend 2 items
|
|
1067
|
-
const newItems = [ { id: -1 }, { id: -2 }, ...items ];
|
|
1068
|
-
props.value.items = newItems;
|
|
1069
|
-
await nextTick();
|
|
1070
|
-
// Trigger initializeSizes
|
|
1071
|
-
await nextTick();
|
|
1072
|
-
|
|
1073
|
-
// Should have adjusted scroll by 2 * 50 = 100px. New scrollTop should be 200.
|
|
1074
|
-
expect(container.scrollTop).toBe(200);
|
|
1075
|
-
vi.useRealTimers();
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
it('should restore scroll position when items are prepended (horizontal)', async () => {
|
|
1079
|
-
vi.useFakeTimers();
|
|
1080
|
-
const container = document.createElement('div');
|
|
1081
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
1082
|
-
Object.defineProperty(container, 'scrollLeft', { value: 100, writable: true });
|
|
1083
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
213
|
+
it('renders sticky indices correctly using optimized search', async () => {
|
|
214
|
+
// Use an isolated container to avoid window pollution
|
|
215
|
+
const container = document.createElement('div');
|
|
216
|
+
Object.defineProperty(container, 'clientHeight', { configurable: true, value: 200 });
|
|
217
|
+
Object.defineProperty(container, 'clientWidth', { configurable: true, value: 500 });
|
|
218
|
+
// Mock scrollTo on container
|
|
219
|
+
container.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
220
|
+
if (options.left !== undefined) {
|
|
1084
221
|
container.scrollLeft = options.left;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1088
|
-
const { result, props } = setup({
|
|
1089
|
-
...defaultProps,
|
|
1090
|
-
direction: 'horizontal',
|
|
1091
|
-
items,
|
|
1092
|
-
container,
|
|
1093
|
-
itemSize: 50,
|
|
1094
|
-
restoreScrollOnPrepend: true,
|
|
1095
|
-
});
|
|
1096
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1097
|
-
await nextTick();
|
|
1098
|
-
|
|
1099
|
-
expect(result.scrollDetails.value.scrollOffset.x).toBe(100);
|
|
1100
|
-
|
|
1101
|
-
// Prepend 2 items
|
|
1102
|
-
const newItems = [ { id: -1 }, { id: -2 }, ...items ];
|
|
1103
|
-
props.value.items = newItems;
|
|
1104
|
-
await nextTick();
|
|
1105
|
-
await nextTick();
|
|
1106
|
-
|
|
1107
|
-
expect(container.scrollLeft).toBe(200);
|
|
1108
|
-
vi.useRealTimers();
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
it('should restore scroll position with itemSize as function when prepending', async () => {
|
|
1112
|
-
vi.useFakeTimers();
|
|
1113
|
-
const container = document.createElement('div');
|
|
1114
|
-
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1115
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
222
|
+
}
|
|
223
|
+
if (options.top !== undefined) {
|
|
1116
224
|
container.scrollTop = options.top;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1120
|
-
const { props } = setup({
|
|
1121
|
-
...defaultProps,
|
|
1122
|
-
items,
|
|
1123
|
-
container,
|
|
1124
|
-
itemSize: (item: { id: number; }) => (item.id < 0 ? 100 : 50),
|
|
1125
|
-
restoreScrollOnPrepend: true,
|
|
1126
|
-
});
|
|
1127
|
-
await nextTick();
|
|
1128
|
-
|
|
1129
|
-
// Prepend 1 item with id -1 (size 100)
|
|
1130
|
-
const newItems = [ { id: -1 }, ...items ];
|
|
1131
|
-
props.value.items = newItems;
|
|
1132
|
-
await nextTick();
|
|
1133
|
-
await nextTick();
|
|
1134
|
-
|
|
1135
|
-
// Should have adjusted scroll by 100px. New scrollTop should be 200.
|
|
1136
|
-
expect(container.scrollTop).toBe(200);
|
|
1137
|
-
vi.useRealTimers();
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
it('should NOT restore scroll position when restoreScrollOnPrepend is false', async () => {
|
|
1141
|
-
const container = document.createElement('div');
|
|
1142
|
-
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1143
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1144
|
-
const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: false });
|
|
1145
|
-
await nextTick();
|
|
1146
|
-
|
|
1147
|
-
const newItems = [ { id: -1 }, ...items ];
|
|
1148
|
-
props.value.items = newItems;
|
|
1149
|
-
await nextTick();
|
|
1150
|
-
await nextTick();
|
|
1151
|
-
expect(container.scrollTop).toBe(100);
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
it('should NOT restore scroll position when first item does not match', async () => {
|
|
1155
|
-
const container = document.createElement('div');
|
|
1156
|
-
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1157
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1158
|
-
const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: true });
|
|
1159
|
-
await nextTick();
|
|
1160
|
-
|
|
1161
|
-
const newItems = [ { id: -1 }, { id: 9999 } ];
|
|
1162
|
-
props.value.items = newItems;
|
|
1163
|
-
await nextTick();
|
|
1164
|
-
await nextTick();
|
|
1165
|
-
expect(container.scrollTop).toBe(100);
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it('should update pendingScroll rowIndex when items are prepended', async () => {
|
|
1169
|
-
const container = document.createElement('div');
|
|
1170
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1171
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1172
|
-
const { result, props } = setup({ ...defaultProps, container, restoreScrollOnPrepend: true });
|
|
1173
|
-
await nextTick();
|
|
1174
|
-
|
|
1175
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
1176
|
-
props.value.items = [ { id: -1 }, ...props.value.items ];
|
|
1177
|
-
await nextTick();
|
|
1178
|
-
});
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
describe('advanced logic and edge cases', () => {
|
|
1182
|
-
it('should trigger scroll correction when isScrolling becomes false', async () => {
|
|
1183
|
-
vi.useFakeTimers();
|
|
1184
|
-
const { result } = setup({ ...defaultProps, container: window, itemSize: undefined });
|
|
1185
|
-
await nextTick();
|
|
1186
|
-
result.scrollToIndex(10, 0, 'start');
|
|
1187
|
-
document.dispatchEvent(new Event('scroll'));
|
|
1188
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
1189
|
-
vi.advanceTimersByTime(250);
|
|
1190
|
-
await nextTick();
|
|
1191
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1192
|
-
vi.useRealTimers();
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
it('should trigger scroll correction when treeUpdateFlag changes', async () => {
|
|
1196
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
1197
|
-
await nextTick();
|
|
1198
|
-
result.scrollToIndex(10, 0, 'start');
|
|
1199
|
-
// Trigger tree update
|
|
1200
|
-
result.updateItemSize(5, 100, 100);
|
|
1201
|
-
await nextTick();
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
it('should cover updateHostOffset when container is window', async () => {
|
|
1205
|
-
const { result, props } = setup({ ...defaultProps, container: window });
|
|
1206
|
-
const host = document.createElement('div');
|
|
1207
|
-
props.value.hostElement = host;
|
|
1208
|
-
await nextTick();
|
|
1209
|
-
result.updateHostOffset();
|
|
1210
|
-
});
|
|
1211
|
-
|
|
1212
|
-
it('should cover updateHostOffset when container is hostElement', async () => {
|
|
1213
|
-
const host = document.createElement('div');
|
|
1214
|
-
const { result } = setup({ ...defaultProps, container: host, hostElement: host });
|
|
1215
|
-
await nextTick();
|
|
1216
|
-
result.updateHostOffset();
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
it('should handle updateHostOffset with window fallback when container is missing', async () => {
|
|
1220
|
-
const { result, props } = setup({ ...defaultProps, container: undefined });
|
|
1221
|
-
const host = document.createElement('div');
|
|
1222
|
-
props.value.hostElement = host;
|
|
1223
|
-
await nextTick();
|
|
1224
|
-
result.updateHostOffset();
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
|
-
it('should correctly calculate hostOffset when container is an HTMLElement', async () => {
|
|
1228
|
-
const container = document.createElement('div');
|
|
1229
|
-
const hostElement = document.createElement('div');
|
|
1230
|
-
|
|
1231
|
-
container.getBoundingClientRect = vi.fn(() => ({ top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100, x: 100, y: 100, toJSON: () => '' }));
|
|
1232
|
-
hostElement.getBoundingClientRect = vi.fn(() => ({ top: 150, left: 150, bottom: 200, right: 200, width: 50, height: 50, x: 150, y: 150, toJSON: () => '' }));
|
|
1233
|
-
Object.defineProperty(container, 'scrollTop', { value: 50, writable: true, configurable: true });
|
|
1234
|
-
|
|
1235
|
-
const { result } = setup({ ...defaultProps, container, hostElement });
|
|
1236
|
-
await nextTick();
|
|
1237
|
-
result.updateHostOffset();
|
|
1238
|
-
expect(result.scrollDetails.value.scrollOffset.y).toBeDefined();
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
it('should cover refresh method', async () => {
|
|
1242
|
-
const { result } = setup({ ...defaultProps, itemSize: 0 });
|
|
1243
|
-
result.updateItemSize(0, 100, 100);
|
|
1244
|
-
await nextTick();
|
|
1245
|
-
expect(result.totalHeight.value).toBe(4060);
|
|
1246
|
-
|
|
1247
|
-
result.refresh();
|
|
1248
|
-
await nextTick();
|
|
1249
|
-
expect(result.totalHeight.value).toBe(4000);
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
it('should trigger scroll correction on tree update with string alignment', async () => {
|
|
1253
|
-
const container = document.createElement('div');
|
|
1254
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1255
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1256
|
-
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1257
|
-
// Set a pending scroll with string alignment
|
|
1258
|
-
result.scrollToIndex(10, null, 'start');
|
|
1259
|
-
|
|
1260
|
-
// Trigger tree update
|
|
1261
|
-
result.updateItemSize(0, 100, 100);
|
|
1262
|
-
await nextTick();
|
|
1263
|
-
});
|
|
1264
|
-
|
|
1265
|
-
it('should trigger scroll correction on tree update with pending scroll', async () => {
|
|
1266
|
-
const container = document.createElement('div');
|
|
1267
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1268
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1269
|
-
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1270
|
-
// Set a pending scroll
|
|
1271
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
1272
|
-
|
|
1273
|
-
// Trigger tree update
|
|
1274
|
-
result.updateItemSize(0, 100, 100);
|
|
1275
|
-
await nextTick();
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
it('should trigger scroll correction when scrolling stops with pending scroll', async () => {
|
|
1279
|
-
vi.useFakeTimers();
|
|
1280
|
-
const container = document.createElement('div');
|
|
1281
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1282
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1283
|
-
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1284
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
1285
|
-
|
|
1286
|
-
// Start scrolling
|
|
225
|
+
}
|
|
1287
226
|
container.dispatchEvent(new Event('scroll'));
|
|
1288
|
-
await nextTick();
|
|
1289
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
1290
|
-
|
|
1291
|
-
// Wait for scroll timeout
|
|
1292
|
-
vi.advanceTimersByTime(250);
|
|
1293
|
-
await nextTick();
|
|
1294
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1295
|
-
vi.useRealTimers();
|
|
1296
227
|
});
|
|
1297
228
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
// Item 0 is now 70 instead of 50. Total: 50 * 99 + 70 = 4950 + 70 = 5020.
|
|
1338
|
-
expect(result.totalHeight.value).toBe(5020);
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
it('should update totals via measurements even if columnWidth is a function', async () => {
|
|
1342
|
-
const getColWidth = () => 100;
|
|
1343
|
-
|
|
1344
|
-
const propsValue = ref({
|
|
1345
|
-
items: mockItems,
|
|
1346
|
-
direction: 'both' as const,
|
|
1347
|
-
columnCount: 5,
|
|
1348
|
-
columnWidth: getColWidth,
|
|
1349
|
-
itemSize: 50,
|
|
1350
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1351
|
-
|
|
1352
|
-
const result = useVirtualScroll(propsValue);
|
|
1353
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1354
|
-
|
|
1355
|
-
// Simulate ResizeObserver measurement on a cell
|
|
1356
|
-
// We need to provide an element with data-col-index
|
|
1357
|
-
const element = document.createElement('div');
|
|
1358
|
-
const cell = document.createElement('div');
|
|
1359
|
-
cell.dataset.colIndex = '0';
|
|
1360
|
-
Object.defineProperty(cell, 'offsetWidth', { value: 120 });
|
|
1361
|
-
element.appendChild(cell);
|
|
1362
|
-
|
|
1363
|
-
result.updateItemSizes([ { index: 0, inlineSize: 120, blockSize: 50, element } ]);
|
|
1364
|
-
await nextTick();
|
|
1365
|
-
|
|
1366
|
-
// Column 0 is now 120 instead of 100. Total: 100 * 4 + 120 = 520.
|
|
1367
|
-
expect(result.totalWidth.value).toBe(520);
|
|
1368
|
-
});
|
|
229
|
+
const { result } = setup({
|
|
230
|
+
container,
|
|
231
|
+
direction: 'vertical',
|
|
232
|
+
itemSize: 50,
|
|
233
|
+
items: Array.from({ length: 20 }, (_, i) => ({ id: i })),
|
|
234
|
+
stickyIndices: [ 0, 10, 19 ],
|
|
235
|
+
bufferBefore: 0,
|
|
236
|
+
bufferAfter: 0,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await nextTick();
|
|
240
|
+
await nextTick();
|
|
241
|
+
|
|
242
|
+
// 1. Initial scroll 0. Range [0, 4].
|
|
243
|
+
expect(result.renderedItems.value.map((i) => i.index)).toEqual([ 0, 1, 2, 3 ]);
|
|
244
|
+
|
|
245
|
+
// 2. Scroll to 100 (item 2). Range [2, 6].
|
|
246
|
+
container.scrollTop = 100;
|
|
247
|
+
container.dispatchEvent(new Event('scroll'));
|
|
248
|
+
await nextTick();
|
|
249
|
+
await nextTick();
|
|
250
|
+
|
|
251
|
+
const indices2 = result.renderedItems.value.map((i) => i.index).sort((a, b) => a - b);
|
|
252
|
+
expect(indices2).toEqual([ 0, 2, 3, 4, 5 ]);
|
|
253
|
+
expect(result.renderedItems.value.find((i) => i.index === 0)?.isStickyActive).toBe(true);
|
|
254
|
+
|
|
255
|
+
// 3. Scroll to 500 (item 10). Range [10, 14].
|
|
256
|
+
container.scrollTop = 500;
|
|
257
|
+
container.dispatchEvent(new Event('scroll'));
|
|
258
|
+
await nextTick();
|
|
259
|
+
await nextTick();
|
|
260
|
+
|
|
261
|
+
const indices3 = result.renderedItems.value.map((i) => i.index).sort((a, b) => a - b);
|
|
262
|
+
expect(indices3).toContain(0);
|
|
263
|
+
expect(indices3).toContain(10);
|
|
264
|
+
expect(indices3).toContain(11);
|
|
265
|
+
expect(indices3).toContain(12);
|
|
266
|
+
expect(indices3).toContain(13);
|
|
1369
267
|
});
|
|
1370
268
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
Object.defineProperty(container, 'scrollLeft', { value: 0, writable: true, configurable: true });
|
|
1378
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1379
|
-
if (options.left !== undefined) {
|
|
1380
|
-
Object.defineProperty(container, 'scrollLeft', { value: options.left, writable: true, configurable: true });
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
const { result } = setup({
|
|
1385
|
-
...defaultProps,
|
|
1386
|
-
container,
|
|
1387
|
-
direction: 'both',
|
|
1388
|
-
columnCount: 20,
|
|
1389
|
-
columnWidth: 100,
|
|
1390
|
-
ssrRange: { start: 0, end: 10, colStart: 1, colEnd: 2 },
|
|
1391
|
-
initialScrollIndex: 0,
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
await nextTick(); // onMounted schedules hydration
|
|
1395
|
-
await nextTick(); // hydration tick 1
|
|
1396
|
-
await nextTick(); // hydration tick 2 (isHydrating = false)
|
|
1397
|
-
|
|
1398
|
-
expect(result.isHydrated.value).toBe(true);
|
|
1399
|
-
|
|
1400
|
-
// Scroll to col 5 (offset 500)
|
|
1401
|
-
result.scrollToIndex(null, 5, { align: 'start', behavior: 'auto' });
|
|
1402
|
-
await nextTick();
|
|
1403
|
-
|
|
1404
|
-
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1405
|
-
await nextTick();
|
|
1406
|
-
|
|
1407
|
-
// start = findLowerBound(500) = 5.
|
|
1408
|
-
// colBuffer should be 0 because ssrRange is present and isScrolling is false.
|
|
1409
|
-
expect(result.columnRange.value.start).toBe(5);
|
|
1410
|
-
|
|
1411
|
-
// Now trigger a scroll to make isScrolling true
|
|
1412
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1413
|
-
await nextTick();
|
|
1414
|
-
// isScrolling is now true. colBuffer should be 2.
|
|
1415
|
-
expect(result.columnRange.value.start).toBe(3);
|
|
1416
|
-
vi.useRealTimers();
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
it('should handle bufferBefore when ssrRange is present and not scrolling', async () => {
|
|
1420
|
-
vi.useFakeTimers();
|
|
1421
|
-
const container = document.createElement('div');
|
|
1422
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
1423
|
-
Object.defineProperty(container, 'scrollTop', { value: 0, writable: true, configurable: true });
|
|
1424
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1425
|
-
if (options.top !== undefined) {
|
|
1426
|
-
Object.defineProperty(container, 'scrollTop', { value: options.top, writable: true, configurable: true });
|
|
1427
|
-
}
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
const { result } = setup({
|
|
1431
|
-
...defaultProps,
|
|
1432
|
-
container,
|
|
1433
|
-
itemSize: 50,
|
|
1434
|
-
bufferBefore: 5,
|
|
1435
|
-
ssrRange: { start: 0, end: 10 },
|
|
1436
|
-
initialScrollIndex: 10,
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
await nextTick(); // schedules hydration
|
|
1440
|
-
await nextTick(); // hydration tick scrolls to 10
|
|
1441
|
-
await nextTick();
|
|
1442
|
-
|
|
1443
|
-
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1444
|
-
await nextTick();
|
|
1445
|
-
|
|
1446
|
-
expect(result.isHydrated.value).toBe(true);
|
|
1447
|
-
// start = floor(500 / 50) = 10.
|
|
1448
|
-
// Since ssrRange is present and isScrolling is false, bufferBefore should be 0.
|
|
1449
|
-
expect(result.renderedItems.value[ 0 ]?.index).toBe(10);
|
|
1450
|
-
|
|
1451
|
-
// Now trigger a scroll to make isScrolling true
|
|
1452
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1453
|
-
await nextTick();
|
|
1454
|
-
// isScrolling is now true. bufferBefore should be 5.
|
|
1455
|
-
expect(result.renderedItems.value[ 0 ]?.index).toBe(5);
|
|
1456
|
-
vi.useRealTimers();
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
it('should handle SSR range in range calculation', () => {
|
|
1460
|
-
const props = ref({
|
|
1461
|
-
items: mockItems,
|
|
1462
|
-
ssrRange: { start: 0, end: 10 },
|
|
1463
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1464
|
-
const result = useVirtualScroll(props);
|
|
1465
|
-
expect(result.renderedItems.value.length).toBe(10);
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
it('should handle SSR range in columnRange calculation', () => {
|
|
1469
|
-
const props = ref({
|
|
1470
|
-
items: mockItems,
|
|
1471
|
-
columnCount: 10,
|
|
1472
|
-
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 5 },
|
|
1473
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1474
|
-
const result = useVirtualScroll(props);
|
|
1475
|
-
expect(result.columnRange.value.end).toBe(5);
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
it('should handle SSR range with colEnd fallback in columnRange calculation', () => {
|
|
1479
|
-
const props = ref({
|
|
1480
|
-
items: mockItems,
|
|
1481
|
-
columnCount: 10,
|
|
1482
|
-
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 0 },
|
|
1483
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1484
|
-
const result = useVirtualScroll(props);
|
|
1485
|
-
// colEnd is 0, so it should use columnCount (10)
|
|
1486
|
-
expect(result.columnRange.value.end).toBe(10);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
it('should handle SSR range with both directions for total sizes', () => {
|
|
1490
|
-
const props = ref({
|
|
1491
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1492
|
-
direction: 'both',
|
|
1493
|
-
columnCount: 10,
|
|
1494
|
-
columnWidth: 100,
|
|
1495
|
-
itemSize: 50,
|
|
1496
|
-
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1497
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1498
|
-
const result = useVirtualScroll(props);
|
|
1499
|
-
expect(result.totalWidth.value).toBe(300); // (5-2) * 100 - gap(0)
|
|
1500
|
-
expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - gap(0)
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
it('should handle SSR range with horizontal direction for total sizes', () => {
|
|
1504
|
-
const props = ref({
|
|
1505
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1506
|
-
direction: 'horizontal',
|
|
1507
|
-
itemSize: 50,
|
|
1508
|
-
ssrRange: { start: 10, end: 20 },
|
|
1509
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1510
|
-
const result = useVirtualScroll(props);
|
|
1511
|
-
expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - gap(0)
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
it('should handle SSR range with vertical offset in renderedItems', () => {
|
|
1515
|
-
const props = ref({
|
|
1516
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1517
|
-
direction: 'vertical',
|
|
1518
|
-
itemSize: 50,
|
|
1519
|
-
ssrRange: { start: 10, end: 20 },
|
|
1520
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1521
|
-
const result = useVirtualScroll(props);
|
|
1522
|
-
expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
it('should handle SSR range with dynamic horizontal offsets in renderedItems', () => {
|
|
1526
|
-
const props = ref({
|
|
1527
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1528
|
-
direction: 'horizontal',
|
|
1529
|
-
itemSize: undefined, // dynamic
|
|
1530
|
-
ssrRange: { start: 10, end: 20 },
|
|
1531
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1532
|
-
const result = useVirtualScroll(props);
|
|
1533
|
-
// ssrOffsetX = itemSizesX.query(10) = 10 * 40 = 400
|
|
1534
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(400);
|
|
269
|
+
it('updates item sizes and compensates scroll position', async () => {
|
|
270
|
+
const { result } = setup({
|
|
271
|
+
container: window,
|
|
272
|
+
direction: 'vertical',
|
|
273
|
+
itemSize: 0,
|
|
274
|
+
items: mockItems,
|
|
1535
275
|
});
|
|
1536
276
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1540
|
-
direction: 'vertical',
|
|
1541
|
-
itemSize: 0,
|
|
1542
|
-
ssrRange: { start: 10, end: 20 },
|
|
1543
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1544
|
-
const result = useVirtualScroll(props);
|
|
1545
|
-
expect(result.totalHeight.value).toBe(400); // (20-10) * 40 - gap(0)
|
|
1546
|
-
});
|
|
277
|
+
await nextTick();
|
|
278
|
+
await nextTick();
|
|
1547
279
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
itemSize: 0,
|
|
1553
|
-
ssrRange: { start: 10, end: 20 },
|
|
1554
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1555
|
-
const result = useVirtualScroll(props);
|
|
1556
|
-
expect(result.totalWidth.value).toBe(400); // (20-10) * 40 - 0 gap
|
|
1557
|
-
});
|
|
280
|
+
// Scroll to item 10 (10 * 40 = 400px)
|
|
281
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 400, writable: true });
|
|
282
|
+
document.dispatchEvent(new Event('scroll'));
|
|
283
|
+
await nextTick();
|
|
1558
284
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
direction: 'both',
|
|
1563
|
-
columnCount: 10,
|
|
1564
|
-
itemSize: 0,
|
|
1565
|
-
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1566
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1567
|
-
const result = useVirtualScroll(props);
|
|
1568
|
-
expect(result.totalWidth.value).toBe(300); // (5-2) * 100
|
|
1569
|
-
expect(result.totalHeight.value).toBe(400); // (20-10) * 40
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
it('should scroll to ssrRange on mount', async () => {
|
|
1573
|
-
setup({ ...defaultProps, ssrRange: { start: 50, end: 60 } });
|
|
1574
|
-
await nextTick();
|
|
1575
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
1576
|
-
});
|
|
285
|
+
// Update item 0 (above viewport) from 40 to 100
|
|
286
|
+
result.updateItemSize(0, 100, 100);
|
|
287
|
+
await nextTick();
|
|
1577
288
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1581
|
-
direction: 'horizontal',
|
|
1582
|
-
itemSize: 50,
|
|
1583
|
-
ssrRange: { start: 0, end: 10, colStart: 5 },
|
|
1584
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1585
|
-
const result = useVirtualScroll(props);
|
|
1586
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-250);
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
it('should handle SSR range with direction "both" and colStart', () => {
|
|
1590
|
-
const props = ref({
|
|
1591
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1592
|
-
direction: 'both',
|
|
1593
|
-
columnCount: 20,
|
|
1594
|
-
columnWidth: 100,
|
|
1595
|
-
ssrRange: { start: 0, end: 10, colStart: 5, colEnd: 15 },
|
|
1596
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1597
|
-
const result = useVirtualScroll(props);
|
|
1598
|
-
// ssrOffsetX = columnSizes.query(5) = 5 * 100 = 500
|
|
1599
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-500);
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
it('should handle SSR range with direction "both" and colEnd falsy', () => {
|
|
1603
|
-
const propsValue = ref({
|
|
1604
|
-
columnCount: 10,
|
|
1605
|
-
columnWidth: 100,
|
|
1606
|
-
direction: 'both' as const,
|
|
1607
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1608
|
-
ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
|
|
1609
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1610
|
-
const result = useVirtualScroll(propsValue);
|
|
1611
|
-
// colEnd is 0, so it should use colCount (10)
|
|
1612
|
-
// totalWidth = columnSizes.query(10) - columnSizes.query(5) = 1000 - 500 = 500
|
|
1613
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
it('should handle SSR range with colCount > 0 in totalWidth', () => {
|
|
1617
|
-
const props = ref({
|
|
1618
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1619
|
-
direction: 'both',
|
|
1620
|
-
columnCount: 10,
|
|
1621
|
-
columnWidth: 100,
|
|
1622
|
-
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 5 },
|
|
1623
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1624
|
-
const result = useVirtualScroll(props);
|
|
1625
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1626
|
-
});
|
|
289
|
+
// Scroll position should have been adjusted by 60px
|
|
290
|
+
expect(window.scrollY).toBe(460);
|
|
1627
291
|
});
|
|
1628
292
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
it('should handle vertical direction in totalWidth', () => {
|
|
1636
|
-
const { result } = setup({ ...defaultProps, direction: 'vertical' });
|
|
1637
|
-
expect(result.totalWidth.value).toBe(0);
|
|
293
|
+
it('supports refresh method', async () => {
|
|
294
|
+
const { result } = setup({
|
|
295
|
+
container: window,
|
|
296
|
+
direction: 'vertical',
|
|
297
|
+
itemSize: 50,
|
|
298
|
+
items: mockItems,
|
|
1638
299
|
});
|
|
1639
300
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
it('should handle zero items in totalWidth/totalHeight', async () => {
|
|
1646
|
-
const { result } = setup({ ...defaultProps, items: [] });
|
|
1647
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1648
|
-
|
|
1649
|
-
const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', items: [] });
|
|
1650
|
-
expect(rH.totalWidth.value).toBe(0);
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
it('should cover SSR with zero items/columns', () => {
|
|
1654
|
-
const props = ref({
|
|
1655
|
-
items: [],
|
|
1656
|
-
direction: 'both',
|
|
1657
|
-
columnCount: 0,
|
|
1658
|
-
ssrRange: { start: 0, end: 0, colStart: 0, colEnd: 0 },
|
|
1659
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1660
|
-
const result = useVirtualScroll(props);
|
|
1661
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1662
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
it('should handle SSR range with both directions and no columns', () => {
|
|
1666
|
-
const props = ref({
|
|
1667
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1668
|
-
direction: 'both',
|
|
1669
|
-
columnCount: 0,
|
|
1670
|
-
ssrRange: { start: 10, end: 20, colStart: 0, colEnd: 0 },
|
|
1671
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1672
|
-
const result = useVirtualScroll(props);
|
|
1673
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
|
-
it('should handle SSR range with direction both and colCount > 0 but colEnd <= colStart', () => {
|
|
1677
|
-
const props = ref({
|
|
1678
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1679
|
-
direction: 'both',
|
|
1680
|
-
columnCount: 10,
|
|
1681
|
-
ssrRange: { start: 0, end: 10, colStart: 5, colEnd: 5 },
|
|
1682
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1683
|
-
const result = useVirtualScroll(props);
|
|
1684
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1685
|
-
});
|
|
1686
|
-
|
|
1687
|
-
it('should handle SSR range with vertical/both and end <= start', () => {
|
|
1688
|
-
const props = ref({
|
|
1689
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1690
|
-
direction: 'vertical',
|
|
1691
|
-
ssrRange: { start: 10, end: 10 },
|
|
1692
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1693
|
-
const result = useVirtualScroll(props);
|
|
1694
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
it('should handle SSR range with dynamic horizontal sizes for total sizes', () => {
|
|
1698
|
-
const props = ref({
|
|
1699
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1700
|
-
direction: 'horizontal',
|
|
1701
|
-
itemSize: 0,
|
|
1702
|
-
ssrRange: { start: 10, end: 20 },
|
|
1703
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1704
|
-
const result = useVirtualScroll(props);
|
|
1705
|
-
expect(result.totalWidth.value).toBe(400); // (20-10) * 40
|
|
1706
|
-
});
|
|
1707
|
-
|
|
1708
|
-
it('should handle SSR range with both directions and dynamic offsets for total width', () => {
|
|
1709
|
-
const props = ref({
|
|
1710
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1711
|
-
direction: 'both',
|
|
1712
|
-
columnCount: 10,
|
|
1713
|
-
itemSize: 0,
|
|
1714
|
-
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1715
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1716
|
-
const result = useVirtualScroll(props);
|
|
1717
|
-
expect(result.totalWidth.value).toBe(300);
|
|
1718
|
-
});
|
|
1719
|
-
|
|
1720
|
-
it('should handle updateItemSizes with index < 0', async () => {
|
|
1721
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
1722
|
-
result.updateItemSizes([ { index: -1, inlineSize: 100, blockSize: 100 } ]);
|
|
1723
|
-
await nextTick();
|
|
1724
|
-
// Should not change total height
|
|
1725
|
-
expect(result.totalHeight.value).toBe(4000);
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
it('should handle updateItemSizes with direction vertical and dynamic itemSize for X', async () => {
|
|
1729
|
-
const { result } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
1730
|
-
// Measured Items X should not be updated if direction is vertical
|
|
1731
|
-
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1732
|
-
await nextTick();
|
|
1733
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1734
|
-
});
|
|
1735
|
-
|
|
1736
|
-
it('should handle SSR with horizontal direction and fixedItemSize', () => {
|
|
1737
|
-
const propsValue = ref({
|
|
1738
|
-
direction: 'horizontal' as const,
|
|
1739
|
-
itemSize: 50,
|
|
1740
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1741
|
-
ssrRange: { end: 20, start: 10 },
|
|
1742
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1743
|
-
const result = useVirtualScroll(propsValue);
|
|
1744
|
-
expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - 0 gap
|
|
1745
|
-
});
|
|
1746
|
-
|
|
1747
|
-
it('should handle SSR with vertical direction and fixedItemSize', () => {
|
|
1748
|
-
const propsValue = ref({
|
|
1749
|
-
direction: 'vertical' as const,
|
|
1750
|
-
itemSize: 50,
|
|
1751
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1752
|
-
ssrRange: { end: 20, start: 10 },
|
|
1753
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1754
|
-
const result = useVirtualScroll(propsValue);
|
|
1755
|
-
expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - 0 gap
|
|
1756
|
-
});
|
|
1757
|
-
|
|
1758
|
-
it('should handle SSR with direction both and fixedItemSize for totalHeight', () => {
|
|
1759
|
-
const propsValue = ref({
|
|
1760
|
-
direction: 'both' as const,
|
|
1761
|
-
columnCount: 10,
|
|
1762
|
-
itemSize: 50,
|
|
1763
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1764
|
-
ssrRange: { end: 20, start: 10 },
|
|
1765
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1766
|
-
const result = useVirtualScroll(propsValue);
|
|
1767
|
-
expect(result.totalHeight.value).toBe(500);
|
|
1768
|
-
});
|
|
1769
|
-
|
|
1770
|
-
it('should handle SSR range with direction both and colEnd falsy', () => {
|
|
1771
|
-
const propsValue = ref({
|
|
1772
|
-
columnCount: 10,
|
|
1773
|
-
columnWidth: 100,
|
|
1774
|
-
direction: 'both' as const,
|
|
1775
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1776
|
-
ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
|
|
1777
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1778
|
-
const result = useVirtualScroll(propsValue);
|
|
1779
|
-
// colEnd is 0, so it should use colCount (10)
|
|
1780
|
-
// totalWidth = (10 - 5) * 100 = 500
|
|
1781
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1782
|
-
});
|
|
1783
|
-
|
|
1784
|
-
it('should handle updateItemSizes with direction both and dynamic itemSize for Y', async () => {
|
|
1785
|
-
const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 2, itemSize: undefined });
|
|
1786
|
-
// First measurement
|
|
1787
|
-
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1788
|
-
await nextTick();
|
|
1789
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
1790
|
-
|
|
1791
|
-
// Increase
|
|
1792
|
-
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 120 } ]);
|
|
1793
|
-
await nextTick();
|
|
1794
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(120);
|
|
1795
|
-
|
|
1796
|
-
// Significant decrease
|
|
1797
|
-
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1798
|
-
await nextTick();
|
|
1799
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
1800
|
-
});
|
|
1801
|
-
|
|
1802
|
-
it('should handle object padding branches in helpers', () => {
|
|
1803
|
-
expect(getPaddingX({ x: 10 }, 'horizontal')).toBe(10);
|
|
1804
|
-
expect(getPaddingY({ y: 20 }, 'vertical')).toBe(20);
|
|
1805
|
-
});
|
|
301
|
+
await nextTick();
|
|
302
|
+
result.refresh();
|
|
303
|
+
await nextTick();
|
|
304
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
305
|
+
});
|
|
1806
306
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1814
|
-
const result = useVirtualScroll(propsValue);
|
|
1815
|
-
expect(result.totalWidth.value).toBe(0);
|
|
307
|
+
it('supports getColumnWidth with various types', async () => {
|
|
308
|
+
const { result } = setup({
|
|
309
|
+
columnCount: 10,
|
|
310
|
+
columnWidth: [ 100, 200 ],
|
|
311
|
+
direction: 'both',
|
|
312
|
+
items: mockItems,
|
|
1816
313
|
});
|
|
1817
314
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
ssrRange: { start: 10, end: 10 },
|
|
1824
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1825
|
-
const result = useVirtualScroll(propsValue);
|
|
1826
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1827
|
-
});
|
|
315
|
+
await nextTick();
|
|
316
|
+
expect(result.getColumnWidth(0)).toBe(100);
|
|
317
|
+
expect(result.getColumnWidth(1)).toBe(200);
|
|
318
|
+
expect(result.getColumnWidth(2)).toBe(100);
|
|
319
|
+
});
|
|
1828
320
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const result = useVirtualScroll(propsValue);
|
|
1836
|
-
expect(result.totalWidth.value).toBe(0);
|
|
321
|
+
it('updates column sizes from row element', async () => {
|
|
322
|
+
const { result } = setup({
|
|
323
|
+
columnCount: 5,
|
|
324
|
+
columnWidth: 0, // dynamic
|
|
325
|
+
direction: 'both',
|
|
326
|
+
items: mockItems,
|
|
1837
327
|
});
|
|
1838
328
|
|
|
1839
|
-
|
|
1840
|
-
const propsValue = ref({
|
|
1841
|
-
items: mockItems,
|
|
1842
|
-
direction: 'vertical' as const,
|
|
1843
|
-
itemSize: 50,
|
|
1844
|
-
ssrRange: { start: 10, end: 10 },
|
|
1845
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1846
|
-
const result = useVirtualScroll(propsValue);
|
|
1847
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1848
|
-
});
|
|
329
|
+
await nextTick();
|
|
1849
330
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
ssrRange: { start: 10, end: 10 },
|
|
1856
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1857
|
-
const result = useVirtualScroll(propsValue);
|
|
1858
|
-
expect(result.totalHeight.value).toBe(0);
|
|
331
|
+
const rowEl = document.createElement('div');
|
|
332
|
+
const cell0 = document.createElement('div');
|
|
333
|
+
cell0.dataset.colIndex = '0';
|
|
334
|
+
Object.defineProperty(cell0, 'getBoundingClientRect', {
|
|
335
|
+
value: () => ({ width: 150 }),
|
|
1859
336
|
});
|
|
337
|
+
rowEl.appendChild(cell0);
|
|
1860
338
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const result = useVirtualScroll(propsValue);
|
|
1868
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1869
|
-
});
|
|
339
|
+
result.updateItemSizes([ {
|
|
340
|
+
blockSize: 100,
|
|
341
|
+
element: rowEl,
|
|
342
|
+
index: 0,
|
|
343
|
+
inlineSize: 0,
|
|
344
|
+
} ]);
|
|
1870
345
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
it('should skip re-initializing sizes for already measured dynamic items', async () => {
|
|
1875
|
-
const props = ref({
|
|
1876
|
-
items: mockItems,
|
|
1877
|
-
direction: 'both' as const,
|
|
1878
|
-
columnCount: 2,
|
|
1879
|
-
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1880
|
-
|
|
1881
|
-
const result = useVirtualScroll(props);
|
|
1882
|
-
await nextTick();
|
|
1883
|
-
|
|
1884
|
-
// Measure item 0 and col 0
|
|
1885
|
-
const parent = document.createElement('div');
|
|
1886
|
-
const col0 = document.createElement('div');
|
|
1887
|
-
col0.dataset.colIndex = '0';
|
|
1888
|
-
Object.defineProperty(col0, 'offsetWidth', { value: 200 });
|
|
1889
|
-
parent.appendChild(col0);
|
|
1890
|
-
|
|
1891
|
-
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 150, element: parent } ]);
|
|
1892
|
-
await nextTick();
|
|
1893
|
-
|
|
1894
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
1895
|
-
expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
|
|
1896
|
-
|
|
1897
|
-
// Trigger initializeSizes by changing items length
|
|
1898
|
-
props.value.items = Array.from({ length: 11 }, (_, i) => ({ id: i }));
|
|
1899
|
-
await nextTick();
|
|
1900
|
-
|
|
1901
|
-
// Should NOT reset already measured item 0
|
|
1902
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
1903
|
-
expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
|
|
1904
|
-
});
|
|
1905
|
-
|
|
1906
|
-
it('should mark items as measured when fixed size matches current size within tolerance', async () => {
|
|
1907
|
-
const props = ref({
|
|
1908
|
-
items: mockItems,
|
|
1909
|
-
direction: 'horizontal' as const,
|
|
1910
|
-
itemSize: 50,
|
|
1911
|
-
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1912
|
-
|
|
1913
|
-
useVirtualScroll(props);
|
|
1914
|
-
await nextTick();
|
|
1915
|
-
|
|
1916
|
-
// Trigger initializeSizes again with same prop
|
|
1917
|
-
props.value.columnGap = 0;
|
|
1918
|
-
await nextTick();
|
|
1919
|
-
// Hits the branch where Math.abs(current - target) <= 0.5
|
|
1920
|
-
});
|
|
1921
|
-
|
|
1922
|
-
it('should mark columns as measured when fixed width matches current width within tolerance', async () => {
|
|
1923
|
-
const props = ref({
|
|
1924
|
-
items: mockItems,
|
|
1925
|
-
direction: 'both' as const,
|
|
1926
|
-
columnCount: 2,
|
|
1927
|
-
columnWidth: 100,
|
|
1928
|
-
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1929
|
-
|
|
1930
|
-
useVirtualScroll(props);
|
|
1931
|
-
await nextTick();
|
|
1932
|
-
|
|
1933
|
-
props.value.columnGap = 0;
|
|
1934
|
-
await nextTick();
|
|
1935
|
-
});
|
|
1936
|
-
|
|
1937
|
-
it('should reset item sizes when switching between horizontal and vertical directions', async () => {
|
|
1938
|
-
const props = ref({
|
|
1939
|
-
items: mockItems,
|
|
1940
|
-
direction: 'horizontal' as const,
|
|
1941
|
-
itemSize: 50,
|
|
1942
|
-
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1943
|
-
|
|
1944
|
-
const result = useVirtualScroll(props);
|
|
1945
|
-
await nextTick();
|
|
1946
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1947
|
-
|
|
1948
|
-
// Switch to vertical (resets X)
|
|
1949
|
-
props.value.direction = 'vertical';
|
|
1950
|
-
await nextTick();
|
|
1951
|
-
expect(result.totalWidth.value).toBe(0);
|
|
1952
|
-
|
|
1953
|
-
// Switch to both
|
|
1954
|
-
props.value.direction = 'both';
|
|
1955
|
-
props.value.columnCount = 10;
|
|
1956
|
-
props.value.columnWidth = 100;
|
|
1957
|
-
await nextTick();
|
|
1958
|
-
expect(result.totalHeight.value).toBe(500);
|
|
1959
|
-
expect(result.totalWidth.value).toBe(1000);
|
|
1960
|
-
|
|
1961
|
-
// Switch to horizontal (resets Y)
|
|
1962
|
-
props.value.direction = 'horizontal';
|
|
1963
|
-
await nextTick();
|
|
1964
|
-
await nextTick();
|
|
1965
|
-
expect(result.totalHeight.value).toBe(0);
|
|
1966
|
-
});
|
|
1967
|
-
|
|
1968
|
-
it('should skip re-initialization if dynamic size is already measured and non-zero', async () => {
|
|
1969
|
-
const props = ref({
|
|
1970
|
-
items: mockItems,
|
|
1971
|
-
direction: 'horizontal' as const,
|
|
1972
|
-
itemSize: undefined, // dynamic
|
|
1973
|
-
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1974
|
-
|
|
1975
|
-
const result = useVirtualScroll(props);
|
|
1976
|
-
await nextTick();
|
|
1977
|
-
|
|
1978
|
-
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 50, element: document.createElement('div') } ]);
|
|
1979
|
-
await nextTick();
|
|
1980
|
-
|
|
1981
|
-
props.value.gap = 1;
|
|
1982
|
-
await nextTick();
|
|
1983
|
-
|
|
1984
|
-
expect(result.totalWidth.value).toBeGreaterThan(0);
|
|
1985
|
-
});
|
|
1986
|
-
});
|
|
346
|
+
await nextTick();
|
|
347
|
+
expect(result.getColumnWidth(0)).toBe(150);
|
|
1987
348
|
});
|
|
1988
349
|
});
|