152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
|
|
// @ts-nocheck
|
||
|
|
'use client';
|
||
|
|
import * as React from 'react';
|
||
|
|
import Autocomplete from '@/components/ui/autocomplete';
|
||
|
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||
|
|
|
||
|
|
export default function ExampleVirtualizedAutocomplete() {
|
||
|
|
const virtualizerRef = React.useRef<Virtualizer | null>(null);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Autocomplete.Root
|
||
|
|
virtualized
|
||
|
|
items={virtualizedItems}
|
||
|
|
openOnInputClick
|
||
|
|
itemToStringValue={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 font-bold text-neutral-950 dark:text-white">
|
||
|
|
Search 10,000 items
|
||
|
|
<Autocomplete.Input />
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<Autocomplete.Portal>
|
||
|
|
<Autocomplete.Positioner sideOffset={4}>
|
||
|
|
<Autocomplete.Popup>
|
||
|
|
<Autocomplete.Empty>
|
||
|
|
<div className="py-3 px-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||
|
|
No items found.
|
||
|
|
</div>
|
||
|
|
</Autocomplete.Empty>
|
||
|
|
<Autocomplete.List className="p-0">
|
||
|
|
<VirtualizedList virtualizerRef={virtualizerRef} />
|
||
|
|
</Autocomplete.List>
|
||
|
|
</Autocomplete.Popup>
|
||
|
|
</Autocomplete.Positioner>
|
||
|
|
</Autocomplete.Portal>
|
||
|
|
</Autocomplete.Root>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function VirtualizedList({
|
||
|
|
virtualizerRef,
|
||
|
|
}: {
|
||
|
|
virtualizerRef: React.RefObject<Virtualizer | null>;
|
||
|
|
}) {
|
||
|
|
const filteredItems = Autocomplete.useFilteredItems<VirtualizedItem>();
|
||
|
|
|
||
|
|
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
|
||
|
|
|
||
|
|
const virtualizer = useVirtualizer({
|
||
|
|
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 (
|
||
|
|
<Autocomplete.Item
|
||
|
|
key={virtualItem.key}
|
||
|
|
index={virtualItem.index}
|
||
|
|
data-index={virtualItem.index}
|
||
|
|
ref={virtualizer.measureElement}
|
||
|
|
value={item}
|
||
|
|
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)`,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{item.name}
|
||
|
|
</Autocomplete.Item>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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>>;
|