Skip to content

Commit 0c490be

Browse files
committed
Merge parts of dynamic size branch
1 parent 0b871d6 commit 0c490be

16 files changed

+343
-230
lines changed

lib/components/grid/Grid.test.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
44
import { EMPTY_OBJECT } from "../../../src/constants";
55
import {
66
disableForCurrentTest,
7-
updateMockResizeObserver
7+
setDefaultElementSize
88
} from "../../utils/test/mockResizeObserver";
99
import { Grid } from "./Grid";
1010
import type { CellComponentProps, GridImperativeAPI } from "./types";
@@ -35,7 +35,7 @@ describe("Grid", () => {
3535
beforeEach(() => {
3636
CellComponent.mockReset();
3737

38-
updateMockResizeObserver(new DOMRect(0, 0, 100, 40));
38+
setDefaultElementSize({ height: 40, width: 100 });
3939

4040
mountedCells = new Map();
4141
});
@@ -272,7 +272,6 @@ describe("Grid", () => {
272272

273273
const items = screen.queryAllByRole("gridcell");
274274
expect(items).toHaveLength(8);
275-
// TODO
276275
});
277276

278277
test("should call onCellsRendered", () => {

lib/components/grid/Grid.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function Grid<
4848
const isRtl = useIsRtl(element, dir);
4949

5050
const {
51-
getCellBounds: getColumnBounds,
51+
cachedBounds: cachedColumnBounds,
5252
getEstimatedSize: getEstimatedWidth,
5353
startIndexOverscan: columnStartIndexOverscan,
5454
startIndexVisible: columnStartIndexVisible,
@@ -68,7 +68,7 @@ export function Grid<
6868
});
6969

7070
const {
71-
getCellBounds: getRowBounds,
71+
cachedBounds: cachedRowBounds,
7272
getEstimatedSize: getEstimatedHeight,
7373
startIndexOverscan: rowStartIndexOverscan,
7474
startIndexVisible: rowStartIndexVisible,
@@ -218,7 +218,8 @@ export function Grid<
218218
rowIndex <= rowStopIndexOverscan;
219219
rowIndex++
220220
) {
221-
const rowBounds = getRowBounds(rowIndex);
221+
const rowBounds = cachedRowBounds.getItemBounds(rowIndex);
222+
const rowOffset = rowBounds?.scrollOffset;
222223

223224
const columns: ReactNode[] = [];
224225

@@ -227,7 +228,8 @@ export function Grid<
227228
columnIndex <= columnStopIndexOverscan;
228229
columnIndex++
229230
) {
230-
const columnBounds = getColumnBounds(columnIndex);
231+
const columnBounds = cachedColumnBounds.getItemBounds(columnIndex);
232+
const columnOffset = columnBounds?.scrollOffset;
231233

232234
columns.push(
233235
<CellComponent
@@ -243,9 +245,9 @@ export function Grid<
243245
position: "absolute",
244246
left: isRtl ? undefined : 0,
245247
right: isRtl ? 0 : undefined,
246-
transform: `translate(${isRtl ? -columnBounds.scrollOffset : columnBounds.scrollOffset}px, ${rowBounds.scrollOffset}px)`,
247-
height: rowBounds.size,
248-
width: columnBounds.size
248+
transform: `translate(${isRtl ? -columnOffset : columnOffset}px, ${rowOffset}px)`,
249+
height: rowBounds?.size,
250+
width: columnBounds?.size
249251
}}
250252
/>
251253
);
@@ -260,13 +262,13 @@ export function Grid<
260262
}
261263
return children;
262264
}, [
265+
cachedColumnBounds,
266+
cachedRowBounds,
263267
CellComponent,
264268
cellProps,
265269
columnCount,
266270
columnStartIndexOverscan,
267271
columnStopIndexOverscan,
268-
getColumnBounds,
269-
getRowBounds,
270272
isRtl,
271273
rowCount,
272274
rowStartIndexOverscan,

lib/components/list/List.test.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { act, render, screen } from "@testing-library/react";
2+
import { assert } from "../../utils/assert";
23
import { createRef, useLayoutEffect } from "react";
34
import { beforeEach, describe, expect, test, vi } from "vitest";
45
import { EMPTY_OBJECT } from "../../../src/constants";
56
import {
67
disableForCurrentTest,
7-
updateMockResizeObserver
8+
setDefaultElementSize,
9+
setElementSize
810
} from "../../utils/test/mockResizeObserver";
911
import { List } from "./List";
1012
import { type ListImperativeAPI, type RowComponentProps } from "./types";
@@ -33,7 +35,7 @@ describe("List", () => {
3335
beforeEach(() => {
3436
RowComponent.mockReset();
3537

36-
updateMockResizeObserver(new DOMRect(0, 0, 50, 100));
38+
setDefaultElementSize({ height: 100, width: 50 });
3739

3840
mountedRows = new Map();
3941
});
@@ -55,7 +57,7 @@ describe("List", () => {
5557
test("should render enough rows to fill the available height", () => {
5658
const onResize = vi.fn();
5759

58-
render(
60+
const { container } = render(
5961
<List
6062
onResize={onResize}
6163
overscanCount={0}
@@ -84,7 +86,14 @@ describe("List", () => {
8486
);
8587

8688
act(() => {
87-
updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
89+
const listElement = container.querySelector<HTMLElement>('[role="list"]');
90+
assert(listElement !== null);
91+
92+
setElementSize({
93+
element: listElement,
94+
height: 75,
95+
width: 50
96+
});
8897
});
8998

9099
items = screen.queryAllByRole("listitem");
@@ -106,7 +115,7 @@ describe("List", () => {
106115
});
107116

108117
test("should render enough rows to fill the available height with overscan", () => {
109-
render(
118+
const { container } = render(
110119
<List
111120
overscanCount={2}
112121
rowCount={100}
@@ -122,7 +131,14 @@ describe("List", () => {
122131
expect(items[5]).toHaveTextContent("Row 5");
123132

124133
act(() => {
125-
updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
134+
const listElement = container.querySelector<HTMLElement>('[role="list"]');
135+
assert(listElement !== null);
136+
137+
setElementSize({
138+
element: listElement,
139+
height: 75,
140+
width: 50
141+
});
126142
});
127143

128144
items = screen.queryAllByRole("listitem");

lib/components/list/List.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useImperativeHandle,
66
useMemo,
77
useState,
8+
type CSSProperties,
89
type ReactNode
910
} from "react";
1011
import { useVirtualizer } from "../../core/useVirtualizer";
@@ -41,7 +42,7 @@ export function List<
4142
const [element, setElement] = useState<HTMLDivElement | null>(null);
4243

4344
const {
44-
getCellBounds,
45+
cachedBounds,
4546
getEstimatedSize,
4647
scrollToIndex,
4748
startIndexOverscan,
@@ -113,6 +114,13 @@ export function List<
113114
stopIndexVisible
114115
]);
115116

117+
const hasRowHeight = rowHeight !== undefined;
118+
119+
const offset =
120+
startIndexOverscan >= 0
121+
? cachedBounds.getItemBounds(startIndexOverscan).scrollOffset
122+
: 0;
123+
116124
const rows = useMemo(() => {
117125
const children: ReactNode[] = [];
118126
if (rowCount > 0) {
@@ -121,7 +129,15 @@ export function List<
121129
index <= stopIndexOverscan;
122130
index++
123131
) {
124-
const bounds = getCellBounds(index);
132+
const bounds = cachedBounds.getItemBounds(index);
133+
const style: CSSProperties = {
134+
height: hasRowHeight ? bounds.size : undefined,
135+
width: "100%"
136+
};
137+
138+
if (index === startIndexOverscan) {
139+
style.marginTop = `${offset}px`;
140+
}
125141

126142
children.push(
127143
<RowComponent
@@ -133,21 +149,17 @@ export function List<
133149
}}
134150
key={index}
135151
index={index}
136-
style={{
137-
position: "absolute",
138-
left: 0,
139-
transform: `translateY(${bounds.scrollOffset}px)`,
140-
height: bounds.size,
141-
width: "100%"
142-
}}
152+
style={style}
143153
/>
144154
);
145155
}
146156
}
147157
return children;
148158
}, [
149159
RowComponent,
150-
getCellBounds,
160+
cachedBounds,
161+
hasRowHeight,
162+
offset,
151163
rowCount,
152164
rowProps,
153165
startIndexOverscan,

lib/core/createCachedBounds.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ describe("createCachedBounds", () => {
1313
expect(itemSize).not.toHaveBeenCalled();
1414
expect(cachedBounds.size).toBe(0);
1515

16-
expect(cachedBounds.get(2)).toEqual({
16+
expect(cachedBounds.getItemBounds(2)).toEqual({
1717
scrollOffset: 21,
1818
size: 12
1919
});
2020
expect(itemSize).toHaveBeenCalledTimes(3);
2121
expect(cachedBounds.size).toBe(3);
2222

23-
expect(cachedBounds.get(3)).toEqual({
23+
expect(cachedBounds.getItemBounds(3)).toEqual({
2424
scrollOffset: 33,
2525
size: 13
2626
});
@@ -40,13 +40,13 @@ describe("createCachedBounds", () => {
4040
expect(itemSize).not.toHaveBeenCalled();
4141
expect(cachedBounds.size).toBe(0);
4242

43-
cachedBounds.get(9);
43+
cachedBounds.getItemBounds(9);
4444

4545
expect(itemSize).toHaveBeenCalledTimes(10);
4646
expect(cachedBounds.size).toBe(10);
4747

4848
for (let index = 0; index < 10; index++) {
49-
cachedBounds.get(index);
49+
cachedBounds.getItemBounds(index);
5050
}
5151

5252
expect(itemSize).toHaveBeenCalledTimes(10);
@@ -63,7 +63,7 @@ describe("createCachedBounds", () => {
6363
expect(cachedBounds.size).toBe(0);
6464

6565
expect(() => {
66-
cachedBounds.get(1);
66+
cachedBounds.getItemBounds(1);
6767
}).toThrow("Invalid index 1");
6868
});
6969
});

lib/core/createCachedBounds.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,23 @@ export function createCachedBounds<Props extends object>({
1212
}): CachedBounds {
1313
const cache = new Map<number, Bounds>();
1414

15-
return {
16-
get(index: number) {
15+
const api = {
16+
getEstimatedSize() {
17+
const lastBounds = cache.get(cache.size - 1);
18+
if (lastBounds) {
19+
return (lastBounds.scrollOffset + lastBounds.size) / cache.size;
20+
} else {
21+
const firstBounds = api.getItemBounds(0);
22+
return firstBounds.size * itemCount;
23+
}
24+
},
25+
getItemBounds(index: number) {
1726
assert(index < itemCount, `Invalid index ${index}`);
1827

1928
while (cache.size - 1 < index) {
2029
const currentIndex = cache.size;
2130

22-
let size: number;
31+
let size: number = 0;
2332
switch (typeof itemSize) {
2433
case "function": {
2534
size = itemSize(currentIndex, itemProps);
@@ -33,8 +42,8 @@ export function createCachedBounds<Props extends object>({
3342

3443
if (currentIndex === 0) {
3544
cache.set(currentIndex, {
36-
size,
37-
scrollOffset: 0
45+
scrollOffset: 0,
46+
size
3847
});
3948
} else {
4049
const previousRowBounds = cache.get(currentIndex - 1);
@@ -59,11 +68,13 @@ export function createCachedBounds<Props extends object>({
5968

6069
return bounds;
6170
},
62-
set(index: number, bounds: Bounds) {
63-
cache.set(index, bounds);
71+
hasItemBounds(index: number) {
72+
return cache.has(index);
6473
},
6574
get size() {
6675
return cache.size;
6776
}
6877
};
78+
79+
return api;
6980
}

lib/core/getEstimatedSize.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("getEstimatedSize", () => {
4141
itemProps: EMPTY_OBJECT,
4242
itemSize
4343
});
44-
cachedBounds.get(4);
44+
cachedBounds.getItemBounds(4);
4545

4646
expect(
4747
getEstimatedSize({
@@ -59,7 +59,7 @@ describe("getEstimatedSize", () => {
5959
itemSize
6060
});
6161

62-
cachedBounds.get(9);
62+
cachedBounds.getItemBounds(9);
6363

6464
expect(
6565
getEstimatedSize({

lib/core/getEstimatedSize.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,22 @@ export function getEstimatedSize<Props extends object>({
1212
}) {
1313
if (itemCount === 0) {
1414
return 0;
15-
} else if (typeof itemSize === "number") {
16-
return itemCount * itemSize;
1715
} else {
18-
const bounds = cachedBounds.get(
19-
cachedBounds.size === 0 ? 0 : cachedBounds.size - 1
20-
);
21-
assert(bounds !== undefined, "Unexpected bounds cache miss");
16+
switch (typeof itemSize) {
17+
case "function": {
18+
const bounds = cachedBounds.getItemBounds(
19+
cachedBounds.size === 0 ? 0 : cachedBounds.size - 1
20+
);
21+
assert(bounds !== undefined, "Unexpected bounds cache miss");
2222

23-
const averageItemSize =
24-
(bounds.scrollOffset + bounds.size) / cachedBounds.size;
23+
const averageItemSize =
24+
(bounds.scrollOffset + bounds.size) / cachedBounds.size;
2525

26-
return itemCount * averageItemSize;
26+
return itemCount * averageItemSize;
27+
}
28+
case "number": {
29+
return itemCount * itemSize;
30+
}
31+
}
2732
}
2833
}

0 commit comments

Comments
 (0)