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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user