import { useCallback, useEffect, useState, type RefObject } from "react"; import { Box, Input, Text, type InputRenderable } from "../ui"; import { colors } from "../theme/colors"; import { useAppInputCapture } from "../state/app/input-capture"; import { useShortcut } from "../react/input"; import { isPlainArrowDown, stopSearchFocusNavigation } from "../utils/search-focus-navigation"; export function InputSearchBar({ value, focused, active, width, focusToken, inputRef, placeholder, debounceMs, glyph = "/", appearance = "strip", normalizeValue = identity, onNavigateDown, onFocus, onBlur, onQueryChange, onSubmit, }: { value: string; focused: boolean; active: boolean; width: number | "100%"; focusToken: number; inputRef: RefObject; placeholder: string; debounceMs: number; /** Leading marker; override when a pane shows more than one field. */ glyph?: string; /** `plain` drops the strip background and glyph for a host that draws its own field (QueryBar). */ appearance?: "strip" | "plain"; normalizeValue?: (value: string) => string; onNavigateDown?: () => void; onFocus: () => void; onBlur: () => void; onQueryChange: (query: string) => void; /** Enter: called after the value is committed through `onQueryChange`. */ onSubmit?: (query: string) => void; }) { const [draft, setDraft] = useState(value); useAppInputCapture(focused && active); useShortcut((event) => { if (!onNavigateDown || !isPlainArrowDown(event)) return; stopSearchFocusNavigation(event); onNavigateDown(); }, { allowEditable: true, enabled: focused && active && !!onNavigateDown, phase: "before", }); // A focused input consumes every key, so Escape has to be intercepted before // it reaches the field. Without this there is no way back out of a search. useShortcut((event) => { if (event.name !== "escape") return; stopSearchFocusNavigation(event); setDraft(""); onQueryChange(""); onBlur(); }, { allowEditable: true, enabled: focused && active, phase: "before", }); useEffect(() => { setDraft(value); }, [value]); useEffect(() => { if (focused && active) inputRef.current?.focus?.(); }, [active, focused, focusToken, inputRef]); useEffect(() => { if (normalizeValue(draft) === normalizeValue(value)) return; const timer = setTimeout(() => { onQueryChange(draft); }, debounceMs); return () => clearTimeout(timer); }, [debounceMs, draft, normalizeValue, onQueryChange, value]); const commitNow = useCallback((nextValue: string) => { onQueryChange(nextValue); onSubmit?.(nextValue); onBlur(); }, [onBlur, onQueryChange, onSubmit]); return ( { event.preventDefault?.(); event.stopPropagation?.(); onFocus(); inputRef.current?.focus?.(); }} > {appearance === "strip" && {glyph}} {appearance === "strip" && } ); } function identity(value: string): string { return value; }