Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(search): Add hotkey nav, tweak UI slightly for better clarity between results #255

Merged
merged 3 commits into from
Apr 27, 2023
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 109 additions & 24 deletions src/components/Search/Results.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,83 @@
import { Link } from "@/components/Link";
import { Search } from "@/types";
import { Markup } from "interweave";
import React from "react";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
import tinykeys from "tinykeys";
import tw from "twin.macro";

interface Props {
closeModal: () => void
closeModal: () => void;
half0wl marked this conversation as resolved.
Show resolved Hide resolved
results: Search.Result;
}

type SelectedResult = { idx: number; slug: string };

const withoutBaseUri = (slug: string) => {
const url = new URL(slug);
const { hash, pathname } = url;
return `${pathname}${hash}`;
};

// @FIXME: Indexer is grabbing #__next from anchor hrefs. This should be
// fixed upstream, but no harm in just hacking it in place for now.
const cleanSlug = (slug: string) => withoutBaseUri(slug.replace("#__next", ""));
ndneighbor marked this conversation as resolved.
Show resolved Hide resolved

const Results: React.FC<Props> = ({ closeModal, results }) => {
const withoutBaseUri = (slug: string) => {
const url = new URL(slug);
const { hash, pathname } = url;
return `${pathname}${hash}`;
};
const router = useRouter();

// A result is selected when navigated to using arrow keys, or on mouse
// hover.
const [selectedResult, setSelectedResult] = useState<SelectedResult | null>(
null,
);

const resultsFlat: SelectedResult[] = Object.values(results)
.flat()
.map((r, idx) => ({ idx, slug: cleanSlug(r.slug) }));

// @FIXME: Indexer is grabbing #__next from anchor hrefs. This should be
// fixed upstream, but no harm in just hacking it in place for now.
const cleanSlug = (slug: string) =>
withoutBaseUri(slug.replace("#__next", ""));
const onArrowKeyDown = useCallback(() => {
if (selectedResult && selectedResult.idx + 1 >= resultsFlat.length) {
// End of results; nothing to go down from.
return;
}

setSelectedResult(prev => {
// On key down, go to the next item.
const next = prev ? resultsFlat[prev.idx + 1] : resultsFlat[0];
return { ...next };
});
}, [resultsFlat, selectedResult, setSelectedResult]);

const onArrowKeyUp = useCallback(() => {
setSelectedResult(prev => {
if (prev === null || prev.idx === 0) {
// Start of results. Going up from here is the search input, so it's
// a no-op.
return null;
}
half0wl marked this conversation as resolved.
Show resolved Hide resolved
// On key up, go to the previous item.
const next = prev ? resultsFlat[prev.idx - 1] : resultsFlat[0];
return { ...next };
});
}, [resultsFlat, setSelectedResult]);

const onEnter = useCallback(() => {
if (selectedResult === null) {
return;
}
closeModal();
router.push(selectedResult.slug);
}, [selectedResult]);

useEffect(() => {
const unsubscribe = tinykeys(window, {
ArrowDown: () => onArrowKeyDown(),
ArrowUp: () => onArrowKeyUp(),
Enter: () => onEnter(),
});
return () => unsubscribe();
}, [onArrowKeyDown]);

return (
<div css={tw`p-2 m-2`}>
Expand All @@ -36,27 +94,54 @@ const Results: React.FC<Props> = ({ closeModal, results }) => {
<h4 css={[tw`font-bold text-lg mb-2`]}>{chapter}</h4>
<ul>
{hits.map(h => {
const slug = cleanSlug(h.slug);
const isSelected = selectedResult?.slug === slug;
return (
<li key={cleanSlug(h.slug)} css={tw`flex flex-col mb-2`}>
<li
key={slug}
css={tw`flex flex-col mb-2`}
onMouseEnter={() => {
if (isSelected) {
return;
}
const result = resultsFlat.find(r => r.slug === slug);
if (!result) {
return;
}
setSelectedResult(result);
}}
>
<Link
href={cleanSlug(h.slug)}
onClick={(e) => closeModal()}
css={[tw`flex flex-col font-medium hover:text-pink-700`]}
href={slug}
onClick={closeModal}
css={[
tw`flex flex-col font-medium p-3 rounded rounded-lg border`,
isSelected && tw`bg-pink-200`,
ndneighbor marked this conversation as resolved.
Show resolved Hide resolved
]}
>
{h.hierarchies.join(" -> ")}
<span
css={[
tw`leading-[1.4] text-gray-500 dark:text-gray-600`,
tw`[&>.rendered span]:bg-yellow-200`,
tw`[&>.rendered span]:p-1`,
tw`[&>.rendered span]:text-black`,
tw`[&>.rendered span]:dark:text-white`,
tw`flex flex-col justify-center h-12`,
tw`font-bold text-lg truncate text-ellipsis`,
]}
>
{h.text !== "" && (
<Markup className="rendered" content={h.text} />
)}
{h.hierarchies.join(" -> ")}
</span>
{h.text !== "" && (
<span
css={[
tw`leading-[1.6] text-gray-500 dark:text-gray-600`,
tw`[&>.rendered span]:bg-yellow-200`,
tw`[&>.rendered span]:p-1`,
tw`[&>.rendered span]:text-black`,
tw`[&>.rendered span]:dark:text-white`,
isSelected &&
tw`text-black font-light dark:text-gray-700`,
]}
>
<Markup className="rendered" content={h.text} />
</span>
)}
</Link>
</li>
);
Expand Down