search results - keyboard nav
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user