diff --git a/.changeset/unlucky-cars-yell.md b/.changeset/unlucky-cars-yell.md new file mode 100644 index 000000000..6616d74b1 --- /dev/null +++ b/.changeset/unlucky-cars-yell.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/virtual": minor +--- + +Add a headless virtual list primitive: `createVirtualList` in #702 diff --git a/packages/virtual/README.md b/packages/virtual/README.md index 23af11d6c..80f3a4c40 100644 --- a/packages/virtual/README.md +++ b/packages/virtual/README.md @@ -9,7 +9,8 @@ [![version](https://img.shields.io/npm/v/@solid-primitives/virtual?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/virtual) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -A basic [virtualized list](https://www.patterns.dev/vanilla/virtual-lists/) component for improving performance when rendering very large lists +- [`createVirtualList`](#createvirtuallist) - A headless utility function for [virtualized lists](https://www.patterns.dev/vanilla/virtual-lists/) +- [`VirtualList`](#virtuallist) - a basic, unstyled component based on `createVirtualList` ## Installation @@ -23,18 +24,80 @@ pnpm add @solid-primitives/virtual ## How to use it +### `createVirtualList` + +`createVirtualList` is a headless utility for constructing your own virtualized list components with maximum flexibility. + +```tsx +function MyComp(): JSX.Element { + const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const rootHeight = 20; + const rowHeight = 10; + const overscanCount = 5; + + const [{ containerHeight, viewerTop, visibleItems }, onScroll] = createVirtualList({ + // the list of items - can be a signal + items, + // the height of the root element of the virtualizedList - can be a signal + rootHeight, + // the height of individual rows in the virtualizedList - can be a signal + rowHeight, + // the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling - can be a signal + overscanCount, + }); + + return ( +
+
+
+ {/* only visibleItems() are ultimately rendered */} + + {item =>
{item}
} +
+
+
+
+ ); +} +``` + +### `` + +`` is a basic, unstyled virtual list component you can drop into projects without modification. + ```tsx No items} // the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling overscanCount={5} // the height of the root element of the virtualizedList itself rootHeight={20} // the height of individual rows in the virtualizedList rowHeight={10} - // the class applied to the root element of the virtualizedList - class={"my-class-name"} > { // the flowComponent that will be used to transform the items into rows in the list @@ -43,12 +106,12 @@ pnpm add @solid-primitives/virtual ``` -The tests describe the component's exact behavior and how overscanCount handles the start/end of the list in more detail. +The tests describe the exact behavior and how overscanCount handles the start/end of the list in more detail. Note that the component only handles vertical lists where the number of items is known and the height of an individual item is fixed. ## Demo -You can see the VirtualizedList in action in the following sandbox: https://primitives.solidjs.community/playground/virtual +You can see the VirtualList in action in the following sandbox: https://primitives.solidjs.community/playground/virtual ## Changelog diff --git a/packages/virtual/dev/index.tsx b/packages/virtual/dev/index.tsx index 1f4bab098..36f865147 100644 --- a/packages/virtual/dev/index.tsx +++ b/packages/virtual/dev/index.tsx @@ -22,7 +22,7 @@ const App: Component = () => { { View the devtools console for log of items being added and removed from the visible list - - {item => } - +
+ no items
} + overscanCount={overscanCount()} + rootHeight={rootHeight()} + rowHeight={rowHeight()} + > + {item => } +
+ ); }; diff --git a/packages/virtual/package.json b/packages/virtual/package.json index bca03da44..a2bbe4842 100644 --- a/packages/virtual/package.json +++ b/packages/virtual/package.json @@ -17,6 +17,7 @@ "name": "virtual", "stage": 0, "list": [ + "createVirutalList", "VirtualList" ], "category": "UI Patterns" @@ -54,6 +55,9 @@ "test": "pnpm run vitest", "test:ssr": "pnpm run vitest --mode ssr" }, + "dependencies": { + "@solid-primitives/utils": "workspace:^" + }, "peerDependencies": { "solid-js": "^1.6.12" } diff --git a/packages/virtual/src/index.tsx b/packages/virtual/src/index.tsx index e81e14728..dbebd2b4c 100644 --- a/packages/virtual/src/index.tsx +++ b/packages/virtual/src/index.tsx @@ -1,71 +1,119 @@ import { For, createSignal } from "solid-js"; import type { Accessor, JSX } from "solid-js"; +import { access } from "@solid-primitives/utils"; +import type { MaybeAccessor } from "@solid-primitives/utils"; + +type VirtualListConfig = { + items: MaybeAccessor; + rootHeight: MaybeAccessor; + rowHeight: MaybeAccessor; + overscanCount?: MaybeAccessor; +}; + +type VirtualListReturn = [ + Accessor<{ + containerHeight: number; + viewerTop: number; + visibleItems: T; + }>, + onScroll: (e: Event) => void, +]; /** - * A basic virtualized list (see https://www.patterns.dev/vanilla/virtual-lists/) component for improving performance when rendering very large lists + * A headless virtualized list (see https://www.patterns.dev/vanilla/virtual-lists/) utility for constructing your own virtualized list components with maximum flexibility. * - * @param children the flowComponent that will be used to transform the items into rows in the list - * @param class the class applied to the root element of the virtualizedList - * @param each the list of items - * @param overscanCount the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling - * @param rootHeight the height of the root element of the virtualizedList itself + * @param items the list of items + * @param rootHeight the height of the root element of the virtualizedList * @param rowHeight the height of individual rows in the virtualizedList - * @return virtualized list component + * @param overscanCount the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling + * @returns {VirtualListReturn} to use in the list's jsx */ -export function VirtualList(props: { - children: (item: T[number], index: Accessor) => U; - fallback?: JSX.Element; - class?: string; - each: T | undefined | null | false; - overscanCount?: number; - rootHeight: number; - rowHeight: number; -}): JSX.Element { - let rootElement!: HTMLDivElement; +export function createVirtualList({ + items, + rootHeight, + rowHeight, + overscanCount, +}: VirtualListConfig): VirtualListReturn { + items = access(items) || ([] as any as T); + rootHeight = access(rootHeight); + rowHeight = access(rowHeight); + overscanCount = access(overscanCount) || 1; const [offset, setOffset] = createSignal(0); - const items = () => props.each || ([] as any as T); - const getFirstIdx = () => - Math.max(0, Math.floor(offset() / props.rowHeight) - (props.overscanCount || 1)); + const getFirstIdx = () => Math.max(0, Math.floor(offset() / rowHeight) - overscanCount); const getLastIdx = () => Math.min( - items().length, - Math.floor(offset() / props.rowHeight) + - Math.ceil(props.rootHeight / props.rowHeight) + - (props.overscanCount || 1), + items.length, + Math.floor(offset() / rowHeight) + Math.ceil(rootHeight / rowHeight) + overscanCount, ); + return [ + () => ({ + containerHeight: items.length * rowHeight, + viewerTop: getFirstIdx() * rowHeight, + visibleItems: items.slice(getFirstIdx(), getLastIdx()) as unknown as T, + }), + e => { + // @ts-expect-error + if (e.target?.scrollTop !== undefined) setOffset(e.target.scrollTop); + }, + ]; +} + +type VirtualListProps = { + children: (item: T[number], index: Accessor) => U; + each: T | undefined | null | false; + fallback?: JSX.Element; + overscanCount?: number; + rootHeight: number; + rowHeight: number; +}; + +/** + * A basic, unstyled virtualized list (see https://www.patterns.dev/vanilla/virtual-lists/) component you can drop into projects without modification + * + * @param children the flowComponent that will be used to transform the items into rows in the list + * @param each the list of items + * @param fallback the optional fallback to display if the list of items to display is empty + * @param overscanCount the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling + * @param rootHeight the height of the root element of the virtualizedList itself + * @param rowHeight the height of individual rows in the virtualizedList + * @returns virtualized list component + */ +export function VirtualList( + props: VirtualListProps, +): JSX.Element { + const [virtual, onScroll] = createVirtualList({ + items: () => props.each, + rootHeight: () => props.rootHeight, + rowHeight: () => props.rowHeight, + overscanCount: () => props.overscanCount || 1, + }); + return (
{ - setOffset(rootElement.scrollTop); - }} + onScroll={onScroll} >
- + {props.children}
diff --git a/packages/virtual/test/index.test.tsx b/packages/virtual/test/index.test.tsx index 67f0aa4c0..f30abccb0 100644 --- a/packages/virtual/test/index.test.tsx +++ b/packages/virtual/test/index.test.tsx @@ -1,96 +1,239 @@ import { describe, test, expect } from "vitest"; -import { VirtualList } from "../src/index.jsx"; import { render } from "solid-js/web"; +import { DOMElement } from "solid-js/jsx-runtime"; + +import { createVirtualList, VirtualList } from "../src/index.jsx"; const TEST_LIST = Array.from({ length: 1000 }, (_, i) => i); -const SELECTOR_CLASS_NAME = "scroll-container-selector"; +const ROOT = document.createElement("div"); const SCROLL_EVENT = new Event("scroll"); -let root = document.createElement("div"); +const TARGETED_SCROLL_EVENT = (el: DOMElement) => ({ ...SCROLL_EVENT, target: el }); -function get_scroll_continer() { - const scroll_container = root.querySelector("." + SELECTOR_CLASS_NAME); - if (scroll_container == null) { - throw "." + SELECTOR_CLASS_NAME + " was not found"; +function getScrollContainer() { + const scrollContainer = ROOT.querySelector("div"); + if (scrollContainer === null) { + throw "scrollContainer not found"; } - return scroll_container; + return scrollContainer; } +describe("createVirtualList", () => { + test("returns containerHeight representing the size of the list container element within the root", () => { + const [virtual] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtual().containerHeight).toEqual(10_000); + }); + + test("returns viewerTop representing the location of the list viewer element within the list container", () => { + const [virtual] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtual().viewerTop).toEqual(0); + }); + + test("returns visibleList representing the subset of items to render", () => { + const [virtual] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtual().visibleItems).toEqual([0, 1, 2]); + }); + + test("returns onScroll which sets viewerTop and visibleItems based on rootElement's scrolltop", () => { + const el = document.createElement("div"); + + const [virtual, onScroll] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtual().visibleItems).toEqual([0, 1, 2]); + expect(virtual().viewerTop).toEqual(0); + + el.scrollTop += 10; + + // no change until onScroll is called + expect(virtual().visibleItems).toEqual([0, 1, 2]); + expect(virtual().viewerTop).toEqual(0); + + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([0, 1, 2, 3]); + expect(virtual().viewerTop).toEqual(0); + + el.scrollTop += 10; + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([1, 2, 3, 4]); + expect(virtual().viewerTop).toEqual(10); + + el.scrollTop -= 10; + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([0, 1, 2, 3]); + expect(virtual().viewerTop).toEqual(0); + + el.scrollTop -= 10; + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([0, 1, 2]); + expect(virtual().viewerTop).toEqual(0); + }); + + test("onScroll handles reaching the bottom of the list", () => { + const el = document.createElement("div"); + + const [virtual, onScroll] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtual().visibleItems).toEqual([0, 1, 2]); + expect(virtual().viewerTop).toEqual(0); + + el.scrollTop += 9_980; + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([997, 998, 999]); + expect(virtual().viewerTop).toEqual(9_970); + }); + + test("visibleList takes `overscanCount` into account", () => { + const el = document.createElement("div"); + + const [virtual, onScroll] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + overscanCount: 2, + }); + + el.scrollTop += 100; + onScroll(TARGETED_SCROLL_EVENT(el)); + + expect(virtual().visibleItems).toEqual([8, 9, 10, 11, 12, 13]); + }); + + test("overscanCount defaults to 1 if undefined or zero", () => { + const [virtualUndefined] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + }); + + expect(virtualUndefined().visibleItems).toEqual([0, 1, 2]); + + const [virtualZero] = createVirtualList({ + items: TEST_LIST, + rootHeight: 20, + rowHeight: 10, + overscanCount: 0, + }); + + expect(virtualZero().visibleItems).toEqual([0, 1, 2]); + }); + + test("handles empty list", () => { + const [virtual] = createVirtualList({ + items: [], + rootHeight: 20, + rowHeight: 10, + overscanCount: 0, + }); + + expect(virtual().containerHeight).toEqual(0); + expect(virtual().viewerTop).toEqual(0); + expect(virtual().visibleItems).toEqual([]); + }); +}); + describe("VirtualList", () => { test("renders a subset of the items", () => { - let root = document.createElement("div"); const dispose = render( () => ( - {item =>
} + {item =>
} ), - root, + ROOT, ); - expect(root.querySelector("#item-0")).not.toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).toBeNull(); + expect(ROOT.querySelector("#item-0")).not.toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).toBeNull(); dispose(); }); - test("scrolling renders the correct subset of the items", () => { + test("renders the correct subset of the items based on scrolling", () => { const dispose = render( () => ( - - {item =>
} + + {item =>
} ), - root, + ROOT, ); - const scroll_container = get_scroll_continer(); - scroll_container.dispatchEvent(SCROLL_EVENT); + const scrollContainer = getScrollContainer(); + + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-0")).not.toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).toBeNull(); + expect(ROOT.querySelector("#item-0")).not.toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).toBeNull(); - scroll_container.scrollTop += 10; - scroll_container.dispatchEvent(SCROLL_EVENT); + scrollContainer.scrollTop += 10; + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-0")).not.toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).not.toBeNull(); - expect(root.querySelector("#item-4")).toBeNull(); + expect(ROOT.querySelector("#item-0")).not.toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).not.toBeNull(); + expect(ROOT.querySelector("#item-4")).toBeNull(); - scroll_container.scrollTop += 10; - scroll_container.dispatchEvent(SCROLL_EVENT); + scrollContainer.scrollTop += 10; + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-0")).toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).not.toBeNull(); - expect(root.querySelector("#item-4")).not.toBeNull(); - expect(root.querySelector("#item-5")).toBeNull(); + expect(ROOT.querySelector("#item-0")).toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).not.toBeNull(); + expect(ROOT.querySelector("#item-4")).not.toBeNull(); + expect(ROOT.querySelector("#item-5")).toBeNull(); - scroll_container.scrollTop -= 10; - scroll_container.dispatchEvent(SCROLL_EVENT); + scrollContainer.scrollTop -= 10; + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-0")).not.toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).not.toBeNull(); - expect(root.querySelector("#item-4")).toBeNull(); + expect(ROOT.querySelector("#item-0")).not.toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).not.toBeNull(); + expect(ROOT.querySelector("#item-4")).toBeNull(); - scroll_container.scrollTop -= 10; - scroll_container.dispatchEvent(SCROLL_EVENT); + scrollContainer.scrollTop -= 10; + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-0")).not.toBeNull(); - expect(root.querySelector("#item-1")).not.toBeNull(); - expect(root.querySelector("#item-2")).not.toBeNull(); - expect(root.querySelector("#item-3")).toBeNull(); + expect(ROOT.querySelector("#item-0")).not.toBeNull(); + expect(ROOT.querySelector("#item-1")).not.toBeNull(); + expect(ROOT.querySelector("#item-2")).not.toBeNull(); + expect(ROOT.querySelector("#item-3")).toBeNull(); dispose(); }); @@ -98,108 +241,82 @@ describe("VirtualList", () => { test("renders the correct subset of the items for the end of the list", () => { const dispose = render( () => ( - - {item =>
} + + {item =>
} ), - root, + ROOT, ); - const scroll_container = get_scroll_continer(); - scroll_container.scrollTop += 9_980; - scroll_container.dispatchEvent(SCROLL_EVENT); + const scrollContainer = getScrollContainer(); - expect(root.querySelector("#item-996")).toBeNull(); - expect(root.querySelector("#item-997")).not.toBeNull(); - expect(root.querySelector("#item-998")).not.toBeNull(); - expect(root.querySelector("#item-999")).not.toBeNull(); - expect(root.querySelector("#item-1000")).toBeNull(); + scrollContainer.scrollTop += 9_980; + scrollContainer.dispatchEvent(SCROLL_EVENT); + + expect(ROOT.querySelector("#item-996")).toBeNull(); + expect(ROOT.querySelector("#item-997")).not.toBeNull(); + expect(ROOT.querySelector("#item-998")).not.toBeNull(); + expect(ROOT.querySelector("#item-999")).not.toBeNull(); + expect(ROOT.querySelector("#item-1000")).toBeNull(); dispose(); }); - test("renders `overScan` rows above and below the visible rendered items", () => { + test("renders `overscanCount` rows above and below the visible rendered items", () => { const dispose = render( () => ( - - {item =>
} + + {item =>
} ), - root, + ROOT, ); - const scroll_container = get_scroll_continer(); - scroll_container.scrollTop += 100; - scroll_container.dispatchEvent(SCROLL_EVENT); + const scrollContainer = getScrollContainer(); + + scrollContainer.scrollTop += 100; + scrollContainer.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-7")).toBeNull(); - expect(root.querySelector("#item-8")).not.toBeNull(); - expect(root.querySelector("#item-9")).not.toBeNull(); - expect(root.querySelector("#item-10")).not.toBeNull(); - expect(root.querySelector("#item-11")).not.toBeNull(); - expect(root.querySelector("#item-12")).not.toBeNull(); - expect(root.querySelector("#item-13")).not.toBeNull(); - expect(root.querySelector("#item-14")).toBeNull(); + expect(ROOT.querySelector("#item-7")).toBeNull(); + expect(ROOT.querySelector("#item-8")).not.toBeNull(); + expect(ROOT.querySelector("#item-9")).not.toBeNull(); + expect(ROOT.querySelector("#item-10")).not.toBeNull(); + expect(ROOT.querySelector("#item-11")).not.toBeNull(); + expect(ROOT.querySelector("#item-12")).not.toBeNull(); + expect(ROOT.querySelector("#item-13")).not.toBeNull(); + expect(ROOT.querySelector("#item-14")).toBeNull(); dispose(); }); - test("overscanCount defaults to 1 if undefined", () => { + test("renders when list is empty", () => { const dispose = render( () => ( - - {item =>
} + + {item =>
} ), - root, + ROOT, ); - const scroll_container = get_scroll_continer(); - - scroll_container.scrollTop += 100; - scroll_container.dispatchEvent(SCROLL_EVENT); - expect(root.querySelector("#item-8")).toBeNull(); - expect(root.querySelector("#item-9")).not.toBeNull(); - expect(root.querySelector("#item-10")).not.toBeNull(); - expect(root.querySelector("#item-11")).not.toBeNull(); - expect(root.querySelector("#item-12")).not.toBeNull(); - expect(root.querySelector("#item-13")).toBeNull(); + expect(getScrollContainer()).not.toBeNull(); dispose(); }); - test("overscanCount defaults to 1 if set to zero", () => { + test("renders when list is empty with optional fallback", () => { const dispose = render( () => ( - - {item =>
} + } rootHeight={20} rowHeight={10}> + {item =>
} ), - root, + ROOT, ); - const scroll_container = get_scroll_continer(); - scroll_container.scrollTop += 100; - scroll_container.dispatchEvent(SCROLL_EVENT); + expect(getScrollContainer()).not.toBeNull(); - expect(root.querySelector("#item-8")).toBeNull(); - expect(root.querySelector("#item-9")).not.toBeNull(); - expect(root.querySelector("#item-10")).not.toBeNull(); - expect(root.querySelector("#item-11")).not.toBeNull(); - expect(root.querySelector("#item-12")).not.toBeNull(); - expect(root.querySelector("#item-13")).toBeNull(); + expect(ROOT.querySelector("#fallback")).not.toBeNull(); dispose(); }); diff --git a/packages/virtual/test/server.test.tsx b/packages/virtual/test/server.test.tsx index 903be5a97..b70da7ff0 100644 --- a/packages/virtual/test/server.test.tsx +++ b/packages/virtual/test/server.test.tsx @@ -7,25 +7,19 @@ const TEST_LIST = Array.from({ length: 1000 }, (_, i) => i); describe("VirtualList", () => { test("doesn't break in SSR", () => { const virtualListStr = renderToString(() => ( - - {item =>
{item}
} + + {item =>
{item}
}
)); expect(virtualListStr).toEqual( [ - '
', + '
', '
', '
', - '
0
', - '
1
', - '
2
', + '
0
', + '
1
', + '
2
', "
", "
", "
", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1994beda5..8c5165ac0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -956,6 +956,9 @@ importers: packages/virtual: dependencies: + '@solid-primitives/utils': + specifier: workspace:^ + version: link:../utils solid-js: specifier: ^1.6.12 version: 1.8.20