From 2846dbc3af3ab0cdcf39b6e7adecab5cb9cb63c2 Mon Sep 17 00:00:00 2001 From: matei jordache Date: Sun, 29 Mar 2026 20:05:49 -0700 Subject: [PATCH] search navigation --- frontend/src/App.jsx | 34 ++++++++++++++++++++++++++++++++-- frontend/src/styles.css | 3 ++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ea936bc..583cb48 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -573,11 +573,13 @@ function ContextMenu({ }) { const [openCat, setOpenCat] = useState(null); const [search, setSearch] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); const menuRef = useRef(null); const [menuPos, setMenuPos] = useState({ x, y }); const subMenuRef = useRef(null); const [subPos, setSubPos] = useState({ x: 0, y: 0 }); const catRowRefs = useRef({}); + const selectedItemRef = useRef(null); // Group by category, optionally filtering to compatible nodes const categories = useMemo(() => { @@ -658,6 +660,31 @@ function ContextMenu({ return results; }, [search, categories]); + // Reset selection to top whenever results change + useEffect(() => { + setSelectedIndex(0); + }, [searchResults]); + + // Scroll selected item into view + useEffect(() => { + selectedItemRef.current?.scrollIntoView({ block: 'nearest' }); + }, [selectedIndex]); + + const handleSearchKeyDown = useCallback((e) => { + if (!searchResults || searchResults.length === 0) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((i) => Math.min(i + 1, searchResults.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((i) => Math.max(i - 1, 0)); + } else if (e.key === 'Enter') { + e.preventDefault(); + const item = searchResults[selectedIndex]; + if (item) { onAdd(item.className, item.def); onClose(); } + } + }, [searchResults, selectedIndex, onAdd, onClose]); + // Clamp main menu position to viewport on mount useEffect(() => { const el = menuRef.current; @@ -740,6 +767,7 @@ function ContextMenu({ placeholder="Search…" value={search} onChange={(e) => { setSearch(e.target.value); setOpenCat(null); }} + onKeyDown={handleSearchKeyDown} autoFocus /> @@ -758,11 +786,13 @@ function ContextMenu({ {searchResults.length === 0 ? (
No matches
) : ( - searchResults.map(({ className, def }) => ( + searchResults.map(({ className, def }, idx) => (
{ onAdd(className, def); onClose(); }} + onMouseEnter={() => setSelectedIndex(idx)} > {def.display_name || className}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 09f4763..8b32c93 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1493,7 +1493,8 @@ html, body, #root { color: var(--text-primary); white-space: nowrap; } -.context-item:hover { +.context-item:hover, +.context-item--selected { background: var(--accent-bg); }