Files
netto/site/src/examples/components/autocomplete/virtualized.tsx
T
Maofeng 4efe3f3c64 feat(site): add component documentation site
Add Vite, React, MDX, and Tailwind documentation infrastructure.

Document component, handbook, overview, and utility routes.

Add interactive examples for every component family.

Deduplicate React resolution and load the Tailwind 4 theme configuration.
2026-07-25 06:04:19 +08:00

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>>;