Files
netto/site/src/examples/components/combobox/virtualized.tsx
T

177 lines
5.5 KiB
TypeScript
Raw Normal View History

// @ts-nocheck
'use client';
import * as React from 'react';
import Combobox from '@/components/ui/combobox';
import { useVirtualizer } from '@tanstack/react-virtual';
export default function ExampleVirtualizedCombobox() {
const [open, setOpen] = React.useState(false);
const virtualizerRef = React.useRef<Virtualizer | null>(null);
return (
<Combobox.Root
virtualized
items={virtualizedItems}
open={open}
onOpenChange={setOpen}
itemToStringLabel={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
const virtualizer = virtualizerRef.current;
if (!item || !virtualizer) {
return;
}
const isStart = index === 0;
const isEnd = index === virtualizer.options.count - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
});
}
}}
>
<label className="flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white">
Search 10,000 items
<Combobox.Input className="h-8 w-64 border border-neutral-950 bg-white dark:bg-neutral-950 px-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white" />
</label>
<Combobox.Portal>
<Combobox.Positioner sideOffset={4}>
<Combobox.Popup>
<Combobox.Empty>
<div className="py-3 px-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
No items found.
</div>
</Combobox.Empty>
<Combobox.List className="p-0">
<VirtualizedList open={open} virtualizerRef={virtualizerRef} />
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function VirtualizedList({
open,
virtualizerRef,
}: {
open: boolean;
virtualizerRef: React.RefObject<Virtualizer | null>;
}) {
const filteredItems = Combobox.useFilteredItems<VirtualizedItem>();
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
const virtualizer = useVirtualizer({
enabled: open,
count: filteredItems.length,
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
scrollPaddingEnd: 4,
scrollPaddingStart: 4,
});
React.useImperativeHandle(virtualizerRef, () => virtualizer);
const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
}
},
[virtualizer],
);
const totalSize = virtualizer.getTotalSize();
if (!filteredItems.length) {
return null;
}
return (
<div
role="presentation"
ref={handleScrollElementRef}
className="h-[min(22.5rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-py-1"
style={{ '--total-size': `${totalSize}px` } as React.CSSProperties}
>
<div role="presentation" className="relative w-full" style={{ height: totalSize }}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index];
if (!item) {
return null;
}
return (
<Combobox.Item
key={virtualItem.key}
index={virtualItem.index}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
value={item}
className="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-none select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-white data-highlighted:before:absolute data-highlighted:before:inset-0 data-highlighted:before:z-[-1] data-highlighted:before:bg-neutral-950 dark:data-highlighted:text-neutral-950 dark:data-highlighted:before:bg-white"
aria-setsize={filteredItems.length}
aria-posinset={virtualItem.index + 1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<Combobox.ItemIndicator>
<CheckIcon />
</Combobox.ItemIndicator>
<span className="col-start-2">{item.name}</span>
</Combobox.Item>
);
})}
</div>
</div>
);
}
function CheckIcon(props: React.ComponentProps<'svg'>) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={{ display: 'block', ...props.style }}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
interface VirtualizedItem {
id: string;
name: string;
}
function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
type Virtualizer = ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;