import './ContextCombobox.css'; import { useEffect, useId, useMemo, useRef, useState } from 'react'; import { FiCheck, FiChevronDown, FiSearch } from 'react-icons/fi'; import type { InwinkContextOption } from '../context/InwinkContextProvider'; interface ContextComboboxProps { label: string; placeholder: string; value?: string; options: InwinkContextOption[]; isLoading: boolean; error: string | null; onChange: (id: string) => void; /** * Si fourni, la recherche est déléguée au serveur (le terme saisi est transmis) * et la liste n'est PAS refiltrée côté client. Sans ce prop, le filtrage se fait * côté client sur le libellé. */ onSearch?: (query: string) => void; } /** * Sélecteur ergonomique réutilisable : bouton déclencheur + popover avec champ de * recherche, liste filtrable et navigation clavier. Mutualise l'UI des pickers de * contexte (Event / Community / Audience). */ export default function ContextCombobox({ label, placeholder, value, options, isLoading, error, onChange, onSearch, }: ContextComboboxProps) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [highlight, setHighlight] = useState(0); const containerRef = useRef(null); const inputRef = useRef(null); const listId = useId(); const selected = options.find((option) => option.id === value); const triggerLabel = selected?.label ?? (value && isLoading ? 'Chargement…' : placeholder); // En mode serveur (`onSearch`), la liste est déjà filtrée par l'API : on ne refiltre // pas côté client (une recherche par id n'apparaît pas dans le libellé). const filtered = useMemo(() => { if (onSearch) return options; const q = query.trim().toLowerCase(); return q ? options.filter((option) => option.label.toLowerCase().includes(q)) : options; }, [options, query, onSearch]); // Fermeture au clic extérieur. useEffect(() => { if (!open) return; const onPointerDown = (event: MouseEvent) => { if (!containerRef.current?.contains(event.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', onPointerDown); return () => document.removeEventListener('mousedown', onPointerDown); }, [open]); // Focus du champ de recherche à l'ouverture, et réinitialisation de la liste // (en mode serveur, on recharge la liste complète non filtrée). useEffect(() => { if (open) { setQuery(''); setHighlight(0); inputRef.current?.focus(); onSearch?.(''); } }, [open, onSearch]); const select = (id: string) => { onChange(id); setOpen(false); }; const onInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown') { event.preventDefault(); setHighlight((h) => Math.min(h + 1, filtered.length - 1)); } else if (event.key === 'ArrowUp') { event.preventDefault(); setHighlight((h) => Math.max(h - 1, 0)); } else if (event.key === 'Enter') { event.preventDefault(); const option = filtered[highlight]; if (option) select(option.id); } else if (event.key === 'Escape') { setOpen(false); } }; return (
{label} {open && (
{ setQuery(e.target.value); setHighlight(0); onSearch?.(e.target.value); }} onKeyDown={onInputKeyDown} />
    {isLoading &&
  • Chargement…
  • } {!isLoading && filtered.length === 0 && (
  • Aucun résultat
  • )} {!isLoading && filtered.map((option, index) => { const isSelected = option.id === value; const isHighlighted = index === highlight; return (
  • ); })}
)}
); }