Skip to content

Commit

Permalink
make docsearch universally accessible
Browse files Browse the repository at this point in the history
  • Loading branch information
brookslybrand committed Nov 25, 2024
1 parent 9621e0d commit 10469b3
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 179 deletions.
12 changes: 2 additions & 10 deletions app/components/docs-header/docs-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Link, useNavigate } from "react-router";
import iconsHref from "~/icons.svg";
import { ColorSchemeToggle } from "../color-scheme-toggle";
import classNames from "classnames";
import { DocSearch } from "~/modules/docsearch";
import { DocSearchButton } from "~/modules/docsearch";
import { VersionNav } from "../version-nav";

export function Header() {
Expand All @@ -16,7 +16,7 @@ export function Header() {
</div>

<div className="flex gap-2 md:gap-4">
<DocSearchSection />
<DocSearchButton />
<ColorSchemeToggle />
<ExternalLinks />
</div>
Expand Down Expand Up @@ -100,11 +100,3 @@ function HeaderSvgLink({
</a>
);
}

function DocSearchSection() {
return (
<div className="lg:bg-white lg:dark:bg-gray-900">
<DocSearch />
</div>
);
}
109 changes: 91 additions & 18 deletions app/modules/docsearch.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import {
Suspense,
createContext,
lazy,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
import { useDocSearchKeyboardEvents } from "@docsearch/react/dist/esm";
import type { DocSearchProps } from "@docsearch/react";
import { useHydrated } from "~/ui/utils";
import { Suspense, lazy } from "react";

const OriginalDocSearch = lazy(() =>
let OriginalDocSearchModal = lazy(() =>
import("@docsearch/react").then((module) => ({
default: module.DocSearch,
default: module.DocSearchModal,
}))
);

let OriginalDocSearchButton = lazy(() =>
import("@docsearch/react").then((module) => ({
default: module.DocSearchButton,
}))
);

Expand All @@ -14,24 +30,81 @@ let docSearchProps = {
apiKey: "b50c5d7d9f4610c9785fa945fdc97476",
} satisfies DocSearchProps;

// TODO: Refactor a bit when we add Vite with css imports per component
// This will allow us to have two versions of the component, one that has
// the button with display: none, and the other with button styles
export function DocSearch() {
let hydrated = useHydrated();

if (!hydrated) {
// The Algolia doc search container is hard-coded at 40px. It doesn't
// render anything on the server, so we get a mis-match after hydration.
// This placeholder prevents layout shift when the search appears.
return <div className="h-10" />;
const DocSearchContext = createContext<{
onOpen: () => void;
searchButtonRef: React.RefObject<HTMLButtonElement>;
} | null>(null);

/**
* DocSearch but only the modal accessible by keyboard command
* Intended for people instinctively pressing cmd+k on a non-doc page
*
* If you need a DocSearch button to appear, use the DocSearch component
* Modified from https://github.com/algolia/docsearch/blob/main/packages/docsearch-react/src/DocSearch.tsx
*/
export function DocSearch({ children }: { children: React.ReactNode }) {
const searchButtonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);

const onOpen = useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);

const onClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);

const onInput = useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);

useDocSearchKeyboardEvents({
isOpen,
onOpen,
onClose,
onInput,
searchButtonRef,
});

const contextValue = useMemo(
() => ({
onOpen,
searchButtonRef,
}),
[onOpen, searchButtonRef]
);

return (
<DocSearchContext.Provider value={contextValue}>
{children}
{isOpen
? createPortal(
<Suspense fallback={null}>
<OriginalDocSearchModal
initialScrollY={window.scrollY}
onClose={onClose}
{...docSearchProps}
/>
</Suspense>,
document.body
)
: null}
</DocSearchContext.Provider>
);
}

export function DocSearchButton() {
const docSearchContext = useContext(DocSearchContext);

if (!docSearchContext) {
throw new Error("DocSearch must be used within a DocSearchModal");
}

const { onOpen, searchButtonRef } = docSearchContext;

return (
<Suspense fallback={<div className="h-10" />}>
<div className="animate-[fadeIn_100ms_ease-in_1]">
<OriginalDocSearch {...docSearchProps} />
</div>
<OriginalDocSearchButton ref={searchButtonRef} onClick={onOpen} />
</Suspense>
);
}
11 changes: 7 additions & 4 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ import { isHost } from "./modules/http-utils/is-host";
import iconsHref from "~/icons.svg";
import { useRef } from "react";
import { useCodeBlockCopyButton } from "./ui/utils";
import { DocSearch } from "./modules/docsearch";

import "~/styles/tailwind.css";
// FIXUP: Importing in `root` because we have a bug where the styles get offloaded
// see: https://github.com/remix-run/react-router-website/issues/139
import "~/styles/docs.css";
import "@docsearch/css/dist/style.css";
import "~/styles/docsearch.css";
// FIXUP: Styles need to all be imported in root until this is fixed:
// https://github.com/remix-run/react-router/issues/12382
import "~/styles/docs.css";

export async function loader({ request }: LoaderFunctionArgs) {
await middlewares(request);
Expand Down Expand Up @@ -88,7 +89,9 @@ export default function App() {
// eslint-disable-next-line react/no-unknown-property
fetchpriority="high"
/>
<Outlet />
<DocSearch>
<Outlet />
</DocSearch>
<ScrollRestoration />
<Scripts />
</body>
Expand Down
Loading

0 comments on commit 10469b3

Please sign in to comment.