Skip to content

Commit 879a95e

Browse files
committed
Relax List rowHeight constraint
1 parent 4f2a0f2 commit 879a95e

27 files changed

+1071
-122
lines changed

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 ?? 0;
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 ?? 0;
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,20 @@ describe("List", () => {
538538

539539
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(4);
540540
});
541+
542+
// Most dynamic size tests are in useVirtualizer
543+
test("type: dynamic (lazily measured)", () => {
544+
render(
545+
<List
546+
overscanCount={0}
547+
rowCount={50}
548+
rowComponent={RowComponent}
549+
rowProps={EMPTY_OBJECT}
550+
/>
551+
);
552+
553+
// TODO
554+
});
541555
});
542556

543557
describe("edge cases", () => {

lib/components/list/List.tsx

Lines changed: 30 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 ?? 0)
122+
: 0;
123+
116124
const rows = useMemo(() => {
117125
const children: ReactNode[] = [];
118126
if (rowCount > 0) {
@@ -121,7 +129,23 @@ export function List<
121129
index <= stopIndexOverscan;
122130
index++
123131
) {
124-
const bounds = getCellBounds(index);
132+
const bounds = cachedBounds.getItemBounds(index);
133+
134+
let style: CSSProperties = {};
135+
if (bounds) {
136+
style = {
137+
height: hasRowHeight ? bounds.size : undefined,
138+
width: "100%"
139+
};
140+
} else {
141+
style = {
142+
width: "100%"
143+
};
144+
}
145+
146+
if (index === startIndexOverscan) {
147+
style.marginTop = `${offset}px`;
148+
}
125149

126150
children.push(
127151
<RowComponent
@@ -133,21 +157,17 @@ export function List<
133157
}}
134158
key={index}
135159
index={index}
136-
style={{
137-
position: "absolute",
138-
left: 0,
139-
transform: `translateY(${bounds.scrollOffset}px)`,
140-
height: bounds.size,
141-
width: "100%"
142-
}}
160+
style={style}
143161
/>
144162
);
145163
}
146164
}
147165
return children;
148166
}, [
149167
RowComponent,
150-
getCellBounds,
168+
cachedBounds,
169+
hasRowHeight,
170+
offset,
151171
rowCount,
152172
rowProps,
153173
startIndexOverscan,

lib/components/list/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,15 @@ export type ListProps<
9595
* - number of pixels (number)
9696
* - percentage of the grid's current height (string)
9797
* - function that returns the row height (in pixels) given an index and `cellProps`
98+
*
99+
* If this property is not provided, a ResizeObserver will be used to measure the height of a row's content.
100+
*
101+
* ⚠️ Row height should be provided if it is known ahead of time as that is the most efficient way to render the list.
98102
*/
99-
rowHeight: number | string | ((index: number, cellProps: RowProps) => number);
103+
rowHeight?:
104+
| number
105+
| string
106+
| ((index: number, cellProps: RowProps) => number);
100107

101108
/**
102109
* Additional props to be passed to the row-rendering component.

lib/core/createCachedBounds.test.ts

Lines changed: 29 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,31 @@ 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
});
69+
70+
test("should gracefully handle undefined item sizes", () => {
71+
const cachedBounds = createCachedBounds({
72+
itemCount: 10,
73+
itemProps: {},
74+
itemSize: undefined
75+
});
76+
77+
cachedBounds.setItemSize(0, 10);
78+
79+
expect(cachedBounds.size).toBe(1);
80+
expect(cachedBounds.getItemBounds(0)).toEqual({
81+
scrollOffset: 0,
82+
size: 10
83+
});
84+
85+
cachedBounds.setItemSize(1, 20);
86+
87+
expect(cachedBounds.size).toBe(2);
88+
expect(cachedBounds.getItemBounds(1)).toEqual({
89+
scrollOffset: 10,
90+
size: 20
91+
});
92+
});
6993
});

0 commit comments

Comments
 (0)