RegistrySearch

tcmts.registry.pages.RegistrySearch · #ntn7ahpg · term · runs in the browser

uses document

The header's search (React Aria's Autocomplete, as the forge's finder is): names, docs or :types across the libraries, with results as you type, by library: each library, then what in it matches. ⌘K or / goes to it, arrows choose, Enter opens the one 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 { registrySearchFor } from "#codebase/tcmts/registry";
import { at } from "#codebase/tcmts/share";
import { useEffect, useRef, useState } from "react";
import {
  Autocomplete, Collection, Input, Keyboard, ListBox, ListBoxItem, ListBoxSection, Popover, SearchField,
} from "react-aria-components";

export function RegistrySearch() {
  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;
    libraries: Awaited<ReturnType<typeof registrySearchFor>>;
  }>({ for: "", libraries: [] });
  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(() => {
      registrySearchFor(t).then(
        (r) => live && setFound({ for: t, libraries: r }),
        () => live && setFound({ for: t, libraries: [] }),
      );
    }, 150);
    return () => {
      live = false;
      clearTimeout(timer);
    };
  }, [t]);
  const libraries = t ? found.libraries : [];
  return (
    <div className="search" ref={field}>
      <Autocomplete inputValue={text} onInputChange={(v) => (setText(v), setOpen(true))}>
        <SearchField
          aria-label="Search libraries"
          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={libraries}
            renderEmptyState={() => (
              <div className="none">{found.for === t ? "Nothing found" : "Searching…"}</div>
            )}
          >
            {(l) => (
              <ListBoxSection id={l.id} aria-label={l.library} className="result-sec">
                <ListBoxItem
                  id={`library ${l.id}`}
                  textValue={l.library}
                  href={l.href}
                  className="result"
                >
                  <b className="mono">{l.library}</b> <span className="muted">{l.version}</span>
                  {l.description && <span className="desc">{l.description}</span>}
                </ListBoxItem>
                <Collection items={l.hits}>
                  {(h) => (
                    <ListBoxItem id={h.id} textValue={h.name} href={h.href} className="result hit">
                      <span className="mono">{h.name}</span>
                      <span className="sig">{h.signature}</span>
                    </ListBoxItem>
                  )}
                </Collection>
              </ListBoxSection>
            )}
          </ListBox>
        </Popover>
      </Autocomplete>
    </div>
  );
}

uses

used by