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.
This commit is contained in:
@@ -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<HTMLInputElement>(null);
|
||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -49,6 +47,20 @@ export function TagInput({
|
||||
} = useTagsAutocomplete();
|
||||
const [allTags, setAllTags] = useState<Tag[]>([]);
|
||||
|
||||
// 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();
|
||||
// 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,22 +246,20 @@ export function TagInput({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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(
|
||||
<div
|
||||
ref={suggestionsRef}
|
||||
className="fixed z-[9999] max-h-64 overflow-y-auto rounded-xl border border-[var(--border)]/60 bg-[var(--card)]/80 backdrop-blur-xl shadow-2xl shadow-[var(--card-shadow-medium)] relative"
|
||||
className="fixed z-[99999] max-h-64 overflow-y-auto rounded-xl border border-[var(--border)]/60 bg-[var(--card)]/80 backdrop-blur-xl shadow-2xl shadow-[var(--card-shadow-medium)]"
|
||||
style={{
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-xl bg-gradient-to-br from-[color-mix(in_srgb,var(--primary)_12%,transparent)] via-[color-mix(in_srgb,var(--primary)_6%,transparent)] to-transparent opacity-90 pointer-events-none" />
|
||||
<div className="relative z-10">
|
||||
{loading ? (
|
||||
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
||||
Recherche...
|
||||
@@ -318,7 +301,6 @@ export function TagInput({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user