From 255b959102707d7bebe08c0af2991d63950ce3c6 Mon Sep 17 00:00:00 2001 From: inokawa <48897392+inokawa@users.noreply.github.com> Date: Sat, 30 Dec 2023 21:10:17 +0900 Subject: [PATCH] Add Virtualizer and WindowVirtualizer --- .size-limit.json | 10 +- .storybook/preview.jsx | 2 +- README.md | 40 +- e2e/VList.spec.ts | 42 - e2e/Virtualizer.spec.ts | 106 ++ ...List.spec.ts => WindowVirtualizer.spec.ts} | 56 +- package-lock.json | 1034 ++++++++++- package.json | 3 +- src/core/resizer.ts | 77 +- src/core/scroller.ts | 40 +- src/core/store.ts | 59 +- src/react/ListItem.tsx | 2 +- src/react/VGrid.tsx | 87 +- src/react/VList.spec.tsx | 27 +- src/react/VList.tsx | 402 +---- src/react/Viewport.tsx | 64 - src/react/Virtualizer.spec.tsx | 57 + src/react/Virtualizer.tsx | 364 ++++ src/react/WVList.ssr.spec.tsx | 93 - ...pec.tsx => WindowVirtualizer.rtl.spec.tsx} | 10 +- ...st.spec.tsx => WindowVirtualizer.spec.tsx} | 163 +- src/react/WindowVirtualizer.ssr.spec.tsx | 99 ++ .../{WVList.tsx => WindowVirtualizer.tsx} | 161 +- .../__snapshots__/VList.rtl.spec.tsx.snap | 4 +- src/react/__snapshots__/VList.spec.tsx.snap | 94 +- .../__snapshots__/VList.ssr.spec.tsx.snap | 8 +- .../__snapshots__/Virtualizer.spec.tsx.snap | 49 + .../__snapshots__/WVList.rtl.spec.tsx.snap | 272 --- src/react/__snapshots__/WVList.spec.tsx.snap | 1505 ----------------- .../__snapshots__/WVList.ssr.spec.tsx.snap | 9 - .../WindowVirtualizer.rtl.spec.tsx.snap | 264 +++ .../WindowVirtualizer.spec.tsx.snap | 1360 +++++++++++++++ .../WindowVirtualizer.ssr.spec.tsx.snap | 9 + src/react/index.ts | 15 +- src/react/types.ts | 17 + src/react/utils.ts | 5 - src/vue/VList.tsx | 66 +- src/vue/__snapshots__/VList.spec.ts.snap | 34 +- stories/react/advanced/Table.stories.tsx | 104 -- stories/react/advanced/With cmdk.stories.tsx | 85 + .../react/advanced/With radix-ui.stories.tsx | 64 +- .../With react-beautiful-dnd.stories.tsx | 117 +- .../advanced/With react-select.stories.tsx | 124 -- stories/react/basics/VList.stories.tsx | 155 +- stories/react/basics/Virtualizer.stories.tsx | 380 +++++ ...ries.tsx => WindowVirtualizer.stories.tsx} | 103 +- stories/react/common.tsx | 10 +- stories/vue/Basic.vue | 2 +- tsconfig.json | 2 +- 49 files changed, 4394 insertions(+), 3461 deletions(-) create mode 100644 e2e/Virtualizer.spec.ts rename e2e/{WVList.spec.ts => WindowVirtualizer.spec.ts} (79%) delete mode 100644 src/react/Viewport.tsx create mode 100644 src/react/Virtualizer.spec.tsx create mode 100644 src/react/Virtualizer.tsx delete mode 100644 src/react/WVList.ssr.spec.tsx rename src/react/{WVList.rtl.spec.tsx => WindowVirtualizer.rtl.spec.tsx} (91%) rename src/react/{WVList.spec.tsx => WindowVirtualizer.spec.tsx} (82%) create mode 100644 src/react/WindowVirtualizer.ssr.spec.tsx rename src/react/{WVList.tsx => WindowVirtualizer.tsx} (62%) create mode 100644 src/react/__snapshots__/Virtualizer.spec.tsx.snap delete mode 100644 src/react/__snapshots__/WVList.rtl.spec.tsx.snap delete mode 100644 src/react/__snapshots__/WVList.spec.tsx.snap delete mode 100644 src/react/__snapshots__/WVList.ssr.spec.tsx.snap create mode 100644 src/react/__snapshots__/WindowVirtualizer.rtl.spec.tsx.snap create mode 100644 src/react/__snapshots__/WindowVirtualizer.spec.tsx.snap create mode 100644 src/react/__snapshots__/WindowVirtualizer.ssr.spec.tsx.snap create mode 100644 src/react/types.ts delete mode 100644 stories/react/advanced/Table.stories.tsx create mode 100644 stories/react/advanced/With cmdk.stories.tsx delete mode 100644 stories/react/advanced/With react-select.stories.tsx create mode 100644 stories/react/basics/Virtualizer.stories.tsx rename stories/react/basics/{WVList.stories.tsx => WindowVirtualizer.stories.tsx} (84%) diff --git a/.size-limit.json b/.size-limit.json index ffc33dd4e..6980801da 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -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" }, { diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx index ce73dbe35..d56b59afc 100644 --- a/.storybook/preview.jsx +++ b/.storybook/preview.jsx @@ -25,7 +25,7 @@ export default { storySort: { order: [ "basics", - ["VList", "WVList", "VGrid"], + ["VList", "Virtualizer", "WindowVirtualizer", "VGrid"], "advanced", "comparisons", ], diff --git a/README.md b/README.md index 91208c6ad..cc4b2ef44 100644 --- a/README.md +++ b/README.md @@ -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 ( +
+
header
+ + {Array.from({ length: 1000 }).map((_, i) => ( +
+ {i} +
+ ))} +
+
+ ); +}; +``` + #### Window scroll ```tsx -import { WVList } from "virtua"; +import { WindowVirtualizer } from "virtua"; export const App = () => { return (
- + {Array.from({ length: 1000 }).map((_, i) => (
{ {i}
))} -
+
); }; @@ -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 @@ -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 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | diff --git a/e2e/VList.spec.ts b/e2e/VList.spec.ts index 87cc24ebd..a4c8f5527 100644 --- a/e2e/VList.spec.ts +++ b/e2e/VList.spec.ts @@ -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")); diff --git a/e2e/Virtualizer.spec.ts b/e2e/Virtualizer.spec.ts new file mode 100644 index 000000000..6a4e3f5b9 --- /dev/null +++ b/e2e/Virtualizer.spec.ts @@ -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 } + ); +}); diff --git a/e2e/WVList.spec.ts b/e2e/WindowVirtualizer.spec.ts similarity index 79% rename from e2e/WVList.spec.ts rename to e2e/WindowVirtualizer.spec.ts index bdd8ec98c..49e0f131f 100644 --- a/e2e/WVList.spec.ts +++ b/e2e/WindowVirtualizer.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, Page } from "@playwright/test"; import { storyUrl, getFirstItem, @@ -8,13 +8,19 @@ import { windowScrollToLeft, } from "./utils"; -const wvListSelector = '*[style*="height: auto"],*[style*="width: auto"]'; +const getWindowVirtualizer = async (page: Page) => { + const selector = '*[style*="contain: content"]'; + const component = await page.waitForSelector(selector, { state: "attached" }); + await component.evaluate((e) => (e.style.visibility = "visible")); + await page.waitForSelector(selector); + return component; +}; test.describe("smoke", () => { test("vertically scrollable", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--default")); + await page.goto(storyUrl("basics-windowvirtualizer--default")); - const component = await page.waitForSelector(wvListSelector); + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); // check if start is displayed @@ -32,10 +38,9 @@ test.describe("smoke", () => { }); test("horizontally scrollable", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--horizontal")); + await page.goto(storyUrl("basics-windowvirtualizer--horizontal")); - await page.waitForSelector(wvListSelector); - const component = (await page.$$(wvListSelector))[0]!; + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); // check if start is displayed @@ -53,13 +58,13 @@ test.describe("smoke", () => { }); test("display: none", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--default")); + await page.goto(storyUrl("basics-windowvirtualizer--default")); - const component = await page.waitForSelector(wvListSelector); + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); const initialTotalHeight = await component.evaluate( - (s) => getComputedStyle(s.childNodes[0] as HTMLElement).height + (s) => getComputedStyle(s as HTMLElement).height ); await component.evaluate((s) => (s.style.display = "none")); @@ -67,7 +72,7 @@ test.describe("smoke", () => { await component.waitForElementState("stable"); const changedTotalHeight = await component.evaluate( - (s) => getComputedStyle(s.childNodes[0] as HTMLElement).height + (s) => getComputedStyle(s as HTMLElement).height ); expect(initialTotalHeight).toBeTruthy(); @@ -75,9 +80,9 @@ test.describe("smoke", () => { }); test("should not have minimum size", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--increasing-items")); + await page.goto(storyUrl("basics-windowvirtualizer--increasing-items")); - const component = await page.waitForSelector(wvListSelector); + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); expect(await component.evaluate((s) => document.body.scrollHeight)).toBe( @@ -88,8 +93,8 @@ test.describe("smoke", () => { // test.describe("check if scroll jump compensation works", () => { // test("vertical start -> end", async ({ page }) => { -// await page.goto(storyUrl("basics-wvlist--default")); -// const component = await page.waitForSelector(scrollableSelector); +// await page.goto(storyUrl("basics-windowvirtualizer--default")); +// const component = await getWindowVirtualizer(page); // await component.waitForElementState("stable"); // // check if start is displayed @@ -110,8 +115,8 @@ test.describe("smoke", () => { // }); // test("vertical end -> start", async ({ page }) => { -// await page.goto(storyUrl("basics-wvlist--default")); -// const component = await page.waitForSelector(scrollableSelector); +// await page.goto(storyUrl("basics-windowvirtualizer--default")); +// const component = await getWindowVirtualizer(page); // await component.waitForElementState("stable"); // // check if start is displayed @@ -139,8 +144,8 @@ test.describe("smoke", () => { // }); // test("horizontal start -> end", async ({ page }) => { -// await page.goto(storyUrl("basics-wvlist--horizontal")); -// const component = await page.waitForSelector(scrollableSelector); +// await page.goto(storyUrl("basics-windowvirtualizer--horizontal")); +// const component = await getWindowVirtualizer(page); // await component.waitForElementState("stable"); // // check if start is displayed @@ -161,8 +166,8 @@ test.describe("smoke", () => { // }); // test("horizontal end -> start", async ({ page }) => { -// await page.goto(storyUrl("basics-wvlist--horizontal")); -// const component = await page.waitForSelector(scrollableSelector); +// await page.goto(storyUrl("basics-windowvirtualizer--horizontal")); +// const component = await getWindowVirtualizer(page); // await component.waitForElementState("stable"); // // check if start is displayed @@ -192,14 +197,14 @@ test.describe("smoke", () => { test.describe("RTL", () => { test("vertically scrollable", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--default"), { + await page.goto(storyUrl("basics-windowvirtualizer--default"), { waitUntil: "domcontentloaded", }); await page.evaluate(() => { document.documentElement.dir = "rtl"; }); - const component = await page.waitForSelector(wvListSelector); + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); // check if start is displayed @@ -217,15 +222,14 @@ test.describe("RTL", () => { }); test("horizontally scrollable", async ({ page }) => { - await page.goto(storyUrl("basics-wvlist--horizontal"), { + await page.goto(storyUrl("basics-windowvirtualizer--horizontal"), { waitUntil: "domcontentloaded", }); await page.evaluate(() => { document.documentElement.dir = "rtl"; }); - await page.waitForSelector(wvListSelector); - const component = (await page.$$(wvListSelector))[0]!; + const component = await getWindowVirtualizer(page); await component.waitForElementState("stable"); // check if start is displayed diff --git a/package-lock.json b/package-lock.json index 9bd3532c4..e61b3111f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@types/react-window": "1.8.8", "@typescript-eslint/parser": "6.17.0", "@vitejs/plugin-vue-jsx": "3.1.0", + "cmdk": "0.2.0", "concurrently": "8.2.2", "eslint": "8.56.0", "eslint-plugin-react-hooks": "4.6.0", @@ -50,8 +51,6 @@ "react-content-loader": "6.2.1", "react-dom": "18.2.0", "react-is": "18.2.0", - "react-merge-refs": "2.1.1", - "react-select": "5.8.0", "react-virtualized": "9.22.5", "react-virtuoso": "4.6.2", "react-window": "1.8.10", @@ -4456,6 +4455,120 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz", + "integrity": "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.0", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-slot": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -4474,6 +4587,241 @@ } } }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", + "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", + "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", + "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", @@ -4592,6 +4940,56 @@ } } }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", + "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-escape-keydown/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", @@ -8137,6 +8535,18 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -9068,6 +9478,20 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-0.2.0.tgz", + "integrity": "sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==", + "dev": true, + "dependencies": { + "@radix-ui/react-dialog": "1.0.0", + "command-score": "0.1.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -9111,6 +9535,12 @@ "node": ">= 0.8" } }, + "node_modules/command-score": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", + "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==", + "dev": true + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -9988,6 +10418,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true + }, "node_modules/detect-package-manager": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", @@ -11640,6 +12076,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/get-npm-tarball-url": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", @@ -12281,6 +12726,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -17787,16 +18241,6 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "dev": true }, - "node_modules/react-merge-refs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-2.1.1.tgz", - "integrity": "sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, "node_modules/react-redux": { "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", @@ -17828,32 +18272,75 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "node_modules/react-select": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", - "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "node_modules/react-remove-scroll": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", + "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", + "dev": true, + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", "dev": true, "dependencies": { - "@babel/runtime": "^7.12.0", - "@emotion/cache": "^11.4.0", - "@emotion/react": "^11.8.1", - "@floating-ui/dom": "^1.0.1", - "@types/react-transition-group": "^4.4.0", - "memoize-one": "^6.0.0", - "prop-types": "^15.6.0", - "react-transition-group": "^4.3.0", - "use-isomorphic-layout-effect": "^1.1.2" + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/react-select/node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "dev": true + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dev": true, + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/react-transition-group": { "version": "4.4.5", @@ -19855,12 +20342,19 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { @@ -19878,6 +20372,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dev": true, + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -24013,6 +24529,98 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-dialog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz", + "integrity": "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.0", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-slot": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.4" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, "@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -24022,6 +24630,192 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-dismissable-layer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", + "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.0" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, + "@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-focus-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", + "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, + "@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, + "@radix-ui/react-portal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", + "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } + } + }, "@radix-ui/react-presence": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", @@ -24080,6 +24874,48 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", + "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + } + } + }, "@radix-ui/react-use-layout-effect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", @@ -26578,6 +27414,15 @@ "sprintf-js": "~1.0.2" } }, + "aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, "aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -27263,6 +28108,16 @@ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "dev": true }, + "cmdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-0.2.0.tgz", + "integrity": "sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==", + "dev": true, + "requires": { + "@radix-ui/react-dialog": "1.0.0", + "command-score": "0.1.2" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -27299,6 +28154,12 @@ "delayed-stream": "~1.0.0" } }, + "command-score": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", + "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==", + "dev": true + }, "commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -27970,6 +28831,12 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true + }, "detect-package-manager": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", @@ -29254,6 +30121,12 @@ "hasown": "^2.0.0" } }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true + }, "get-npm-tarball-url": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", @@ -29718,6 +30591,15 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -33783,12 +34665,6 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "dev": true }, - "react-merge-refs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-2.1.1.tgz", - "integrity": "sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==", - "dev": true - }, "react-redux": { "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", @@ -33811,29 +34687,38 @@ } } }, - "react-select": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", - "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "react-remove-scroll": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", + "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", "dev": true, "requires": { - "@babel/runtime": "^7.12.0", - "@emotion/cache": "^11.4.0", - "@emotion/react": "^11.8.1", - "@floating-ui/dom": "^1.0.1", - "@types/react-transition-group": "^4.4.0", - "memoize-one": "^6.0.0", - "prop-types": "^15.6.0", - "react-transition-group": "^4.3.0", - "use-isomorphic-layout-effect": "^1.1.2" - }, - "dependencies": { - "memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "dev": true - } + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + } + }, + "react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dev": true, + "requires": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + } + }, + "react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dev": true, + "requires": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" } }, "react-transition-group": { @@ -35346,11 +36231,14 @@ "requires-port": "^1.0.0" } }, - "use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "dev": true + "use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } }, "use-memo-one": { "version": "1.1.3", @@ -35358,6 +36246,16 @@ "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", "dev": true }, + "use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dev": true, + "requires": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + } + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index 6b9b876f5..7a69fca21 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/react-window": "1.8.8", "@typescript-eslint/parser": "6.17.0", "@vitejs/plugin-vue-jsx": "3.1.0", + "cmdk": "0.2.0", "concurrently": "8.2.2", "eslint": "8.56.0", "eslint-plugin-react-hooks": "4.6.0", @@ -79,8 +80,6 @@ "react-content-loader": "6.2.1", "react-dom": "18.2.0", "react-is": "18.2.0", - "react-merge-refs": "2.1.1", - "react-select": "5.8.0", "react-virtualized": "9.22.5", "react-virtuoso": "4.6.2", "react-window": "1.8.10", diff --git a/src/core/resizer.ts b/src/core/resizer.ts index dcf595f83..84ab7a1d4 100644 --- a/src/core/resizer.ts +++ b/src/core/resizer.ts @@ -4,9 +4,7 @@ import { ItemResize, VirtualStore, } from "./store"; -import { exists, computeStyle, getStyleNumber, max, once } from "./utils"; - -const rootObserveOpts: ResizeObserverOptions = { box: "border-box" }; +import { exists, max, once } from "./utils"; /** * @internal @@ -14,8 +12,9 @@ const rootObserveOpts: ResizeObserverOptions = { box: "border-box" }; export type ItemResizeObserver = (el: HTMLElement, i: number) => () => void; interface ListResizer { - _observeRoot(viewportElement: HTMLElement): () => void; + _observeRoot(viewportElement: HTMLElement): void; _observeItem: ItemResizeObserver; + _dispose(): void; } /** @@ -39,17 +38,7 @@ export const createResizer = ( if (!(target as HTMLElement).offsetParent) continue; if (target === viewportElement) { - store._update(ACTION_VIEWPORT_RESIZE, [ - contentRect[sizeKey], - contentRect[isHorizontal ? "left" : "top"], - // contentRect doesn't have paddingRight/paddingBottom so get them from computed style - // https://www.w3.org/TR/resize-observer/#css-definitions - getStyleNumber( - computeStyle(viewportElement)[ - isHorizontal ? "paddingRight" : "paddingBottom" - ] - ), - ]); + store._update(ACTION_VIEWPORT_RESIZE, contentRect[sizeKey]); } else { const index = mountedIndexes.get(target); if (exists(index)) { @@ -66,12 +55,7 @@ export const createResizer = ( return { _observeRoot(viewport: HTMLElement) { - viewportElement = viewport; - const ro = getResizeObserver(); - ro.observe(viewport, rootObserveOpts); - return () => { - ro.disconnect(); - }; + getResizeObserver().observe((viewportElement = viewport)); }, _observeItem: (el: HTMLElement, i: number) => { const ro = getResizeObserver(); @@ -82,12 +66,16 @@ export const createResizer = ( ro.unobserve(el); }; }, + _dispose() { + getResizeObserver().disconnect(); + }, }; }; interface WindowListResizer { - _observeRoot(): () => void; + _observeRoot(): void; _observeItem: ItemResizeObserver; + _dispose(): void; } /** @@ -121,18 +109,14 @@ export const createWindowResizer = ( } }); }); + const onWindowResize = () => { + store._update(ACTION_VIEWPORT_RESIZE, window[windowSizeKey]); + }; return { _observeRoot() { - const cb = () => { - store._update(ACTION_VIEWPORT_RESIZE, [window[windowSizeKey], 0, 0]); - }; - window.addEventListener("resize", cb); - cb(); - return () => { - window.removeEventListener("resize", cb); - getResizeObserver().disconnect(); - }; + window.addEventListener("resize", onWindowResize); + onWindowResize(); }, _observeItem: (el: HTMLElement, i: number) => { const ro = getResizeObserver(); @@ -143,6 +127,10 @@ export const createWindowResizer = ( ro.unobserve(el); }; }, + _dispose() { + window.removeEventListener("resize", onWindowResize); + getResizeObserver().disconnect(); + }, }; }; @@ -180,21 +168,8 @@ export const createGridResizer = ( if (!(target as HTMLElement).offsetParent) continue; if (target === viewportElement) { - // contentRect doesn't have paddingRight/paddingBottom so get them from computed style - // https://www.w3.org/TR/resize-observer/#css-definitions - // TODO subtract scroll bar width/height - // https://github.com/w3c/csswg-drafts/issues/3536 - const style = computeStyle(viewportElement); - vStore._update(ACTION_VIEWPORT_RESIZE, [ - contentRect[heightKey], - contentRect.top, - getStyleNumber(style.paddingBottom), - ]); - hStore._update(ACTION_VIEWPORT_RESIZE, [ - contentRect[widthKey], - contentRect.left, - getStyleNumber(style.paddingRight), - ]); + vStore._update(ACTION_VIEWPORT_RESIZE, contentRect[heightKey]); + hStore._update(ACTION_VIEWPORT_RESIZE, contentRect[widthKey]); } else { const cell = mountedIndexes.get(target); if (cell) { @@ -267,12 +242,7 @@ export const createGridResizer = ( return { _observeRoot(viewport: HTMLElement) { - viewportElement = viewport; - const ro = getResizeObserver(); - ro.observe(viewport, rootObserveOpts); - return () => { - ro.disconnect(); - }; + getResizeObserver().observe((viewportElement = viewport)); }, _observeItem(el: HTMLElement, rowIndex: number, colIndex: number) { const ro = getResizeObserver(); @@ -285,6 +255,9 @@ export const createGridResizer = ( ro.unobserve(el); }; }, + _dispose() { + getResizeObserver().disconnect(); + }, }; }; diff --git a/src/core/scroller.ts b/src/core/scroller.ts index 0e3eb7f57..b4a51a427 100644 --- a/src/core/scroller.ts +++ b/src/core/scroller.ts @@ -21,7 +21,7 @@ import { debounce, throttle, timeout, clamp } from "./utils"; const createOnWheel = ( store: VirtualStore, isHorizontal: boolean, - onScrollStopped: () => void + onScrollEnd: () => void ) => { return throttle((e: WheelEvent) => { if (store._getScrollDirection() === SCROLL_IDLE) { @@ -38,7 +38,7 @@ const createOnWheel = ( if (isHorizontal ? e.deltaX : e.deltaY) { const offset = store._getScrollOffset(); if (offset > 0 && offset < store._getMaxScrollOffset()) { - onScrollStopped(); + onScrollEnd(); } } }, 50); @@ -61,7 +61,8 @@ const normalizeRTLOffset = ( * @internal */ export type Scroller = { - _observe: (viewportElement: HTMLElement) => () => void; + _observe: (viewportElement: HTMLElement) => void; + _dispose(): void; _scrollTo: (offset: number) => void; _scrollBy: (offset: number) => void; _scrollToIndex: (index: number, opts?: ScrollToIndexOpts) => void; @@ -76,6 +77,7 @@ export const createScroller = ( isHorizontal: boolean ): Scroller => { let viewportElement: HTMLElement | undefined; + let cleanup: (() => void) | undefined; let cancelScroll: (() => void) | undefined; let stillMomentumScrolling = false; const scrollToKey = isHorizontal ? "scrollLeft" : "scrollTop"; @@ -168,10 +170,10 @@ export const createScroller = ( let touching = false; let justTouchEnded = false; - const onScrollStopped = debounce(() => { + const onScrollEnd = debounce(() => { if (touching) { // Wait while touching - onScrollStopped(); + onScrollEnd(); return; } @@ -186,10 +188,10 @@ export const createScroller = ( } store._update(ACTION_SCROLL, normalizeOffset(viewport[scrollToKey])); - onScrollStopped(); + onScrollEnd(); }; - const onWheel = createOnWheel(store, isHorizontal, onScrollStopped); + const onWheel = createOnWheel(store, isHorizontal, onScrollEnd); const onTouchStart = () => { touching = true; @@ -207,14 +209,17 @@ export const createScroller = ( viewport.addEventListener("touchstart", onTouchStart, { passive: true }); viewport.addEventListener("touchend", onTouchEnd, { passive: true }); - return () => { + cleanup = () => { viewport.removeEventListener("scroll", onScroll); viewport.removeEventListener("wheel", onWheel); viewport.removeEventListener("touchstart", onTouchStart); viewport.removeEventListener("touchend", onTouchEnd); - onScrollStopped._cancel(); + onScrollEnd._cancel(); }; }, + _dispose() { + cleanup && cleanup(); + }, _scrollTo(offset) { scrollManually(() => offset); }, @@ -283,7 +288,8 @@ export const createScroller = ( * @internal */ export type WindowScroller = { - _observe: (containerElement: HTMLElement) => () => void; + _observe(containerElement: HTMLElement): void; + _dispose(): void; _fixScrollJump: (jump: number) => void; }; @@ -295,6 +301,7 @@ export const createWindowScroller = ( isHorizontal: boolean ): WindowScroller => { let containerElement: HTMLElement | undefined; + let cleanup: (() => void) | undefined; const scrollToKey = isHorizontal ? "scrollX" : "scrollY"; const offsetKey = isHorizontal ? "offsetLeft" : "offsetTop"; @@ -325,7 +332,7 @@ export const createWindowScroller = ( return getOffsetToWindow(parent as HTMLElement, nodeOffset); }; - const onScrollStopped = debounce(() => { + const onScrollEnd = debounce(() => { store._update(ACTION_SCROLL_END); }, 150); @@ -334,20 +341,23 @@ export const createWindowScroller = ( ACTION_SCROLL, normalizeOffset(window[scrollToKey]) - getOffsetToWindow(container, 0) ); - onScrollStopped(); + onScrollEnd(); }; - const onWheel = createOnWheel(store, isHorizontal, onScrollStopped); + const onWheel = createOnWheel(store, isHorizontal, onScrollEnd); window.addEventListener("scroll", onScroll); window.addEventListener("wheel", onWheel, { passive: true }); - return () => { + cleanup = () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("wheel", onWheel); - onScrollStopped._cancel(); + onScrollEnd._cancel(); }; }, + _dispose() { + cleanup && cleanup(); + }, _fixScrollJump: (jump) => { // TODO support case two window scrollers exist in the same view window.scrollBy( diff --git a/src/core/store.ts b/src/core/store.ts index 63f76e847..2be21fb6b 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -54,7 +54,7 @@ export const ACTION_BEFORE_MANUAL_SMOOTH_SCROLL = 7; type Actions = | [type: typeof ACTION_ITEM_RESIZE, entries: ItemResize[]] - | [type: typeof ACTION_VIEWPORT_RESIZE, size: ViewportResize] + | [type: typeof ACTION_VIEWPORT_RESIZE, size: number] | [ type: typeof ACTION_ITEMS_LENGTH_CHANGE, arg: [length: number, isShift?: boolean | undefined] @@ -71,14 +71,31 @@ export const UPDATE_SIZE_STATE = 0b0010; /** @internal */ export const UPDATE_SCROLL_EVENT = 0b0100; /** @internal */ -export const UPDATE_SCROLL_STOP_EVENT = 0b1000; - -type ViewportResize = [size: number, paddingStart: number, paddingEnd: number]; +export const UPDATE_SCROLL_END_EVENT = 0b1000; /** @internal */ export type ItemResize = Readonly<[index: number, size: number]>; type ItemsRange = Readonly<[startIndex: number, endIndex: number]>; +/** + * @internal + */ +export const getScrollSize = (store: VirtualStore): number => { + return max(store._getTotalSize(), store._getViewportSize()); +}; + +/** + * @internal + */ +export const getMinContainerSize = (store: VirtualStore): number => { + return max( + store._getTotalSize(), + store._getViewportSize() - + store._getStartSpacerSize() - + store._getEndSpacerSize() + ); +}; + /** * @internal */ @@ -143,7 +160,7 @@ export type VirtualStore = { _getScrollDirection(): ScrollDirection; _getViewportSize(): number; _getStartSpacerSize(): number; - _getScrollSize(): number; + _getEndSpacerSize(): number; _getTotalSize(): number; _getJumpCount(): number; _flushJump(): number; @@ -159,13 +176,13 @@ export const createVirtualStore = ( itemSize: number = 40, ssrCount: number = 0, cache: Cache = initCache(elementsCount, itemSize), - shouldAutoEstimateItemSize?: boolean + shouldAutoEstimateItemSize?: boolean | undefined, + startSpacerSize: number = 0, + endSpacerSize: number = 0 ): VirtualStore => { let isSSR = !!ssrCount; let stateVersion: StateVersion = []; let viewportSize = 0; - let startSpacerSize = 0; - let endSpacerSize = 0; let scrollOffset = 0; let jumpCount = 0; let jump = 0; @@ -180,11 +197,12 @@ export const createVirtualStore = ( const subscribers = new Set<[number, Subscriber]>(); const getTotalSize = (): number => computeTotalSize(cache); - const getViewportSizeWithoutSpacer = () => - viewportSize - startSpacerSize - endSpacerSize; - const getMaxScrollOffset = () => + const getScrollableSize = (): number => + getTotalSize() + startSpacerSize + endSpacerSize; + const getRelativeScrollOffset = (): number => scrollOffset - startSpacerSize; + const getMaxScrollOffset = (): number => // total size can become smaller than viewport size - max(0, getTotalSize() - getViewportSizeWithoutSpacer()); + max(0, getScrollableSize() - viewportSize); const applyJump = (j: number) => { // In iOS WebKit browsers, updating scroll position will stop scrolling so it have to be deferred during scrolling. @@ -216,7 +234,7 @@ export const createVirtualStore = ( } return (_prevRange = computeRange( cache, - scrollOffset + pendingJump + jump, + getRelativeScrollOffset() + pendingJump + jump, _prevRange[0], viewportSize )); @@ -255,15 +273,15 @@ export const createVirtualStore = ( _getStartSpacerSize() { return startSpacerSize; }, - _getScrollSize() { - return max(getTotalSize(), getViewportSizeWithoutSpacer()); + _getEndSpacerSize() { + return endSpacerSize; }, _getTotalSize: getTotalSize, _getJumpCount() { return jumpCount; }, _flushJump() { - if (getViewportSizeWithoutSpacer() > getTotalSize()) { + if (viewportSize > getScrollableSize()) { // In this case applying jump will not cause scroll. // Current logic expects scroll event occurs after applying jump so discard it. return (jump = 0); @@ -349,11 +367,8 @@ export const createVirtualStore = ( break; } case ACTION_VIEWPORT_RESIZE: { - const total = payload[0] + payload[1] + payload[2]; - if (viewportSize !== total) { - viewportSize = total; - startSpacerSize = payload[1]; - endSpacerSize = payload[2]; + if (viewportSize !== payload) { + viewportSize = payload; mutated = UPDATE_SIZE_STATE; } break; @@ -427,7 +442,7 @@ export const createVirtualStore = ( break; } case ACTION_SCROLL_END: { - mutated = UPDATE_SCROLL_STOP_EVENT; + mutated = UPDATE_SCROLL_END_EVENT; if (_scrollDirection !== SCROLL_IDLE) { shouldFlushPendingJump = true; mutated += UPDATE_SCROLL_STATE; diff --git a/src/react/ListItem.tsx b/src/react/ListItem.tsx index eabf5924b..4a571fc1f 100644 --- a/src/react/ListItem.tsx +++ b/src/react/ListItem.tsx @@ -12,7 +12,7 @@ import { refKey } from "./utils"; import { isRTLDocument } from "../core/environment"; /** - * Props of customized item component for {@link VList}. + * Props of customized item component for {@link Virtualizer} or {@link WindowVirtualizer}. */ export interface CustomItemComponentProps { style: CSSProperties; diff --git a/src/react/VGrid.tsx b/src/react/VGrid.tsx index 7cb642bef..f46876612 100644 --- a/src/react/VGrid.tsx +++ b/src/react/VGrid.tsx @@ -16,23 +16,70 @@ import { overscanStartIndex, createVirtualStore, SCROLL_IDLE, + getScrollSize, } from "../core/store"; import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; import { values } from "../core/utils"; import { createScroller } from "../core/scroller"; -import { emptyComponents, refKey } from "./utils"; +import { refKey } from "./utils"; import { useStatic } from "./useStatic"; import { createGridResizer, GridResizer } from "../core/resizer"; -import { - Viewport as DefaultViewport, - CustomViewportComponent, - CustomViewportComponentProps, - ViewportComponentAttributes, -} from "./Viewport"; +import { ViewportComponentAttributes } from "./types"; import { flushSync } from "react-dom"; import { isRTLDocument } from "../core/environment"; import { useRerender } from "./useRerender"; +/** + * Props of customized scrollable component. + */ +interface CustomViewportComponentProps { + /** + * Renderable item elements. + */ + children: ReactNode; + /** + * Attributes that should be passed to the scrollable element. + */ + attrs: ViewportComponentAttributes; + /** + * Total height of items. It's undefined if component is not vertically scrollable. + */ + height: number | undefined; + /** + * Total width of items. It's undefined if component is not horizontally scrollable. + */ + width: number | undefined; + /** + * Currently component is scrolling or not. + */ + scrolling: boolean; +} + +const DefaultViewport = forwardRef( + ({ children, attrs, width, height, scrolling }, ref): ReactElement => { + return ( +
+
{ + return { + contain: "content", + position: "relative", + visibility: "hidden", + width: width ?? "100%", + height: height ?? "100%", + pointerEvents: scrolling ? "none" : "auto", + }; + }, [width, height, scrolling])} + > + {children} +
+
+ ); + } +); + +export type CustomViewportComponent = typeof DefaultViewport; + const genKey = (i: number, j: number) => `${i}-${j}`; /** @@ -47,10 +94,6 @@ export type CustomCellComponent = React.ForwardRefExoticComponent< React.PropsWithoutRef & React.RefAttributes >; -type CustomCellComponentOrElement = - | keyof JSX.IntrinsicElements - | CustomCellComponent; - type CellProps = { _children: ReactNode; _resizer: GridResizer; @@ -206,7 +249,7 @@ export interface VGridProps extends ViewportComponentAttributes { * Component or element type for cell element. This component will get {@link CustomCellComponentProps} as props. * @defaultValue "div" */ - Cell?: CustomCellComponentOrElement; + Cell?: keyof JSX.IntrinsicElements | CustomCellComponent; }; } @@ -227,7 +270,7 @@ export const VGrid = forwardRef( components: { Root: Viewport = DefaultViewport, Cell: ItemElement = "div", - } = emptyComponents as { + } = {} as { Root?: undefined; Cell?: undefined; }, @@ -263,8 +306,8 @@ export const VGrid = forwardRef( const hScrollDirection = hStore._getScrollDirection(); const vJumpCount = vStore._getJumpCount(); const hJumpCount = hStore._getJumpCount(); - const height = vStore._getScrollSize(); - const width = hStore._getScrollSize(); + const height = getScrollSize(vStore); + const width = getScrollSize(hStore); const rootRef = useRef(null); useIsomorphicLayoutEffect(() => { @@ -290,15 +333,15 @@ export const VGrid = forwardRef( } } ); - const cleanUpResizer = resizer._observeRoot(root); - const cleanupVScroller = vScroller._observe(root); - const cleanupHScroller = hScroller._observe(root); + resizer._observeRoot(root); + vScroller._observe(root); + hScroller._observe(root); return () => { unsubscribeVStore(); unsubscribeHStore(); - cleanUpResizer(); - cleanupVScroller(); - cleanupHScroller(); + resizer._dispose(); + vScroller._dispose(); + hScroller._dispose(); }; }, []); @@ -323,7 +366,7 @@ export const VGrid = forwardRef( return [hStore._getScrollOffset(), vStore._getScrollOffset()]; }, get scrollSize(): [number, number] { - return [hStore._getScrollSize(), vStore._getScrollSize()]; + return [getScrollSize(hStore), getScrollSize(vStore)]; }, get viewportSize(): [number, number] { return [hStore._getViewportSize(), vStore._getViewportSize()]; diff --git a/src/react/VList.spec.tsx b/src/react/VList.spec.tsx index 4fa110ebf..42e27f4d1 100644 --- a/src/react/VList.spec.tsx +++ b/src/react/VList.spec.tsx @@ -2,7 +2,6 @@ import { afterEach, it, expect, describe, jest } from "@jest/globals"; import { render, cleanup } from "@testing-library/react"; import { VList } from "./VList"; import { Profiler, ReactElement, forwardRef, useEffect, useState } from "react"; -import { CustomViewportComponentProps } from "./Viewport"; import { CustomItemComponentProps } from "./ListItem"; const ITEM_HEIGHT = 50; @@ -60,30 +59,6 @@ it("should pass attributes to element", () => { expect(asFragment()).toMatchSnapshot(); }); -it("should change components", () => { - const UlList = forwardRef( - ({ children, attrs, height }, ref) => { - return ( -
-
    - {children} -
-
- ); - } - ); - const { asFragment } = render( - -
0
-
1
-
2
-
3
-
4
-
- ); - expect(asFragment()).toMatchSnapshot(); -}); - it("should pass index to items", () => { const Item = forwardRef( ({ children, index, style }, ref) => { @@ -95,7 +70,7 @@ it("should pass index to items", () => { } ); const { asFragment } = render( - +
0
1
2
diff --git a/src/react/VList.tsx b/src/react/VList.tsx index bdc9ca0e4..fee3339e5 100644 --- a/src/react/VList.tsx +++ b/src/react/VList.tsx @@ -1,169 +1,37 @@ +import { ReactElement, forwardRef } from "react"; +import { ViewportComponentAttributes } from "./types"; import { - useRef, - useMemo, - ReactElement, - forwardRef, - useImperativeHandle, - ReactNode, - useEffect, -} from "react"; -import { - UPDATE_SCROLL_EVENT, - ACTION_ITEMS_LENGTH_CHANGE, - overscanEndIndex, - overscanStartIndex, - createVirtualStore, - UPDATE_SIZE_STATE, - UPDATE_SCROLL_STATE, - SCROLL_IDLE, - UPDATE_SCROLL_STOP_EVENT, -} from "../core/store"; -import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; -import { max, values } from "../core/utils"; -import { createScroller } from "../core/scroller"; -import { emptyComponents, getKey, refKey } from "./utils"; -import { useStatic } from "./useStatic"; -import { useLatestRef } from "./useLatestRef"; -import { createResizer } from "../core/resizer"; -import { - CustomViewportComponent, - CustomViewportComponentProps, - Viewport as DefaultViewport, - ViewportComponentAttributes, -} from "./Viewport"; -import { CustomItemComponent, ListItem } from "./ListItem"; -import { CacheSnapshot, ScrollToIndexOpts } from "../core/types"; -import { Cache } from "../core/cache"; -import { flushSync } from "react-dom"; -import { useRerender } from "./useRerender"; -import { useChildren } from "./useChildren"; - -type CustomItemComponentOrElement = - | keyof JSX.IntrinsicElements - | CustomItemComponent; + Virtualizer, + VirtualizerHandle, + VirtualizerProps, +} from "./Virtualizer"; /** * Methods of {@link VList}. */ -export interface VListHandle { - /** - * Get current {@link CacheSnapshot}. - */ - readonly cache: CacheSnapshot; - /** - * Get current scrollTop or scrollLeft. - */ - readonly scrollOffset: number; - /** - * Get current scrollHeight or scrollWidth. - */ - readonly scrollSize: number; - /** - * Get current offsetHeight or offsetWidth. - */ - readonly viewportSize: number; - /** - * Scroll to the item specified by index. - * @param index index of item - * @param opts options - */ - scrollToIndex(index: number, opts?: ScrollToIndexOpts): void; - /** - * Scroll to the given offset. - * @param offset offset from start - */ - scrollTo(offset: number): void; - /** - * Scroll by the given offset. - * @param offset offset from current position - */ - scrollBy(offset: number): void; -} +export interface VListHandle extends VirtualizerHandle {} /** * Props of {@link VList}. */ -export interface VListProps extends ViewportComponentAttributes { - /** - * Elements rendered by this component. - * - * You can also pass a function and set {@link VListProps.count} to create elements lazily. - */ - children: ReactNode | ((index: number) => ReactElement); - /** - * If you set a function to {@link VListProps.children}, you have to set total number of items to this prop. - */ - count?: number; - /** - * Number of items to render above/below the visible bounds of the list. Lower value will give better performance but you can increase to avoid showing blank items in fast scrolling. - * @defaultValue 4 - */ - overscan?: number; - /** - * Item size hint for unmeasured items. It will help to reduce scroll jump when items are measured if used properly. - * - * - If not set, initial item sizes will be automatically estimated from measured sizes. This is recommended for most cases. - * - If set, you can opt out estimation and use the value as initial item size. - */ - initialItemSize?: number; - /** - * While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling. - */ - shift?: boolean; - /** - * If true, rendered as a horizontally scrollable list. Otherwise rendered as a vertically scrollable list. - */ - horizontal?: boolean; - /** - * If true, items are aligned to the end of the list when total size of items are smaller than viewport size. It's useful for chat like app. - */ - reverse?: boolean; - /** - * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link VListHandle.cache}. - */ - cache?: CacheSnapshot; - /** - * A prop for SSR. If set, the specified amount of items will be mounted in the initial rendering regardless of the container size until hydrated. - */ - ssrCount?: number; - /** - * Customized components for advanced usage. - */ - components?: { - /** - * Component for scrollable element. This component will get {@link CustomViewportComponentProps} as props. - * @defaultValue {@link DefaultViewport} - */ - Root?: CustomViewportComponent; - /** - * Component or element type for item element. This component will get {@link CustomItemComponentProps} as props. - * @defaultValue "div" - */ - Item?: CustomItemComponentOrElement; - }; - /** - * Callback invoked whenever scroll offset changes. - * @param offset Current scrollTop or scrollLeft. - */ - onScroll?: (offset: number) => void; - /** - * Callback invoked when scrolling stops. - */ - onScrollStop?: () => void; - /** - * Callback invoked when visible items range changes. - */ - onRangeChange?: ( - /** - * The start index of viewable items. - */ - startIndex: number, - /** - * The end index of viewable items. - */ - endIndex: number - ) => void; -} +export interface VListProps + extends Pick< + VirtualizerProps, + | "children" + | "count" + | "overscan" + | "itemSize" + | "shift" + | "horizontal" + | "reverse" + | "cache" + | "ssrCount" + | "item" + | "onScroll" + | "onScrollEnd" + | "onRangeChange" + >, + ViewportComponentAttributes {} /** * Virtualized list component. See {@link VListProps} and {@link VListHandle}. @@ -172,197 +40,53 @@ export const VList = forwardRef( ( { children, - count: renderCountProp, - overscan = 4, - initialItemSize, + count, + overscan, + itemSize, shift, - horizontal: horizontalProp, + horizontal, reverse, cache, ssrCount, - components: { - Root: Viewport = DefaultViewport, - Item: ItemElement = "div", - } = emptyComponents as { - Root?: undefined; - Item?: undefined; - }, - onScroll: onScrollProp, - onScrollStop: onScrollStopProp, - onRangeChange: onRangeChangeProp, - ...viewportAttrs + item, + onScroll, + onScrollEnd, + onRangeChange, + style, + ...attrs }, ref ): ReactElement => { - const [getElement, count] = useChildren(children, renderCountProp); - - const onScroll = useLatestRef(onScrollProp); - const onScrollStop = useLatestRef(onScrollStopProp); - - const isSSR = useRef(!!ssrCount); - - const [store, resizer, scroller, isHorizontal] = useStatic(() => { - const _isHorizontal = !!horizontalProp; - const _store = createVirtualStore( - count, - initialItemSize, - ssrCount, - cache as unknown as Cache | undefined, - !initialItemSize - ); - return [ - _store, - createResizer(_store, _isHorizontal), - createScroller(_store, _isHorizontal), - _isHorizontal, - ]; - }); - - // The elements length and cached items length are different just after element is added/removed. - if (count !== store._getItemsLength()) { - store._update(ACTION_ITEMS_LENGTH_CHANGE, [count, shift]); - } - - const rerender = useRerender(store); - - const [startIndex, endIndex] = store._getRange(); - const scrollDirection = store._getScrollDirection(); - const jumpCount = store._getJumpCount(); - const scrollSize = store._getScrollSize(); - - const rootRef = useRef(null); - - useIsomorphicLayoutEffect(() => { - isSSR[refKey] = false; - - const root = rootRef[refKey]!; - // store must be subscribed first because others may dispatch update on init depending on implementation - const unsubscribeStore = store._subscribe( - UPDATE_SCROLL_STATE + UPDATE_SIZE_STATE, - (sync) => { - if (sync) { - flushSync(rerender); - } else { - rerender(); - } - } - ); - const unsubscribeOnScroll = store._subscribe(UPDATE_SCROLL_EVENT, () => { - onScroll[refKey] && onScroll[refKey](store._getScrollOffset()); - }); - const unsubscribeOnScrollStop = store._subscribe( - UPDATE_SCROLL_STOP_EVENT, - () => { - onScrollStop[refKey] && onScrollStop[refKey](); - } - ); - const cleanupResizer = resizer._observeRoot(root); - const cleanupScroller = scroller._observe(root); - - return () => { - unsubscribeStore(); - unsubscribeOnScroll(); - unsubscribeOnScrollStop(); - cleanupResizer(); - cleanupScroller(); - }; - }, []); - - useIsomorphicLayoutEffect(() => { - const jump = store._flushJump(); - if (!jump) return; - - scroller._fixScrollJump(jump); - }, [jumpCount]); - - useEffect(() => { - if (!onRangeChangeProp) return; - - onRangeChangeProp(startIndex, endIndex); - }, [startIndex, endIndex]); - - useImperativeHandle( - ref, - () => { - return { - get cache() { - return store._getCache(); - }, - get scrollOffset() { - return store._getScrollOffset(); - }, - get scrollSize() { - return store._getScrollSize(); - }, - get viewportSize() { - return store._getViewportSize(); - }, - scrollToIndex: scroller._scrollToIndex, - scrollTo: scroller._scrollTo, - scrollBy: scroller._scrollBy, - }; - }, - [] - ); - - const overscanedStartIndex = overscanStartIndex( - startIndex, - overscan, - scrollDirection - ); - const overscanedEndIndex = overscanEndIndex( - endIndex, - overscan, - scrollDirection, - count - ); - - const items: ReactElement[] = []; - for (let i = overscanedStartIndex; i <= overscanedEndIndex; i++) { - const e = getElement(i); - let offset = store._getItemOffset(i); - if (reverse) { - offset += max(0, store._getViewportSize() - store._getTotalSize()); - } - items.push( - - ); - } - return ( - ({ - ...viewportAttrs, - style: { - display: isHorizontal ? "inline-block" : "block", - [isHorizontal ? "overflowX" : "overflowY"]: "auto", - overflowAnchor: "none", - contain: "strict", - width: "100%", - height: "100%", - ...viewportAttrs.style, - }, - }), - values(viewportAttrs) - )} +
- {items} - + + {children} + +
); } ); diff --git a/src/react/Viewport.tsx b/src/react/Viewport.tsx deleted file mode 100644 index 848428b9c..000000000 --- a/src/react/Viewport.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - CSSProperties, - ReactElement, - ReactNode, - forwardRef, - useMemo, -} from "react"; - -export type ViewportComponentAttributes = Pick< - React.HTMLAttributes, - "className" | "style" | "id" | "role" | "tabIndex" | "onKeyDown" -> & - React.AriaAttributes; - -/** - * Props of customized scrollable component. - */ -export interface CustomViewportComponentProps { - /** - * Renderable item elements. - */ - children: ReactNode; - /** - * Attributes that should be passed to the scrollable element. - */ - attrs: ViewportComponentAttributes; - /** - * Total height of items. It's undefined if component is not vertically scrollable. - */ - height: number | undefined; - /** - * Total width of items. It's undefined if component is not horizontally scrollable. - */ - width: number | undefined; - /** - * Currently component is scrolling or not. - */ - scrolling: boolean; -} - -export const Viewport = forwardRef( - ({ children, attrs, width, height, scrolling }, ref): ReactElement => { - return ( -
-
{ - return { - contain: "content", - position: "relative", - visibility: "hidden", - width: width ?? "100%", - height: height ?? "100%", - pointerEvents: scrolling ? "none" : "auto", - }; - }, [width, height, scrolling])} - > - {children} -
-
- ); - } -); - -export type CustomViewportComponent = typeof Viewport; diff --git a/src/react/Virtualizer.spec.tsx b/src/react/Virtualizer.spec.tsx new file mode 100644 index 000000000..16e9f2cfc --- /dev/null +++ b/src/react/Virtualizer.spec.tsx @@ -0,0 +1,57 @@ +import { afterEach, it, expect } from "@jest/globals"; +import { render, cleanup } from "@testing-library/react"; +import { Virtualizer } from "."; + +const ITEM_HEIGHT = 50; +const ITEM_WIDTH = 100; +const VIEWPORT_HEIGHT = ITEM_HEIGHT * 10; + +// https://github.com/jsdom/jsdom/issues/1261#issuecomment-362928131 +Object.defineProperty(HTMLElement.prototype, "offsetParent", { + get() { + return this.parentNode; + }, +}); + +global.ResizeObserver = class { + first = false; + constructor(private callback: ResizeObserverCallback) {} + disconnect() {} + observe(e: HTMLElement) { + const entry: Pick = { + contentRect: { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: ITEM_WIDTH, + height: this.first ? VIEWPORT_HEIGHT : ITEM_HEIGHT, + x: 0, + y: 0, + toJSON() {}, + }, + target: e, + }; + this.callback([entry] as any, this); + // HACK: first observing should be root + this.first = false; + } + unobserve(_target: Element) {} +}; + +afterEach(cleanup); + +it("should change components", () => { + const { asFragment } = render( +
+ +
0
+
1
+
2
+
3
+
4
+
+
+ ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/src/react/Virtualizer.tsx b/src/react/Virtualizer.tsx new file mode 100644 index 000000000..1f4342e4b --- /dev/null +++ b/src/react/Virtualizer.tsx @@ -0,0 +1,364 @@ +import { + ReactElement, + forwardRef, + useImperativeHandle, + ReactNode, + useEffect, + useRef, + RefObject, +} from "react"; +import { + UPDATE_SCROLL_EVENT, + ACTION_ITEMS_LENGTH_CHANGE, + overscanEndIndex, + overscanStartIndex, + createVirtualStore, + UPDATE_SIZE_STATE, + UPDATE_SCROLL_STATE, + SCROLL_IDLE, + UPDATE_SCROLL_END_EVENT, + getScrollSize, + getMinContainerSize, +} from "../core/store"; +import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; +import { createScroller } from "../core/scroller"; +import { getKey, refKey } from "./utils"; +import { useStatic } from "./useStatic"; +import { useLatestRef } from "./useLatestRef"; +import { createResizer } from "../core/resizer"; +import { CustomItemComponent, ListItem } from "./ListItem"; +import { CacheSnapshot, ScrollToIndexOpts } from "../core/types"; +import { Cache } from "../core/cache"; +import { flushSync } from "react-dom"; +import { useRerender } from "./useRerender"; +import { useChildren } from "./useChildren"; +import { CustomContainerComponent } from "./types"; +import { max } from "../core/utils"; + +/** + * Methods of {@link Virtualizer}. + */ +export interface VirtualizerHandle { + /** + * Get current {@link CacheSnapshot}. + */ + readonly cache: CacheSnapshot; + /** + * Get current scrollTop or scrollLeft. + */ + readonly scrollOffset: number; + /** + * Get current scrollHeight or scrollWidth. + */ + readonly scrollSize: number; + /** + * Get current offsetHeight or offsetWidth. + */ + readonly viewportSize: number; + /** + * Scroll to the item specified by index. + * @param index index of item + * @param opts options + */ + scrollToIndex(index: number, opts?: ScrollToIndexOpts): void; + /** + * Scroll to the given offset. + * @param offset offset from start + */ + scrollTo(offset: number): void; + /** + * Scroll by the given offset. + * @param offset offset from current position + */ + scrollBy(offset: number): void; +} + +/** + * Props of {@link Virtualizer}. + */ +export interface VirtualizerProps { + /** + * Elements rendered by this component. + * + * You can also pass a function and set {@link VirtualizerProps.count} to create elements lazily. + */ + children: ReactNode | ((index: number) => ReactElement); + /** + * If you set a function to {@link VirtualizerProps.children}, you have to set total number of items to this prop. + */ + count?: number; + /** + * Number of items to render above/below the visible bounds of the list. Lower value will give better performance but you can increase to avoid showing blank items in fast scrolling. + * @defaultValue 4 + */ + overscan?: number; + /** + * Item size hint for unmeasured items. It will help to reduce scroll jump when items are measured if used properly. + * + * - If not set, initial item sizes will be automatically estimated from measured sizes. This is recommended for most cases. + * - If set, you can opt out estimation and use the value as initial item size. + */ + itemSize?: number; + /** + * While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling. + */ + shift?: boolean; + /** + * If true, rendered as a horizontally scrollable list. Otherwise rendered as a vertically scrollable list. + */ + horizontal?: boolean; + /** + * If true, items are aligned to the end of the list when total size of items are smaller than viewport size. It's useful for chat like app. + */ + reverse?: boolean; + /** + * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link VirtualizerHandle.cache}. + */ + cache?: CacheSnapshot; + /** + * If you put an element before virtualizer, you have to define its height with this prop. + */ + startMargin?: number; + /** + * If you put an element after virtualizer, you have to define its height with this prop. + */ + endMargin?: number; + /** + * A prop for SSR. If set, the specified amount of items will be mounted in the initial rendering regardless of the container size until hydrated. + */ + ssrCount?: number; + /** + * Component or element type for container element. + * @defaultValue "div" + */ + as?: keyof JSX.IntrinsicElements | CustomContainerComponent; + /** + * Component or element type for item element. This component will get {@link CustomItemComponentProps} as props. + * @defaultValue "div" + */ + item?: keyof JSX.IntrinsicElements | CustomItemComponent; + /** + * Reference to the scrollable element. The default will get the parent element of virtualizer. + */ + scrollRef?: RefObject; + /** + * Callback invoked whenever scroll offset changes. + * @param offset Current scrollTop or scrollLeft. + */ + onScroll?: (offset: number) => void; + /** + * Callback invoked when scrolling stops. + */ + onScrollEnd?: () => void; + /** + * Callback invoked when visible items range changes. + */ + onRangeChange?: ( + /** + * The start index of viewable items. + */ + startIndex: number, + /** + * The end index of viewable items. + */ + endIndex: number + ) => void; +} + +/** + * Customizable list virtualizer for advanced usage. See {@link VirtualizerProps} and {@link VirtualizerHandle}. + */ +export const Virtualizer = forwardRef( + ( + { + children, + count: renderCountProp, + overscan = 4, + itemSize, + shift, + horizontal: horizontalProp, + reverse, + cache, + startMargin, + endMargin, + ssrCount, + as: Element = "div", + item: ItemElement = "div", + scrollRef, + onScroll: onScrollProp, + onScrollEnd: onScrollEndProp, + onRangeChange: onRangeChangeProp, + }, + ref + ): ReactElement => { + Element = Element as "div"; + + const [getElement, count] = useChildren(children, renderCountProp); + + const containerRef = useRef(null); + + const isSSR = useRef(!!ssrCount); + + const onScroll = useLatestRef(onScrollProp); + const onScrollEnd = useLatestRef(onScrollEndProp); + + const [store, resizer, scroller, isHorizontal] = useStatic(() => { + const _isHorizontal = !!horizontalProp; + const _store = createVirtualStore( + count, + itemSize, + ssrCount, + cache as unknown as Cache | undefined, + !itemSize, + startMargin, + endMargin + ); + return [ + _store, + createResizer(_store, _isHorizontal), + createScroller(_store, _isHorizontal), + _isHorizontal, + ]; + }); + + // The elements length and cached items length are different just after element is added/removed. + if (count !== store._getItemsLength()) { + store._update(ACTION_ITEMS_LENGTH_CHANGE, [count, shift]); + } + + const rerender = useRerender(store); + + const [startIndex, endIndex] = store._getRange(); + const scrollDirection = store._getScrollDirection(); + const jumpCount = store._getJumpCount(); + const totalSize = store._getTotalSize(); + + // https://github.com/inokawa/virtua/issues/252#issuecomment-1822861368 + const minSize = getMinContainerSize(store); + const reverseOffset = reverse ? max(0, minSize - totalSize) : 0; + + const items: ReactElement[] = []; + + useIsomorphicLayoutEffect(() => { + isSSR[refKey] = false; + + // store must be subscribed first because others may dispatch update on init depending on implementation + const unsubscribeStore = store._subscribe( + UPDATE_SCROLL_STATE + UPDATE_SIZE_STATE, + (sync) => { + if (sync) { + flushSync(rerender); + } else { + rerender(); + } + } + ); + const unsubscribeOnScroll = store._subscribe(UPDATE_SCROLL_EVENT, () => { + onScroll[refKey] && onScroll[refKey](store._getScrollOffset()); + }); + const unsubscribeOnScrollEnd = store._subscribe( + UPDATE_SCROLL_END_EVENT, + () => { + onScrollEnd[refKey] && onScrollEnd[refKey](); + } + ); + const assignScrollableElement = (e: HTMLElement) => { + resizer._observeRoot(e); + scroller._observe(e); + }; + if (scrollRef) { + // parent's ref doesn't exist when useLayoutEffect is called + Promise.resolve().then(() => + assignScrollableElement(scrollRef[refKey]!) + ); + } else { + assignScrollableElement(containerRef[refKey]!.parentElement!); + } + + return () => { + unsubscribeStore(); + unsubscribeOnScroll(); + unsubscribeOnScrollEnd(); + resizer._dispose(); + scroller._dispose(); + }; + }, []); + + useIsomorphicLayoutEffect(() => { + const jump = store._flushJump(); + if (!jump) return; + + scroller._fixScrollJump(jump); + }, [jumpCount]); + + useEffect(() => { + if (!onRangeChangeProp) return; + + onRangeChangeProp(startIndex, endIndex); + }, [startIndex, endIndex]); + + useImperativeHandle( + ref, + () => { + return { + get cache() { + return store._getCache(); + }, + get scrollOffset() { + return store._getScrollOffset(); + }, + get scrollSize() { + return getScrollSize(store); + }, + get viewportSize() { + return store._getViewportSize(); + }, + scrollToIndex: scroller._scrollToIndex, + scrollTo: scroller._scrollTo, + scrollBy: scroller._scrollBy, + }; + }, + [] + ); + + for ( + let i = overscanStartIndex(startIndex, overscan, scrollDirection), + j = overscanEndIndex(endIndex, overscan, scrollDirection, count); + i <= j; + i++ + ) { + const e = getElement(i); + items.push( + + ); + } + + return ( + + {items} + + ); + } +); diff --git a/src/react/WVList.ssr.spec.tsx b/src/react/WVList.ssr.spec.tsx deleted file mode 100644 index 3a803e5aa..000000000 --- a/src/react/WVList.ssr.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @jest-environment node - */ -import { it, describe, expect } from "@jest/globals"; -import { renderToString, renderToStaticMarkup } from "react-dom/server"; -import { WVList } from "./WVList"; -import { JSDOM } from "jsdom"; - -const LIST_ID = "list-id"; - -describe("SSR", () => { - it("should render items with renderToString and vertical", () => { - const COUNT = 10; - const OVERSCAN = 4; - const html = renderToString( - - {Array.from({ length: 1000 }).map((_, i) => ( -
{i}
- ))} -
- ); - expect(html).toMatchSnapshot(); - - expect( - new JSDOM(html).window.document.getElementById(LIST_ID)!.children[0]! - .childElementCount - ).toEqual(COUNT + OVERSCAN); - }); - - it("should render items with renderToStaticMarkup and vertical", () => { - const COUNT = 10; - const OVERSCAN = 4; - const html = renderToStaticMarkup( - - {Array.from({ length: 1000 }).map((_, i) => ( -
{i}
- ))} -
- ); - expect(html).toMatchSnapshot(); - - expect( - new JSDOM(html).window.document.getElementById(LIST_ID)!.children[0]! - .childElementCount - ).toEqual(COUNT + OVERSCAN); - }); - - it("should render items with renderToString and horizontal", () => { - const COUNT = 10; - const OVERSCAN = 4; - const html = renderToString( - - {Array.from({ length: 1000 }).map((_, i) => ( -
{i}
- ))} -
- ); - expect(html).toMatchSnapshot(); - - expect( - new JSDOM(html).window.document.getElementById(LIST_ID)!.children[0]! - .childElementCount - ).toEqual(COUNT + OVERSCAN); - }); - - it("should render items with renderToStaticMarkup and horizontal", () => { - const COUNT = 10; - const OVERSCAN = 4; - const html = renderToStaticMarkup( - - {Array.from({ length: 1000 }).map((_, i) => ( -
{i}
- ))} -
- ); - expect(html).toMatchSnapshot(); - - expect( - new JSDOM(html).window.document.getElementById(LIST_ID)!.children[0]! - .childElementCount - ).toEqual(COUNT + OVERSCAN); - }); -}); diff --git a/src/react/WVList.rtl.spec.tsx b/src/react/WindowVirtualizer.rtl.spec.tsx similarity index 91% rename from src/react/WVList.rtl.spec.tsx rename to src/react/WindowVirtualizer.rtl.spec.tsx index f4a8e4912..79c9ddbb6 100644 --- a/src/react/WVList.rtl.spec.tsx +++ b/src/react/WindowVirtualizer.rtl.spec.tsx @@ -1,6 +1,6 @@ import { afterEach, it, expect, describe, jest } from "@jest/globals"; import { render, cleanup } from "@testing-library/react"; -import { WVList } from "./WVList"; +import { WindowVirtualizer } from "./WindowVirtualizer"; jest.mock("../core/environment", () => { const originalModule = jest.requireActual("../core/environment"); @@ -60,22 +60,22 @@ afterEach(cleanup); describe("rtl", () => { it("should not work in vertical", () => { const { asFragment } = render( - + {Array.from({ length: 100 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should work in horizontal", () => { const { asFragment } = render( - + {Array.from({ length: 100 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); diff --git a/src/react/WVList.spec.tsx b/src/react/WindowVirtualizer.spec.tsx similarity index 82% rename from src/react/WVList.spec.tsx rename to src/react/WindowVirtualizer.spec.tsx index ab0c51304..25e9abacb 100644 --- a/src/react/WVList.spec.tsx +++ b/src/react/WindowVirtualizer.spec.tsx @@ -1,8 +1,7 @@ import { afterEach, it, expect, describe, jest } from "@jest/globals"; import { render, cleanup } from "@testing-library/react"; -import { WVList } from "./WVList"; +import { WindowVirtualizer } from "./WindowVirtualizer"; import { Profiler, ReactElement, forwardRef, useEffect, useState } from "react"; -import { CustomViewportComponentProps } from "./Viewport"; import { CustomItemComponentProps } from "./ListItem"; const ITEM_HEIGHT = 50; @@ -52,46 +51,6 @@ global.IntersectionObserver = class { afterEach(cleanup); -it("should pass attributes to element", () => { - const { asFragment } = render( - -
0
-
- ); - expect(asFragment()).toMatchSnapshot(); -}); - -it("should change components", () => { - const UlList = forwardRef( - ({ children, attrs, height }, ref) => { - return ( -
-
    - {children} -
-
- ); - } - ); - const { asFragment } = render( - -
0
-
1
-
2
-
3
-
4
-
- ); - expect(asFragment()).toMatchSnapshot(); -}); - it("should pass index to items", () => { const Item = forwardRef( ({ children, index, style }, ref) => { @@ -103,13 +62,13 @@ it("should pass index to items", () => { } ); const { asFragment } = render( - +
0
1
2
3
4
-
+ ); expect(asFragment()).toMatchSnapshot(); }); @@ -120,12 +79,12 @@ it("should render with render prop", () => { label: "This is " + i, })); const { asFragment } = render( - + {(i) => { const item = items[i]!; return
{item.label}
; }} -
+ ); expect(asFragment()).toMatchSnapshot(); }); @@ -133,76 +92,76 @@ it("should render with render prop", () => { describe("vertical", () => { it("should render 1 children", () => { const { asFragment } = render( - +
0
-
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 5 children", () => { const { asFragment } = render( - +
0
1
2
3
4
-
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 100 children", () => { const { asFragment } = render( - + {Array.from({ length: 100 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 1000 children", () => { const { asFragment } = render( - + {Array.from({ length: 1000 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 10000 children", () => { const { asFragment } = render( - + {Array.from({ length: 10000 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render non elements", () => { const { asFragment } = render( - + string {true} {false} {null} {undefined} {123} - + ); expect(asFragment()).toMatchSnapshot(); }); it("should render fragments", () => { const { asFragment } = render( - + <>
fragment
fragment
@@ -211,7 +170,7 @@ describe("vertical", () => { <>
fragment
-
+ ); expect(asFragment()).toMatchSnapshot(); }); @@ -221,24 +180,26 @@ describe("vertical", () => {
{children}
); const { asFragment } = render( - + component component component - + ); expect(asFragment()).toMatchSnapshot(); }); it("should render with given width / height", () => { const { asFragment } = render( - -
0
-
1
-
2
-
3
-
4
-
+
+ +
0
+
1
+
2
+
3
+
4
+
+
); expect(asFragment()).toMatchSnapshot(); }); @@ -247,76 +208,76 @@ describe("vertical", () => { describe("horizontal", () => { it("should render 1 children", () => { const { asFragment } = render( - +
0
-
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 5 children", () => { const { asFragment } = render( - +
0
1
2
3
4
-
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 100 children", () => { const { asFragment } = render( - + {Array.from({ length: 100 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 1000 children", () => { const { asFragment } = render( - + {Array.from({ length: 1000 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render 10000 children", () => { const { asFragment } = render( - + {Array.from({ length: 10000 }).map((_, i) => (
{i}
))} -
+ ); expect(asFragment()).toMatchSnapshot(); }); it("should render non elements", () => { const { asFragment } = render( - + string {true} {false} {null} {undefined} {123} - + ); expect(asFragment()).toMatchSnapshot(); }); it("should render fragments", () => { const { asFragment } = render( - + <>
fragment
fragment
@@ -325,7 +286,7 @@ describe("horizontal", () => { <>
fragment
-
+ ); expect(asFragment()).toMatchSnapshot(); }); @@ -335,24 +296,26 @@ describe("horizontal", () => {
{children}
); const { asFragment } = render( - + component component component - + ); expect(asFragment()).toMatchSnapshot(); }); it("should render with given width / height", () => { const { asFragment } = render( - -
0
-
1
-
2
-
3
-
4
-
+
+ +
0
+
1
+
2
+
3
+
4
+
+
); expect(asFragment()).toMatchSnapshot(); }); @@ -366,7 +329,7 @@ describe("render count", () => { render( - + {Array.from({ length: itemCount }, (_, i) => { const key = `item-${i}`; return ( @@ -375,7 +338,7 @@ describe("render count", () => { ); })} - + ); @@ -392,7 +355,7 @@ describe("render count", () => { render( - + {Array.from({ length: itemCount }, (_, i) => { const key = `item-${i}`; return ( @@ -401,7 +364,7 @@ describe("render count", () => { ); })} - + ); @@ -445,7 +408,7 @@ describe("render count", () => { {(count) => ( - + {Array.from({ length: count }, (_, i) => { const key = `item-${i}`; return ( @@ -454,7 +417,7 @@ describe("render count", () => { ); })} - + )} @@ -500,7 +463,7 @@ describe("render count", () => { {(count) => ( - + {Array.from({ length: count }, (_, i) => { const key = `item-${i}`; return ( @@ -509,7 +472,7 @@ describe("render count", () => { ); })} - + )} diff --git a/src/react/WindowVirtualizer.ssr.spec.tsx b/src/react/WindowVirtualizer.ssr.spec.tsx new file mode 100644 index 000000000..68907f689 --- /dev/null +++ b/src/react/WindowVirtualizer.ssr.spec.tsx @@ -0,0 +1,99 @@ +/** + * @jest-environment node + */ +import { it, describe, expect } from "@jest/globals"; +import { renderToString, renderToStaticMarkup } from "react-dom/server"; +import { WindowVirtualizer } from "./WindowVirtualizer"; +import { JSDOM } from "jsdom"; + +const LIST_ID = "list-id"; + +describe("SSR", () => { + it("should render items with renderToString and vertical", () => { + const COUNT = 10; + const OVERSCAN = 4; + const html = renderToString( +
+ + {Array.from({ length: 1000 }).map((_, i) => ( +
{i}
+ ))} +
+
+ ); + expect(html).toMatchSnapshot(); + + expect( + new JSDOM(html).window.document.getElementById(LIST_ID)! + .firstElementChild!.childElementCount + ).toEqual(COUNT + OVERSCAN); + }); + + it("should render items with renderToStaticMarkup and vertical", () => { + const COUNT = 10; + const OVERSCAN = 4; + const html = renderToStaticMarkup( +
+ + {Array.from({ length: 1000 }).map((_, i) => ( +
{i}
+ ))} +
+
+ ); + expect(html).toMatchSnapshot(); + + expect( + new JSDOM(html).window.document.getElementById(LIST_ID)! + .firstElementChild!.childElementCount + ).toEqual(COUNT + OVERSCAN); + }); + + it("should render items with renderToString and horizontal", () => { + const COUNT = 10; + const OVERSCAN = 4; + const html = renderToString( +
+ + {Array.from({ length: 1000 }).map((_, i) => ( +
{i}
+ ))} +
+
+ ); + expect(html).toMatchSnapshot(); + + expect( + new JSDOM(html).window.document.getElementById(LIST_ID)! + .firstElementChild!.childElementCount + ).toEqual(COUNT + OVERSCAN); + }); + + it("should render items with renderToStaticMarkup and horizontal", () => { + const COUNT = 10; + const OVERSCAN = 4; + const html = renderToStaticMarkup( +
+ + {Array.from({ length: 1000 }).map((_, i) => ( +
{i}
+ ))} +
+
+ ); + expect(html).toMatchSnapshot(); + + expect( + new JSDOM(html).window.document.getElementById(LIST_ID)! + .firstElementChild!.childElementCount + ).toEqual(COUNT + OVERSCAN); + }); +}); diff --git a/src/react/WVList.tsx b/src/react/WindowVirtualizer.tsx similarity index 62% rename from src/react/WVList.tsx rename to src/react/WindowVirtualizer.tsx index e963fa050..8111faccd 100644 --- a/src/react/WVList.tsx +++ b/src/react/WindowVirtualizer.tsx @@ -1,11 +1,10 @@ import { - useRef, - useMemo, ReactElement, ReactNode, useEffect, forwardRef, useImperativeHandle, + useRef, } from "react"; import { ACTION_ITEMS_LENGTH_CHANGE, @@ -15,36 +14,26 @@ import { overscanStartIndex, createVirtualStore, SCROLL_IDLE, - UPDATE_SCROLL_STOP_EVENT, + UPDATE_SCROLL_END_EVENT, } from "../core/store"; import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; -import { values } from "../core/utils"; import { createWindowScroller } from "../core/scroller"; -import { emptyComponents, getKey, refKey } from "./utils"; +import { getKey, refKey } from "./utils"; import { useStatic } from "./useStatic"; import { useLatestRef } from "./useLatestRef"; import { createWindowResizer } from "../core/resizer"; import { CacheSnapshot } from "../core/types"; -import { - CustomViewportComponent, - CustomViewportComponentProps, - Viewport as DefaultViewport, - ViewportComponentAttributes, -} from "./Viewport"; +import { CustomContainerComponent } from "./types"; import { CustomItemComponent, ListItem } from "./ListItem"; import { Cache } from "../core/cache"; import { flushSync } from "react-dom"; import { useRerender } from "./useRerender"; import { useChildren } from "./useChildren"; -type CustomItemComponentOrElement = - | keyof JSX.IntrinsicElements - | CustomItemComponent; - /** - * Methods of {@link WVList}. + * Methods of {@link WindowVirtualizer}. */ -export interface WVListHandle { +export interface WindowVirtualizerHandle { /** * Get current {@link CacheSnapshot}. */ @@ -52,17 +41,17 @@ export interface WVListHandle { } /** - * Props of {@link WVList}. + * Props of {@link WindowVirtualizer}. */ -export interface WVListProps extends ViewportComponentAttributes { +export interface WindowVirtualizerProps { /** * Elements rendered by this component. * - * You can also pass a function and set {@link WVListProps.count} to create elements lazily. + * You can also pass a function and set {@link WindowVirtualizerProps.count} to create elements lazily. */ children: ReactNode | ((index: number) => ReactElement); /** - * If you set a function to {@link WVListProps.children}, you have to set total number of items to this prop. + * If you set a function to {@link WindowVirtualizerProps.children}, you have to set total number of items to this prop. */ count?: number; /** @@ -76,7 +65,7 @@ export interface WVListProps extends ViewportComponentAttributes { * - If not set, initial item sizes will be automatically estimated from measured sizes. This is recommended for most cases. * - If set, you can opt out estimation and use the value as initial item size. */ - initialItemSize?: number; + itemSize?: number; /** * While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling. */ @@ -86,7 +75,7 @@ export interface WVListProps extends ViewportComponentAttributes { */ horizontal?: boolean; /** - * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link WVListHandle.cache}. + * You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link WindowVirtualizerHandle.cache}. */ cache?: CacheSnapshot; /** @@ -94,24 +83,19 @@ export interface WVListProps extends ViewportComponentAttributes { */ ssrCount?: number; /** - * Customized components for advanced usage. + * Component or element type for container element. + * @defaultValue "div" */ - components?: { - /** - * Component for scrollable element. This component will get {@link CustomViewportComponentProps} as props. - * @defaultValue {@link DefaultViewport} - */ - Root?: CustomViewportComponent; - /** - * Component or element type for item element. This component will get {@link CustomItemComponentProps} as props. - * @defaultValue "div" - */ - Item?: CustomItemComponentOrElement; - }; + as?: keyof JSX.IntrinsicElements | CustomContainerComponent; + /** + * Component or element type for item element. This component will get {@link CustomItemComponentProps} as props. + * @defaultValue "div" + */ + item?: keyof JSX.IntrinsicElements | CustomItemComponent; /** * Callback invoked when scrolling stops. */ - onScrollStop?: () => void; + onScrollEnd?: () => void; /** * Callback invoked when visible items range changes. */ @@ -128,35 +112,36 @@ export interface WVListProps extends ViewportComponentAttributes { } /** - * Virtualized list component controlled by the window scrolling. See {@link WVListProps} and {@link WVListHandle}. + * {@link Virtualizer} controlled by the window scrolling. See {@link WindowVirtualizerProps} and {@link WindowVirtualizer}. */ -export const WVList = forwardRef( +export const WindowVirtualizer = forwardRef< + WindowVirtualizerHandle, + WindowVirtualizerProps +>( ( { children, count: renderCountProp, overscan = 4, - initialItemSize, + itemSize, shift, horizontal: horizontalProp, cache, ssrCount, - components: { - Root: Viewport = DefaultViewport, - Item: ItemElement = "div", - } = emptyComponents as { - Root?: undefined; - Item?: undefined; - }, - onScrollStop: onScrollStopProp, + as: Element = "div", + item: ItemElement = "div", + onScrollEnd: onScrollEndProp, onRangeChange: onRangeChangeProp, - ...viewportAttrs }, ref ): ReactElement => { + Element = Element as "div"; + const [getElement, count] = useChildren(children, renderCountProp); - const onScrollStop = useLatestRef(onScrollStopProp); + const containerRef = useRef(null); + + const onScrollEnd = useLatestRef(onScrollEndProp); const isSSR = useRef(!!ssrCount); @@ -164,10 +149,10 @@ export const WVList = forwardRef( const _isHorizontal = !!horizontalProp; const _store = createVirtualStore( count, - initialItemSize, + itemSize, ssrCount, cache as unknown as Cache | undefined, - !initialItemSize + !itemSize ); return [ @@ -187,15 +172,13 @@ export const WVList = forwardRef( const [startIndex, endIndex] = store._getRange(); const scrollDirection = store._getScrollDirection(); const jumpCount = store._getJumpCount(); - // https://github.com/inokawa/virtua/issues/252 - const scrollSize = store._getTotalSize(); + const totalSize = store._getTotalSize(); - const rootRef = useRef(null); + const items: ReactElement[] = []; useIsomorphicLayoutEffect(() => { isSSR[refKey] = false; - const root = rootRef[refKey]!; // store must be subscribed first because others may dispatch update on init depending on implementation const unsubscribeStore = store._subscribe( UPDATE_SCROLL_STATE + UPDATE_SIZE_STATE, @@ -207,21 +190,20 @@ export const WVList = forwardRef( } } ); - const unsubscribeOnScrollStop = store._subscribe( - UPDATE_SCROLL_STOP_EVENT, + const unsubscribeOnScrollEnd = store._subscribe( + UPDATE_SCROLL_END_EVENT, () => { - onScrollStop[refKey] && onScrollStop[refKey](); + onScrollEnd[refKey] && onScrollEnd[refKey](); } ); - const cleanupResizer = resizer._observeRoot(); - const cleanupScroller = scroller._observe(root); - + resizer._observeRoot(); + scroller._observe(containerRef[refKey]!); return () => { unsubscribeStore(); - unsubscribeOnScrollStop(); - cleanupResizer(); - cleanupScroller(); + unsubscribeOnScrollEnd(); + resizer._dispose(); + scroller._dispose(); }; }, []); @@ -250,20 +232,12 @@ export const WVList = forwardRef( [] ); - const overscanedStartIndex = overscanStartIndex( - startIndex, - overscan, - scrollDirection - ); - const overscanedEndIndex = overscanEndIndex( - endIndex, - overscan, - scrollDirection, - count - ); - - const items: ReactElement[] = []; - for (let i = overscanedStartIndex; i <= overscanedEndIndex; i++) { + for ( + let i = overscanStartIndex(startIndex, overscan, scrollDirection), + j = overscanEndIndex(endIndex, overscan, scrollDirection, count); + i <= j; + i++ + ) { const e = getElement(i); items.push( ( } return ( - ({ - ...viewportAttrs, - style: { - display: isHorizontal ? "inline-block" : "block", - width: isHorizontal ? "auto" : "100%", - height: isHorizontal ? "100%" : "auto", - ...viewportAttrs.style, - }, - }), - values(viewportAttrs) - )} + {items} - + ); } ); diff --git a/src/react/__snapshots__/VList.rtl.spec.tsx.snap b/src/react/__snapshots__/VList.rtl.spec.tsx.snap index 4fbc68611..a8b7b899d 100644 --- a/src/react/__snapshots__/VList.rtl.spec.tsx.snap +++ b/src/react/__snapshots__/VList.rtl.spec.tsx.snap @@ -6,7 +6,7 @@ exports[`rtl should not work in vertical 1`] = ` style="display: block; overflow-y: auto; contain: strict; width: 100%; height: 100%;" >
`; -exports[`should change components 1`] = ` - -
-
    -
  • -
    - 0 -
    -
  • -
  • -
    - 1 -
    -
  • -
  • -
    - 2 -
    -
  • -
  • -
    - 3 -
    -
  • -
  • -
    - 4 -
    -
  • -
-
-
-`; - exports[`should pass attributes to element 1`] = `
@@ -625,7 +577,7 @@ exports[`vertical should render 1 children 1`] = ` style="display: block; overflow-y: auto; contain: strict; width: 100%; height: 100%;" >
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; +exports[`SSR should render items with renderToStaticMarkup and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; -exports[`SSR should render items with renderToStaticMarkup and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; +exports[`SSR should render items with renderToStaticMarkup and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; -exports[`SSR should render items with renderToString and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; +exports[`SSR should render items with renderToString and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; -exports[`SSR should render items with renderToString and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; +exports[`SSR should render items with renderToString and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; diff --git a/src/react/__snapshots__/Virtualizer.spec.tsx.snap b/src/react/__snapshots__/Virtualizer.spec.tsx.snap new file mode 100644 index 000000000..673bbcad7 --- /dev/null +++ b/src/react/__snapshots__/Virtualizer.spec.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should change components 1`] = ` + +
+
    +
  • +
    + 0 +
    +
  • +
  • +
    + 1 +
    +
  • +
  • +
    + 2 +
    +
  • +
  • +
    + 3 +
    +
  • +
  • +
    + 4 +
    +
  • +
+
+
+`; diff --git a/src/react/__snapshots__/WVList.rtl.spec.tsx.snap b/src/react/__snapshots__/WVList.rtl.spec.tsx.snap deleted file mode 100644 index 6f8924d0f..000000000 --- a/src/react/__snapshots__/WVList.rtl.spec.tsx.snap +++ /dev/null @@ -1,272 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`rtl should not work in vertical 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
- 15 -
-
-
-
- 16 -
-
-
-
- 17 -
-
-
-
- 18 -
-
-
-
- 19 -
-
-
-
-
-`; - -exports[`rtl should work in horizontal 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
-
-`; diff --git a/src/react/__snapshots__/WVList.spec.tsx.snap b/src/react/__snapshots__/WVList.spec.tsx.snap deleted file mode 100644 index 8b2fda765..000000000 --- a/src/react/__snapshots__/WVList.spec.tsx.snap +++ /dev/null @@ -1,1505 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`horizontal should render 1 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
-
-`; - -exports[`horizontal should render 5 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
-
-`; - -exports[`horizontal should render 100 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
-
-`; - -exports[`horizontal should render 1000 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
-
-`; - -exports[`horizontal should render 10000 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
-
-`; - -exports[`horizontal should render component 1`] = ` - -
-
-
-
- component -
-
-
-
- component -
-
-
-
- component -
-
-
-
-
-`; - -exports[`horizontal should render fragments 1`] = ` - -
-
-
-
- fragment -
-
- fragment -
-
- fragment -
-
-
-
- fragment -
-
-
-
-
-`; - -exports[`horizontal should render non elements 1`] = ` - -
-
-
- string -
-
- 123 -
-
-
-
-`; - -exports[`horizontal should render with given width / height 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
-
-`; - -exports[`should change components 1`] = ` - -
-
    -
  • -
    - 0 -
    -
  • -
  • -
    - 1 -
    -
  • -
  • -
    - 2 -
    -
  • -
  • -
    - 3 -
    -
  • -
  • -
    - 4 -
    -
  • -
-
-
-`; - -exports[`should pass attributes to element 1`] = ` - -
-
-
-
- 0 -
-
-
-
-
-`; - -exports[`should pass index to items 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
-
-`; - -exports[`should render with render prop 1`] = ` - -
-
-
-
- This is 0 -
-
-
-
- This is 1 -
-
-
-
- This is 2 -
-
-
-
- This is 3 -
-
-
-
- This is 4 -
-
-
-
- This is 5 -
-
-
-
- This is 6 -
-
-
-
- This is 7 -
-
-
-
- This is 8 -
-
-
-
- This is 9 -
-
-
-
- This is 10 -
-
-
-
- This is 11 -
-
-
-
- This is 12 -
-
-
-
- This is 13 -
-
-
-
- This is 14 -
-
-
-
- This is 15 -
-
-
-
- This is 16 -
-
-
-
- This is 17 -
-
-
-
- This is 18 -
-
-
-
- This is 19 -
-
-
-
-
-`; - -exports[`vertical should render 1 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
-
-`; - -exports[`vertical should render 5 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
-
-`; - -exports[`vertical should render 100 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
- 15 -
-
-
-
- 16 -
-
-
-
- 17 -
-
-
-
- 18 -
-
-
-
- 19 -
-
-
-
-
-`; - -exports[`vertical should render 1000 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
- 15 -
-
-
-
- 16 -
-
-
-
- 17 -
-
-
-
- 18 -
-
-
-
- 19 -
-
-
-
-
-`; - -exports[`vertical should render 10000 children 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
- 5 -
-
-
-
- 6 -
-
-
-
- 7 -
-
-
-
- 8 -
-
-
-
- 9 -
-
-
-
- 10 -
-
-
-
- 11 -
-
-
-
- 12 -
-
-
-
- 13 -
-
-
-
- 14 -
-
-
-
- 15 -
-
-
-
- 16 -
-
-
-
- 17 -
-
-
-
- 18 -
-
-
-
- 19 -
-
-
-
-
-`; - -exports[`vertical should render component 1`] = ` - -
-
-
-
- component -
-
-
-
- component -
-
-
-
- component -
-
-
-
-
-`; - -exports[`vertical should render fragments 1`] = ` - -
-
-
-
- fragment -
-
- fragment -
-
- fragment -
-
-
-
- fragment -
-
-
-
-
-`; - -exports[`vertical should render non elements 1`] = ` - -
-
-
- string -
-
- 123 -
-
-
-
-`; - -exports[`vertical should render with given width / height 1`] = ` - -
-
-
-
- 0 -
-
-
-
- 1 -
-
-
-
- 2 -
-
-
-
- 3 -
-
-
-
- 4 -
-
-
-
-
-`; diff --git a/src/react/__snapshots__/WVList.ssr.spec.tsx.snap b/src/react/__snapshots__/WVList.ssr.spec.tsx.snap deleted file mode 100644 index 339dfa456..000000000 --- a/src/react/__snapshots__/WVList.ssr.spec.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SSR should render items with renderToStaticMarkup and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; - -exports[`SSR should render items with renderToStaticMarkup and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; - -exports[`SSR should render items with renderToString and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; - -exports[`SSR should render items with renderToString and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; diff --git a/src/react/__snapshots__/WindowVirtualizer.rtl.spec.tsx.snap b/src/react/__snapshots__/WindowVirtualizer.rtl.spec.tsx.snap new file mode 100644 index 000000000..09406806f --- /dev/null +++ b/src/react/__snapshots__/WindowVirtualizer.rtl.spec.tsx.snap @@ -0,0 +1,264 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rtl should not work in vertical 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+ 15 +
+
+
+
+ 16 +
+
+
+
+ 17 +
+
+
+
+ 18 +
+
+
+
+ 19 +
+
+
+
+`; + +exports[`rtl should work in horizontal 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+`; diff --git a/src/react/__snapshots__/WindowVirtualizer.spec.tsx.snap b/src/react/__snapshots__/WindowVirtualizer.spec.tsx.snap new file mode 100644 index 000000000..2b6f16767 --- /dev/null +++ b/src/react/__snapshots__/WindowVirtualizer.spec.tsx.snap @@ -0,0 +1,1360 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`horizontal should render 1 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+`; + +exports[`horizontal should render 5 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+`; + +exports[`horizontal should render 100 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+`; + +exports[`horizontal should render 1000 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+`; + +exports[`horizontal should render 10000 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+`; + +exports[`horizontal should render component 1`] = ` + +
+
+
+ component +
+
+
+
+ component +
+
+
+
+ component +
+
+
+
+`; + +exports[`horizontal should render fragments 1`] = ` + +
+
+
+ fragment +
+
+ fragment +
+
+ fragment +
+
+
+
+ fragment +
+
+
+
+`; + +exports[`horizontal should render non elements 1`] = ` + +
+
+ string +
+
+ 123 +
+
+
+`; + +exports[`horizontal should render with given width / height 1`] = ` + +
+
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+
+`; + +exports[`should pass index to items 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+`; + +exports[`should render with render prop 1`] = ` + +
+
+
+ This is 0 +
+
+
+
+ This is 1 +
+
+
+
+ This is 2 +
+
+
+
+ This is 3 +
+
+
+
+ This is 4 +
+
+
+
+ This is 5 +
+
+
+
+ This is 6 +
+
+
+
+ This is 7 +
+
+
+
+ This is 8 +
+
+
+
+ This is 9 +
+
+
+
+ This is 10 +
+
+
+
+ This is 11 +
+
+
+
+ This is 12 +
+
+
+
+ This is 13 +
+
+
+
+ This is 14 +
+
+
+
+ This is 15 +
+
+
+
+ This is 16 +
+
+
+
+ This is 17 +
+
+
+
+ This is 18 +
+
+
+
+ This is 19 +
+
+
+
+`; + +exports[`vertical should render 1 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+`; + +exports[`vertical should render 5 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+`; + +exports[`vertical should render 100 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+ 15 +
+
+
+
+ 16 +
+
+
+
+ 17 +
+
+
+
+ 18 +
+
+
+
+ 19 +
+
+
+
+`; + +exports[`vertical should render 1000 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+ 15 +
+
+
+
+ 16 +
+
+
+
+ 17 +
+
+
+
+ 18 +
+
+
+
+ 19 +
+
+
+
+`; + +exports[`vertical should render 10000 children 1`] = ` + +
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+ 5 +
+
+
+
+ 6 +
+
+
+
+ 7 +
+
+
+
+ 8 +
+
+
+
+ 9 +
+
+
+
+ 10 +
+
+
+
+ 11 +
+
+
+
+ 12 +
+
+
+
+ 13 +
+
+
+
+ 14 +
+
+
+
+ 15 +
+
+
+
+ 16 +
+
+
+
+ 17 +
+
+
+
+ 18 +
+
+
+
+ 19 +
+
+
+
+`; + +exports[`vertical should render component 1`] = ` + +
+
+
+ component +
+
+
+
+ component +
+
+
+
+ component +
+
+
+
+`; + +exports[`vertical should render fragments 1`] = ` + +
+
+
+ fragment +
+
+ fragment +
+
+ fragment +
+
+
+
+ fragment +
+
+
+
+`; + +exports[`vertical should render non elements 1`] = ` + +
+
+ string +
+
+ 123 +
+
+
+`; + +exports[`vertical should render with given width / height 1`] = ` + +
+
+
+
+ 0 +
+
+
+
+ 1 +
+
+
+
+ 2 +
+
+
+
+ 3 +
+
+
+
+ 4 +
+
+
+
+
+`; diff --git a/src/react/__snapshots__/WindowVirtualizer.ssr.spec.tsx.snap b/src/react/__snapshots__/WindowVirtualizer.ssr.spec.tsx.snap new file mode 100644 index 000000000..64d6815a5 --- /dev/null +++ b/src/react/__snapshots__/WindowVirtualizer.ssr.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SSR should render items with renderToStaticMarkup and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; + +exports[`SSR should render items with renderToStaticMarkup and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; + +exports[`SSR should render items with renderToString and horizontal 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; + +exports[`SSR should render items with renderToString and vertical 1`] = `"
0
1
2
3
4
5
6
7
8
9
10
11
12
13
"`; diff --git a/src/react/index.ts b/src/react/index.ts index 9a99a9940..566665097 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,7 +1,12 @@ export { VList } from "./VList"; export type { VListProps, VListHandle } from "./VList"; -export { WVList } from "./WVList"; -export type { WVListProps, WVListHandle } from "./WVList"; +export { Virtualizer } from "./Virtualizer"; +export type { VirtualizerProps, VirtualizerHandle } from "./Virtualizer"; +export { WindowVirtualizer } from "./WindowVirtualizer"; +export type { + WindowVirtualizerProps, + WindowVirtualizerHandle, +} from "./WindowVirtualizer"; export { VGrid as experimental_VGrid } from "./VGrid"; export type { VGridProps, @@ -9,9 +14,5 @@ export type { CustomCellComponent, CustomCellComponentProps, } from "./VGrid"; -export type { - ViewportComponentAttributes, - CustomViewportComponent, - CustomViewportComponentProps, -} from "./Viewport"; +export type * from "./types"; export type { CustomItemComponent, CustomItemComponentProps } from "./ListItem"; diff --git a/src/react/types.ts b/src/react/types.ts new file mode 100644 index 000000000..8595fa531 --- /dev/null +++ b/src/react/types.ts @@ -0,0 +1,17 @@ +import { CSSProperties, ReactNode } from "react"; + +export type ViewportComponentAttributes = Pick< + React.HTMLAttributes, + "className" | "style" | "id" | "role" | "tabIndex" | "onKeyDown" +> & + React.AriaAttributes; + +export interface CustomContainerComponentProps { + style: CSSProperties; + children: ReactNode; +} + +export type CustomContainerComponent = React.ForwardRefExoticComponent< + React.PropsWithoutRef & + React.RefAttributes +>; diff --git a/src/react/utils.ts b/src/react/utils.ts index cfbc3ddc4..441e6ee93 100644 --- a/src/react/utils.ts +++ b/src/react/utils.ts @@ -6,11 +6,6 @@ import { exists, isArray } from "../core/utils"; */ export const refKey = "current"; -/** - * @internal - */ -export const emptyComponents = {}; - /** * @internal */ diff --git a/src/vue/VList.tsx b/src/vue/VList.tsx index efc286f7f..a93f3a108 100644 --- a/src/vue/VList.tsx +++ b/src/vue/VList.tsx @@ -15,12 +15,14 @@ import { SCROLL_IDLE, UPDATE_SCROLL_STATE, UPDATE_SCROLL_EVENT, - UPDATE_SCROLL_STOP_EVENT, + UPDATE_SCROLL_END_EVENT, UPDATE_SIZE_STATE, overscanEndIndex, overscanStartIndex, createVirtualStore, ACTION_ITEMS_LENGTH_CHANGE, + getScrollSize, + getMinContainerSize, } from "../core/store"; import { createResizer } from "../core/resizer"; import { createScroller } from "../core/scroller"; @@ -75,7 +77,7 @@ const props = { * - If not set, initial item sizes will be automatically estimated from measured sizes. This is recommended for most cases. * - If set, you can opt out estimation and use the value as initial item size. */ - initialItemSize: Number, + itemSize: Number, /** * While true is set, scroll position will be maintained from the end not usual start when items are added to/removed from start. It's recommended to set false if you add to/remove from mid/end of the list because it can cause unexpected behavior. This prop is useful for reverse infinite scrolling. */ @@ -88,16 +90,16 @@ const props = { export const VList = /*#__PURE__*/ defineComponent({ props: props, - emits: ["scroll", "scrollStop", "rangeChange"], + emits: ["scroll", "scrollEnd", "rangeChange"], setup(props, { emit, expose, slots }) { const isHorizontal = props.horizontal; const rootRef = ref(); const store = createVirtualStore( props.data.length, - props.initialItemSize ?? 40, + props.itemSize ?? 40, undefined, undefined, - !props.initialItemSize + !props.itemSize ); const resizer = createResizer(store, isHorizontal); const scroller = createScroller(store, isHorizontal); @@ -113,27 +115,25 @@ export const VList = /*#__PURE__*/ defineComponent({ const unsubscribeOnScroll = store._subscribe(UPDATE_SCROLL_EVENT, () => { emit("scroll", store._getScrollOffset()); }); - const unsubscribeOnScrollStop = store._subscribe( - UPDATE_SCROLL_STOP_EVENT, + const unsubscribeOnScrollEnd = store._subscribe( + UPDATE_SCROLL_END_EVENT, () => { - emit("scrollStop"); + emit("scrollEnd"); } ); - let cleanupResizer: (() => void) | undefined; - let cleanupScroller: (() => void) | undefined; onMounted(() => { const root = rootRef.value; if (!root) return; - cleanupResizer = resizer._observeRoot(root); - cleanupScroller = scroller._observe(root); + resizer._observeRoot(root); + scroller._observe(root); }); onUnmounted(() => { unsubscribeStore(); unsubscribeOnScroll(); - unsubscribeOnScrollStop(); - if (cleanupResizer) cleanupResizer(); - if (cleanupScroller) cleanupScroller(); + unsubscribeOnScrollEnd(); + resizer._dispose(); + scroller._dispose(); }); watch( @@ -171,7 +171,7 @@ export const VList = /*#__PURE__*/ defineComponent({ return store._getScrollOffset(); }, get scrollSize() { - return store._getScrollSize(); + return getScrollSize(store); }, get viewportSize() { return store._getViewportSize(); @@ -188,22 +188,23 @@ export const VList = /*#__PURE__*/ defineComponent({ const [startIndex, endIndex] = store._getRange(); const scrollDirection = store._getScrollDirection(); - const scrollSize = store._getScrollSize(); + const totalSize = store._getTotalSize(); - const overscanedStartIndex = overscanStartIndex( - startIndex, - props.overscan, - scrollDirection - ); - const overscanedEndIndex = overscanEndIndex( - endIndex, - props.overscan, - scrollDirection, - count - ); + // https://github.com/inokawa/virtua/issues/252#issuecomment-1822861368 + const minSize = getMinContainerSize(store); const items: VNode[] = []; - for (let i = overscanedStartIndex; i <= overscanedEndIndex; i++) { + for ( + let i = overscanStartIndex(startIndex, props.overscan, scrollDirection), + j = overscanEndIndex( + endIndex, + props.overscan, + scrollDirection, + count + ); + i <= j; + i++ + ) { const e = slots.default(props.data![i]!)[0]! as VNode; const key = e.key; items.push( @@ -237,8 +238,9 @@ export const VList = /*#__PURE__*/ defineComponent({ contain: "content", position: "relative", visibility: "hidden", - width: isHorizontal ? scrollSize + "px" : "100%", - height: isHorizontal ? "100%" : scrollSize + "px", + width: isHorizontal ? totalSize + "px" : "100%", + height: isHorizontal ? "100%" : totalSize + "px", + [isHorizontal ? "minWidth" : "minHeight"]: minSize, pointerEvents: scrollDirection !== SCROLL_IDLE ? "none" : "auto", }} > @@ -265,7 +267,7 @@ export const VList = /*#__PURE__*/ defineComponent({ /** * Callback invoked when scrolling stops. */ - scrollStop: () => void; + scrollEnd: () => void; /** * Callback invoked when visible items range changes. */ diff --git a/src/vue/__snapshots__/VList.spec.ts.snap b/src/vue/__snapshots__/VList.spec.ts.snap index b08f4cad6..a4f098adf 100644 --- a/src/vue/__snapshots__/VList.spec.ts.snap +++ b/src/vue/__snapshots__/VList.spec.ts.snap @@ -7,7 +7,7 @@ exports[`horizontal should render 0 children 1`] = ` style="display: inline-block; overflow-x: auto; contain: strict; width: 100%; height: 100%;" >
@@ -21,7 +21,7 @@ exports[`horizontal should render 1 children 1`] = ` style="display: inline-block; overflow-x: auto; contain: strict; width: 100%; height: 100%;" >
@@ -370,7 +370,7 @@ exports[`vertical should render 1 children 1`] = ` style="display: block; overflow-y: auto; contain: strict; width: 100%; height: 100%;" >
(({ children, height, attrs }, ref) => { - const [headerHeight, setHeaderHeight] = useState(0); - const headerRef = useRef(null); - useLayoutEffect(() => { - if (!headerRef.current) return; - setHeaderHeight(headerRef.current.getBoundingClientRect().height); - }, []); - - return ( -
- - - - {COLUMN_WIDTHS.map((width, i) => ( - - ))} - - - - {children} - -
- {i} -
-
- ); -}); - -export const Table: StoryObj = { - render: () => { - return ( - - {Array.from({ length: 1000 }).map((_, i) => ( - - {COLUMN_WIDTHS.map((width, j) => ( - - {i}, {j} - - ))} - - ))} - - ); - }, -}; diff --git a/stories/react/advanced/With cmdk.stories.tsx b/stories/react/advanced/With cmdk.stories.tsx new file mode 100644 index 000000000..a1dd05c58 --- /dev/null +++ b/stories/react/advanced/With cmdk.stories.tsx @@ -0,0 +1,85 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { CustomItemComponentProps, Virtualizer } from "../../../src"; +import React, { + ReactElement, + cloneElement, + forwardRef, + useMemo, + useRef, + useState, +} from "react"; +import { Command } from "cmdk"; +import { faker } from "@faker-js/faker"; + +const TAGS = Array.from({ length: 1000 }).map((_, i) => ({ + id: String(i), + label: faker.person.fullName(), +})); + +const Item = forwardRef( + ({ children, style }, ref) => { + children = children as ReactElement; + + return cloneElement(children, { + ref, + style: { ...children.props.style, ...style }, + }); + } +); + +export default { + component: Virtualizer, +} as Meta; + +export const Default: StoryObj = { + name: "With cmdk", + render: () => { + const ref = useRef(null); + const [selected, setSelected] = useState(""); + const [value, setValue] = useState(""); + const filtered = useMemo(() => { + if (!value) return TAGS; + const normalizedValue = value.toLowerCase(); + return TAGS.filter((t) => + t.label.toLowerCase().includes(normalizedValue) + ); + }, [value]); + + return ( + <> + + + No results found. + + + {filtered.map((t) => ( + + {t.label} + + ))} + + + + + + ); + }, +}; diff --git a/stories/react/advanced/With radix-ui.stories.tsx b/stories/react/advanced/With radix-ui.stories.tsx index e9f07fc82..fd5eae406 100644 --- a/stories/react/advanced/With radix-ui.stories.tsx +++ b/stories/react/advanced/With radix-ui.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; -import { CustomViewportComponentProps, VList } from "../../../src"; -import React, { ReactNode, forwardRef } from "react"; +import { Virtualizer } from "../../../src"; +import React, { useRef } from "react"; import { faker } from "@faker-js/faker"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import "./radix.css"; @@ -11,57 +11,29 @@ const TAGS = Array.from({ length: 1000 }).map((_, i) => ({ })); export default { - component: VList, + component: Virtualizer, } as Meta; -const Root = forwardRef( - ({ children, attrs, width, height, scrolling }, ref) => { - return ( - -
- {children} -
-
- ); - } -); - -const VirtualizedViewport = ({ - children, - style, -}: { - children: ReactNode; - style; -}) => { - return ( - - {children} - - ); -}; - export const Default: StoryObj = { name: "With radix-ui", render: () => { + const ref = useRef(null); return ( - -
Tags
- {TAGS.map((tag) => ( -
- {tag.label} -
- ))} -
+ + +
Tags
+ {TAGS.map((tag) => ( +
+ {tag.label} +
+ ))} +
+
(null!); - const ItemWithMinHeight = forwardRef( ({ children, style }, ref) => { return ( @@ -66,47 +51,40 @@ const ItemWithMinHeight = forwardRef( } ); -const Viewport = forwardRef( - ({ children, attrs, width, height, scrolling }, ref) => { - useLayoutEffect(() => { - // Ignore ResizeObserver errors because ResizeObserver used in virtua can cause error on window - // (https://github.com/inokawa/virtua#what-is-resizeobserver-loop-completed-with-undelivered-notifications-error) - // and react-beautiful-dnd aborts dragging when it detects any errors on window. - // (https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/setup-problem-detection-and-error-recovery.md#error-is-caught-by-window-error-listener) - // - // Set event listener here in this example because useLayoutEffect/componentDidMount will be called from children to parent usually. - const onError = (e: ErrorEvent) => { - if (e.message.includes("ResizeObserver")) { - e.stopImmediatePropagation(); - } - }; - window.addEventListener("error", onError); - return () => { - window.removeEventListener("error", onError); - }; - }, []); +const VirtualList = ({ + children, + innerRef: droppableRef, +}: { + children: ReactNode; + innerRef: DroppableProvided["innerRef"]; +}) => { + useLayoutEffect(() => { + // Ignore ResizeObserver errors because ResizeObserver used in virtua can cause error on window + // (https://github.com/inokawa/virtua#what-is-resizeobserver-loop-completed-with-undelivered-notifications-error) + // and react-beautiful-dnd aborts dragging when it detects any errors on window. + // (https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/setup-problem-detection-and-error-recovery.md#error-is-caught-by-window-error-listener) + // + // Set event listener here in this example because useLayoutEffect/componentDidMount will be called from children to parent usually. + const onError = (e: ErrorEvent) => { + if (e.message.includes("ResizeObserver")) { + e.stopImmediatePropagation(); + } + }; + window.addEventListener("error", onError); + return () => { + window.removeEventListener("error", onError); + }; + }, []); - const droppableRef = useContext(ScrollerRefContext); - return ( -
-
{ - return { - contain: "content", - position: "relative", - visibility: "hidden", - width: width ?? "100%", - height: height ?? "100%", - pointerEvents: scrolling ? "none" : "auto", - }; - }, [width, height, scrolling])} - > - {children} -
-
- ); - } -); + return ( +
+ {children} +
+ ); +}; export const Default: StoryObj = { name: "With react-beautiful-dnd", @@ -149,20 +127,15 @@ export const Default: StoryObj = { )} > {(provided) => ( - - - {items.map((id, i) => ( - - {(provided) => ( - - )} - - ))} - - + + {items.map((id, i) => ( + + {(provided) => ( + + )} + + ))} + )} diff --git a/stories/react/advanced/With react-select.stories.tsx b/stories/react/advanced/With react-select.stories.tsx deleted file mode 100644 index 67ed9fde3..000000000 --- a/stories/react/advanced/With react-select.stories.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { - CustomItemComponentProps, - CustomViewportComponentProps, - VList, - VListHandle, -} from "../../../src"; -import React, { - CSSProperties, - Ref, - createContext, - forwardRef, - useContext, - useEffect, - useMemo, - useRef, -} from "react"; -import Select, { MenuListProps, OptionProps, components } from "react-select"; -import { faker } from "@faker-js/faker"; -import { mergeRefs } from "react-merge-refs"; - -export default { - component: VList, -} as Meta; - -type OptionValue = { - value: string; - label: string; -}; - -const options = Array.from( - { length: 1000 }, - (_, i): OptionValue => ({ - value: String(i), - label: faker.music.songName(), - }) -); - -const MenuListContext = createContext< - Omit, "children"> ->(null!); -const OptionContext = createContext<{ - style: CSSProperties; - ref: Ref; -}>(null!); - -const Viewport = forwardRef( - ({ children, attrs, height }, ref) => { - const { maxHeight, innerProps, innerRef } = useContext(MenuListContext); - return ( -
-
{children}
-
- ); - } -); -const Item = forwardRef( - ({ style, children }, ref) => { - return ( - - {children} - - ); - } -); - -const MenuList = ({ children, ...rest }: MenuListProps) => { - const ref = useRef(null); - - // // https://github.com/jacobworrel/react-windowed-select/blob/master/src/MenuList.tsx - // const selectedIndex = useMemo(() => { - // let focusedIndex = -1; - // React.Children.forEach(children, (c, i) => { - // if ((c as React.ReactElement>).props.isFocused) { - // focusedIndex = i; - // } - // }); - // return focusedIndex; - // }, [children]); - // useEffect(() => { - // if (selectedIndex === -1) return; - // ref.current?.scrollToIndex(selectedIndex); - // }, [selectedIndex]); - - return ( - - - {children} - - - ); -}; -const Option = ({ - innerRef, - innerProps, - children, - ...rest -}: OptionProps) => { - const { style, ref } = useContext(OptionContext); - return ( - - {children} - - ); -}; - -export const Default: StoryObj = { - name: "With react-select", - render: () => { - return