SearchBox
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> ); }