clear-virtualizer 0.1.3 → 0.1.5
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 +1 -0
- package/lib/hooks/useVirtualizer.d.ts +3 -1
- package/lib/index.cjs +34 -17
- package/lib/index.js +34 -17
- package/lib/utils.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ const { virtualItems, scrollHeight, scrollRef, getMeasureRef, scrollToIndex } =
|
|
|
31
31
|
| `count` | `number` | Total number of rows |
|
|
32
32
|
| `estimateSize` | `((index: number) => number) \| undefined` | Approximate row height in px, used until a row is measured |
|
|
33
33
|
| `overscan` | `number \| undefined` | Extra rows rendered above/below the viewport (default: 3) |
|
|
34
|
+
| `headerHeight` | `number \| undefined` | Sticky header height in px, excluded from the window (default: 0) |
|
|
34
35
|
|
|
35
36
|
| Return value | Type | Description |
|
|
36
37
|
| --------------- | ------------------------------------------------------------ | -------------------------------------------------- |
|
|
@@ -8,9 +8,11 @@ export type Options = {
|
|
|
8
8
|
count: number;
|
|
9
9
|
estimateSize?: (index: number) => number;
|
|
10
10
|
overscan?: number;
|
|
11
|
+
/** Height in px of a sticky header overlaying the top of the scroll container. Excluded from the visible window. */
|
|
12
|
+
headerHeight?: number;
|
|
11
13
|
};
|
|
12
14
|
export type ScrollAlign = 'start' | 'center' | 'end';
|
|
13
|
-
export declare function useVirtualizer({ count, estimateSize, overscan }: Options): {
|
|
15
|
+
export declare function useVirtualizer({ count, estimateSize, overscan, headerHeight }: Options): {
|
|
14
16
|
virtualItems: VirtualItem[];
|
|
15
17
|
scrollHeight: number;
|
|
16
18
|
scrollToIndex: (index: number, options?: {
|
package/lib/index.cjs
CHANGED
|
@@ -90,24 +90,26 @@ const getViewportRange = (tree, logicalScrollOffset, containerHeight, scale, ove
|
|
|
90
90
|
viewportEnd
|
|
91
91
|
};
|
|
92
92
|
};
|
|
93
|
-
const computeTargetScrollTop = (tree, index, align, containerHeight, scale, count) => {
|
|
93
|
+
const computeTargetScrollTop = (tree, index, align, containerHeight, scale, count, headerHeight = 0) => {
|
|
94
94
|
const targetIndex = Math.max(0, Math.min(index, count - 1));
|
|
95
95
|
const targetOffset = tree.prefixSum(targetIndex);
|
|
96
96
|
const itemLogicalSize = tree.prefixSum(targetIndex + 1) - targetOffset;
|
|
97
|
+
const visibleHeight = Math.max(containerHeight - headerHeight, 0);
|
|
97
98
|
let logicalScrollOffset;
|
|
98
99
|
switch (align) {
|
|
99
100
|
case "center":
|
|
100
|
-
logicalScrollOffset = targetOffset + (itemLogicalSize -
|
|
101
|
+
logicalScrollOffset = targetOffset + (itemLogicalSize - visibleHeight / scale) / 2;
|
|
101
102
|
break;
|
|
102
103
|
case "end":
|
|
103
|
-
logicalScrollOffset = targetOffset + itemLogicalSize -
|
|
104
|
+
logicalScrollOffset = targetOffset + itemLogicalSize - visibleHeight / scale;
|
|
104
105
|
break;
|
|
105
106
|
default:
|
|
106
107
|
logicalScrollOffset = targetOffset;
|
|
107
108
|
}
|
|
108
|
-
return { targetScrollTop: Math.max(0, logicalScrollOffset * scale), logicalScrollOffset };
|
|
109
|
+
return { targetScrollTop: Math.round(Math.max(0, logicalScrollOffset * scale - headerHeight)), logicalScrollOffset };
|
|
109
110
|
};
|
|
110
|
-
function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
111
|
+
function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN, headerHeight = 0 }) {
|
|
112
|
+
const headerOffset = Math.max(headerHeight, 0);
|
|
111
113
|
const [scrollOffset, setScrollOffset] = react.useState({ value: 0 });
|
|
112
114
|
const [forcedRange, setForcedRange] = react.useState(null);
|
|
113
115
|
const scrollElementRef = react.useRef(null);
|
|
@@ -129,7 +131,18 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
129
131
|
if (fenwickRef.current === null || !fenwickRef.current.total())
|
|
130
132
|
fenwickRef.current = getFenwickTree(count, estimateSize);
|
|
131
133
|
if (prevCountRef.current !== count) {
|
|
132
|
-
fenwickRef.current
|
|
134
|
+
const prevTree = fenwickRef.current;
|
|
135
|
+
const prevCount = prevCountRef.current;
|
|
136
|
+
const nextTree = getFenwickTree(count, estimateSize);
|
|
137
|
+
if (prevTree && prevCount > 0) {
|
|
138
|
+
const keep = Math.min(prevCount, count);
|
|
139
|
+
for (let i = 0; i < keep; i++) {
|
|
140
|
+
const delta = prevTree.prefixSum(i + 1) - prevTree.prefixSum(i) - (nextTree.prefixSum(i + 1) - nextTree.prefixSum(i));
|
|
141
|
+
if (delta !== 0)
|
|
142
|
+
nextTree.update(i, delta);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
fenwickRef.current = nextTree;
|
|
133
146
|
prevCountRef.current = count;
|
|
134
147
|
}
|
|
135
148
|
const computeItems = react.useCallback(
|
|
@@ -141,6 +154,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
141
154
|
const containerHeight = el.clientHeight;
|
|
142
155
|
const logicalTotal = tree.total();
|
|
143
156
|
const { scale, physicalTotal } = getScale(logicalTotal);
|
|
157
|
+
const visibleHeight = Math.max(containerHeight - headerOffset, 0);
|
|
144
158
|
const pendingTarget = pendingTargetRef.current;
|
|
145
159
|
let logicalScrollOffset;
|
|
146
160
|
if (pendingTarget !== null) {
|
|
@@ -150,14 +164,15 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
150
164
|
pendingTarget.align,
|
|
151
165
|
containerHeight,
|
|
152
166
|
scale,
|
|
153
|
-
count
|
|
167
|
+
count,
|
|
168
|
+
headerOffset
|
|
154
169
|
);
|
|
155
170
|
logicalScrollOffset = targetLogicalOffset;
|
|
156
171
|
if (Math.abs(el.scrollTop - targetScrollTop) > SCROLL_CONVERGED_THRESHOLD) {
|
|
157
172
|
el.scrollTop = targetScrollTop;
|
|
158
173
|
}
|
|
159
174
|
} else {
|
|
160
|
-
logicalScrollOffset = physicalScrollOffset / scale;
|
|
175
|
+
logicalScrollOffset = (physicalScrollOffset + headerOffset) / scale;
|
|
161
176
|
}
|
|
162
177
|
let startIndex;
|
|
163
178
|
let endIndex;
|
|
@@ -168,7 +183,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
168
183
|
const { startIndex: viewportStartIndex, endIndex: viewportEndIndex } = getViewportRange(
|
|
169
184
|
tree,
|
|
170
185
|
logicalScrollOffset,
|
|
171
|
-
containerHeight,
|
|
186
|
+
pendingTarget !== null ? containerHeight : visibleHeight,
|
|
172
187
|
scale,
|
|
173
188
|
overscan,
|
|
174
189
|
count
|
|
@@ -177,7 +192,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
177
192
|
endIndex = viewportEndIndex;
|
|
178
193
|
}
|
|
179
194
|
const startOffsetLogical = tree.prefixSum(startIndex);
|
|
180
|
-
const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
|
|
195
|
+
const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset + headerOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
|
|
181
196
|
const virtualItems2 = [];
|
|
182
197
|
let offset = startOffsetPhysical;
|
|
183
198
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -187,7 +202,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
187
202
|
}
|
|
188
203
|
return { virtualItems: virtualItems2, scrollHeight: physicalTotal };
|
|
189
204
|
},
|
|
190
|
-
[count, forcedRange, overscan]
|
|
205
|
+
[count, forcedRange, overscan, headerOffset]
|
|
191
206
|
);
|
|
192
207
|
const observeItem = react.useCallback((index, element) => {
|
|
193
208
|
if (typeof ResizeObserver === "undefined")
|
|
@@ -278,7 +293,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
278
293
|
pending.align,
|
|
279
294
|
el.clientHeight,
|
|
280
295
|
scale,
|
|
281
|
-
countValue
|
|
296
|
+
countValue,
|
|
297
|
+
headerOffset
|
|
282
298
|
);
|
|
283
299
|
const kind = classifySettleScroll(
|
|
284
300
|
el.scrollTop,
|
|
@@ -309,8 +325,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
309
325
|
}
|
|
310
326
|
const { startIndex, endIndex } = getViewportRange(
|
|
311
327
|
tree,
|
|
312
|
-
el.scrollTop / scale,
|
|
313
|
-
el.clientHeight,
|
|
328
|
+
(el.scrollTop + headerOffset) / scale,
|
|
329
|
+
Math.max(el.clientHeight - headerOffset, 0),
|
|
314
330
|
scale,
|
|
315
331
|
overscanValue,
|
|
316
332
|
countValue
|
|
@@ -322,7 +338,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
322
338
|
}
|
|
323
339
|
releaseSettle(el.scrollTop);
|
|
324
340
|
});
|
|
325
|
-
}, [releaseSettle, countRef, overscanRef]);
|
|
341
|
+
}, [releaseSettle, countRef, overscanRef, headerOffset]);
|
|
326
342
|
const handleScroll = react.useCallback(() => {
|
|
327
343
|
if (rafRef.current !== null)
|
|
328
344
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -341,7 +357,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
341
357
|
pending.align,
|
|
342
358
|
el.clientHeight,
|
|
343
359
|
scale,
|
|
344
|
-
countRef.current
|
|
360
|
+
countRef.current,
|
|
361
|
+
headerOffset
|
|
345
362
|
);
|
|
346
363
|
const kind = classifySettleScroll(
|
|
347
364
|
el.scrollTop,
|
|
@@ -358,7 +375,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
358
375
|
}
|
|
359
376
|
setScrollOffset({ value: el.scrollTop });
|
|
360
377
|
});
|
|
361
|
-
}, [releaseSettle, countRef]);
|
|
378
|
+
}, [releaseSettle, countRef, headerOffset]);
|
|
362
379
|
const scrollRef = react.useCallback((element) => {
|
|
363
380
|
scrollElementRef.current = element;
|
|
364
381
|
setScrollElement(element);
|
package/lib/index.js
CHANGED
|
@@ -88,24 +88,26 @@ const getViewportRange = (tree, logicalScrollOffset, containerHeight, scale, ove
|
|
|
88
88
|
viewportEnd
|
|
89
89
|
};
|
|
90
90
|
};
|
|
91
|
-
const computeTargetScrollTop = (tree, index, align, containerHeight, scale, count) => {
|
|
91
|
+
const computeTargetScrollTop = (tree, index, align, containerHeight, scale, count, headerHeight = 0) => {
|
|
92
92
|
const targetIndex = Math.max(0, Math.min(index, count - 1));
|
|
93
93
|
const targetOffset = tree.prefixSum(targetIndex);
|
|
94
94
|
const itemLogicalSize = tree.prefixSum(targetIndex + 1) - targetOffset;
|
|
95
|
+
const visibleHeight = Math.max(containerHeight - headerHeight, 0);
|
|
95
96
|
let logicalScrollOffset;
|
|
96
97
|
switch (align) {
|
|
97
98
|
case "center":
|
|
98
|
-
logicalScrollOffset = targetOffset + (itemLogicalSize -
|
|
99
|
+
logicalScrollOffset = targetOffset + (itemLogicalSize - visibleHeight / scale) / 2;
|
|
99
100
|
break;
|
|
100
101
|
case "end":
|
|
101
|
-
logicalScrollOffset = targetOffset + itemLogicalSize -
|
|
102
|
+
logicalScrollOffset = targetOffset + itemLogicalSize - visibleHeight / scale;
|
|
102
103
|
break;
|
|
103
104
|
default:
|
|
104
105
|
logicalScrollOffset = targetOffset;
|
|
105
106
|
}
|
|
106
|
-
return { targetScrollTop: Math.max(0, logicalScrollOffset * scale), logicalScrollOffset };
|
|
107
|
+
return { targetScrollTop: Math.round(Math.max(0, logicalScrollOffset * scale - headerHeight)), logicalScrollOffset };
|
|
107
108
|
};
|
|
108
|
-
function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
109
|
+
function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN, headerHeight = 0 }) {
|
|
110
|
+
const headerOffset = Math.max(headerHeight, 0);
|
|
109
111
|
const [scrollOffset, setScrollOffset] = useState({ value: 0 });
|
|
110
112
|
const [forcedRange, setForcedRange] = useState(null);
|
|
111
113
|
const scrollElementRef = useRef(null);
|
|
@@ -127,7 +129,18 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
127
129
|
if (fenwickRef.current === null || !fenwickRef.current.total())
|
|
128
130
|
fenwickRef.current = getFenwickTree(count, estimateSize);
|
|
129
131
|
if (prevCountRef.current !== count) {
|
|
130
|
-
fenwickRef.current
|
|
132
|
+
const prevTree = fenwickRef.current;
|
|
133
|
+
const prevCount = prevCountRef.current;
|
|
134
|
+
const nextTree = getFenwickTree(count, estimateSize);
|
|
135
|
+
if (prevTree && prevCount > 0) {
|
|
136
|
+
const keep = Math.min(prevCount, count);
|
|
137
|
+
for (let i = 0; i < keep; i++) {
|
|
138
|
+
const delta = prevTree.prefixSum(i + 1) - prevTree.prefixSum(i) - (nextTree.prefixSum(i + 1) - nextTree.prefixSum(i));
|
|
139
|
+
if (delta !== 0)
|
|
140
|
+
nextTree.update(i, delta);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
fenwickRef.current = nextTree;
|
|
131
144
|
prevCountRef.current = count;
|
|
132
145
|
}
|
|
133
146
|
const computeItems = useCallback(
|
|
@@ -139,6 +152,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
139
152
|
const containerHeight = el.clientHeight;
|
|
140
153
|
const logicalTotal = tree.total();
|
|
141
154
|
const { scale, physicalTotal } = getScale(logicalTotal);
|
|
155
|
+
const visibleHeight = Math.max(containerHeight - headerOffset, 0);
|
|
142
156
|
const pendingTarget = pendingTargetRef.current;
|
|
143
157
|
let logicalScrollOffset;
|
|
144
158
|
if (pendingTarget !== null) {
|
|
@@ -148,14 +162,15 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
148
162
|
pendingTarget.align,
|
|
149
163
|
containerHeight,
|
|
150
164
|
scale,
|
|
151
|
-
count
|
|
165
|
+
count,
|
|
166
|
+
headerOffset
|
|
152
167
|
);
|
|
153
168
|
logicalScrollOffset = targetLogicalOffset;
|
|
154
169
|
if (Math.abs(el.scrollTop - targetScrollTop) > SCROLL_CONVERGED_THRESHOLD) {
|
|
155
170
|
el.scrollTop = targetScrollTop;
|
|
156
171
|
}
|
|
157
172
|
} else {
|
|
158
|
-
logicalScrollOffset = physicalScrollOffset / scale;
|
|
173
|
+
logicalScrollOffset = (physicalScrollOffset + headerOffset) / scale;
|
|
159
174
|
}
|
|
160
175
|
let startIndex;
|
|
161
176
|
let endIndex;
|
|
@@ -166,7 +181,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
166
181
|
const { startIndex: viewportStartIndex, endIndex: viewportEndIndex } = getViewportRange(
|
|
167
182
|
tree,
|
|
168
183
|
logicalScrollOffset,
|
|
169
|
-
containerHeight,
|
|
184
|
+
pendingTarget !== null ? containerHeight : visibleHeight,
|
|
170
185
|
scale,
|
|
171
186
|
overscan,
|
|
172
187
|
count
|
|
@@ -175,7 +190,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
175
190
|
endIndex = viewportEndIndex;
|
|
176
191
|
}
|
|
177
192
|
const startOffsetLogical = tree.prefixSum(startIndex);
|
|
178
|
-
const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
|
|
193
|
+
const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset + headerOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
|
|
179
194
|
const virtualItems2 = [];
|
|
180
195
|
let offset = startOffsetPhysical;
|
|
181
196
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -185,7 +200,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
185
200
|
}
|
|
186
201
|
return { virtualItems: virtualItems2, scrollHeight: physicalTotal };
|
|
187
202
|
},
|
|
188
|
-
[count, forcedRange, overscan]
|
|
203
|
+
[count, forcedRange, overscan, headerOffset]
|
|
189
204
|
);
|
|
190
205
|
const observeItem = useCallback((index, element) => {
|
|
191
206
|
if (typeof ResizeObserver === "undefined")
|
|
@@ -276,7 +291,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
276
291
|
pending.align,
|
|
277
292
|
el.clientHeight,
|
|
278
293
|
scale,
|
|
279
|
-
countValue
|
|
294
|
+
countValue,
|
|
295
|
+
headerOffset
|
|
280
296
|
);
|
|
281
297
|
const kind = classifySettleScroll(
|
|
282
298
|
el.scrollTop,
|
|
@@ -307,8 +323,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
307
323
|
}
|
|
308
324
|
const { startIndex, endIndex } = getViewportRange(
|
|
309
325
|
tree,
|
|
310
|
-
el.scrollTop / scale,
|
|
311
|
-
el.clientHeight,
|
|
326
|
+
(el.scrollTop + headerOffset) / scale,
|
|
327
|
+
Math.max(el.clientHeight - headerOffset, 0),
|
|
312
328
|
scale,
|
|
313
329
|
overscanValue,
|
|
314
330
|
countValue
|
|
@@ -320,7 +336,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
320
336
|
}
|
|
321
337
|
releaseSettle(el.scrollTop);
|
|
322
338
|
});
|
|
323
|
-
}, [releaseSettle, countRef, overscanRef]);
|
|
339
|
+
}, [releaseSettle, countRef, overscanRef, headerOffset]);
|
|
324
340
|
const handleScroll = useCallback(() => {
|
|
325
341
|
if (rafRef.current !== null)
|
|
326
342
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -339,7 +355,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
339
355
|
pending.align,
|
|
340
356
|
el.clientHeight,
|
|
341
357
|
scale,
|
|
342
|
-
countRef.current
|
|
358
|
+
countRef.current,
|
|
359
|
+
headerOffset
|
|
343
360
|
);
|
|
344
361
|
const kind = classifySettleScroll(
|
|
345
362
|
el.scrollTop,
|
|
@@ -356,7 +373,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
|
|
|
356
373
|
}
|
|
357
374
|
setScrollOffset({ value: el.scrollTop });
|
|
358
375
|
});
|
|
359
|
-
}, [releaseSettle, countRef]);
|
|
376
|
+
}, [releaseSettle, countRef, headerOffset]);
|
|
360
377
|
const scrollRef = useCallback((element) => {
|
|
361
378
|
scrollElementRef.current = element;
|
|
362
379
|
setScrollElement(element);
|
package/lib/utils.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const getViewportRange: (tree: FenwickTree, logicalScrollOffset:
|
|
|
12
12
|
viewportStart: number;
|
|
13
13
|
viewportEnd: number;
|
|
14
14
|
};
|
|
15
|
-
export declare const computeTargetScrollTop: (tree: FenwickTree, index: number, align: 'start' | 'center' | 'end', containerHeight: number, scale: number, count: number) => {
|
|
15
|
+
export declare const computeTargetScrollTop: (tree: FenwickTree, index: number, align: 'start' | 'center' | 'end', containerHeight: number, scale: number, count: number, headerHeight?: number) => {
|
|
16
16
|
targetScrollTop: number;
|
|
17
17
|
logicalScrollOffset: number;
|
|
18
18
|
};
|
package/package.json
CHANGED