{"version":3,"file":"Combobox.cjs","names":[],"sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract (label, placeholder, helperText, error, disabled,\n * className) and the three combobox-specific knobs: filter, emptyMessage. The body\n * is the listbox — open state, active descendant, type-ahead and the ARIA wiring —\n * and those cannot be separated without passing the same active index back and\n * forth.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Combobox.module.css\";\n\nexport interface ComboboxOption {\n    value: string;\n    label: string;\n    disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n    options: ComboboxOption[];\n    value: string;\n    onChange: (value: string) => void;\n    label?: string;\n    placeholder?: string;\n    helperText?: string;\n    error?: string;\n    disabled?: boolean;\n    /** Custom filter — return true to keep the option. Default is case-insensitive substring match on label. */\n    filter?: (option: ComboboxOption, query: string) => boolean;\n    /** Message shown when no option matches. */\n    emptyMessage?: string;\n    className?: string;\n}\n\nfunction defaultFilter(option: ComboboxOption, query: string): boolean {\n    if (!query) return true;\n    return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * Combobox — text input with a filterable dropdown of options.\n *\n * Selecting an option fires `onChange(value)`. Typing filters the list.\n * Keyboard: ArrowUp/ArrowDown to navigate, Enter to select, Esc to close.\n */\nexport function Combobox({\n    options,\n    value,\n    onChange,\n    label,\n    placeholder = \"Selecione\",\n    helperText,\n    error,\n    disabled,\n    filter = defaultFilter,\n    emptyMessage = \"Nenhuma opção encontrada\",\n    className,\n}: ComboboxProps) {\n    const id = useId();\n    const rootRef = useRef<HTMLDivElement>(null);\n    const inputRef = useRef<HTMLInputElement>(null);\n    const [open, setOpen] = useState(false);\n    const [query, setQuery] = useState(\"\");\n    const [activeIndex, setActiveIndex] = useState(0);\n\n    const selectedLabel = useMemo(\n        () => options.find((o) => o.value === value)?.label ?? \"\",\n        [options, value],\n    );\n\n    const filtered = useMemo(\n        () => options.filter((o) => filter(o, query)),\n        [options, filter, query],\n    );\n\n    const closeAndReset = useCallback((): void => {\n        setOpen(false);\n        setQuery(\"\");\n        setActiveIndex(0);\n    }, []);\n\n    const handleSelect = useCallback(\n        (option: ComboboxOption): void => {\n            if (option.disabled) return;\n            onChange(option.value);\n            closeAndReset();\n        },\n        [onChange, closeAndReset],\n    );\n\n    useEffect(() => {\n        if (!open) return;\n        const onDown = (event: MouseEvent): void => {\n            if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n                closeAndReset();\n            }\n        };\n        window.addEventListener(\"mousedown\", onDown);\n        return () => window.removeEventListener(\"mousedown\", onDown);\n    }, [open, closeAndReset]);\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n        if (event.key === \"ArrowDown\") {\n            event.preventDefault();\n            setOpen(true);\n            setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n        } else if (event.key === \"ArrowUp\") {\n            event.preventDefault();\n            setActiveIndex((current) => Math.max(0, current - 1));\n        } else if (event.key === \"Enter\") {\n            event.preventDefault();\n            const option = filtered[activeIndex];\n            if (option) handleSelect(option);\n        } else if (event.key === \"Escape\") {\n            closeAndReset();\n        }\n    };\n\n    return (\n        <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n            {label && (\n                <label htmlFor={id} className={styles.label}>\n                    {label}\n                </label>\n            )}\n            <div className={styles.field}>\n                <input\n                    id={id}\n                    ref={inputRef}\n                    type=\"text\"\n                    role=\"combobox\"\n                    aria-expanded={open}\n                    aria-controls={`${id}-listbox`}\n                    aria-autocomplete=\"list\"\n                    className={styles.input}\n                    placeholder={placeholder}\n                    disabled={disabled}\n                    value={open ? query : selectedLabel}\n                    onFocus={() => setOpen(true)}\n                    onChange={(event) => {\n                        setQuery(event.target.value);\n                        setOpen(true);\n                        setActiveIndex(0);\n                    }}\n                    onKeyDown={handleKeyDown}\n                />\n                <span className={styles.caret} aria-hidden>\n                    ▾\n                </span>\n            </div>\n            {open && (\n                <ul id={`${id}-listbox`} role=\"listbox\" className={styles.menu}>\n                    {filtered.length === 0 ? (\n                        <li className={styles.empty}>{emptyMessage}</li>\n                    ) : (\n                        filtered.map((option, index) => (\n                            <li\n                                key={option.value}\n                                role=\"option\"\n                                aria-selected={option.value === value}\n                                className={cn(\n                                    styles.option,\n                                    index === activeIndex && styles.active,\n                                    option.value === value && styles.selected,\n                                )}\n                                onMouseEnter={() => setActiveIndex(index)}\n                                onMouseDown={(event) => {\n                                    event.preventDefault();\n                                    handleSelect(option);\n                                }}\n                            >\n                                {option.label}\n                            </li>\n                        ))\n                    )}\n                </ul>\n            )}\n            {error ? (\n                <span className={styles.errorText}>{error}</span>\n            ) : helperText ? (\n                <span className={styles.helper}>{helperText}</span>\n            ) : null}\n        </div>\n    );\n}\n"],"mappings":"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"}