import { useEffect, useRef, useState, type KeyboardEvent } from 'react'; import { extractApiError } from '@/api/client'; import type { MitreTechnique } from '@/api/types'; import { useMitreSearch } from '@/hooks/useMitre'; interface MitreTechniquePickerProps { onSelect: (technique: MitreTechnique) => void; disabled?: boolean; } const DEBOUNCE_MS = 200; export function MitreTechniquePicker({ onSelect, disabled = false, }: MitreTechniquePickerProps): JSX.Element { const [inputValue, setInputValue] = useState(''); const [query, setQuery] = useState(''); const [open, setOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const debounceRef = useRef | null>(null); const containerRef = useRef(null); const listRef = useRef(null); const { data: results, isFetching, isError, error } = useMitreSearch(query, open); const items = results ?? []; const handleInputChange = (value: string) => { setInputValue(value); setOpen(true); setActiveIndex(-1); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { setQuery(value); }, DEBOUNCE_MS); }; const selectItem = (item: MitreTechnique) => { onSelect(item); // Reset to empty after selection — parent handles append + dedup setInputValue(''); setQuery(''); setOpen(false); setActiveIndex(-1); }; const handleKeyDown = (e: KeyboardEvent) => { if (!open || items.length === 0) { if (e.key === 'Escape') setOpen(false); return; } if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, items.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)); } else if (e.key === 'Enter') { e.preventDefault(); if (activeIndex >= 0 && items[activeIndex]) { selectItem(items[activeIndex]); } } else if (e.key === 'Escape') { setOpen(false); setActiveIndex(-1); } }; useEffect(() => { if (activeIndex >= 0 && listRef.current) { const el = listRef.current.children[activeIndex] as HTMLElement | undefined; el?.scrollIntoView?.({ block: 'nearest' }); } }, [activeIndex]); useEffect(() => { const onPointerDown = (e: PointerEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('pointerdown', onPointerDown); return () => document.removeEventListener('pointerdown', onPointerDown); }, []); const listboxId = 'mitre-picker-listbox'; return (
= 0 ? `mitre-option-${activeIndex}` : undefined} aria-label="Search MITRE technique" className="text-input" value={inputValue} onChange={(e) => handleInputChange(e.target.value)} onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} disabled={disabled} placeholder="Search by ID or name (e.g. T1059)" autoComplete="off" /> {open && (
{isFetching && (
Searching…
)} {isError && !isFetching && (
{extractApiError(error, 'MITRE search unavailable')}
)} {!isFetching && !isError && items.length === 0 && query.trim().length > 0 && (
No results
)} {!isFetching && items.length > 0 && (
    {items.map((item, i) => (
  • { e.preventDefault(); selectItem(item); }} > {item.id} — {item.name} {item.tactics.length > 0 && ( ({item.tactics[0]}) )}
  • ))}
)}
)}
); }