'use client' import { useDocsSearchV2, type DocsSearchV2Result } from 'common' import { Loader2 } from 'lucide-react' import { useRouter } from 'next/navigation' import { VisuallyHidden } from 'radix-ui' import { useEffect, useState } from 'react' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogDescription, DialogTitle, } from 'ui' import { formatHeadingPath, highlightMatches } from './SearchV2.utils' interface SearchV2DialogProps { open: boolean onOpenChange: (open: boolean) => void } export function SearchV2Dialog({ open, onOpenChange }: SearchV2DialogProps) { const router = useRouter() const { searchState, handleDocsSearchDebounced, resetSearch } = useDocsSearchV2() const [highlightQuery, setHighlightQuery] = useState('') // Clear stale results once the dialog closes useEffect(() => { if (!open) resetSearch() }, [open, resetSearch]) // Only update the highlighted query once a new result set actually lands, so highlights // don't shift on every keystroke while the debounced search is still in flight. useEffect(() => { if (searchState.status === 'results' || searchState.status === 'noResults') { setHighlightQuery(searchState.query) } else if (searchState.status === 'initial') { setHighlightQuery('') } }, [searchState]) const results: DocsSearchV2Result[] = 'results' in searchState ? searchState.results : 'staleResults' in searchState ? searchState.staleResults : [] function handleValueChange(value: string) { if (value) { handleDocsSearchDebounced(value) } else { resetSearch() } } function handleSelect(path: string) { router.push(path) onOpenChange(false) } // Announced via the aria-live region below — a sighted user sees the spinner/list update, // but a screen reader user gets no equivalent signal unless we say so explicitly. Also covers // the result count, which isn't reliably announced by the listbox/option roles alone. function getStatusMessage(): string { if (searchState.status === 'loading') return 'Searching the docs…' if (searchState.status === 'noResults') return 'No results found.' if (searchState.status === 'error') return 'Something went wrong. Please try again.' if (results.length > 0) { return `${results.length} result${results.length === 1 ? '' : 's'} found.` } return '' } return ( {/* hideClose: this is a search box, not a form — closing is Escape/click-outside only, no "X" */} Search docs Search the Supabase documentation {/* Screen-reader-only status announcement. Focus stays in the input as results come in (that's what lets people keep typing), so nothing else here gets read aloud on its own — this is what tells a screen reader user a search ran and how many results it found. */}
{getStatusMessage()}
{searchState.status === 'initial' && ( Start typing to search the docs. )} {searchState.status === 'loading' && results.length === 0 && (
)} {searchState.status === 'noResults' && No results found.} {searchState.status === 'error' && ( Something went wrong. Please try again. )} {results.length > 0 && ( {results.map((page) => ( handleSelect(page.path)} >
{highlightMatches(formatHeadingPath(page.headingPath), highlightQuery)} {page.excerpt && ( {highlightMatches(page.excerpt, highlightQuery)} )}
))}
)}
) }