search results - keyboard nav
This commit is contained in:
@@ -27,12 +27,14 @@ type Props = {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
className?: string;
|
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 ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]";
|
||||||
const [isAnswerExpanded, setIsAnswerExpanded] = useState(false);
|
const [isAnswerExpanded, setIsAnswerExpanded] = useState(false);
|
||||||
const [canExpandAnswer, setCanExpandAnswer] = useState(false);
|
const [canExpandAnswer, setCanExpandAnswer] = useState(false);
|
||||||
|
const [activeResultIndex, setActiveResultIndex] = useState(-1);
|
||||||
const answerBodyRef = useRef<HTMLDivElement | null>(null);
|
const answerBodyRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,6 +55,50 @@ export function SearchResultsPanel({ search, isLoading, isRunning, className }:
|
|||||||
setCanExpandAnswer(el.scrollHeight - el.clientHeight > 1);
|
setCanExpandAnswer(el.scrollHeight - el.clientHeight > 1);
|
||||||
}, [search?.answerText, isAnswerExpanded]);
|
}, [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 ?? [])
|
const citationEntries = (search?.answerCitations ?? [])
|
||||||
.map((citation, index) => {
|
.map((citation, index) => {
|
||||||
const href = citation.url || citation.id || "";
|
const href = citation.url || citation.id || "";
|
||||||
@@ -162,9 +208,15 @@ export function SearchResultsPanel({ search, isLoading, isRunning, className }:
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{search?.results.map((result) => {
|
{search?.results.map((result, index) => {
|
||||||
return (
|
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>
|
<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">
|
<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}
|
{result.title || result.url}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export default function SearchRoutePage() {
|
|||||||
|
|
||||||
{error ? <p className="text-sm text-red-600">{error}</p> : null}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user