search navigation
This commit is contained in:
@@ -573,11 +573,13 @@ function ContextMenu({
|
|||||||
}) {
|
}) {
|
||||||
const [openCat, setOpenCat] = useState(null);
|
const [openCat, setOpenCat] = useState(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
const [menuPos, setMenuPos] = useState({ x, y });
|
const [menuPos, setMenuPos] = useState({ x, y });
|
||||||
const subMenuRef = useRef(null);
|
const subMenuRef = useRef(null);
|
||||||
const [subPos, setSubPos] = useState({ x: 0, y: 0 });
|
const [subPos, setSubPos] = useState({ x: 0, y: 0 });
|
||||||
const catRowRefs = useRef({});
|
const catRowRefs = useRef({});
|
||||||
|
const selectedItemRef = useRef(null);
|
||||||
|
|
||||||
// Group by category, optionally filtering to compatible nodes
|
// Group by category, optionally filtering to compatible nodes
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
@@ -658,6 +660,31 @@ function ContextMenu({
|
|||||||
return results;
|
return results;
|
||||||
}, [search, categories]);
|
}, [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
|
// Clamp main menu position to viewport on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = menuRef.current;
|
const el = menuRef.current;
|
||||||
@@ -740,6 +767,7 @@ function ContextMenu({
|
|||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setOpenCat(null); }}
|
onChange={(e) => { setSearch(e.target.value); setOpenCat(null); }}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -758,11 +786,13 @@ function ContextMenu({
|
|||||||
{searchResults.length === 0 ? (
|
{searchResults.length === 0 ? (
|
||||||
<div className="context-item" style={{ color: 'var(--text-muted)' }}>No matches</div>
|
<div className="context-item" style={{ color: 'var(--text-muted)' }}>No matches</div>
|
||||||
) : (
|
) : (
|
||||||
searchResults.map(({ className, def }) => (
|
searchResults.map(({ className, def }, idx) => (
|
||||||
<div
|
<div
|
||||||
key={className}
|
key={className}
|
||||||
className="context-item"
|
ref={idx === selectedIndex ? selectedItemRef : null}
|
||||||
|
className={`context-item${idx === selectedIndex ? ' context-item--selected' : ''}`}
|
||||||
onClick={() => { onAdd(className, def); onClose(); }}
|
onClick={() => { onAdd(className, def); onClose(); }}
|
||||||
|
onMouseEnter={() => setSelectedIndex(idx)}
|
||||||
>
|
>
|
||||||
{def.display_name || className}
|
{def.display_name || className}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1493,7 +1493,8 @@ html, body, #root {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.context-item:hover {
|
.context-item:hover,
|
||||||
|
.context-item--selected {
|
||||||
background: var(--accent-bg);
|
background: var(--accent-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user