Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Virtualizer and WindowVirtualizer #303

Merged
merged 1 commit into from
Jan 7, 2024
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
10 changes: 8 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
"limit": "4 kB"
},
{
"name": "WVList",
"name": "Virtualizer",
"path": "lib/index.mjs",
"import": "{ WVList }",
"import": "{ Virtualizer }",
"limit": "4 kB"
},
{
"name": "WindowVirtualizer",
"path": "lib/index.mjs",
"import": "{ WindowVirtualizer }",
"limit": "4 kB"
},
{
Expand Down
2 changes: 1 addition & 1 deletion .storybook/preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default {
storySort: {
order: [
"basics",
["VList", "WVList", "VGrid"],
["VList", "Virtualizer", "WindowVirtualizer", "VGrid"],
"advanced",
"comparisons",
],
Expand Down
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,45 @@ export const App = () => {
};
```

#### Customization

`VList` is a recommended solution which works like a drop-in replacement of simple list built with scrollable `div` (or removed [virtual-scroller element](https://github.com/WICG/virtual-scroller)). For more complicated styling or markup, use `Virtualizer`.

```tsx
import { Virtualizer } from "virtua";

export const App = () => {
return (
<div style={{ overflowY: "auto", height: 800 }}>
<div style={{ height: 40 }}>header</div>
<Virtualizer startMargin={40}>
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
style={{
height: Math.floor(Math.random() * 10) * 10 + 10,
borderBottom: "solid 1px gray",
background: "white",
}}
>
{i}
</div>
))}
</Virtualizer>
</div>
);
};
```

#### Window scroll

```tsx
import { WVList } from "virtua";
import { WindowVirtualizer } from "virtua";

