From 55422d7ccc941b9ec8d3e33438406e1584c628bb Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 14 Feb 2026 21:37:58 -0800 Subject: [PATCH] search results - keyboard nav --- .../search/search-results-panel.tsx | 58 ++++++++++++++++++- web/src/pages/search-route-page.tsx | 2 +- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/web/src/components/search/search-results-panel.tsx b/web/src/components/search/search-results-panel.tsx index 59207c2..8830ea0 100644 --- a/web/src/components/search/search-results-panel.tsx +++ b/web/src/components/search/search-results-panel.tsx @@ -27,12 +27,14 @@ type Props = { isLoading: boolean; isRunning: boolean; className?: string; + enableKeyboardNavigation?: boolean; }; -export function SearchResultsPanel({ search, isLoading, isRunning, className }: Props) { +export function SearchResultsPanel({ search, isLoading, isRunning, className, enableKeyboardNavigation = false }: Props) { const ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]"; const [isAnswerExpanded, setIsAnswerExpanded] = useState(false); const [canExpandAnswer, setCanExpandAnswer] = useState(false); + const [activeResultIndex, setActiveResultIndex] = useState(-1); const answerBodyRef = useRef(null); useEffect(() => { @@ -53,6 +55,50 @@ export function SearchResultsPanel({ search, isLoading, isRunning, className }: setCanExpandAnswer(el.scrollHeight - el.clientHeight > 1); }, [search?.answerText, isAnswerExpanded]); + useEffect(() => { + setActiveResultIndex(search?.results.length ? 0 : -1); + }, [search?.id, search?.results.length]); + + useEffect(() => { + if (!enableKeyboardNavigation) return; + + const onKeyDown = (event: KeyboardEvent) => { + if (!search?.results.length || event.metaKey || event.ctrlKey || event.altKey) return; + + const target = event.target; + if ( + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement || + (target instanceof HTMLElement && target.isContentEditable) + ) { + return; + } + + if (event.key === "ArrowLeft") { + event.preventDefault(); + setActiveResultIndex((current) => Math.max(0, (current < 0 ? 0 : current) - 1)); + return; + } + + if (event.key === "ArrowRight") { + event.preventDefault(); + setActiveResultIndex((current) => Math.min(search.results.length - 1, current + 1)); + return; + } + + if (event.key !== "Enter") return; + + const result = search.results[activeResultIndex >= 0 ? activeResultIndex : 0]; + if (!result?.url) return; + event.preventDefault(); + window.open(result.url, "_blank", "noopener,noreferrer"); + }; + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [activeResultIndex, enableKeyboardNavigation, search]); + const citationEntries = (search?.answerCitations ?? []) .map((citation, index) => { const href = citation.url || citation.id || ""; @@ -162,9 +208,15 @@ export function SearchResultsPanel({ search, isLoading, isRunning, className }: ) : null}
- {search?.results.map((result) => { + {search?.results.map((result, index) => { return ( -
);