import { forwardRef, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { Check } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "../lib/utils"; export interface InlineEntityOption { id: string; label: string; searchText?: string; } interface InlineEntitySelectorProps { value: string; options: InlineEntityOption[]; placeholder: string; noneLabel: string; searchPlaceholder: string; emptyMessage: string; onChange: (id: string) => void; onConfirm?: () => void; className?: string; renderTriggerValue?: (option: InlineEntityOption | null) => ReactNode; renderOption?: (option: InlineEntityOption, isSelected: boolean) => ReactNode; } export const InlineEntitySelector = forwardRef( function InlineEntitySelector( { value, options, placeholder, noneLabel, searchPlaceholder, emptyMessage, onChange, onConfirm, className, renderTriggerValue, renderOption, }, ref, ) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(0); const inputRef = useRef(null); const shouldPreventCloseAutoFocusRef = useRef(false); const allOptions = useMemo( () => [{ id: "", label: noneLabel, searchText: noneLabel }, ...options], [noneLabel, options], ); const filteredOptions = useMemo(() => { const term = query.trim().toLowerCase(); if (!term) return allOptions; return allOptions.filter((option) => { const haystack = `${option.label} ${option.searchText ?? ""}`.toLowerCase(); return haystack.includes(term); }); }, [allOptions, query]); const currentOption = options.find((option) => option.id === value) ?? null; useEffect(() => { if (!open) return; const selectedIndex = filteredOptions.findIndex((option) => option.id === value); setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0); }, [filteredOptions, open, value]); const commitSelection = (index: number, moveNext: boolean) => { const option = filteredOptions[index] ?? filteredOptions[0]; if (option) onChange(option.id); shouldPreventCloseAutoFocusRef.current = moveNext; setOpen(false); setQuery(""); if (moveNext && onConfirm) { requestAnimationFrame(() => { onConfirm(); }); } }; return ( { setOpen(next); if (!next) setQuery(""); }} > { event.preventDefault(); inputRef.current?.focus(); }} onCloseAutoFocus={(event) => { if (!shouldPreventCloseAutoFocusRef.current) return; event.preventDefault(); shouldPreventCloseAutoFocusRef.current = false; }} > { setQuery(event.target.value); }} onKeyDown={(event) => { if (event.key === "ArrowDown") { event.preventDefault(); setHighlightedIndex((current) => filteredOptions.length === 0 ? 0 : (current + 1) % filteredOptions.length, ); return; } if (event.key === "ArrowUp") { event.preventDefault(); setHighlightedIndex((current) => { if (filteredOptions.length === 0) return 0; return current <= 0 ? filteredOptions.length - 1 : current - 1; }); return; } if (event.key === "Enter") { event.preventDefault(); commitSelection(highlightedIndex, true); return; } if (event.key === "Tab" && !event.shiftKey) { event.preventDefault(); commitSelection(highlightedIndex, true); return; } if (event.key === "Escape") { event.preventDefault(); setOpen(false); } }} />
{filteredOptions.length === 0 ? (

{emptyMessage}

) : ( filteredOptions.map((option, index) => { const isSelected = option.id === value; const isHighlighted = index === highlightedIndex; return ( ); }) )}
); }, );