feat: improve TagInput component with dropdown positioning

- Added logic to calculate and update the dropdown position dynamically based on the input container's position, enhancing the user experience.
- Implemented portal rendering for the suggestions dropdown to avoid z-index issues, ensuring it displays correctly above other elements.
- Refactored the component to use a `containerRef` for better positioning management.
This commit is contained in:
Julien Froidefond
2025-09-30 10:15:02 +02:00
parent 270a2bd4d0
commit 9d63d31064

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Tag } from '@/lib/types';
import { useTagsAutocomplete } from '@/hooks/useTags';
import { Badge } from './Badge';
@@ -25,11 +26,25 @@ 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 inputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { suggestions, loading, searchTags, clearSuggestions, loadPopularTags } = useTagsAutocomplete();
// 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
});
}
};
// Rechercher des suggestions quand l'input change
useEffect(() => {
if (inputValue.trim()) {
@@ -42,6 +57,24 @@ export function TagInput({
}
}, [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]);
const addTag = (tagName: string) => {
const trimmedTag = tagName.trim();
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
@@ -96,6 +129,7 @@ export function TagInput({
};
const handleFocus = () => {
updateDropdownPosition();
if (inputValue.trim()) {
// Si il y a du texte, afficher les suggestions existantes
setShowSuggestions(true);
@@ -108,7 +142,7 @@ export function TagInput({
};
return (
<div className={`relative ${className}`}>
<div ref={containerRef} className={`relative ${className}`}>
{/* Container des tags et input */}
<div className="min-h-[42px] p-2 border border-[var(--border)] rounded-lg bg-[var(--input)] focus-within:border-[var(--primary)] focus-within:ring-1 focus-within:ring-[var(--primary)]/20 transition-colors">
<div className="flex flex-wrap gap-1 items-center">
@@ -148,11 +182,16 @@ export function TagInput({
</div>
</div>
{/* Suggestions dropdown */}
{showSuggestions && (suggestions.length > 0 || loading) && (
{/* Suggestions dropdown rendu via portail pour éviter les problèmes de z-index */}
{showSuggestions && (suggestions.length > 0 || loading) && typeof window !== 'undefined' && createPortal(
<div
ref={suggestionsRef}
className="absolute top-full left-0 right-0 mt-1 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[9999] max-h-64 overflow-y-auto"
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] max-h-64 overflow-y-auto"
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
width: dropdownPosition.width
}}
>
{loading ? (
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
@@ -185,7 +224,8 @@ export function TagInput({
))}
</div>
)}
</div>
</div>,
document.body
)}
{/* Indicateur de limite */}