Versione: 1.0
Ultima Modifica: 2024-01-15
Stato: Stabile
Il componente SearchBar fornisce una barra di ricerca intelligente con autocompletamento e suggerimenti per lemmi e forme. Supporta ricerca debounced, navigazione da tastiera, e integrazione con il sistema di evidenziazione cross-componente.
- Ricerca in tempo reale con debouncing (300ms) per ottimizzare le performance
- Autocompletamento con suggerimenti limitati a 10 risultati unici
- Evidenziazione testo con highlight dei caratteri corrispondenti
- Navigazione tastiera: Arrow Up/Down, Enter, Escape
- Integrazione highlight: Evidenziazione temporanea al hover e permanente alla selezione
- Deduplicazione: Previene suggerimenti duplicati per lemma+forma identici
- Animazioni fluide per apertura/chiusura suggerimenti e cancellazione
- Feedback visivo: Stato vuoto con icona e messaggio quando nessun risultato
Il componente SearchBar non accetta props. Interagisce direttamente con i context globali.
const [query, setQuery] = useState(''); // Testo input utente
const [suggestions, setSuggestions] = useState<Lemma[]>([]); // Risultati autocomplete
const [isOpen, setIsOpen] = useState(false); // Visibilità dropdown suggerimenti
const [highlightedIndex, setHighlightedIndex] = useState(-1); // Indice navigazione tastieraLa ricerca è ottimizzata con timeout di 300ms per evitare query eccessive durante la digitazione:
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query.length > 0) {
const uniqueResults = new Map<string, Lemma>();
const lowerQuery = query.toLowerCase();
for (const lemma of lemmi) {
if (uniqueResults.size >= 10) break; // Max 10 risultati
const matches =
lemma.Lemma.toLowerCase().includes(lowerQuery) ||
lemma.Forma.toLowerCase().includes(lowerQuery);
if (matches) {
// Chiave normalizzata per evitare duplicati
const key = `${lemma.Lemma.toLowerCase().trim()}|${lemma.Forma.toLowerCase().trim()}`;
if (!uniqueResults.has(key)) {
uniqueResults.set(key, lemma);
}
}
}
setSuggestions(Array.from(uniqueResults.values()));
setIsOpen(uniqueResults.size > 0);
}
}, 300);
return () => clearTimeout(timeoutId);
}, [query, lemmi]);Utilizza una Map con chiave composta lemma|forma normalizzata per garantire unicità dei suggerimenti anche in presenza di varianti case-insensitive o spazi.
Quando l'utente seleziona un lemma, il componente:
- Reset completo dei filtri per evitare conflitti
- Imposta filtri specifici per il lemma selezionato
- Evidenzia lemma, area geografica e anno correlati
- Chiude dropdown suggerimenti
const handleSelect = (lemma: Lemma) => {
// Reset completo filtri
setFilters({
searchQuery: lemma.Lemma,
selectedLemmaId: lemma.IdLemma,
selectedLetter: null,
selectedYear: null,
categorie: [],
periodi: []
});
setQuery(lemma.Lemma);
setIsOpen(false);
// Evidenzia lemma + correlati
const year = parseInt(lemma.Anno);
highlightMultiple({
lemmaIds: [lemma.IdLemma],
geoAreaIds: [lemma.CollGeografica],
years: !isNaN(year) ? [year] : [],
source: 'search',
type: 'select'
});
};Supporto completo per interazione da tastiera:
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setHighlightedIndex((prev) =>
prev < suggestions.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) {
handleSelect(suggestions[highlightedIndex]);
}
break;
case 'Escape':
setIsOpen(false);
setHighlightedIndex(-1);
break;
}
};Funzione utility per evidenziare caratteri corrispondenti nei suggerimenti:
const highlightText = (text: string, query: string) => {
if (!query) return text;
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return parts.map((part, i) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark key={i} className="bg-yellow-200 text-gray-900">
{part}
</mark>
) : (
part
)
);
};Al hover sui suggerimenti, evidenziazione temporanea degli elementi correlati:
const handleSuggestionHover = (lemma: Lemma, index: number) => {
setHighlightedIndex(index);
const year = parseInt(lemma.Anno);
highlightMultiple({
lemmaIds: [lemma.IdLemma],
geoAreaIds: [lemma.CollGeografica],
years: !isNaN(year) ? [year] : [],
source: 'search',
type: 'hover' // Evidenziazione temporanea
});
};useApp(): Accesso alemmi,setFilters()useHighlight(): FunzionihighlightMultiple(),clearHighlight(),isLemmaHighlighted()useState(): Gestione stato locale (query, suggestions, isOpen, highlightedIndex)useEffect(): Debouncing ricerca, gestione click outsideuseRef(): Riferimenti DOM per input e dropdown suggerimenti
- Framer Motion: AnimatePresence, motion components per animazioni
- Lucide React: Icone Search, X
- MotionWrapper: StaggerContainer, StaggerItem per animazioni scaglionate
- lib/motion-config: Configurazioni preset per animazioni
// AppContext
const { lemmi, setFilters } = useApp();
// HighlightContext
const { highlightMultiple, clearHighlight, isLemmaHighlighted } = useHighlight();import { SearchBar } from '@/components/SearchBar';
export default function Page() {
return (
<div className="container">
<SearchBar />
{/* Altri componenti reagiranno ai filtri impostati */}
</div>
);
}const { filters } = useApp();
// Verifica se ricerca attiva
if (filters.searchQuery) {
console.log(`Ricerca: "${filters.searchQuery}"`);
console.log(`Lemma selezionato: ${filters.selectedLemmaId}`);
}const { setFilters } = useApp();
const { clearHighlight } = useHighlight();
// Reset ricerca
setFilters({ searchQuery: '', selectedLemmaId: null });
clearHighlight();- Debouncing 300ms: Riduce query durante digitazione rapida
- Limite 10 risultati: Previene rendering eccessivo nel dropdown
- Early break: Loop interrotto appena raggiunti 10 risultati
- Deduplicazione O(1): Usa Map invece di array con
find()
- ARIA attributes:
aria-label="Cerca lemmi o forme"aria-autocomplete="list"aria-controls="search-suggestions"aria-expanded={isOpen}role="listbox"per dropdownrole="option"per suggerimentiaria-selectedper navigazione tastiera
1000: Dropdown suggerimenti (sotto Filters 9999)
- Ricerca case-insensitive: No supporto per ricerca case-sensitive
- No fuzzy search: Ricerca esatta con
includes(), no tolleranza errori - No regex avanzate: Ricerca semplice senza pattern complessi
- Memoria: Mantiene tutti i lemmi in memoria per ricerca
- Idle: Input vuoto, nessun suggerimento
- Loading: Debouncing attivo (300ms), visualmente identico a idle
- Results: Suggerimenti visibili con animazioni
- No Results: Messaggio "Nessun risultato trovato" con icona
- Selected: Suggerimenti chiusi, filtri applicati
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
suggestionsRef.current && !suggestionsRef.current.contains(event.target as Node) &&
inputRef.current && !inputRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);- Implementare
SearchIndexcon trie o indice invertito - Aggiungere ricerca fuzzy con Levenshtein distance
- Virtualizzazione per > 100 risultati
- Web Workers per ricerca in background
- Caching risultati recenti