From 0b17934ca1faa868a6f93335c7e27ec0996af3cb Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 9 Oct 2025 21:47:59 +0200 Subject: [PATCH] refactor(TagInput): optimize dropdown position handling and improve tag loading logic - Replaced the dropdown position update logic with a dedicated calculatePosition function for clarity. - Introduced a new state to track if popular tags have been loaded, enhancing the suggestion display logic. - Cleaned up unnecessary event listeners and streamlined the component's focus handling. --- src/components/ui/TagInput.tsx | 172 +++++++++++++++------------------ 1 file changed, 77 insertions(+), 95 deletions(-) diff --git a/src/components/ui/TagInput.tsx b/src/components/ui/TagInput.tsx index 4d4d56b..e2297ca 100644 --- a/src/components/ui/TagInput.tsx +++ b/src/components/ui/TagInput.tsx @@ -31,11 +31,9 @@ export function TagInput({ const [inputValue, setInputValue] = useState(''); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); - const [dropdownPosition, setDropdownPosition] = useState({ - top: 0, - left: 0, - width: 0, - }); + const [hasLoadedPopularTags, setHasLoadedPopularTags] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); + const [positionCalculated, setPositionCalculated] = useState(false); const inputRef = useRef(null); const suggestionsRef = useRef(null); const containerRef = useRef(null); @@ -49,6 +47,20 @@ export function TagInput({ } = useTagsAutocomplete(); const [allTags, setAllTags] = useState([]); + // Calculer la position du dropdown (copié du composant Dropdown) + const calculatePosition = () => { + if (!containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + + // Placement bottom-start (comme dans Dropdown) + const top = rect.bottom + 4; + const left = rect.left; + + setDropdownPosition({ top, left }); + setPositionCalculated(true); + }; + // Charger tous les tags au début pour pouvoir identifier le tag prioritaire useEffect(() => { const loadTags = async () => { @@ -63,17 +75,15 @@ export function TagInput({ loadTags(); }, []); - // Calculer la position du dropdown - const updateDropdownPosition = () => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - setDropdownPosition({ - top: rect.bottom + window.scrollY + 4, - left: rect.left + window.scrollX, - width: rect.width, - }); + // Mettre à jour la position quand le dropdown s'ouvre (comme dans Dropdown) + useEffect(() => { + if (showSuggestions) { + setPositionCalculated(false); + calculatePosition(); + } else { + setPositionCalculated(false); } - }; + }, [showSuggestions]); // Rechercher des suggestions quand l'input change useEffect(() => { @@ -81,29 +91,15 @@ export function TagInput({ searchTags(inputValue); setShowSuggestions(true); setSelectedIndex(-1); + setHasLoadedPopularTags(false); // Reset flag when typing } else { clearSuggestions(); - setShowSuggestions(false); + // Only hide suggestions if we haven't just loaded popular tags + if (!hasLoadedPopularTags) { + setShowSuggestions(false); + } } - }, [inputValue, searchTags, clearSuggestions]); - - // Mettre à jour la position du dropdown quand nécessaire - useEffect(() => { - if (showSuggestions) { - updateDropdownPosition(); - - const handleResize = () => updateDropdownPosition(); - const handleScroll = () => updateDropdownPosition(); - - window.addEventListener('resize', handleResize); - window.addEventListener('scroll', handleScroll, true); - - return () => { - window.removeEventListener('resize', handleResize); - window.removeEventListener('scroll', handleScroll, true); - }; - } - }, [showSuggestions]); + }, [inputValue, searchTags, clearSuggestions, hasLoadedPopularTags]); const addTag = (tagName: string) => { const trimmedTag = tagName.trim(); @@ -170,23 +166,13 @@ export function TagInput({ addTag(tag.name); }; - const handleBlur = (e: React.FocusEvent) => { - // Délai pour permettre le clic sur une suggestion - setTimeout(() => { - if (!suggestionsRef.current?.contains(e.relatedTarget as Node)) { - setShowSuggestions(false); - setSelectedIndex(-1); - } - }, 150); - }; - const handleFocus = () => { - updateDropdownPosition(); if (inputValue.trim()) { // Si il y a du texte, afficher les suggestions existantes setShowSuggestions(true); } else { // Si l'input est vide, charger les tags populaires + setHasLoadedPopularTags(true); loadPopularTags(20); setShowSuggestions(true); } @@ -252,7 +238,6 @@ export function TagInput({ value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} - onBlur={handleBlur} onFocus={handleFocus} placeholder={tags.length === 0 ? placeholder : ''} className="flex-1 min-w-[120px] bg-transparent border-none outline-none text-[var(--foreground)] placeholder-[var(--muted-foreground)] text-sm" @@ -261,64 +246,61 @@ export function TagInput({ - {/* Suggestions dropdown rendu via portail pour éviter les problèmes de z-index */} + {/* Suggestions dropdown */} {showSuggestions && (suggestions.length > 0 || loading) && + positionCalculated && typeof window !== 'undefined' && createPortal(
-
-
- {loading ? ( -
- Recherche... -
- ) : ( -
- {suggestions.map((tag, index) => ( - - ))} -
- )} -
+ )} + + ))} +
+ )}
, document.body )}