Skip to content
Closed
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
7 changes: 3 additions & 4 deletions lib/components/grid/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
import { EMPTY_OBJECT } from "../../../src/constants";
import {
disableResizeObserverForCurrentTest,
simulateUnsupportedEnvironmentForTest,
updateMockResizeObserver
setDefaultElementSize,
simulateUnsupportedEnvironmentForTest
} from "../../utils/test/mockResizeObserver";
import { Grid } from "./Grid";
import type { CellComponentProps, GridImperativeAPI } from "./types";
Expand Down Expand Up @@ -36,7 +36,7 @@ describe("Grid", () => {
beforeEach(() => {
CellComponent.mockReset();

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

mountedCells = new Map();
});
Expand Down Expand Up @@ -273,7 +273,6 @@ describe("Grid", () => {

const items = screen.queryAllByRole("gridcell");
expect(items).toHaveLength(8);
// TODO
});

test("should call onCellsRendered", () => {
Expand Down
20 changes: 11 additions & 9 deletions lib/components/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function Grid<
const isRtl = useIsRtl(element, dir);

const {
getCellBounds: getColumnBounds,
cachedBounds: cachedColumnBounds,
getEstimatedSize: getEstimatedWidth,
startIndexOverscan: columnStartIndexOverscan,
startIndexVisible: columnStartIndexVisible,
Expand All @@ -69,7 +69,7 @@ export function Grid<
});

const {
getCellBounds: getRowBounds,
cachedBounds: cachedRowBounds,
getEstimatedSize: getEstimatedHeight,
startIndexOverscan: rowStartIndexOverscan,
startIndexVisible: rowStartIndexVisible,
Expand Down Expand Up @@ -220,7 +220,8 @@ export function Grid<
rowIndex <= rowStopIndexOverscan;
rowIndex++
) {
const rowBounds = getRowBounds(rowIndex);
const rowBounds = cachedRowBounds.getItemBounds(rowIndex);
const rowOffset = rowBounds?.scrollOffset;

const columns: ReactNode[] = [];

Expand All @@ -229,7 +230,8 @@ export function Grid<
columnIndex <= columnStopIndexOverscan;
columnIndex++
) {
const columnBounds = getColumnBounds(columnIndex);
const columnBounds = cachedColumnBounds.getItemBounds(columnIndex);
const columnOffset = columnBounds?.scrollOffset;

columns.push(
<CellComponent
Expand All @@ -245,9 +247,9 @@ export function Grid<
position: "absolute",
left: isRtl ? undefined : 0,
right: isRtl ? 0 : undefined,
transform: `translate(${isRtl ? -columnBounds.scrollOffset : columnBounds.scrollOffset}px, ${rowBounds.scrollOffset}px)`,
height: rowBounds.size,
width: columnBounds.size
transform: `translate(${isRtl ? -columnOffset : columnOffset}px, ${rowOffset}px)`,
height: rowBounds?.size,
width: columnBounds?.size
}}
/>
);
Expand All @@ -262,13 +264,13 @@ export function Grid<
}
return children;
}, [
cachedColumnBounds,
cachedRowBounds,
CellComponent,
cellProps,
columnCount,
columnStartIndexOverscan,
columnStopIndexOverscan,
getColumnBounds,
getRowBounds,
isRtl,
rowCount,
rowStartIndexOverscan,
Expand Down
30 changes: 23 additions & 7 deletions lib/components/list/List.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { act, render, screen } from "@testing-library/react";
import { createRef, useLayoutEffect } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { EMPTY_OBJECT } from "../../../src/constants";
import { assert } from "../../utils/assert";
import {
disableResizeObserverForCurrentTest,
simulateUnsupportedEnvironmentForTest,
updateMockResizeObserver
setDefaultElementSize,
setElementSize,
simulateUnsupportedEnvironmentForTest
} from "../../utils/test/mockResizeObserver";
import { List } from "./List";
import { type ListImperativeAPI, type RowComponentProps } from "./types";
Expand Down Expand Up @@ -34,7 +36,7 @@ describe("List", () => {
beforeEach(() => {
RowComponent.mockReset();

updateMockResizeObserver(new DOMRect(0, 0, 50, 100));
setDefaultElementSize({ height: 100, width: 50 });

mountedRows = new Map();
});
Expand All @@ -56,7 +58,7 @@ describe("List", () => {
test("should render enough rows to fill the available height", () => {
const onResize = vi.fn();

render(
const { container } = render(
<List
onResize={onResize}
overscanCount={0}
Expand Down Expand Up @@ -85,7 +87,14 @@ describe("List", () => {
);

act(() => {
updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
const listElement = container.querySelector<HTMLElement>('[role="list"]');
assert(listElement !== null);

setElementSize({
element: listElement,
height: 75,
width: 50
});
});

items = screen.queryAllByRole("listitem");
Expand All @@ -107,7 +116,7 @@ describe("List", () => {
});

test("should render enough rows to fill the available height with overscan", () => {
render(
const { container } = render(
<List
overscanCount={2}
rowCount={100}
Expand All @@ -123,7 +132,14 @@ describe("List", () => {
expect(items[5]).toHaveTextContent("Row 5");

act(() => {
updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
const listElement = container.querySelector<HTMLElement>('[role="list"]');
assert(listElement !== null);

setElementSize({
element: listElement,
height: 75,
width: 50
});
});

items = screen.queryAllByRole("listitem");
Expand Down
32 changes: 22 additions & 10 deletions lib/components/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useImperativeHandle,
useMemo,
useState,
type CSSProperties,
type ReactNode
} from "react";
import { useVirtualizer } from "../../core/useVirtualizer";
Expand Down Expand Up @@ -41,7 +42,7 @@ export function List<
const [element, setElement] = useState<HTMLDivElement | null>(null);

const {
getCellBounds,
cachedBounds,
getEstimatedSize,
scrollToIndex,
startIndexOverscan,
Expand Down Expand Up @@ -114,6 +115,13 @@ export function List<
stopIndexVisible
]);

const hasRowHeight = rowHeight !== undefined;

const offset =
startIndexOverscan >= 0
? cachedBounds.getItemBounds(startIndexOverscan).scrollOffset
: 0;

const rows = useMemo(() => {
const children: ReactNode[] = [];
if (rowCount > 0) {
Expand All @@ -122,7 +130,15 @@ export function List<
index <= stopIndexOverscan;
index++
) {
const bounds = getCellBounds(index);
const bounds = cachedBounds.getItemBounds(index);
const style: CSSProperties = {
height: hasRowHeight ? bounds.size : undefined,
width: "100%"
};

if (index === startIndexOverscan) {
style.marginTop = `${offset}px`;
}

children.push(
<RowComponent
Expand All @@ -134,21 +150,17 @@ export function List<
}}
key={index}
index={index}
style={{
position: "absolute",
left: 0,
transform: `translateY(${bounds.scrollOffset}px)`,
height: bounds.size,
width: "100%"
}}
style={style}
/>
);
}
}
return children;
}, [
RowComponent,
getCellBounds,
cachedBounds,
hasRowHeight,
offset,
rowCount,
rowProps,
startIndexOverscan,
Expand Down
12 changes: 7 additions & 5 deletions lib/core/createCachedBounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ describe("createCachedBounds", () => {
expect(itemSize).not.toHaveBeenCalled();
expect(cachedBounds.size).toBe(0);

expect(cachedBounds.get(2)).toEqual({
expect(cachedBounds.getItemBounds(2)).toEqual({
scrollOffset: 21,
size: 12
});
expect(itemSize).toHaveBeenCalledTimes(3);
expect(cachedBounds.size).toBe(3);

expect(cachedBounds.get(3)).toEqual({
expect(cachedBounds.getItemBounds(3)).toEqual({
scrollOffset: 33,
size: 13
});
Expand All @@ -40,13 +40,13 @@ describe("createCachedBounds", () => {
expect(itemSize).not.toHaveBeenCalled();
expect(cachedBounds.size).toBe(0);

cachedBounds.get(9);
cachedBounds.getItemBounds(9);

expect(itemSize).toHaveBeenCalledTimes(10);
expect(cachedBounds.size).toBe(10);

for (let index = 0; index < 10; index++) {
cachedBounds.get(index);
cachedBounds.getItemBounds(index);
}

expect(itemSize).toHaveBeenCalledTimes(10);
Expand All @@ -62,8 +62,10 @@ describe("createCachedBounds", () => {

expect(cachedBounds.size).toBe(0);

expect(cachedBounds.getEstimatedSize()).toEqual(0);

expect(() => {
cachedBounds.get(1);
cachedBounds.getItemBounds(1);
}).toThrow("Invalid index 1");
});
});
25 changes: 17 additions & 8 deletions lib/core/createCachedBounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ export function createCachedBounds<Props extends object>({
}): CachedBounds {
const cache = new Map<number, Bounds>();

return {
get(index: number) {
const api = {
getEstimatedSize() {
if (itemCount === 0) {
return 0;
} else {
const bounds = api.getItemBounds(cache.size === 0 ? 0 : cache.size - 1);
assert(bounds, "Unexpected bounds cache miss");

return (bounds.scrollOffset + bounds.size) / cache.size;
}
},
getItemBounds(index: number) {
assert(index < itemCount, `Invalid index ${index}`);

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

let size: number;
let size: number = 0;
switch (typeof itemSize) {
case "function": {
size = itemSize(currentIndex, itemProps);
Expand All @@ -33,8 +43,8 @@ export function createCachedBounds<Props extends object>({

if (currentIndex === 0) {
cache.set(currentIndex, {
size,
scrollOffset: 0
scrollOffset: 0,
size
});
} else {
const previousRowBounds = cache.get(currentIndex - 1);
Expand All @@ -59,11 +69,10 @@ export function createCachedBounds<Props extends object>({

return bounds;
},
set(index: number, bounds: Bounds) {
cache.set(index, bounds);
},
get size() {
return cache.size;
}
};

return api;
}
18 changes: 16 additions & 2 deletions lib/core/getEstimatedSize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ describe("getEstimatedSize", () => {
).toBe(0);
});

test("should return an estimate based on the first row if no sizes have yet been measured", () => {
expect(
getEstimatedSize({
cachedBounds: createCachedBounds({
itemCount: 1,
itemProps: EMPTY_OBJECT,
itemSize
}),
itemCount: 1,
itemSize
})
).toBe(10);
});

test("should return an average size based on the first item if no measurements have been taken", () => {
expect(
getEstimatedSize({
Expand All @@ -41,7 +55,7 @@ describe("getEstimatedSize", () => {
itemProps: EMPTY_OBJECT,
itemSize
});
cachedBounds.get(4);
cachedBounds.getItemBounds(4);

expect(
getEstimatedSize({
Expand All @@ -59,7 +73,7 @@ describe("getEstimatedSize", () => {
itemSize
});

cachedBounds.get(9);
cachedBounds.getItemBounds(9);

expect(
getEstimatedSize({
Expand Down
Loading