feature(VirtualList): remove tanstack virtual list solution, add self written one

This commit is contained in:
Ilia Mashkov
2026-01-15 13:33:59 +03:00
parent 925d2eec3e
commit 429a9a0877
5 changed files with 175 additions and 208 deletions

View File

@@ -55,53 +55,15 @@ interface Props {
let { items, itemHeight = 80, overscan = 5, class: className, children }: Props = $props();
let activeIndex = $state(0);
const itemRefs = new Map<number, HTMLElement>();
const virtual = createVirtualizer(() => ({
const virtualizer = createVirtualizer(() => ({
count: items.length,
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
overscan,
}));
function registerItem(node: HTMLElement, index: number) {
itemRefs.set(index, node);
return {
destroy() {
itemRefs.delete(index);
},
};
}
async function focusItem(index: number) {
activeIndex = index;
virtual.scrollToIndex(index, { align: 'auto' });
await tick();
itemRefs.get(index)?.focus();
}
async function handleKeydown(event: KeyboardEvent) {
let nextIndex = activeIndex;
if (event.key === 'ArrowDown') nextIndex++;
else if (event.key === 'ArrowUp') nextIndex--;
else if (event.key === 'Home') nextIndex = 0;
else if (event.key === 'End') nextIndex = items.length - 1;
else return;
if (nextIndex >= 0 && nextIndex < items.length) {
event.preventDefault();
await focusItem(nextIndex);
}
}
</script>
<!--
Scroll container with single tab stop pattern:
- tabindex="0" on container, tabindex="-1" on items
- Arrow keys navigate within, Tab moves out
-->
<div
bind:this={virtual.scrollElement}
use:virtualizer.container
class={cn(
'relative overflow-auto border rounded-md bg-background',
'outline-none focus-visible:ring-2 ring-ring ring-offset-2',
@@ -110,29 +72,15 @@ async function handleKeydown(event: KeyboardEvent) {
)}
role="listbox"
tabindex="0"
onkeydown={handleKeydown}
onfocusin={(e => e.target === virtual.scrollElement && focusItem(activeIndex))}
>
<!-- Total scrollable height placeholder -->
<div
class="relative w-full"
style:height="{virtual.totalSize}px"
>
{#each virtual.items as row (row.key)}
<!-- Individual item positioned absolutely via GPU-accelerated transform -->
<div
use:registerItem={row.index}
data-index={row.index}
role="option"
aria-selected={activeIndex === row.index}
tabindex="-1"
onmousedown={() => (activeIndex = row.index)}
class="absolute top-0 left-0 w-full outline-none focus:bg-accent focus:text-accent-foreground"
style:height="{row.size}px"
style:transform="translateY({row.start}px)"
>
{@render children({ item: items[row.index], index: row.index })}
</div>
{/each}
</div>
{#each virtualizer.items as item (item.key)}
<div
use:virtualizer.measureElement
data-index={item.index}
class="absolute top-0 left-0 w-full translate-y-[var(--offset)] will-change-transform"
style:--offset="{item.start}px"
>
{@render children({ item: items[item.index], index: item.index })}
</div>
{/each}
</div>