diff --git a/lib/components/grid/Grid.test.tsx b/lib/components/grid/Grid.test.tsx
index 2f984f25..391eb8ea 100644
--- a/lib/components/grid/Grid.test.tsx
+++ b/lib/components/grid/Grid.test.tsx
@@ -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";
@@ -36,7 +36,7 @@ describe("Grid", () => {
beforeEach(() => {
CellComponent.mockReset();
- updateMockResizeObserver(new DOMRect(0, 0, 100, 40));
+ setDefaultElementSize({ height: 40, width: 100 });
mountedCells = new Map();
});
@@ -273,7 +273,6 @@ describe("Grid", () => {
const items = screen.queryAllByRole("gridcell");
expect(items).toHaveLength(8);
- // TODO
});
test("should call onCellsRendered", () => {
diff --git a/lib/components/grid/Grid.tsx b/lib/components/grid/Grid.tsx
index e0a7d595..18786b86 100644
--- a/lib/components/grid/Grid.tsx
+++ b/lib/components/grid/Grid.tsx
@@ -48,7 +48,7 @@ export function Grid<
const isRtl = useIsRtl(element, dir);
const {
- getCellBounds: getColumnBounds,
+ cachedBounds: cachedColumnBounds,
getEstimatedSize: getEstimatedWidth,
startIndexOverscan: columnStartIndexOverscan,
startIndexVisible: columnStartIndexVisible,
@@ -69,7 +69,7 @@ export function Grid<
});
const {
- getCellBounds: getRowBounds,
+ cachedBounds: cachedRowBounds,
getEstimatedSize: getEstimatedHeight,
startIndexOverscan: rowStartIndexOverscan,
startIndexVisible: rowStartIndexVisible,
@@ -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[] = [];
@@ -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(
);
@@ -262,13 +264,13 @@ export function Grid<
}
return children;
}, [
+ cachedColumnBounds,
+ cachedRowBounds,
CellComponent,
cellProps,
columnCount,
columnStartIndexOverscan,
columnStopIndexOverscan,
- getColumnBounds,
- getRowBounds,
isRtl,
rowCount,
rowStartIndexOverscan,
diff --git a/lib/components/list/List.test.tsx b/lib/components/list/List.test.tsx
index 61cb507f..31e69ec9 100644
--- a/lib/components/list/List.test.tsx
+++ b/lib/components/list/List.test.tsx
@@ -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";
@@ -34,7 +36,7 @@ describe("List", () => {
beforeEach(() => {
RowComponent.mockReset();
- updateMockResizeObserver(new DOMRect(0, 0, 50, 100));
+ setDefaultElementSize({ height: 100, width: 50 });
mountedRows = new Map();
});
@@ -56,7 +58,7 @@ describe("List", () => {
test("should render enough rows to fill the available height", () => {
const onResize = vi.fn();
- render(
+ const { container } = render(
{
);
act(() => {
- updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
+ const listElement = container.querySelector('[role="list"]');
+ assert(listElement !== null);
+
+ setElementSize({
+ element: listElement,
+ height: 75,
+ width: 50
+ });
});
items = screen.queryAllByRole("listitem");
@@ -107,7 +116,7 @@ describe("List", () => {
});
test("should render enough rows to fill the available height with overscan", () => {
- render(
+ const { container } = render(
{
expect(items[5]).toHaveTextContent("Row 5");
act(() => {
- updateMockResizeObserver(new DOMRect(0, 0, 50, 75));
+ const listElement = container.querySelector('[role="list"]');
+ assert(listElement !== null);
+
+ setElementSize({
+ element: listElement,
+ height: 75,
+ width: 50
+ });
});
items = screen.queryAllByRole("listitem");
diff --git a/lib/components/list/List.tsx b/lib/components/list/List.tsx
index a3ce6be0..0d17d4bd 100644
--- a/lib/components/list/List.tsx
+++ b/lib/components/list/List.tsx
@@ -5,6 +5,7 @@ import {
useImperativeHandle,
useMemo,
useState,
+ type CSSProperties,
type ReactNode
} from "react";
import { useVirtualizer } from "../../core/useVirtualizer";
@@ -41,7 +42,7 @@ export function List<
const [element, setElement] = useState(null);
const {
- getCellBounds,
+ cachedBounds,
getEstimatedSize,
scrollToIndex,
startIndexOverscan,
@@ -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) {
@@ -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(
);
}
@@ -148,7 +158,9 @@ export function List<
return children;
}, [
RowComponent,
- getCellBounds,
+ cachedBounds,
+ hasRowHeight,
+ offset,
rowCount,
rowProps,
startIndexOverscan,
diff --git a/lib/core/createCachedBounds.test.ts b/lib/core/createCachedBounds.test.ts
index 367ac0fd..24dc4f1a 100644
--- a/lib/core/createCachedBounds.test.ts
+++ b/lib/core/createCachedBounds.test.ts
@@ -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
});
@@ -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);
@@ -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");
});
});
diff --git a/lib/core/createCachedBounds.ts b/lib/core/createCachedBounds.ts
index 3e527473..487a1d89 100644
--- a/lib/core/createCachedBounds.ts
+++ b/lib/core/createCachedBounds.ts
@@ -12,14 +12,24 @@ export function createCachedBounds({
}): CachedBounds {
const cache = new Map();
- 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);
@@ -33,8 +43,8 @@ export function createCachedBounds({
if (currentIndex === 0) {
cache.set(currentIndex, {
- size,
- scrollOffset: 0
+ scrollOffset: 0,
+ size
});
} else {
const previousRowBounds = cache.get(currentIndex - 1);
@@ -59,11 +69,10 @@ export function createCachedBounds({
return bounds;
},
- set(index: number, bounds: Bounds) {
- cache.set(index, bounds);
- },
get size() {
return cache.size;
}
};
+
+ return api;
}
diff --git a/lib/core/getEstimatedSize.test.ts b/lib/core/getEstimatedSize.test.ts
index 0b65fd0a..56090dd3 100644
--- a/lib/core/getEstimatedSize.test.ts
+++ b/lib/core/getEstimatedSize.test.ts
@@ -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({
@@ -41,7 +55,7 @@ describe("getEstimatedSize", () => {
itemProps: EMPTY_OBJECT,
itemSize
});
- cachedBounds.get(4);
+ cachedBounds.getItemBounds(4);
expect(
getEstimatedSize({
@@ -59,7 +73,7 @@ describe("getEstimatedSize", () => {
itemSize
});
- cachedBounds.get(9);
+ cachedBounds.getItemBounds(9);
expect(
getEstimatedSize({
diff --git a/lib/core/getEstimatedSize.ts b/lib/core/getEstimatedSize.ts
index c0c2bd51..b46491ce 100644
--- a/lib/core/getEstimatedSize.ts
+++ b/lib/core/getEstimatedSize.ts
@@ -1,5 +1,4 @@
import type { CachedBounds, SizeFunction } from "./types";
-import { assert } from "../utils/assert";
export function getEstimatedSize({
cachedBounds,
@@ -12,17 +11,21 @@ export function getEstimatedSize({
}) {
if (itemCount === 0) {
return 0;
- } else if (typeof itemSize === "number") {
- return itemCount * itemSize;
} else {
- const bounds = cachedBounds.get(
- cachedBounds.size === 0 ? 0 : cachedBounds.size - 1
- );
- assert(bounds !== undefined, "Unexpected bounds cache miss");
+ switch (typeof itemSize) {
+ case "function": {
+ const bounds = cachedBounds.getItemBounds(
+ cachedBounds.size === 0 ? 0 : cachedBounds.size - 1
+ );
- const averageItemSize =
- (bounds.scrollOffset + bounds.size) / cachedBounds.size;
+ const averageItemSize =
+ (bounds.scrollOffset + bounds.size) / cachedBounds.size;
- return itemCount * averageItemSize;
+ return itemCount * averageItemSize;
+ }
+ case "number": {
+ return itemCount * itemSize;
+ }
+ }
}
}
diff --git a/lib/core/getOffsetForIndex.ts b/lib/core/getOffsetForIndex.ts
index b27ee4f2..3362d8e6 100644
--- a/lib/core/getOffsetForIndex.ts
+++ b/lib/core/getOffsetForIndex.ts
@@ -25,7 +25,8 @@ export function getOffsetForIndex({
itemSize
});
- const bounds = cachedBounds.get(index);
+ const bounds = cachedBounds.getItemBounds(index);
+
const maxOffset = Math.max(
0,
Math.min(estimatedTotalSize - containerSize, bounds.scrollOffset)
diff --git a/lib/core/getStartStopIndices.ts b/lib/core/getStartStopIndices.ts
index c38d84a3..ac9d82dc 100644
--- a/lib/core/getStartStopIndices.ts
+++ b/lib/core/getStartStopIndices.ts
@@ -25,11 +25,14 @@ export function getStartStopIndices({
let startIndexOverscan = 0;
let stopIndexOverscan = -1;
let currentIndex = 0;
+ let currentOffset = 0;
while (currentIndex < maxIndex) {
- const bounds = cachedBounds.get(currentIndex);
+ const bounds = cachedBounds.getItemBounds(currentIndex);
- if (bounds.scrollOffset + bounds.size > containerScrollOffset) {
+ currentOffset = bounds.scrollOffset + bounds.size;
+
+ if (currentOffset > containerScrollOffset) {
break;
}
@@ -40,12 +43,11 @@ export function getStartStopIndices({
startIndexOverscan = Math.max(0, startIndexVisible - overscanCount);
while (currentIndex < maxIndex) {
- const bounds = cachedBounds.get(currentIndex);
+ const bounds = cachedBounds.getItemBounds(currentIndex);
+
+ currentOffset = bounds.scrollOffset + bounds.size;
- if (
- bounds.scrollOffset + bounds.size >=
- containerScrollOffset + containerSize
- ) {
+ if (currentOffset >= containerScrollOffset + containerSize) {
break;
}
diff --git a/lib/core/types.ts b/lib/core/types.ts
index d9f409f6..eb721981 100644
--- a/lib/core/types.ts
+++ b/lib/core/types.ts
@@ -4,8 +4,8 @@ export type Bounds = {
};
export type CachedBounds = {
- get(index: number): Bounds;
- set(index: number, bounds: Bounds): void;
+ getEstimatedSize(): number;
+ getItemBounds(index: number): Bounds;
size: number;
};
diff --git a/lib/core/useCachedBounds.test.ts b/lib/core/useCachedBounds.test.ts
index 0c3dc2d3..42cc7290 100644
--- a/lib/core/useCachedBounds.test.ts
+++ b/lib/core/useCachedBounds.test.ts
@@ -4,7 +4,7 @@ import { EMPTY_OBJECT } from "../../src/constants";
import { useCachedBounds } from "./useCachedBounds";
describe("useCachedBounds", () => {
- test("should cache the CachedBounds unless props change", () => {
+ test("should memoize CachedBounds value unless props change", () => {
const { result, rerender } = renderHook(
(props?: Partial>[0]) =>
useCachedBounds({
diff --git a/lib/core/useVirtualizer.test.ts b/lib/core/useVirtualizer.test.ts
index 42a16c12..aa79f09d 100644
--- a/lib/core/useVirtualizer.test.ts
+++ b/lib/core/useVirtualizer.test.ts
@@ -1,57 +1,78 @@
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, test } from "vitest";
import { EMPTY_OBJECT, NOOP_FUNCTION } from "../../src/constants";
-import { updateMockResizeObserver } from "../utils/test/mockResizeObserver";
+import {
+ setDefaultElementSize,
+ setElementSizeFunction
+} from "../utils/test/mockResizeObserver";
import { useVirtualizer } from "./useVirtualizer";
describe("useVirtualizer", () => {
- const DEFAULT_ARGS: Parameters[0] = {
- containerElement: document.body,
- defaultContainerSize: 100,
- direction: "vertical",
- itemCount: 25,
- itemProps: EMPTY_OBJECT,
- itemSize: 25,
- onResize: NOOP_FUNCTION,
- overscanCount: 0
- };
-
+ type Params = Parameters[0];
+
+ function testHelper(config: Partial = {}) {
+ const containerElement = document.createElement("div");
+ containerElement.setAttribute("role", "list");
+
+ const args: Params = {
+ containerElement,
+ defaultContainerSize: 100,
+ direction: "vertical",
+ itemCount: 25,
+ itemProps: EMPTY_OBJECT,
+ itemSize: 25,
+ onResize: NOOP_FUNCTION,
+ overscanCount: 0,
+ ...config
+ };
+
+ for (let index = 0; index < args.itemCount; index++) {
+ const element = document.createElement("div");
+ containerElement.appendChild(element);
+ }
+
+ return renderHook(() => useVirtualizer(args));
+ }
beforeEach(() => {
- updateMockResizeObserver(new DOMRect(0, 0, 50, 100));
+ setDefaultElementSize({ height: 100, width: 50 });
+
+ setElementSizeFunction((element) => {
+ if (element.getAttribute("role") === "list") {
+ return new DOMRect(0, 0, 50, 100);
+ } else {
+ return new DOMRect(0, 0, 50, 25);
+ }
+ });
});
- describe("getCellBounds", () => {
+ describe("cachedBounds", () => {
test("itemSize type: number", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize: 25
- })
- );
- expect(result.current.getCellBounds(0)).toEqual({
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize: 25
+ });
+
+ expect(result.current.cachedBounds.getItemBounds(0)).toEqual({
scrollOffset: 0,
size: 25
});
- expect(result.current.getCellBounds(24)).toEqual({
+ expect(result.current.cachedBounds.getItemBounds(24)).toEqual({
scrollOffset: 600,
size: 25
});
});
test("itemSize type: string", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize: "50%"
- })
- );
- expect(result.current.getCellBounds(0)).toEqual({
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize: "50%"
+ });
+
+ expect(result.current.cachedBounds.getItemBounds(0)).toEqual({
scrollOffset: 0,
size: 50
});
- expect(result.current.getCellBounds(24)).toEqual({
+ expect(result.current.cachedBounds.getItemBounds(24)).toEqual({
scrollOffset: 1200,
size: 50
});
@@ -60,19 +81,17 @@ describe("useVirtualizer", () => {
test("itemSize type: function", () => {
const itemSize = (index: number) => 20 + index * 20;
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemCount: 10,
- itemSize
- })
- );
- expect(result.current.getCellBounds(0)).toEqual({
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemCount: 10,
+ itemSize
+ });
+
+ expect(result.current.cachedBounds.getItemBounds(0)).toEqual({
scrollOffset: 0,
size: 20
});
- expect(result.current.getCellBounds(9)).toEqual({
+ expect(result.current.cachedBounds.getItemBounds(9)).toEqual({
scrollOffset: 900,
size: 200
});
@@ -81,61 +100,52 @@ describe("useVirtualizer", () => {
describe("getEstimatedSize", () => {
test("itemSize type: number", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize: 25
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize: 25
+ });
+
expect(result.current.getEstimatedSize()).toBe(625);
});
test("itemSize type: string", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize: "50%"
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize: "50%"
+ });
+
expect(result.current.getEstimatedSize()).toBe(1250);
});
test("itemSize type: function", () => {
const itemSize = (index: number) => 20 + index * 20;
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemCount: 10,
- itemSize
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemCount: 10,
+ itemSize
+ });
// Actual size is 1,100
// Based on the rows measured so far, the estimated size is 400
expect(result.current.getEstimatedSize()).toBe(400);
// Finish measuring the rows and the actual size should be returned now
- result.current.getCellBounds(9);
+ result.current.cachedBounds.getItemBounds(9);
expect(result.current.getEstimatedSize()).toBe(1100);
});
});
- // scrollToIndex is mostly covered by getOffsetForIndex tests
+ // The specific scroll index ranges are mostly covered by getOffsetForIndex tests
describe("startIndex/stopIndex", () => {
test("itemSize type: number", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize: 25,
- overscanCount: 2
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize: 25,
+ overscanCount: 2
+ });
+
expect(result.current.startIndexOverscan).toBe(0);
expect(result.current.startIndexVisible).toBe(0);
expect(result.current.stopIndexOverscan).toBe(5);
@@ -143,14 +153,12 @@ describe("useVirtualizer", () => {
});
test("itemSize type: string", () => {
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- overscanCount: 2,
- itemSize: "50%"
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ overscanCount: 2,
+ itemSize: "50%"
+ });
+
expect(result.current.startIndexOverscan).toBe(0);
expect(result.current.startIndexVisible).toBe(0);
expect(result.current.stopIndexOverscan).toBe(3);
@@ -160,14 +168,12 @@ describe("useVirtualizer", () => {
test("itemSize type: function", () => {
const itemSize = (index: number) => 20 + index * 20;
- const { result } = renderHook(() =>
- useVirtualizer({
- ...DEFAULT_ARGS,
- defaultContainerSize: 100,
- itemSize,
- overscanCount: 2
- })
- );
+ const { result } = testHelper({
+ defaultContainerSize: 100,
+ itemSize,
+ overscanCount: 2
+ });
+
expect(result.current.startIndexOverscan).toBe(0);
expect(result.current.startIndexVisible).toBe(0);
expect(result.current.stopIndexOverscan).toBe(4);
@@ -180,7 +186,7 @@ describe("useVirtualizer", () => {
// @ts-expect-error Testing
HTMLElement.prototype.scrollTo = undefined;
- const { result } = renderHook(() => useVirtualizer(DEFAULT_ARGS));
+ const { result } = testHelper();
result.current.scrollToIndex({ containerScrollOffset: 0, index: 5 });
});
diff --git a/lib/core/useVirtualizer.ts b/lib/core/useVirtualizer.ts
index db427159..8bc47922 100644
--- a/lib/core/useVirtualizer.ts
+++ b/lib/core/useVirtualizer.ts
@@ -111,11 +111,6 @@ export function useVirtualizer({
itemSize
});
- const getCellBounds = useCallback(
- (index: number) => cachedBounds.get(index),
- [cachedBounds]
- );
-
const getEstimatedSize = useCallback(
() =>
getEstimatedSizeUtil({
@@ -251,7 +246,7 @@ export function useVirtualizer({
);
return {
- getCellBounds,
+ cachedBounds,
getEstimatedSize,
scrollToIndex,
startIndexOverscan,
diff --git a/lib/hooks/useResizeObserver.test.ts b/lib/hooks/useResizeObserver.test.ts
index c238701f..23812f8c 100644
--- a/lib/hooks/useResizeObserver.test.ts
+++ b/lib/hooks/useResizeObserver.test.ts
@@ -2,13 +2,14 @@ import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, test } from "vitest";
import {
simulateUnsupportedEnvironmentForTest,
- updateMockResizeObserver
+ setDefaultElementSize,
+ setElementSize
} from "../utils/test/mockResizeObserver";
import { useResizeObserver } from "./useResizeObserver";
describe("useResizeObserver", () => {
beforeEach(() => {
- updateMockResizeObserver({ height: 100, width: 50 });
+ setDefaultElementSize({ height: 100, width: 50 });
});
test("should use default width/height if disabled", () => {
@@ -34,7 +35,7 @@ describe("useResizeObserver", () => {
act(() => {
// Updates should be ignored as well
- updateMockResizeObserver({ height: 25, target: element });
+ setElementSize({ element, height: 25, width: 50 });
});
expect(result.current).toEqual({
@@ -79,9 +80,10 @@ describe("useResizeObserver", () => {
});
act(() => {
- updateMockResizeObserver({
+ setElementSize({
+ element,
height: 50,
- target: element
+ width: 50
});
});
@@ -104,9 +106,10 @@ describe("useResizeObserver", () => {
);
act(() => {
- updateMockResizeObserver({
+ setElementSize({
+ element: otherElement,
height: 50,
- target: otherElement
+ width: 50
});
});
diff --git a/lib/utils/test/mockResizeObserver.ts b/lib/utils/test/mockResizeObserver.ts
index 7320e9ed..e5b7b0b9 100644
--- a/lib/utils/test/mockResizeObserver.ts
+++ b/lib/utils/test/mockResizeObserver.ts
@@ -1,37 +1,55 @@
import EventEmitter from "node:events";
+type GetDOMRect = (element: HTMLElement) => DOMRectReadOnly | undefined | void;
+
const emitter = new EventEmitter();
-emitter.setMaxListeners(25);
+emitter.setMaxListeners(100);
+
+const elementToDOMRect = new Map();
+let defaultDomRect: DOMRectReadOnly = new DOMRect(0, 0, 0, 0);
let disabled: boolean = false;
-let entrySize: DOMRectReadOnly = new DOMRect(0, 0, 0, 0);
+let getDOMRect: GetDOMRect | undefined = undefined;
export function disableResizeObserverForCurrentTest() {
disabled = true;
}
-export function simulateUnsupportedEnvironmentForTest() {
- // @ts-expect-error Simulate API being unsupported
- window.ResizeObserver = null;
+export function setDefaultElementSize({
+ height,
+ width
+}: {
+ height: number;
+ width: number;
+}) {
+ defaultDomRect = new DOMRect(0, 0, width, height);
+
+ emitter.emit("change");
+}
+
+export function setElementSizeFunction(value: GetDOMRect) {
+ getDOMRect = value;
+
+ emitter.emit("change");
}
-export function updateMockResizeObserver({
+export function setElementSize({
+ element,
height,
- target,
width
}: {
- height?: number;
- target?: HTMLElement;
- width?: number;
-}): void {
- entrySize = new DOMRect(
- 0,
- 0,
- width ?? entrySize.width,
- height ?? entrySize.height
- );
-
- emitter.emit("change", target);
+ element: HTMLElement;
+ height: number;
+ width: number;
+}) {
+ elementToDOMRect.set(element, new DOMRect(0, 0, width, height));
+
+ emitter.emit("change", element);
+}
+
+export function simulateUnsupportedEnvironmentForTest() {
+ // @ts-expect-error Simulate API being unsupported
+ window.ResizeObserver = null;
}
export function mockResizeObserver() {
@@ -39,66 +57,103 @@ export function mockResizeObserver() {
const originalResizeObserver = window.ResizeObserver;
- window.ResizeObserver = class implements ResizeObserver {
- readonly #callback: ResizeObserverCallback;
- #disconnected: boolean = false;
- #elements: Set = new Set();
+ window.ResizeObserver = MockResizeObserver;
- constructor(callback: ResizeObserverCallback) {
- this.#callback = callback;
+ return function unmockResizeObserver() {
+ window.ResizeObserver = originalResizeObserver;
- emitter.addListener("change", this.#onChange);
- }
+ defaultDomRect = new DOMRect(0, 0, 0, 0);
+ disabled = false;
+ getDOMRect = undefined;
- observe(element: HTMLElement) {
- if (this.#disconnected) {
- return;
- }
+ elementToDOMRect.clear();
+ };
+}
- this.#elements.add(element);
- this.#notify(element);
- }
+class MockResizeObserver implements ResizeObserver {
+ readonly #callback: ResizeObserverCallback;
+ #disconnected: boolean = false;
+ #elements: Set = new Set();
- unobserve(element: HTMLElement) {
- this.#elements.delete(element);
+ constructor(callback: ResizeObserverCallback) {
+ this.#callback = callback;
+
+ emitter.addListener("change", this.#onChange);
+ }
+
+ observe(element: HTMLElement) {
+ if (this.#disconnected) {
+ return;
}
- disconnect() {
- this.#disconnected = true;
- this.#elements.clear();
+ this.#elements.add(element);
+ this.#notify([element]);
+ }
+
+ unobserve(element: HTMLElement) {
+ this.#elements.delete(element);
+ }
- emitter.removeListener("change", this.#onChange);
+ disconnect() {
+ this.#disconnected = true;
+ this.#elements.clear();
+
+ emitter.removeListener("change", this.#onChange);
+ }
+
+ #notify(elements: HTMLElement[]) {
+ if (disabled) {
+ return;
}
- #notify(target: HTMLElement) {
- if (disabled) {
- return;
+ const entries = elements.map((element) => {
+ const computedStyle = window.getComputedStyle(element);
+ const writingMode = computedStyle.writingMode;
+
+ let contentRect: DOMRectReadOnly =
+ elementToDOMRect.get(element) ?? defaultDomRect;
+
+ if (getDOMRect) {
+ const contentRectOverride = getDOMRect(element);
+ if (contentRectOverride) {
+ contentRect = contentRectOverride;
+ }
}
- this.#callback(
- [
+ let blockSize = 0;
+ let inlineSize = 0;
+ if (writingMode.includes("vertical")) {
+ blockSize = contentRect.width;
+ inlineSize = contentRect.height;
+ } else {
+ blockSize = contentRect.height;
+ inlineSize = contentRect.width;
+ }
+
+ return {
+ borderBoxSize: [
{
- borderBoxSize: [],
- contentBoxSize: [],
- contentRect: entrySize,
- devicePixelContentBoxSize: [],
- target
+ blockSize,
+ inlineSize
}
],
- this
- );
- }
-
- #onChange = (target?: HTMLElement) => {
- if (target) {
- this.#notify(target);
- } else {
- this.#elements.forEach((element) => this.#notify(element));
+ contentBoxSize: [],
+ contentRect,
+ devicePixelContentBoxSize: [],
+ target: element
+ };
+ });
+
+ this.#callback(entries, this);
+ }
+
+ #onChange = (target?: HTMLElement) => {
+ if (target) {
+ if (this.#elements.has(target)) {
+ this.#notify([target]);
}
- };
- };
-
- return function unmockResizeObserver() {
- window.ResizeObserver = originalResizeObserver;
+ } else {
+ this.#notify(Array.from(this.#elements));
+ }
};
}