/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useEffect, useRef, useState } from 'react'; import type { CompletionItem } from '../../../types/completionItemTypes.js'; interface CompletionMenuProps { items: CompletionItem[]; onSelect: (item: CompletionItem) => void; onClose: () => void; title?: string; selectedIndex?: number; } export const CompletionMenu: React.FC = ({ items, onSelect, onClose, title, selectedIndex = 0, }) => { const containerRef = useRef(null); const [selected, setSelected] = useState(selectedIndex); // Mount state to drive a simple Tailwind transition (replaces CSS keyframes) const [mounted, setMounted] = useState(false); useEffect(() => setSelected(selectedIndex), [selectedIndex]); useEffect(() => setMounted(true), []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { onClose(); } }; const handleKeyDown = (event: KeyboardEvent) => { switch (event.key) { case 'ArrowDown': event.preventDefault(); setSelected((prev) => Math.min(prev + 1, items.length - 1)); break; case 'ArrowUp': event.preventDefault(); setSelected((prev) => Math.max(prev - 1, 0)); break; case 'Enter': event.preventDefault(); if (items[selected]) { onSelect(items[selected]); } break; case 'Escape': event.preventDefault(); onClose(); break; default: break; } }; document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleKeyDown); }; }, [items, selected, onSelect, onClose]); useEffect(() => { const selectedEl = containerRef.current?.querySelector( `[data-index="${selected}"]`, ); if (selectedEl) { selectedEl.scrollIntoView({ block: 'nearest' }); } }, [selected]); if (!items.length) { return null; } return (
{/* Optional top spacer for visual separation from the input */}
{title && (
{title}
)} {items.map((item, index) => { const isActive = index === selected; return (
onSelect(item)} onMouseEnter={() => setSelected(index)} className={[ // Semantic 'completion-menu-item', // Hit area 'mx-1 cursor-pointer rounded-[var(--app-list-border-radius)]', 'p-[var(--app-list-item-padding)]', // Active background isActive ? 'bg-[var(--app-list-active-background)]' : '', ].join(' ')} >
{item.icon && ( {item.icon} )} {item.label} {item.description && ( {item.description} )}
); })}
); };