clear-virtualizer 0.1.4 → 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 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 - containerHeight / scale) / 2;
101
+ logicalScrollOffset = targetOffset + (itemLogicalSize - visibleHeight / scale) / 2;
101
102
  break;
102
103
  case "end":
103
- logicalScrollOffset = targetOffset + itemLogicalSize - containerHeight / scale;
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);
@@ -152,6 +154,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
152
154
  const containerHeight = el.clientHeight;
153
155
  const logicalTotal = tree.total();
154
156
  const { scale, physicalTotal } = getScale(logicalTotal);
157
+ const visibleHeight = Math.max(containerHeight - headerOffset, 0);
155
158
  const pendingTarget = pendingTargetRef.current;
156
159
  let logicalScrollOffset;
157
160
  if (pendingTarget !== null) {
@@ -161,14 +164,15 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
161
164
  pendingTarget.align,
162
165
  containerHeight,
163
166
  scale,
164
- count
167
+ count,
168
+ headerOffset
165
169
  );
166
170
  logicalScrollOffset = targetLogicalOffset;
167
171
  if (Math.abs(el.scrollTop - targetScrollTop) > SCROLL_CONVERGED_THRESHOLD) {
168
172
  el.scrollTop = targetScrollTop;
169
173
  }
170
174
  } else {
171
- logicalScrollOffset = physicalScrollOffset / scale;
175
+ logicalScrollOffset = (physicalScrollOffset + headerOffset) / scale;
172
176
  }
173
177
  let startIndex;
174
178
  let endIndex;
@@ -179,7 +183,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
179
183
  const { startIndex: viewportStartIndex, endIndex: viewportEndIndex } = getViewportRange(
180
184
  tree,
181
185
  logicalScrollOffset,
182
- containerHeight,
186
+ pendingTarget !== null ? containerHeight : visibleHeight,
183
187
  scale,
184
188
  overscan,
185
189
  count
@@ -188,7 +192,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
188
192
  endIndex = viewportEndIndex;
189
193
  }
190
194
  const startOffsetLogical = tree.prefixSum(startIndex);
191
- const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
195
+ const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset + headerOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
192
196
  const virtualItems2 = [];
193
197
  let offset = startOffsetPhysical;
194
198
  for (let i = startIndex; i < endIndex; i++) {
@@ -198,7 +202,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
198
202
  }
199
203
  return { virtualItems: virtualItems2, scrollHeight: physicalTotal };
200
204
  },
201
- [count, forcedRange, overscan]
205
+ [count, forcedRange, overscan, headerOffset]
202
206
  );
203
207
  const observeItem = react.useCallback((index, element) => {
204
208
  if (typeof ResizeObserver === "undefined")
@@ -289,7 +293,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
289
293
  pending.align,
290
294
  el.clientHeight,
291
295
  scale,
292
- countValue
296
+ countValue,
297
+ headerOffset
293
298
  );
294
299
  const kind = classifySettleScroll(
295
300
  el.scrollTop,
@@ -320,8 +325,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
320
325
  }
321
326
  const { startIndex, endIndex } = getViewportRange(
322
327
  tree,
323
- el.scrollTop / scale,
324
- el.clientHeight,
328
+ (el.scrollTop + headerOffset) / scale,
329
+ Math.max(el.clientHeight - headerOffset, 0),
325
330
  scale,
326
331
  overscanValue,
327
332
  countValue
@@ -333,7 +338,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
333
338
  }
334
339
  releaseSettle(el.scrollTop);
335
340
  });
336
- }, [releaseSettle, countRef, overscanRef]);
341
+ }, [releaseSettle, countRef, overscanRef, headerOffset]);
337
342
  const handleScroll = react.useCallback(() => {
338
343
  if (rafRef.current !== null)
339
344
  cancelAnimationFrame(rafRef.current);
@@ -352,7 +357,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
352
357
  pending.align,
353
358
  el.clientHeight,
354
359
  scale,
355
- countRef.current
360
+ countRef.current,
361
+ headerOffset
356
362
  );
357
363
  const kind = classifySettleScroll(
358
364
  el.scrollTop,
@@ -369,7 +375,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
369
375
  }
370
376
  setScrollOffset({ value: el.scrollTop });
371
377
  });
372
- }, [releaseSettle, countRef]);
378
+ }, [releaseSettle, countRef, headerOffset]);
373
379
  const scrollRef = react.useCallback((element) => {
374
380
  scrollElementRef.current = element;
375
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 - containerHeight / scale) / 2;
99
+ logicalScrollOffset = targetOffset + (itemLogicalSize - visibleHeight / scale) / 2;
99
100
  break;
100
101
  case "end":
101
- logicalScrollOffset = targetOffset + itemLogicalSize - containerHeight / scale;
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);
@@ -150,6 +152,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
150
152
  const containerHeight = el.clientHeight;