export const App = () => {
return (
<div style={{ padding: 200 }}>
<WVList>
<WindowVirtualizer>
{Array.from({ length: 1000 }).map((_, i) => (
<div
key={i}
Expand All @@ -109,7 +139,7 @@ export const App = () => {
{i}
</div>
))}
</WVList>
</WindowVirtualizer>
</div>
);
};
Expand Down Expand Up @@ -141,7 +171,7 @@ export const App = () => {

#### React Server Components (RSC) support

This library is marked as a Client Component. You can render RSC as children of VList or WVList.
This library is marked as a Client Component. You can render RSC as children of `VList`, `Virtualizer` or `WindowVirtualizer`.

```tsx
// page.tsx in App Router of Next.js
Expand Down Expand Up @@ -257,7 +287,7 @@ It may be dispatched by ResizeObserver in this lib [as described in spec](https:
| Horizontal scroll in RTL direction | ✅ | ❌ | ✅ ([may be dropped in v2](https://github.com/bvaughn/react-window/issues/302)) | ❌ | ❌ | ❌ | ❌ |
| Grid (Virtualization for two dimension) | 🟠 (experimental_VGrid) | ❌ | ✅ (FixedSizeGrid / VariableSizeGrid) | ✅ ([Grid](https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md)) | 🟠 (needs customization) | ❌ | 🟠 (needs customization) |
| Table | 🟠 (needs customization) | ✅ (TableVirtuoso) | 🟠 (needs customization) | ✅ ([Table](https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md)) | 🟠 (needs customization) | ❌ | 🟠 (needs customization) |
| Window scroller | ✅ (WVList) | ✅ | ❌ | ✅ ([WindowScroller](https://github.com/bvaughn/react-virtualized/blob/master/docs/WindowScroller.md)) | ✅ | ❌ | ❌ |
| Window scroller | ✅ (WindowVirtualizer) | ✅ | ❌ | ✅ ([WindowScroller](https://github.com/bvaughn/react-virtualized/blob/master/docs/WindowScroller.md)) | ✅ | ❌ | ❌ |
| Dynamic list size | ✅ | ✅ | 🟠 (needs [AutoSizer](https://github.com/bvaughn/react-virtualized/blob/master/docs/AutoSizer.md)) | 🟠 (needs [AutoSizer](https://github.com/bvaughn/react-virtualized/blob/master/docs/AutoSizer.md)) | ✅ | ❌ | ✅ |
| Dynamic item size | ✅ | ✅ | 🟠 (needs additional codes and has wrong destination when scrolling to item imperatively) | 🟠 (needs [CellMeasurer](https://github.com/bvaughn/react-virtualized/blob/master/docs/CellMeasurer.md) and has wrong destination when scrolling to item imperatively) | 🟠 (has wrong destination when scrolling to item imperatively) | ❌ | 🟠 (has wrong destination when scrolling to item imperatively) |
| Reverse scroll | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Expand Down
42 changes: 0 additions & 42 deletions e2e/VList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,48 +91,6 @@ test.describe("smoke", () => {
expect(initialTotalHeight).toEqual(changedTotalHeight);
});

test("padding", async ({ page }) => {
await page.goto(storyUrl("basics-vlist--padding-and-margin"));

const component = await page.waitForSelector(scrollableSelector);
await component.waitForElementState("stable");

const [topPadding, bottomPadding] = await component.evaluate((e) => {
const s = getComputedStyle(e);
return [parseInt(s.paddingTop), parseInt(s.paddingBottom)];
});
await expect(topPadding).toBeGreaterThan(10);
await expect(bottomPadding).toBeGreaterThan(10);

const itemsSelector = '*[style*="top"]';

// check if start is displayed
const topItem = (await component.$$(itemsSelector))[0];
await expect(await topItem.textContent()).toEqual("0");
await expect(
await (async () => {
const rootRect = (await component.boundingBox())!;
const itemRect = (await topItem.boundingBox())!;
return itemRect.y - rootRect.y;
})()
).toEqual(topPadding);

// scroll to the end
await scrollToBottom(component);

// check if the end is displayed
const items = await component.$$(itemsSelector);
const bottomItem = items[items.length - 1];
await expect(await bottomItem.textContent()).toEqual("999");
await expect(
await (async () => {
const rootRect = (await component.boundingBox())!;
const itemRect = (await bottomItem.boundingBox())!;
return rootRect.y + rootRect.height - (itemRect.y + itemRect.height);
})()
).toEqual(bottomPadding);
});

test("sticky", async ({ page }) => {
await page.goto(storyUrl("basics-vlist--sticky"));

Expand Down
106 changes: 106 additions & 0 deletions e2e/Virtualizer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { test, expect } from "@playwright/test";
import {
storyUrl,
scrollableSelector,
scrollToBottom,
expectInRange,
} from "./utils";

test("header and footer", async ({ page }) => {
await page.goto(storyUrl("basics-virtualizer--header-and-footer"));

const scrollable = await page.waitForSelector(scrollableSelector);
await scrollable.waitForElementState("stable");
const container = await scrollable.evaluateHandle(
(e) => e.firstElementChild!.nextElementSibling!
);

const [topPadding, bottomPadding] = await scrollable.evaluate((e) => {
const topSpacer = e.firstElementChild as HTMLElement;
const bottomSpacer = e.lastElementChild as HTMLElement;
return [
parseInt(getComputedStyle(topSpacer).height),
parseInt(getComputedStyle(bottomSpacer).height),
];
});
await expect(topPadding).toBeGreaterThan(10);
await expect(bottomPadding).toBeGreaterThan(10);

const itemsSelector = '*[style*="top"]';

// check if start is displayed
const topItem = (await container.$$(itemsSelector))[0];
await expect(await topItem.textContent()).toEqual("0");
await expect(
await (async () => {
const rootRect = (await scrollable.boundingBox())!;
const itemRect = (await topItem.boundingBox())!;
return itemRect.y - rootRect.y;
})()
).toEqual(topPadding);

// scroll to the end
await scrollToBottom(scrollable);

// check if the end is displayed
const items = await container.$$(itemsSelector);
const bottomItem = items[items.length - 1];
await expect(await bottomItem.textContent()).toEqual("999");
await expect(
await (async () => {
const rootRect = (await scrollable.boundingBox())!;
const itemRect = (await bottomItem.boundingBox())!;
return rootRect.y + rootRect.height - (itemRect.y + itemRect.height);
})()
).toEqual(bottomPadding);
});

test("sticky header and footer", async ({ page }) => {
await page.goto(storyUrl("basics-virtualizer--sticky-header-and-footer"));

const scrollable = await page.waitForSelector(scrollableSelector);
await scrollable.waitForElementState("stable");
const container = await scrollable.evaluateHandle(
(e) => e.firstElementChild!.nextElementSibling!
);

const [topPadding, bottomPadding] = await scrollable.evaluate((e) => {
const topSpacer = e.firstElementChild as HTMLElement;
const bottomSpacer = e.lastElementChild as HTMLElement;
return [
parseInt(getComputedStyle(topSpacer).height),
parseInt(getComputedStyle(bottomSpacer).height),
];
});
await expect(topPadding).toBeGreaterThan(10);
await expect(bottomPadding).toBeGreaterThan(10);

const itemsSelector = '*[style*="top"]';

// check if start is displayed
const topItem = (await container.$$(itemsSelector))[0];
await expect(await topItem.textContent()).toEqual("0");
await expect(
await (async () => {
const rootRect = (await scrollable.boundingBox())!;
const itemRect = (await topItem.boundingBox())!;
return itemRect.y - rootRect.y;
})()
).toEqual(topPadding);

// scroll to the end
await scrollToBottom(scrollable);

// check if the end is displayed
const items = await container.$$(itemsSelector);
const bottomItem = items[items.length - 1];
await expect(await bottomItem.textContent()).toEqual("999");
expectInRange(
await (async () => {
const rootRect = (await scrollable.boundingBox())!;
const itemRect = (await bottomItem.boundingBox())!;
return rootRect.y + rootRect.height - (itemRect.y + itemRect.height);
})(),
{ min: bottomPadding, max: bottomPadding + 1 }
);
});
Loading