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 [inputValue, setInputValue] = useState('');
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
const [dropdownPosition, setDropdownPosition] = useState({
|
const [hasLoadedPopularTags, setHasLoadedPopularTags] = useState(false);
|
||||||
top: 0,
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||||
left: 0,
|
const [positionCalculated, setPositionCalculated] = useState(false);
|
||||||
width: 0,
|
|
||||||
});
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -49,6 +47,20 @@ export function TagInput({
|
|||||||
} = useTagsAutocomplete();
|
} = useTagsAutocomplete();
|
||||||
const [allTags, setAllTags] = useState<Tag[]>([]);
|
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
|
// Charger tous les tags au début pour pouvoir identifier le tag prioritaire
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTags = async () => {
|
const loadTags = async () => {
|
||||||
@@ -63,17 +75,15 @@ export function TagInput({
|
|||||||
loadTags();
|
loadTags();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Calculer la position du dropdown
|
// Mettre à jour la position quand le dropdown s'ouvre (comme dans Dropdown)
|
||||||
const updateDropdownPosition = () => {
|
useEffect(() => {
|
||||||
if (containerRef.current) {
|
if (showSuggestions) {
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
setPositionCalculated(false);
|
||||||
setDropdownPosition({
|
calculatePosition();
|
||||||
top: rect.bottom + window.scrollY + 4,
|
} else {
|
||||||
left: rect.left + window.scrollX,
|
setPositionCalculated(false);
|
||||||
width: rect.width,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
}, [showSuggestions]);
|
||||||
|
|
||||||
// Rechercher des suggestions quand l'input change
|
// Rechercher des suggestions quand l'input change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,29 +91,15 @@ export function TagInput({
|
|||||||
searchTags(inputValue);
|
searchTags(inputValue);
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
|
setHasLoadedPopularTags(false); // Reset flag when typing
|
||||||
} else {
|
} else {
|
||||||
clearSuggestions();
|
clearSuggestions();
|
||||||
setShowSuggestions(false);
|
// Only hide suggestions if we haven't just loaded popular tags
|
||||||
|
if (!hasLoadedPopularTags) {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [inputValue, searchTags, clearSuggestions]);
|
}, [inputValue, searchTags, clearSuggestions, hasLoadedPopularTags]);
|
||||||
|
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const addTag = (tagName: string) => {
|
const addTag = (tagName: string) => {
|
||||||
const trimmedTag = tagName.trim();
|
const trimmedTag = tagName.trim();
|
||||||
@@ -170,23 +166,13 @@ export function TagInput({
|
|||||||
addTag(tag.name);
|
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 = () => {
|
const handleFocus = () => {
|
||||||
updateDropdownPosition();
|
|
||||||
if (inputValue.trim()) {
|
if (inputValue.trim()) {
|
||||||
// Si il y a du texte, afficher les suggestions existantes
|
// Si il y a du texte, afficher les suggestions existantes
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
} else {
|
} else {
|
||||||
// Si l'input est vide, charger les tags populaires
|
// Si l'input est vide, charger les tags populaires
|
||||||
|
setHasLoadedPopularTags(true);
|
||||||
loadPopularTags(20);
|
loadPopularTags(20);
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
}
|
}
|
||||||
@@ -252,7 +238,6 @@ export function TagInput({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
placeholder={tags.length === 0 ? placeholder : ''}
|
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"
|
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({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Suggestions dropdown rendu via portail pour éviter les problèmes de z-index */}
|
{/* Suggestions dropdown */}
|
||||||
{showSuggestions &&
|
{showSuggestions &&
|
||||||
(suggestions.length > 0 || loading) &&
|
(suggestions.length > 0 || loading) &&
|
||||||
|
positionCalculated &&
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
ref={suggestionsRef}
|
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={{
|
style={{
|
||||||
top: dropdownPosition.top,
|
top: `${dropdownPosition.top}px`,
|
||||||
left: dropdownPosition.left,
|
left: `${dropdownPosition.left}px`,
|
||||||
width: dropdownPosition.width,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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" />
|
{loading ? (
|
||||||
<div className="relative z-10">
|
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
||||||
{loading ? (
|
Recherche...
|
||||||
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
</div>
|
||||||
Recherche...
|
) : (
|
||||||
</div>
|
<div
|
||||||
) : (
|
className={`gap-2 p-3 ${compactSuggestions ? 'grid grid-cols-1' : 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'}`}
|
||||||
<div
|
>
|
||||||
className={`gap-2 p-3 ${compactSuggestions ? 'grid grid-cols-1' : 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'}`}
|
{suggestions.map((tag, index) => (
|
||||||
>
|
<button
|
||||||
{suggestions.map((tag, index) => (
|
key={tag.id}
|
||||||
<button
|
type="button"
|
||||||
key={tag.id}
|
onClick={() => handleSuggestionClick(tag)}
|
||||||
type="button"
|
className={`flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors border ${
|
||||||
onClick={() => handleSuggestionClick(tag)}
|
index === selectedIndex
|
||||||
className={`flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors border ${
|
? 'bg-[var(--primary)]/15 text-[var(--primary)] ring-1 ring-[var(--primary)]/40 border-[var(--primary)]/40 backdrop-blur-sm'
|
||||||
index === selectedIndex
|
: 'text-[var(--foreground)] border-transparent hover:bg-[var(--card)]/40 hover:border-[var(--border)]/40 backdrop-blur-sm'
|
||||||
? 'bg-[var(--primary)]/15 text-[var(--primary)] ring-1 ring-[var(--primary)]/40 border-[var(--primary)]/40 backdrop-blur-sm'
|
} ${tags.includes(tag.name) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
: 'text-[var(--foreground)] border-transparent hover:bg-[var(--card)]/40 hover:border-[var(--border)]/40 backdrop-blur-sm'
|
disabled={tags.includes(tag.name)}
|
||||||
} ${tags.includes(tag.name) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
title={
|
||||||
disabled={tags.includes(tag.name)}
|
tags.includes(tag.name)
|
||||||
title={
|
? 'Déjà ajouté'
|
||||||
tags.includes(tag.name)
|
: `Ajouter ${tag.name}`
|
||||||
? 'Déjà ajouté'
|
}
|
||||||
: `Ajouter ${tag.name}`
|
>
|
||||||
}
|
<div
|
||||||
>
|
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||||
<div
|
style={{ backgroundColor: tag.color }}
|
||||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
/>
|
||||||
style={{ backgroundColor: tag.color }}
|
<span className="truncate text-[var(--foreground)]">
|
||||||
/>
|
{tag.name}
|
||||||
<span className="truncate text-[var(--foreground)]">
|
</span>
|
||||||
{tag.name}
|
{tags.includes(tag.name) && (
|
||||||
|
<span className="text-[var(--muted-foreground)] ml-auto">
|
||||||
|
✓
|
||||||
</span>
|
</span>
|
||||||
{tags.includes(tag.name) && (
|
)}
|
||||||
<span className="text-[var(--muted-foreground)] ml-auto">
|
</button>
|
||||||
✓
|
))}
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user