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';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
import { useTagsAutocomplete } from '@/hooks/useTags';
|
import { useTagsAutocomplete } from '@/hooks/useTags';
|
||||||
import { Badge } from './Badge';
|
import { Badge } from './Badge';
|
||||||
@@ -25,11 +26,25 @@ 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({ top: 0, left: 0, 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 { suggestions, loading, searchTags, clearSuggestions, loadPopularTags } = useTagsAutocomplete();
|
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
|
// Rechercher des suggestions quand l'input change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputValue.trim()) {
|
if (inputValue.trim()) {
|
||||||
@@ -42,6 +57,24 @@ export function TagInput({
|
|||||||
}
|
}
|
||||||
}, [inputValue, searchTags, clearSuggestions]);
|
}, [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 addTag = (tagName: string) => {
|
||||||
const trimmedTag = tagName.trim();
|
const trimmedTag = tagName.trim();
|
||||||
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
|
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
|
||||||
@@ -96,6 +129,7 @@ export function TagInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -108,7 +142,7 @@ export function TagInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${className}`}>
|
<div ref={containerRef} className={`relative ${className}`}>
|
||||||
{/* Container des tags et input */}
|
{/* 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="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">
|
<div className="flex flex-wrap gap-1 items-center">
|
||||||
@@ -148,11 +182,16 @@ export function TagInput({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Suggestions dropdown */}
|
{/* Suggestions dropdown rendu via portail pour éviter les problèmes de z-index */}
|
||||||
{showSuggestions && (suggestions.length > 0 || loading) && (
|
{showSuggestions && (suggestions.length > 0 || loading) && typeof window !== 'undefined' && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={suggestionsRef}
|
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 ? (
|
{loading ? (
|
||||||
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
|
||||||
@@ -185,7 +224,8 @@ export function TagInput({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Indicateur de limite */}
|
{/* Indicateur de limite */}
|
||||||
|
|||||||
Reference in New Issue
Block a user