151
153
  const logicalTotal = tree.total();
152
154
  const { scale, physicalTotal } = getScale(logicalTotal);
155
+ const visibleHeight = Math.max(containerHeight - headerOffset, 0);
153
156
  const pendingTarget = pendingTargetRef.current;
154
157
  let logicalScrollOffset;
155
158
  if (pendingTarget !== null) {
@@ -159,14 +162,15 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
159
162
  pendingTarget.align,
160
163
  containerHeight,
161
164
  scale,
162
- count
165
+ count,
166
+ headerOffset
163
167
  );
164
168
  logicalScrollOffset = targetLogicalOffset;
165
169
  if (Math.abs(el.scrollTop - targetScrollTop) > SCROLL_CONVERGED_THRESHOLD) {
166
170
  el.scrollTop = targetScrollTop;
167
171
  }
168
172
  } else {
169
- logicalScrollOffset = physicalScrollOffset / scale;
173
+ logicalScrollOffset = (physicalScrollOffset + headerOffset) / scale;
170
174
  }
171
175
  let startIndex;
172
176
  let endIndex;
@@ -177,7 +181,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
177
181
  const { startIndex: viewportStartIndex, endIndex: viewportEndIndex } = getViewportRange(
178
182
  tree,
179
183
  logicalScrollOffset,
180
- containerHeight,
184
+ pendingTarget !== null ? containerHeight : visibleHeight,
181
185
  scale,
182
186
  overscan,
183
187
  count
@@ -186,7 +190,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
186
190
  endIndex = viewportEndIndex;
187
191
  }
188
192
  const startOffsetLogical = tree.prefixSum(startIndex);
189
- const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
193
+ const startOffsetPhysical = (pendingTarget !== null ? logicalScrollOffset * scale : physicalScrollOffset + headerOffset) + (startOffsetLogical - logicalScrollOffset) * scale;
190
194
  const virtualItems2 = [];
191
195
  let offset = startOffsetPhysical;
192
196
  for (let i = startIndex; i < endIndex; i++) {
@@ -196,7 +200,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
196
200
  }
197
201
  return { virtualItems: virtualItems2, scrollHeight: physicalTotal };
198
202
  },
199
- [count, forcedRange, overscan]
203
+ [count, forcedRange, overscan, headerOffset]
200
204
  );
201
205
  const observeItem = useCallback((index, element) => {
202
206
  if (typeof ResizeObserver === "undefined")
@@ -287,7 +291,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
287
291
  pending.align,
288
292
  el.clientHeight,
289
293
  scale,
290
- countValue
294
+ countValue,
295
+ headerOffset
291
296
  );
292
297
  const kind = classifySettleScroll(
293
298
  el.scrollTop,
@@ -318,8 +323,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
318
323
  }
319
324
  const { startIndex, endIndex } = getViewportRange(
320
325
  tree,
321
- el.scrollTop / scale,
322
- el.clientHeight,
326
+ (el.scrollTop + headerOffset) / scale,
327
+ Math.max(el.clientHeight - headerOffset, 0),
323
328
  scale,
324
329
  overscanValue,
325
330
  countValue
@@ -331,7 +336,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
331
336
  }
332
337
  releaseSettle(el.scrollTop);
333
338
  });
334
- }, [releaseSettle, countRef, overscanRef]);
339
+ }, [releaseSettle, countRef, overscanRef, headerOffset]);
335
340
  const handleScroll = useCallback(() => {
336
341
  if (rafRef.current !== null)
337
342
  cancelAnimationFrame(rafRef.current);
@@ -350,7 +355,8 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
350
355
  pending.align,
351
356
  el.clientHeight,
352
357
  scale,
353
- countRef.current
358
+ countRef.current,
359
+ headerOffset
354
360
  );
355
361
  const kind = classifySettleScroll(
356
362
  el.scrollTop,
@@ -367,7 +373,7 @@ function useVirtualizer({ count, estimateSize, overscan = DEFAULT_OVERSCAN }) {
367
373
  }
368
374
  setScrollOffset({ value: el.scrollTop });
369
375
  });
370
- }, [releaseSettle, countRef]);
376
+ }, [releaseSettle, countRef, headerOffset]);
371
377
  const scrollRef = useCallback((element) => {
372
378
  scrollElementRef.current = element;
373
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-virtualizer",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A lightweight React hook for virtualized lists and tables with dynamic row heights, measured automatically",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",