返回 DeepSeek-Reasonix
VirtualMenu.tsx
根目录 / desktop / frontend / src / components / VirtualMenu.tsx
1 import { useCallback, useEffect, useRef, type ReactNode } from "react";
2 import { useVirtualizer } from "@tanstack/react-virtual";
3
4 // VirtualMenu is the shared scroll container for the composer's "/" and "@"
5 // dropdowns. Rows are virtualized so a directory with thousands of entries (or a
6 // long command list) only ever mounts the visible rows plus a small overscan —
7 // no truncation, no jank. The caller owns the item data, the active index, and
8 // per-row markup; this owns layout and keeping the active row in view.
9 export function VirtualMenu<T>({
10 items,
11 activeIndex,
12 itemKey,
13 renderItem,
14 estimateSize,
15 }: {
16 items: T[];
17 activeIndex: number;
18 itemKey: (item: T, index: number) => string;
19 renderItem: (item: T, index: number) => ReactNode;
20 estimateSize?: (item: T, index: number) => number;
21 }) {
22 const scrollRef = useRef<HTMLDivElement>(null);
23 const getItemKey = useCallback(
24 (index: number) => itemKey(items[index], index),
25 [itemKey, items],
26 );
27 const virtualizer = useVirtualizer({
28 count: items.length,
29 getScrollElement: () => scrollRef.current,
30 getItemKey,
31 estimateSize: (index) => estimateSize?.(items[index], index) ?? 34,
32 overscan: 10,
33 // Measurement callbacks can arrive during React's commit phase. Let the
34 // virtualizer update stable row positions directly instead of dispatching a
35 // reducer update for every ResizeObserver measurement (React #185).
36 directDomUpdates: true,
37 });
38
39 useEffect(() => {
40 if (activeIndex >= 0 && activeIndex < items.length) {
41 virtualizer.scrollToIndex(activeIndex, { align: "auto" });
42 }
43 }, [activeIndex, items.length, virtualizer]);
44
45 return (
46 <div ref={scrollRef} className="slashmenu" role="listbox">
47 <div ref={virtualizer.containerRef} className="slashmenu__sizer">
48 {virtualizer.getVirtualItems().map((row) => (
49 <div
50 key={row.key}
51 data-index={row.index}
52 ref={virtualizer.measureElement}
53 className="slashmenu__row"
54 >
55 {renderItem(items[row.index], row.index)}
56 </div>
57 ))}
58 </div>
59 </div>
60 );
61 }
62
62 lines Plain Text