Files
frontend-svelte/src/shared/ui/VirtualList/VirtualList.svelte

88 lines
2.5 KiB
Svelte
Raw Normal View History

<!--
Component: VirtualList
High-performance virtualized list for large datasets with:
- Virtual scrolling (only renders visible items + overscan)
- Keyboard navigation (ArrowUp/Down, Home, End)
- Fixed or dynamic item heights
- ARIA listbox/option pattern with single tab stop
-->
<script lang="ts" generics="T">
2026-01-07 16:54:12 +03:00
import { createVirtualizer } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type { Snippet } from 'svelte';
interface Props {
/**
* Array of items to render in the virtual list.
*
* @template T - The type of items in the list
*/
items: T[];
/**
* Height for each item, either as a fixed number
* or a function that returns height per index.
* @default 80
*/
itemHeight?: number | ((index: number) => number);
/**
* Optional overscan value for the virtual list.
* @default 5
*/
overscan?: number;
/**
* Optional CSS class string for styling the container
* (follows shadcn convention for className prop)
*/
class?: string;
/**
* Snippet for rendering individual list items.
*
* The snippet receives an object containing:
* - `item`: The item from the items array (type T)
* - `index`: The current item's index in the array
*
* This pattern provides type safety and flexibility for
* rendering different item types without prop drilling.
*
* @template T - The type of items in the list
*/
children: Snippet<[{ item: T; index: number }]>;
}
let { items, itemHeight = 80, overscan = 5, class: className, children }: Props = $props();
const virtualizer = createVirtualizer(() => ({
count: items.length,
data: items,
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
overscan,
}));
</script>
<div
use:virtualizer.container
class={cn(
'relative overflow-auto border rounded-md bg-background',
'h-150 w-full',
className,
)}
>
<div
style:height="{virtualizer.totalSize}px"
class="w-full pointer-events-none"
>
</div>
{#each virtualizer.items as item (item.key)}
<div
use:virtualizer.measureElement
data-index={item.index}
class="absolute top-0 left-0 w-full"
style:transform="translateY({item.start}px)"
>
{@render children({ item: items[item.index], index: item.index })}
</div>
{/each}
</div>