Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions src/core/cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const initCacheWithSizesAndEmptyOffsets = (
sizes: readonly number[],
defaultSize: number
): Cache => {
return initCache(sizes.length, defaultSize, sizes);
return initCache(sizes.length, defaultSize, 0, sizes);
};

const initCacheWithSizesAndOffsets = (
Expand All @@ -65,7 +65,7 @@ const initCacheWithSizesAndOffsets = (
if (sizes.length + 1 !== offsets.length) {
throw new Error("wrong offsets for sizes");
}
const cache = initCache(sizes.length, defaultSize, sizes);
const cache = initCache(sizes.length, defaultSize, 0, sizes);
return {
...cache,
_computedOffsetIndex: findComputedOffsetIndex(offsets),
Expand Down Expand Up @@ -314,7 +314,7 @@ describe("getTotalSize", () => {
const filledSizes = range(10, () => 20);
const offsets = [0, 11, 22, 33, -1, -1, -1, -1, -1, -1, -1];
const cache: Cache = {
...initCache(filledSizes.length, 30, filledSizes),
...initCache(filledSizes.length, 30, 0, filledSizes),
_computedOffsetIndex: 2,
_offsets: offsets,
};
Expand All @@ -328,7 +328,7 @@ describe("getTotalSize", () => {
const filledSizes = range(10, () => 20);
const offsets = [0, 11, 22, 33, 44, 55, 66, 77, 88, 99, -1];
const cache: Cache = {
...initCache(filledSizes.length, 30, filledSizes),
...initCache(filledSizes.length, 30, 0, filledSizes),
_computedOffsetIndex: 9,
_offsets: offsets,
};
Expand Down Expand Up @@ -816,11 +816,12 @@ describe(estimateDefaultItemSize.name, () => {
describe(initCache.name, () => {
it("should create cache", () => {
const itemLength = 10;
const cache = initCache(itemLength, 23);
const cache = initCache(itemLength, 23, 0);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 23,
"_gap": 0,
"_length": 10,
"_offsets": [
-1,
Expand Down Expand Up @@ -856,11 +857,12 @@ describe(initCache.name, () => {

it("should restore cache from snapshot", () => {
const itemLength = 10;
const cache = initCache(itemLength, 123, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
const cache = initCache(itemLength, 123, 0, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 123,
"_gap": 0,
"_length": 10,
"_offsets": [
-1,
Expand Down Expand Up @@ -896,11 +898,12 @@ describe(initCache.name, () => {

it("should restore cache from snapshot which has shorter length", () => {
const itemLength = 10;
const cache = initCache(itemLength, 123, [0, 1, 2, 3, 4]);
const cache = initCache(itemLength, 123, 0, [0, 1, 2, 3, 4]);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 123,
"_gap": 0,
"_length": 10,
"_offsets": [
-1,
Expand Down Expand Up @@ -939,12 +942,14 @@ describe(initCache.name, () => {
const cache = initCache(
itemLength,
123,
0,
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 123,
"_gap": 0,
"_length": 10,
"_offsets": [
-1,
Expand Down Expand Up @@ -1015,22 +1020,23 @@ describe(takeCacheSnapshot.name, () => {

describe(updateCacheLength.name, () => {
it("should recover cache length from 0", () => {
const cache = initCache(10, 40);
const cache = initCache(10, 40, 0);
const initialCache = structuredClone(cache);
updateCacheLength(cache, 0);
updateCacheLength(cache, 10);
expect(cache).toEqual(initialCache);
});

it("should increase cache length", () => {
const cache = initCache(10, 40);
const cache = initCache(10, 40, 0);
const cloned = structuredClone(cache);
const res = updateCacheLength(cache, 15, undefined);
expect(res).toEqual(40 * 5);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 15,
"_offsets": [
-1,
Expand Down Expand Up @@ -1082,6 +1088,7 @@ describe(updateCacheLength.name, () => {
{
"_computedOffsetIndex": 10,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 15,
"_offsets": [
0,
Expand Down Expand Up @@ -1124,14 +1131,15 @@ describe(updateCacheLength.name, () => {
});

it("should decrease cache length", () => {
const cache = initCache(10, 40);
const cache = initCache(10, 40, 0);
const cloned = structuredClone(cache);
const res = updateCacheLength(cache, 5, undefined);
expect(res).toEqual(-(40 * 5));
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 5,
"_offsets": [
-1,
Expand Down Expand Up @@ -1163,6 +1171,7 @@ describe(updateCacheLength.name, () => {
{
"_computedOffsetIndex": 4,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 5,
"_offsets": [
0,
Expand All @@ -1185,14 +1194,15 @@ describe(updateCacheLength.name, () => {
});

it("should increase cache length with shifting", () => {
const cache = initCache(10, 40);
const cache = initCache(10, 40, 0);
const cloned = structuredClone(cache);
const res = updateCacheLength(cache, 15, true);
expect(res).toEqual(40 * 5);
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 15,
"_offsets": [
-1,
Expand Down Expand Up @@ -1244,6 +1254,7 @@ describe(updateCacheLength.name, () => {
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 15,
"_offsets": [
0,
Expand Down Expand Up @@ -1286,14 +1297,15 @@ describe(updateCacheLength.name, () => {
});

it("should decrease cache length with shifting", () => {
const cache = initCache(10, 40);
const cache = initCache(10, 40, 0);
const cloned = structuredClone(cache);
const res = updateCacheLength(cache, 5, true);
expect(res).toEqual(-(40 * 5));
expect(cache).toMatchInlineSnapshot(`
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 5,
"_offsets": [
-1,
Expand Down Expand Up @@ -1325,6 +1337,7 @@ describe(updateCacheLength.name, () => {
{
"_computedOffsetIndex": -1,
"_defaultItemSize": 40,
"_gap": 0,
"_length": 5,
"_offsets": [
0,
Expand Down
5 changes: 4 additions & 1 deletion src/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Cache = {
// offsets
readonly _computedOffsetIndex: number;
readonly _offsets: number[];
readonly _gap: number;
};

const fill = (array: number[], length: number, prepend?: boolean): number[] => {
Expand Down Expand Up @@ -73,7 +74,7 @@ export const getItemOffset = (
let i = cache._computedOffsetIndex;
let top = cache._offsets[i]!;
while (i < index) {
top += getItemSize(cache, i);
top += getItemSize(cache, i) + cache._gap;
cache._offsets[++i] = top;
}
// mark as measured
Expand Down Expand Up @@ -175,6 +176,7 @@ export const estimateDefaultItemSize = (
export const initCache = (
length: number,
itemSize: number,
gap: number,
sizes?: readonly number[]
): Cache => {
return {
Expand All @@ -189,6 +191,7 @@ export const initCache = (
_length: length,
_computedOffsetIndex: -1,
_offsets: fill([], length + 1),
_gap: gap,
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type VirtualStore = {
export const createVirtualStore = (
elementsCount: number,
itemSize: number = 40,
gap: number = 0,
ssrCount: number = 0,
cacheSnapshot?: CacheSnapshot | undefined,
shouldAutoEstimateItemSize: boolean = false
Expand All @@ -142,6 +143,7 @@ export const createVirtualStore = (
cacheSnapshot
? (cacheSnapshot as unknown as InternalCacheSnapshot)[1]
: itemSize,
gap,
cacheSnapshot && (cacheSnapshot as unknown as InternalCacheSnapshot)[0]
);
const subscribers = new Set<[number, Subscriber]>();
Expand Down
14 changes: 12 additions & 2 deletions src/react/VGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,18 @@
ref
): ReactElement => {
const [rowStore, colStore, resizer, scroller] = useStatic(() => {
const _rowStore = createVirtualStore(rowCount, cellHeight, ssrRowCount);
const _colStore = createVirtualStore(colCount, cellWidth, ssrColCount);
const _rowStore = createVirtualStore(
rowCount,
cellHeight,
undefined, // TODO
ssrRowCount
);
const _colStore = createVirtualStore(
colCount,
cellWidth,
undefined, // TODO
ssrColCount
);
return [
_rowStore,
_colStore,
Expand Down Expand Up @@ -397,7 +407,7 @@
scrollBy: scroller.$scrollBy,
};
},
[]

Check warning on line 410 in src/react/VGrid.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useImperativeHandle has missing dependencies: 'colStore', 'resizer', 'rowStore', 'scroller.$scrollBy', 'scroller.$scrollTo', and 'scroller.$scrollToIndex'. Either include them or remove the dependency array
);

const render = useMemo(() => {
Expand Down
11 changes: 11 additions & 0 deletions src/react/VList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ it("should render with render prop", async () => {
expect(asFragment()).toMatchSnapshot();
});

it("should render with gap", async () => {
const { asFragment } = await render(
<VList gap={10}>
{Array.from({ length: 5 }).map((_, i) => (
<div key={i}>{i}</div>
))}
</VList>
);
expect(asFragment()).toMatchSnapshot();
});

it("should render with keepMounted", async () => {
const { asFragment } = await render(
<VList keepMounted={[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]}>
Expand Down
3 changes: 3 additions & 0 deletions src/react/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface VListProps<T = unknown>
| "data"
| "bufferSize"
| "itemSize"
| "gap"
| "shift"
| "horizontal"
| "cache"
Expand All @@ -47,6 +48,7 @@ export const VList = forwardRef<VListHandle, VListProps>(
data,
bufferSize,
itemSize,
gap,
shift,
horizontal,
keepMounted,
Expand All @@ -71,6 +73,7 @@ export const VList = forwardRef<VListHandle, VListProps>(
data={data}
bufferSize={bufferSize}
itemSize={itemSize}
gap={gap}
shift={shift}
horizontal={horizontal}
keepMounted={keepMounted}
Expand Down
7 changes: 7 additions & 0 deletions src/react/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
* - If set, you can opt out estimation and use the value as initial item size.
*/
itemSize?: number;
/**
* The spacing between items in pixels.
* @defaultValue 0
*/
gap?: number;
/**
* While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling.
*/
Expand Down Expand Up @@ -176,6 +181,7 @@
data,
bufferSize,
itemSize,
gap,
shift,
horizontal: horizontalProp,
keepMounted,
Expand Down Expand Up @@ -206,6 +212,7 @@
const _store = createVirtualStore(
count,
itemSize,
gap,
ssrCount,
cache,
!itemSize
Expand Down Expand Up @@ -323,7 +330,7 @@
scrollBy: scroller.$scrollBy,
};
},
[]

Check warning on line 333 in src/react/Virtualizer.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useImperativeHandle has missing dependencies: 'scroller.$scrollBy', 'scroller.$scrollTo', 'scroller.$scrollToIndex', and 'store'. Either include them or remove the dependency array
);

if (keepMounted) {
Expand Down
7 changes: 7 additions & 0 deletions src/react/WindowVirtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
* - If set, you can opt out estimation and use the value as initial item size.
*/
itemSize?: number;
/**
* The spacing between items in pixels.
* @defaultValue 0
*/
gap?: number;
/**
* While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling.
*/
Expand Down Expand Up @@ -147,6 +152,7 @@
data,
bufferSize,
itemSize,
gap,
shift,
horizontal: horizontalProp,
cache,
Expand Down Expand Up @@ -174,6 +180,7 @@
const _store = createVirtualStore(
count,
itemSize,
gap,
ssrCount,
cache,
!itemSize
Expand Down Expand Up @@ -254,7 +261,7 @@
scrollToIndex: scroller.$scrollToIndex,
};
},
[]

Check warning on line 264 in src/react/WindowVirtualizer.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useImperativeHandle has missing dependencies: 'scroller.$scrollToIndex' and 'store'. Either include them or remove the dependency array
);

for (let [i, j] = store.$getRange(bufferSize); i <= j; i++) {
Expand Down
Loading
Loading