Skip to content

Commit

Permalink
Add Virtualizer and WindowVirtualizer
Browse files Browse the repository at this point in the history
  • Loading branch information
inokawa committed Jan 7, 2024
1 parent 1e7d7e1 commit 5728cb4
Show file tree
Hide file tree
Showing 49 changed files with 4,391 additions and 3,458 deletions.
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

0 comments on commit 5728cb4

Please sign in to comment.