-
Couldn't load subscription status.
- Fork 142
virtual Improvements (Dynamic Item Sizes, Fix Broken Reactivity)
#803
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
41547e7
e64cdd0
a129664
9dc934b
c09050d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,13 @@ | ||
| 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"; | ||
|
|
||
| type VirtualListConfig<T extends readonly any[]> = { | ||
| items: MaybeAccessor<T | undefined | null | false>; | ||
| rootHeight: MaybeAccessor<number>; | ||
| rowHeight: MaybeAccessor<number>; | ||
| overscanCount?: MaybeAccessor<number>; | ||
| rowHeight: MaybeAccessor<number | ((row: T[number], index: number) => number)>; | ||
| }; | ||
|
|
||
| type VirtualListReturn<T extends readonly any[]> = [ | ||
|
|
@@ -17,6 +17,11 @@ type VirtualListReturn<T extends readonly any[]> = [ | |
| visibleItems: T; | ||
| }>, | ||
| onScroll: (e: Event) => void, | ||
| { | ||
| getFirstIdx: () => number; | ||
| getLastIdx: () => number; | ||
| scrollToItem: (itemIndex: number, scrollContainer: HTMLElement) => void; | ||
| }, | ||
| ]; | ||
|
|
||
| /** | ||
|
|
@@ -28,47 +33,87 @@ type VirtualListReturn<T extends readonly any[]> = [ | |
| * @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<T extends readonly any[]>({ | ||
| items, | ||
| rootHeight, | ||
| rowHeight, | ||
| overscanCount, | ||
| }: VirtualListConfig<T>): VirtualListReturn<T> { | ||
| items = access(items) || ([] as any as T); | ||
| rootHeight = access(rootHeight); | ||
| rowHeight = access(rowHeight); | ||
| overscanCount = access(overscanCount) || 1; | ||
| export function createVirtualList<T extends readonly any[]>( | ||
| cfg: VirtualListConfig<T>, | ||
| ): VirtualListReturn<T> { | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole function is already pretty complex without the binary search being included inside. I would like to ask you to extract from the function and use offsets as another parameter. |
||
| 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<T extends readonly any[], U extends JSX.Element> = { | ||
| children: (item: T[number], index: Accessor<number>) => U; | ||
| children: (item: T[number], index: Accessor<number>, rawIndex: Accessor<number>) => 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<T extends readonly any[], U extends JSX.Element> = { | |
| * @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<T extends readonly any[], U extends JSX.Element>( | ||
| props: VirtualListProps<T, U>, | ||
| ): 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 ( | ||
| <div | ||
| ref={scrollContainer} | ||
| style={{ | ||
| overflow: "auto", | ||
| height: `${props.rootHeight}px`, | ||
|
|
@@ -111,10 +161,11 @@ export function VirtualList<T extends readonly any[], U extends JSX.Element>( | |
| style={{ | ||
| position: "absolute", | ||
| top: `${virtual().viewerTop}px`, | ||
| width: "inherit", | ||
| }} | ||
| > | ||
| <For fallback={props.fallback} each={virtual().visibleItems}> | ||
| {props.children} | ||
| {(item, index) => props.children(item, () => getFirstIdx() + index(), index)} | ||
| </For> | ||
| </div> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An accessor that returns a function is a bit stupid. Shouldn't it be
MaybeAccessor<number> | ((row: T[number], index: number) => number)?