"use client"; import * as React from "react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { Input } from "./input"; import { Check, ChevronDown } from "lucide-react"; interface CategoryAutocompleteProps { value: string; onChange: (value: string) => void; suggestions: string[]; placeholder?: string; className?: string; disabled?: boolean; "data-testid"?: string; } export function CategoryAutocomplete({ value, onChange, suggestions, placeholder = "Select or type a category...", className, disabled = false, "data-testid": testId, }: CategoryAutocompleteProps) { const [isOpen, setIsOpen] = useState(false); const [inputValue, setInputValue] = useState(value); const [filteredSuggestions, setFilteredSuggestions] = useState([]); const [highlightedIndex, setHighlightedIndex] = useState(-1); const containerRef = useRef(null); const inputRef = useRef(null); const listRef = useRef(null); // Update internal state when value prop changes useEffect(() => { setInputValue(value); }, [value]); // Filter suggestions based on input useEffect(() => { const searchTerm = inputValue.toLowerCase().trim(); if (searchTerm === "") { setFilteredSuggestions(suggestions); } else { const filtered = suggestions.filter((s) => s.toLowerCase().includes(searchTerm) ); setFilteredSuggestions(filtered); } setHighlightedIndex(-1); }, [inputValue, suggestions]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Scroll highlighted item into view useEffect(() => { if (highlightedIndex >= 0 && listRef.current) { const items = listRef.current.querySelectorAll("li"); const highlightedItem = items[highlightedIndex]; if (highlightedItem) { highlightedItem.scrollIntoView({ block: "nearest" }); } } }, [highlightedIndex]); const handleInputChange = useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value; setInputValue(newValue); onChange(newValue); setIsOpen(true); }, [onChange] ); const handleSelect = useCallback( (suggestion: string) => { setInputValue(suggestion); onChange(suggestion); setIsOpen(false); inputRef.current?.focus(); }, [onChange] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!isOpen) { if (e.key === "ArrowDown" || e.key === "Enter") { e.preventDefault(); setIsOpen(true); } return; } switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightedIndex((prev) => prev < filteredSuggestions.length - 1 ? prev + 1 : prev ); break; case "ArrowUp": e.preventDefault(); setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1)); break; case "Enter": e.preventDefault(); if (highlightedIndex >= 0 && filteredSuggestions[highlightedIndex]) { handleSelect(filteredSuggestions[highlightedIndex]); } else { setIsOpen(false); } break; case "Escape": e.preventDefault(); setIsOpen(false); break; case "Tab": setIsOpen(false); break; } }, [isOpen, highlightedIndex, filteredSuggestions, handleSelect] ); const handleFocus = useCallback(() => { setIsOpen(true); }, []); return (
{isOpen && filteredSuggestions.length > 0 && (
    {filteredSuggestions.map((suggestion, index) => (
  • { e.preventDefault(); handleSelect(suggestion); }} onMouseEnter={() => setHighlightedIndex(index)} data-testid={`category-option-${suggestion.toLowerCase().replace(/\s+/g, "-")}`} > {inputValue === suggestion && ( )} {suggestion}
  • ))}
)}
); }