import { Search, X } from "lucide-react"; import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { useI18n } from "../../application/i18n/I18nProvider"; import { filterSettingsSearchCatalog, type SettingsSearchHit } from "../../domain/settingsSearch"; import { cn } from "../../lib/utils"; import { Input } from "../ui/input"; import { useSettingsFocus } from "./SettingsFocusContext"; type SettingsSearchControlProps = { includePlugins?: boolean; className?: string; }; export function SettingsSearchControl({ includePlugins = true, className, }: SettingsSearchControlProps) { const { t } = useI18n(); const { requestFocus, registerOpenSearch } = useSettingsFocus(); const listId = useId(); const optionIdPrefix = useId(); const rootRef = useRef(null); const inputRef = useRef(null); const openButtonRef = useRef(null); const [expanded, setExpanded] = useState(false); const [query, setQuery] = useState(""); const [activeIndex, setActiveIndex] = useState(0); const [restoreOpenFocus, setRestoreOpenFocus] = useState(false); const hits = useMemo( () => filterSettingsSearchCatalog(query, t, { includePlugins, limit: 12 }), [query, t, includePlugins], ); const collapseSearch = useCallback((restoreFocus = false) => { setExpanded(false); setQuery(""); if (restoreFocus) setRestoreOpenFocus(true); }, []); useEffect(() => { setActiveIndex(0); }, [query, hits.length]); useEffect(() => { const open = () => { setExpanded(true); window.requestAnimationFrame(() => { inputRef.current?.focus(); inputRef.current?.select(); }); }; registerOpenSearch(open); return () => registerOpenSearch(null); }, [registerOpenSearch]); useEffect(() => { if (!expanded) return; const handlePointerDown = (event: MouseEvent) => { if (!rootRef.current?.contains(event.target as Node)) { collapseSearch(false); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.isComposing) return; if (event.key === "Escape") { event.preventDefault(); collapseSearch(true); } }; document.addEventListener("mousedown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [expanded, collapseSearch]); useEffect(() => { if (expanded) { inputRef.current?.focus(); return; } if (restoreOpenFocus) { openButtonRef.current?.focus(); setRestoreOpenFocus(false); } }, [expanded, restoreOpenFocus]); const selectHit = (hit: SettingsSearchHit) => { requestFocus({ tab: hit.entry.tab, aiSubTab: hit.entry.aiSubTab, syncSubTab: hit.entry.syncSubTab, anchorId: hit.entry.id, }); collapseSearch(false); }; const onKeyDown = (event: React.KeyboardEvent) => { // Avoid stealing IME candidate navigation / composition confirm (CJK). if (event.nativeEvent.isComposing || event.keyCode === 229) return; if (event.key === "ArrowDown") { event.preventDefault(); if (hits.length === 0) return; setActiveIndex((index) => (index + 1) % hits.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); if (hits.length === 0) return; setActiveIndex((index) => (index - 1 + hits.length) % hits.length); return; } if (event.key === "Enter") { event.preventDefault(); const hit = hits[activeIndex]; if (hit) selectHit(hit); } }; const activeOptionId = hits[activeIndex] ? `${optionIdPrefix}-${hits[activeIndex].entry.id}` : undefined; if (!expanded) { return (
); } return (
setQuery(event.target.value)} onKeyDown={onKeyDown} placeholder={t("settings.search.placeholder")} aria-label={t("settings.search.placeholder")} aria-controls={listId} aria-expanded={true} aria-autocomplete="list" aria-haspopup="listbox" aria-activedescendant={activeOptionId} role="combobox" className="h-9 pl-8 pr-8 text-sm" />
{hits.length === 0 ? (
{t("settings.search.noResults")}
) : ( hits.map((hit, index) => { const path = [hit.tabLabel, hit.section].filter(Boolean).join(" ยท "); return ( ); }) )}
); }