diff --git a/packages/virtual/dev/index.tsx b/packages/virtual/dev/index.tsx
index bb715cc8f..a36266b40 100644
--- a/packages/virtual/dev/index.tsx
+++ b/packages/virtual/dev/index.tsx
@@ -4,19 +4,25 @@ import { VirtualList } from "../src/index.jsx";
const intl = new Intl.NumberFormat();
-const items = new Array(100_000).fill(0).map((_, i) => i);
-
+const items = new Array(100_000).fill(0).map((_, i) => [i, Math.random() * 72 + 24]);
const clampRange = (min: number, max: number, v: number) => (v < min ? min : v > max ? max : v);
const App: Component = () => {
+ const [dynamicSize, setDynamicSize] = createSignal(true);
const [listLength, setListLength] = createSignal(100_000);
const [overscanCount, setOverscanCount] = createSignal(5);
const [rootHeight, setRootHeight] = createSignal(240);
const [rowHeight, setRowHeight] = createSignal(24);
+ let scrollToItem!: (itemIndex: number) => void;
return (
+
+
+
{
fallback={no items
}
overscanCount={overscanCount()}
rootHeight={rootHeight()}
- rowHeight={rowHeight()}
+ rowHeight={dynamicSize() ? item => item[1]! : rowHeight()}
+ setScrollToItem={fn => (scrollToItem = fn)}
>
- {item => }
+ {item => (
+
+ )}
@@ -122,7 +131,13 @@ const VirtualListItem: Component
= props => {
});
return (
-
+
{intl.format(props.item)}
);
diff --git a/packages/virtual/src/index.tsx b/packages/virtual/src/index.tsx
index dbebd2b4c..b3b573e3b 100644
--- a/packages/virtual/src/index.tsx
+++ b/packages/virtual/src/index.tsx
@@ -1,4 +1,4 @@
-import { For, createSignal } from "solid-js";
+import { For, createMemo, createSignal } from "solid-js";
import type { Accessor, JSX } from "solid-js";
import { access } from "@solid-primitives/utils";
import type { MaybeAccessor } from "@solid-primitives/utils";
@@ -6,8 +6,8 @@ import type { MaybeAccessor } from "@solid-primitives/utils";
type VirtualListConfig
= {
items: MaybeAccessor;
rootHeight: MaybeAccessor;
- rowHeight: MaybeAccessor;
overscanCount?: MaybeAccessor;
+ rowHeight: MaybeAccessor number)>;
};
type VirtualListReturn = [
@@ -17,6 +17,11 @@ type VirtualListReturn = [
visibleItems: T;
}>,
onScroll: (e: Event) => void,
+ {
+ getFirstIdx: () => number;
+ getLastIdx: () => number;
+ scrollToItem: (itemIndex: number, scrollContainer: HTMLElement) => void;
+ },
];
/**
@@ -28,47 +33,87 @@ type VirtualListReturn = [
* @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 createVirtualList({
- items,
- rootHeight,
- rowHeight,
- overscanCount,
-}: VirtualListConfig): VirtualListReturn {
- items = access(items) || ([] as any as T);
- rootHeight = access(rootHeight);
- rowHeight = access(rowHeight);
- overscanCount = access(overscanCount) || 1;
+export function createVirtualList(
+ cfg: VirtualListConfig,
+): VirtualListReturn {
+ const items = () => access(cfg.items) || ([] as any as T);
+ const overscanCount = () => access(cfg.overscanCount) || 1;
const [offset, setOffset] = createSignal(0);
- const getFirstIdx = () => Math.max(0, Math.floor(offset() / rowHeight) - overscanCount);
+ const rowOffsets = createMemo(() => {
+ let offset = 0;
+ return items().map((item, i) => {
+ const current = offset;
+ const rowHeight = access(cfg.rowHeight);
+
+ offset += typeof rowHeight === "function" ? rowHeight(item, i) : rowHeight;
+ return current;
+ });
+ });
+
+ // Binary Search for performance
+ const findRowIndexAtOffset = (offset: number) => {
+ const offsets = rowOffsets();
+
+ let lo = 0,
+ hi = offsets.length - 1,
+ mid: number;
+ while (lo <= hi) {
+ mid = (lo + hi) >>> 1;
+ if (offsets[mid]! > offset) {
+ hi = mid - 1;
+ } else {
+ lo = mid + 1;
+ }
+ }
+ return lo;
+ };
+
+ const getFirstIdx = () => Math.max(0, findRowIndexAtOffset(offset()) - overscanCount());
+
+ // const getFirstIdx = () => Math.max(0, Math.floor(offset() / rowHeight) - overscanCount);
const getLastIdx = () =>
Math.min(
- items.length,
- Math.floor(offset() / rowHeight) + Math.ceil(rootHeight / rowHeight) + overscanCount,
+ items().length,
+ findRowIndexAtOffset(offset() + access(cfg.rootHeight)) + overscanCount(),
);
+ // const getLastIdx = () =>
+ // Math.min(
+ // 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,
+ containerHeight: items().length !== 0 ? rowOffsets()[items().length - 1]! : 0,
+ viewerTop: rowOffsets()[getFirstIdx()]!,
+ visibleItems: items().slice(getFirstIdx(), getLastIdx()) as unknown as T,
}),
e => {
// @ts-expect-error
if (e.target?.scrollTop !== undefined) setOffset(e.target.scrollTop);
},
+ {
+ getFirstIdx,
+ getLastIdx,
+ scrollToItem: (itemIndex: number, scrollContainer: HTMLElement) => {
+ scrollContainer.scrollTop = rowOffsets()[itemIndex]!;
+ },
+ },
];
}
type VirtualListProps = {
- children: (item: T[number], index: Accessor) => U;
+ children: (item: T[number], index: Accessor, rawIndex: Accessor) => U;
each: T | undefined | null | false;
fallback?: JSX.Element;
overscanCount?: number;
+ rowHeight: number | ((row: T[number], index: number) => number);
rootHeight: number;
- rowHeight: number;
+ setScrollToItem: (scrollToItem: (itemIndex: number) => void) => void;
};
/**
@@ -79,21 +124,26 @@ type VirtualListProps = {
* @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
+ * @param rowHeight the height of individual rows in the virtualizedList—can be static if just a number is provided, or dynamic if a callback is passed
* @returns virtualized list component
*/
export function VirtualList(
props: VirtualListProps,
): JSX.Element {
- const [virtual, onScroll] = createVirtualList({
+ const [virtual, onScroll, { scrollToItem, getFirstIdx }] = createVirtualList({
items: () => props.each,
rootHeight: () => props.rootHeight,
rowHeight: () => props.rowHeight,
overscanCount: () => props.overscanCount || 1,
});
+ props.setScrollToItem((itemIndex: number) => scrollToItem(itemIndex, scrollContainer));
+
+ let scrollContainer!: HTMLDivElement;
+
return (
(
style={{
position: "absolute",
top: `${virtual().viewerTop}px`,
+ width: "inherit",
}}
>
- {props.children}
+ {(item, index) => props.children(item, () => getFirstIdx() + index(), index)}
diff --git a/site/src/primitives/DocumentHydrationHelper.tsx b/site/src/primitives/DocumentHydrationHelper.tsx
index 49b5abda3..dd62b47ad 100644
--- a/site/src/primitives/DocumentHydrationHelper.tsx
+++ b/site/src/primitives/DocumentHydrationHelper.tsx
@@ -1,5 +1,5 @@
import { getRequestEvent, HydrationScript, NoHydration } from "solid-js/web";
-import { Asset, PageEvent } from "@solidjs/start/server";
+import type { Asset, PageEvent } from "@solidjs/start/server";
import type { JSX } from "solid-js";
const assetMap = {