SearchBox

tcmts.share.SearchBox · #dyimhgpk · term · runs in the browser

uses document

The finder in the header (React Aria's Autocomplete: a search field over a list of results): names, words in docs, or a type (:(xs: T[]) => T), with results as you type. ⌘K or / goes to it, arrows choose, Enter opens the result chosen (or, with none chosen, the search page). On the search page it holds what was searched for.

import { navigate, useLocation } from "#codebase/tcmts/react";
import { at, searchFor } from "#codebase/tcmts/share";
import { useEffect, useRef, useState } from "react";
import { Autocomplete, Input, Keyboard, ListBox, ListBoxItem, Popover, SearchField } from "react-aria-components";

export function SearchBox() {
  const { path, query } = useLocation();
  const here = `${path}?${query}`;
  const searched = path === "/search" ? (query.get("q") ?? "") : "";
  const [text, setText] = useState(searched);
  const [open, setOpen] = useState(false);
  // (another page: what it searched for, if it's the search page, and the results put away)
  const [was, setWas] = useState(here);
  if (was !== here) {
    setWas(here);
    setText(searched);
    setOpen(false);
  }
  const [found, setFound] = useState<{
    for: string;
    results: { path: string; match: string; signature: string; detail: string; href: string }[];
  }>({ for: "", results: [] });
  const field = useRef<HTMLDivElement>(null);
  const input = useRef<HTMLInputElement>(null);
  useEffect(() => {
    const key = (e: KeyboardEvent) => {
      const typing = /INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName ?? "");
      if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && !typing)) {
        e.preventDefault();
        input.current?.focus();
        input.current?.select();
      }
    };
    document.addEventListener("keydown", key);
    return () => document.removeEventListener("keydown", key);
  }, []);
  const t = text.trim();
  useEffect(() => {
    if (!t) return;
    let live = true;
    const timer = setTimeout(() => {
      searchFor(t).then(
        (r) => live && setFound({ for: t, results: r }),
        () => live && setFound({ for: t, results: [] }),
      );
    }, 120);
    return () => {
      live = false;
      clearTimeout(timer);
    };
  }, [t]);
  const results = t ? found.results : [];
  return (
    <div className="search" ref={field}>
      <Autocomplete inputValue={text} onInputChange={(v) => (setText(v), setOpen(true))}>
        <SearchField
          aria-label="Search the codebase"
          className="searchfield"
          onSubmit={(v) => {
            // (with a result chosen, Enter opens it)
            if (input.current?.getAttribute("aria-activedescendant") || !v.trim()) return;
            setOpen(false);
            navigate(at(`/search?q=${encodeURIComponent(v.trim())}`));
          }}
          onClear={() => setOpen(false)}
        >
          <Input
            ref={input}
            className="field"
            placeholder="Search names, docs, or :types"
            onFocus={() => setOpen(true)}
          />
          <Keyboard className="kbd">⌘K</Keyboard>
        </SearchField>
        <Popover
          triggerRef={field}
          isOpen={open && !!t}
          onOpenChange={setOpen}
          isNonModal
          placement="bottom end"
          offset={6}
          className="results"
        >
          <ListBox
            aria-label="Results"
            className="result-list"
            items={results}
            renderEmptyState={() => (
              <div className="none">{found.for === t ? "Nothing found" : "Searching…"}</div>
            )}
          >
            {(r) => (
              <ListBoxItem id={r.path} textValue={r.path} href={r.href} className="result">
                <b>{r.path}</b> <span className="muted">{r.match}</span>
                <span className="sig">{r.signature || r.detail}</span>
              </ListBoxItem>
            )}
          </ListBox>
        </Popover>
      </Autocomplete>
    </div>
  );
}

uses

used by