search results - keyboard nav

This commit is contained in:
2026-02-14 21:37:58 -08:00
parent 3076f4f1a4
commit 55422d7ccc
2 changed files with 56 additions and 4 deletions

View File

@@ -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<HTMLDivElement | null>(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}
<div className="space-y-6">
{search?.results.map((result) => {
{search?.results.map((result, index) => {
return (
<article key={result.id} className="rounded-lg border border-border bg-[hsl(276_30%_13%)] px-4 py-4 shadow-sm">
<article
key={result.id}
className={cn(
"rounded-lg border border-border bg-[hsl(276_30%_13%)] px-4 py-4 shadow-sm transition-colors",
index === activeResultIndex && "border-violet-300 ring-1 ring-violet-300/80"
)}
>
<p className="text-xs text-violet-300/85">{formatHost(result.url)}</p>
<a href={result.url} target="_blank" rel="noreferrer" className="mt-1 block text-lg font-medium text-violet-300 hover:underline">
{result.title || result.url}

View File

@@ -238,7 +238,7 @@ export default function SearchRoutePage() {
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<SearchResultsPanel search={search} isLoading={false} isRunning={isRunning} className="w-full" />
<SearchResultsPanel search={search} isLoading={false} isRunning={isRunning} className="w-full" enableKeyboardNavigation />
</div>
</div>
);