fix(async-select): fix select loading bugs
This commit is contained in:
137
src/components/async-select/VirtualSelectList.tsx
Normal file
137
src/components/async-select/VirtualSelectList.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { Combobox } from '@base-ui/react/combobox';
|
||||
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
import { ComboboxItem } from '@/components/ui/combobox';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import type { AsyncSelectOption } from './AsyncSelect.types';
|
||||
|
||||
type VirtualizerHandle = Virtualizer<HTMLDivElement, Element>;
|
||||
const LOAD_MORE_THRESHOLD = 3;
|
||||
|
||||
interface VirtualSelectListProps {
|
||||
open: boolean;
|
||||
virtualizerRef: RefObject<VirtualizerHandle | null>;
|
||||
hasNextPage: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
onLoadMore: () => void;
|
||||
estimateSize?: number;
|
||||
}
|
||||
|
||||
export function VirtualSelectList({
|
||||
open,
|
||||
virtualizerRef,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onLoadMore,
|
||||
estimateSize = 36,
|
||||
}: VirtualSelectListProps) {
|
||||
const filteredItems = Combobox.useFilteredItems<AsyncSelectOption>();
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const itemCount = filteredItems.length + (hasNextPage ? 1 : 0);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
enabled: open,
|
||||
count: itemCount,
|
||||
getScrollElement: () => scrollElementRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 8,
|
||||
paddingStart: 4,
|
||||
paddingEnd: 4,
|
||||
});
|
||||
|
||||
useImperativeHandle(virtualizerRef, () => virtualizer, [virtualizer]);
|
||||
|
||||
const handleScrollElementRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
scrollElementRef.current = element;
|
||||
if (element) {
|
||||
virtualizer.measure();
|
||||
}
|
||||
},
|
||||
[virtualizer],
|
||||
);
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems();
|
||||
const totalSize = virtualizer.getTotalSize();
|
||||
const lastVirtualIndex = virtualItems[virtualItems.length - 1]?.index ?? -1;
|
||||
|
||||
useEffect(() => {
|
||||
if (lastVirtualIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastVirtualIndex >= filteredItems.length - LOAD_MORE_THRESHOLD &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
onLoadMore();
|
||||
}
|
||||
}, [
|
||||
filteredItems.length,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
lastVirtualIndex,
|
||||
onLoadMore,
|
||||
]);
|
||||
|
||||
if (!filteredItems.length && !hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={handleScrollElementRef}
|
||||
className={cn(
|
||||
'max-h-[min(18rem,calc(var(--available-height)-2.25rem))] overflow-y-auto overscroll-contain p-1',
|
||||
)}
|
||||
style={{ height: Math.min(totalSize + 8, 200) }}
|
||||
>
|
||||
<div className="relative w-full" style={{ height: totalSize }}>
|
||||
{virtualItems.map((virtualItem) => {
|
||||
const isLoaderRow = virtualItem.index >= filteredItems.length;
|
||||
const item = filteredItems[virtualItem.index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
className="absolute top-0 left-0 w-full"
|
||||
style={{
|
||||
height: virtualItem.size,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
{isLoaderRow ? (
|
||||
<div className="flex items-center justify-center py-2 text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<ComboboxItem
|
||||
value={item}
|
||||
index={virtualItem.index}
|
||||
aria-setsize={filteredItems.length}
|
||||
aria-posinset={virtualItem.index + 1}
|
||||
>
|
||||
{item.label}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user