Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

# 2.0.1

- [#104](https://github.com/bvaughn/react-virtualized-auto-sizer/pull/104): Separate `renderProp` and `ChildComponent` props.

# 2.0.0

Version 2 simplifies the API and improves TypeScript support.
Expand Down
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,7 @@ Measures the available width and height of its parent `HTMLElement` and passes t
#### Required props

<!-- AutoSizer:required-props:begin -->

<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Child</td>
<td><p>Child component to be passed the available width and height values as props.</p>
<p>ℹ️ Width and height are undefined during the during the initial render (including server-rendering).</p>
</td>
</tr>
</tbody>
</table>

None
<!-- AutoSizer:required-props:end -->

#### Optional props
Expand Down Expand Up @@ -102,6 +85,26 @@ in browsers/environments that do not support the <code>ResizeObserver</code> API
<tr>
<td>tagName</td>
<td><p>Optional HTML tag name for root HTMLElement; defaults to <code>&quot;div&quot;</code>.</p>
</td>
</tr>
<tr>
<td>Child</td>
<td><p>Child component to be passed the available width and height values as props.
@deprecated Use the <code>ChildComponent</code> or <code>renderProp</code> props instead.</p>
</td>
</tr>
<tr>
<td>ChildComponent</td>
<td><p>Child component to be passed the available width and height values as props.</p>
<p>ℹ️ Use <code>renderProp</code> instead if you need access to local state.</p>
<p>⚠️ Width and height are undefined during the during the initial render (including server-rendering).</p>
</td>
</tr>
<tr>
<td>renderProp</td>
<td><p>Render prop to be passed the available width and height values as props.</p>
<p>ℹ️ Use <code>ChildComponent</code> instead for better memoization if you do not need access to local state.</p>
<p>⚠️ Width and height are undefined during the during the initial render (including server-rendering).</p>
</td>
</tr>
</tbody>
Expand Down
2 changes: 2 additions & 0 deletions integrations/vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router";
import "./index.css";
import { Home } from "./routes/Home";
import { ScrollingDomState } from "./routes/ScrollingDomState";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/scrolling-dom-state" element={<ScrollingDomState />} />
</Routes>
</BrowserRouter>
</StrictMode>
Expand Down
58 changes: 49 additions & 9 deletions integrations/vite/src/routes/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ import {
useLayoutEffect,
useMemo,
useState,
type FunctionComponent
type FunctionComponent,
type ReactNode
} from "react";
import { createPortal } from "react-dom";
import { useSearchParams } from "react-router";
import {
AutoSizer,
type AutoSizerBox,
type Size
} from "react-virtualized-auto-sizer";
import { Children, type SizeProps } from "../components/Children";
import { useSearchParams } from "react-router";

export type ChildProp = "Child" | "ChildComponent" | "renderProp";

export function Home() {
const [params] = useSearchParams();
const box = (params.get("box") || undefined) as AutoSizerBox | undefined;
const childProp = (params.get("childProp") || "ChildComponent") as ChildProp;
const style = params.get("style") || "";
console.log("Home:", JSON.stringify({ box, style }, null, 2));

const [container] = useState(() => {
const div = document.createElement("div");
Expand All @@ -40,7 +43,7 @@ export function Home() {
const [commits, setCommits] = useState<SizeProps[]>([]);
const [onResizeCalls, setOnResizeCalls] = useState<Size[]>([]);

const Child = useMemo<FunctionComponent<SizeProps>>(
const ChildComponent = useMemo<FunctionComponent<SizeProps>>(
() =>
({ height, width }) => (
<Children
Expand All @@ -62,13 +65,50 @@ export function Home() {
]);
}, []);

let autoSizer: ReactNode;
switch (childProp) {
case "Child": {
autoSizer = (
<AutoSizer box={box} Child={ChildComponent} onResize={onResize} />
);
break;
}
case "ChildComponent": {
autoSizer = (
<AutoSizer
box={box}
ChildComponent={ChildComponent}
onResize={onResize}
/>
);
break;
}
case "renderProp": {
autoSizer = (
<AutoSizer
box={box}
onResize={onResize}
renderProp={({ height, width }: SizeProps) => (
<Children
height={height as number}
onCommitLogsChange={setCommits}
width={width as number}
/>
)}
/>
);
break;
}
}

return (
<div className="w-full h-full">
<iframe className="w-full h-[25%]" ref={setIframe} />
{createPortal(
<AutoSizer box={box} Child={Child} onResize={onResize} />,
container
)}
<iframe
className="w-full h-[25%]"
name="auto-sizer-frame"
ref={setIframe}
/>
{createPortal(autoSizer, container)}
<pre className="text-xs p-2">
<code
className="h-full w-full whitespace-pre-wrap overflow-auto"
Expand Down
30 changes: 30 additions & 0 deletions integrations/vite/src/routes/ScrollingDomState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from "react";
import { AutoSizer } from "react-virtualized-auto-sizer";

export function ScrollingDomState() {
const [count, setCount] = useState(0);

return (
<div className="w-screen h-screen">
<AutoSizer
renderProp={({ height, width }) => (
<div style={{ height, overflow: "auto", width }}>
<button
className="sticky top-0"
onClick={() => setCount(count + 1)}
>
force update ({count})
</button>
<ul>
{Array.from({ length: 100 }, (_, index) => index).map((index) => {
return (
<li key={index}>Row {`${index + 1}`.padStart(2, "0")}</li>
);
})}
</ul>
</div>
)}
/>
</div>
);
}
19 changes: 19 additions & 0 deletions integrations/vite/tests/dom-state.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from "@playwright/test";

test.describe("dom state", () => {
// See issue #103
test("should preserve DOM state across renders when an inline renderProp function is used", async ({
page
}) => {
await page.goto("http://localhost:3010/scrolling-dom-state");

await expect(page.getByText("force update (0)")).toBeVisible();
await expect(page.getByText("Row 01")).toBeInViewport();
await page.getByText("Row 99").scrollIntoViewIfNeeded();

await page.getByText("force update (0)").click();

await expect(page.getByText("force update (1)")).toBeVisible();
await expect(page.getByText("Row 99")).toBeInViewport();
});
});
62 changes: 47 additions & 15 deletions integrations/vite/tests/render.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
import { expect, test, type Page } from "@playwright/test";
import type { AutoSizerBox } from "react-virtualized-auto-sizer";
import { type AutoSizerBox } from "react-virtualized-auto-sizer";
import type { ChildProp } from "../src/routes/Home";

async function startTest({
box,
childProp,
page,
style = ""
}: {
box?: AutoSizerBox;
childProp?: ChildProp;
page: Page;
style?: string;
}) {
// page.on("console", (message) => console.log(message.text()));

const url = `http://localhost:3010/?box=${box ?? ""}&style=${encodeURIComponent(style)}`;
const url = new URL("http://localhost:3010");
if (box) {
url.searchParams.set("box", box);
}
if (childProp) {
url.searchParams.set("childProp", childProp);
}
if (style) {
url.searchParams.set("style", style);
}

console.log(`\n${url}\n`);

await page.goto(url);
await page.goto(url.toString());
}

test.describe("render", () => {
test("should mount with undefined size and then update with measured size", async ({
page
}) => {
await startTest({ page });

await expect(page.getByTestId("code")).toHaveText(
JSON.stringify({
commits: [{}, { height: 150, width: 1000 }],
onResizeCalls: [{ height: 150, width: 1000 }]
})
);
});
for (const childProp of ["Child", "ChildComponent", "renderProp"]) {
test.describe(childProp, () => {
test("should mount with undefined size and then update with measured size", async ({
page
}) => {
await startTest({ childProp: childProp as ChildProp, page });

await expect(page.getByTestId("code")).toHaveText(
JSON.stringify({
commits: [{}, { height: 150, width: 1000 }],
onResizeCalls: [{ height: 150, width: 1000 }]
})
);

await page.setViewportSize({ height: 500, width: 500 });

await expect(page.getByTestId("code")).toHaveText(
JSON.stringify({
commits: [
{},
{ height: 150, width: 1000 },
{ height: 125, width: 500 }
],
onResizeCalls: [
{ height: 150, width: 1000 },
{ height: 125, width: 500 }
]
})
);
});
});
}

test("should support box: content-box", async ({ page }) => {
await startTest({
Expand Down
43 changes: 23 additions & 20 deletions lib/components/AutoSizer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,43 @@ describe("AutoSizer", () => {
vi.resetAllMocks();
});

it("should call Child with undefined size values during the initial render", async () => {
const Child = vi.fn(() => null);
it("should pass undefined size values during the initial render", async () => {
const ChildComponent = vi.fn(() => null);
const onResize = vi.fn();

renderForTest(<AutoSizer Child={Child} onResize={onResize} />);
renderForTest(
<AutoSizer ChildComponent={ChildComponent} onResize={onResize} />
);

expect(Child).toHaveBeenCalledTimes(1);
expect(Child).toHaveBeenCalledWith({}, undefined);
expect(ChildComponent).toHaveBeenCalledTimes(1);
expect(ChildComponent).toHaveBeenCalledWith({}, undefined);
expect(onResize).not.toHaveBeenCalled();
});

it("should pass Child width and height once mounted", async () => {
const Child = vi.fn(() => null);
it("should pass actual width and height once mounted", async () => {
const renderProp = vi.fn(() => null);
const onResize = vi.fn();

const { container } = renderForTest(
<AutoSizer Child={Child} onResize={onResize} />
<AutoSizer onResize={onResize} renderProp={renderProp} />
);

expect(Child).toHaveBeenCalled();
expect(renderProp).toHaveBeenCalled();
expect(onResize).not.toHaveBeenCalled();

Child.mockReset();
renderProp.mockReset();

await simulateResize({
element: container,
height: 100,
width: 200
});

expect(Child).toHaveBeenCalledTimes(1);
expect(Child).toHaveBeenCalledWith(
{
height: 100,
width: 200
},
undefined
);
expect(renderProp).toHaveBeenCalledTimes(1);
expect(renderProp).toHaveBeenCalledWith({
height: 100,
width: 200
});

expect(onResize).toHaveBeenCalledTimes(1);
expect(onResize).toHaveBeenCalledWith({
Expand All @@ -55,10 +54,14 @@ describe("AutoSizer", () => {
});

describe("HTML", () => {
function Child() {
return null;
}

it("pass through additional attributes", () => {
const { container } = renderForTest(
<AutoSizer
Child={undefined}
Child={Child}
className="foo"
data-testid="test-id"
id="auto-sizer"
Expand All @@ -77,7 +80,7 @@ describe("AutoSizer", () => {

it("should support a different tagName", () => {
const { container } = renderForTest(
<AutoSizer Child={undefined} tagName="span" />
<AutoSizer Child={Child} tagName="span" />
);

const element = container.querySelector(
Expand Down
Loading
Loading