From e45ba5af93917013dc97a01d8097c3011a340446 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:26:38 -0400 Subject: [PATCH] Add class property to VirtualList --- .changeset/virtual-list-class-prop.md | 5 + packages/virtual/README.md | 25 +++- packages/virtual/dev/index.tsx | 131 ------------------- packages/virtual/src/index.tsx | 3 + packages/virtual/stories/virtual.stories.tsx | 68 +++++++++- packages/virtual/test/index.test.tsx | 17 +++ packages/virtual/test/server.test.tsx | 2 +- 7 files changed, 117 insertions(+), 134 deletions(-) create mode 100644 .changeset/virtual-list-class-prop.md delete mode 100644 packages/virtual/dev/index.tsx diff --git a/.changeset/virtual-list-class-prop.md b/.changeset/virtual-list-class-prop.md new file mode 100644 index 000000000..ed989baf6 --- /dev/null +++ b/.changeset/virtual-list-class-prop.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/virtual": minor +--- + +Added `class` prop to `` for styling the element that wraps the rendered rows (e.g. `display: flex; flex-flow: column; width: 100%;`). Resolves #698. diff --git a/packages/virtual/README.md b/packages/virtual/README.md index 9d5c7f720..be527feef 100644 --- a/packages/virtual/README.md +++ b/packages/virtual/README.md @@ -28,6 +28,8 @@ pnpm add @solid-primitives/virtual `createVirtualList` is a headless utility for constructing your own virtualized list components with maximum flexibility. +`virtual` is an accessor returning `{ containerHeight, viewerTop, visibleItems }` — call it to read the current values. When rendering `visibleItems` with ``, pass `keyed={false}` so each item is provided as an `Accessor` (`` defaults to `keyed={true}`, which passes raw values instead). + ```tsx function MyComp(): JSX.Element { const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -72,7 +74,8 @@ function MyComp(): JSX.Element { }} > {/* only virtual().visibleItems are ultimately rendered */} - + {/* keyed={false} is required for item to be passed as an Accessor — defaults to keyed={true}, passing raw values instead */} + {item =>
{item()}
}
@@ -92,6 +95,8 @@ function MyComp(): JSX.Element { each={[0, 1, 2, 3, 4, 5, 6, 7]} // the optional fallback to display if the list of items is empty fallback={
No items
} + // an optional class applied to the element wrapping the rendered rows, useful for styling row layout (e.g. flex/grid) + class="my-list" // 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 @@ -110,6 +115,24 @@ function MyComp(): JSX.Element { 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. +#### Styling the rows with `class` + +The rendered rows are wrapped in an absolutely positioned element that `` manages internally (for offsetting the visible window as you scroll). Pass `class` to have it applied to that wrapper, which is useful for laying out rows with flexbox or grid instead of relying on each row's own styles: + +```tsx + + {item =>
{item()}
} +
+``` + +```css +.row-list { + display: flex; + flex-flow: column; + width: 100%; +} +``` + ## Changelog See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/virtual/dev/index.tsx b/packages/virtual/dev/index.tsx deleted file mode 100644 index dd0ee8a61..000000000 --- a/packages/virtual/dev/index.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { createSignal, onSettled, onCleanup, type Component } from "solid-js"; -import { isServer } from "@solidjs/web"; -import { VirtualList } from "../src/index.jsx"; - -const intl = new Intl.NumberFormat(); - -const items = new Array(100_000).fill(0).map((_, i) => i); - -const clampRange = (min: number, max: number, v: number) => (v < min ? min : v > max ? max : v); - -const App: Component = () => { - const [listLength, setListLength] = createSignal(100_000); - const [overscanCount, setOverscanCount] = createSignal(5); - const [rootHeight, setRootHeight] = createSignal(240); - const [rowHeight, setRowHeight] = createSignal(24); - - return ( -
-
-
- -
-
- -
-
- -
-
- -
-
- -
- View the devtools console for log of items being added and removed from the visible list -
- -
- no items
} - overscanCount={overscanCount()} - rootHeight={rootHeight()} - rowHeight={rowHeight()} - > - {item => } - -
- - ); -}; - -type DemoControlProps = { - label: string; - max: number; - min: number; - name: string; - setValue: (v: number) => void; - value: number; -}; - -const DemoControl: Component = props => ( - -); - -type VirtualListItemProps = { - item: number; - height: number; -}; - -const VirtualListItem: Component = props => { - onSettled(() => { - if (!isServer) console.log("item added:", props.item); - }); - - onCleanup(() => { - if (!isServer) console.log("item removed::", props.item); - }); - - return ( -
- {intl.format(props.item)} -
- ); -}; - -export default App; diff --git a/packages/virtual/src/index.tsx b/packages/virtual/src/index.tsx index c517bf86b..1945dd020 100644 --- a/packages/virtual/src/index.tsx +++ b/packages/virtual/src/index.tsx @@ -70,6 +70,7 @@ type VirtualListProps = { children: (item: Accessor, index: number) => U; each: T | undefined | null | false; fallback?: JSX.Element; + class?: string; overscanCount?: number; rootHeight: number; rowHeight: number; @@ -81,6 +82,7 @@ type VirtualListProps = { * @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 class an optional class applied to the element wrapping the rendered rows, useful for styling row layout (e.g. flex/grid) * @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 @@ -112,6 +114,7 @@ export function VirtualList( }} >
{ + const items = ALL_ITEMS.slice(0, 30); + + return ( + +

{''}

+ + + + + {(item: Accessor<(typeof ALL_ITEMS)[number]>) => ( +
+
+ {item().label} +
+ )} + + +

+ Without class, rows would need their own margin to create gaps. With it, the + row wrapper itself becomes a flex column with a uniform gap. +

+ + ); + }, +}); + export const HeadlessVirtualList = meta.story({ name: "createVirtualList (headless)", parameters: { @@ -233,7 +299,7 @@ export const HeadlessVirtualList = meta.story({ width: "100%", }} > - + {(item: Accessor) => (
{ dispose(); }); + test("applies class to the element wrapping the rendered rows", () => { + const dispose = render( + () => ( + + {item =>
} + + ), + ROOT, + ); + + const listEl = ROOT.querySelector(".my-list"); + expect(listEl).not.toBeNull(); + expect(listEl?.querySelector("#item-0")).not.toBeNull(); + + dispose(); + }); + test("renders when list is empty with optional fallback", () => { const dispose = render( () => ( diff --git a/packages/virtual/test/server.test.tsx b/packages/virtual/test/server.test.tsx index 2a6583f8f..305408964 100644 --- a/packages/virtual/test/server.test.tsx +++ b/packages/virtual/test/server.test.tsx @@ -16,7 +16,7 @@ describe("VirtualList", () => { [ '
', '
', - '
', + '
', '
0
', '
1
', '
2
',