fix(ui): render TagInput dropdown via portal to avoid overflow clipping

- import createPortal from react-dom
- add dropdownStyle state to track fixed position coordinates
- calculate and update dropdown position on open, scroll, and resize
- render dropdown and empty-state divs into document.body using createPortal
This commit is contained in:
2026-04-24 15:24:56 -04:00
parent b5d228b8ac
commit 69bc05944c
+27 -6
View File
@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -53,6 +54,7 @@ const TagInput = ({
}) => {
const [inputValue, setInputValue] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [dropdownStyle, setDropdownStyle] = useState({});
const containerRef = useRef(null);
const inputRef = useRef(null);
@@ -72,6 +74,23 @@ const TagInput = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
if (!isOpen || !containerRef.current) return;
const updatePosition = () => {
const rect = containerRef.current?.getBoundingClientRect();
if (rect) {
setDropdownStyle({ top: rect.bottom + 6, left: rect.left, width: rect.width });
}
};
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, [isOpen]);
const selectOption = (option) => {
onChange([...value, option.value]);
setInputValue('');
@@ -124,8 +143,8 @@ const TagInput = ({
className="flex-1 min-w-[80px] bg-transparent outline-none text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 text-sm"
/>
{isOpen && filtered.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1.5 z-50 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg overflow-hidden max-h-56 overflow-y-auto">
{isOpen && filtered.length > 0 && createPortal(
<div className="z-[9999] bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg overflow-hidden max-h-56 overflow-y-auto" style={{ position: 'fixed', ...dropdownStyle }}>
{filtered.map(option => (
<button
key={option.value}
@@ -145,13 +164,15 @@ const TagInput = ({
)}
</button>
))}
</div>
</div>,
document.body
)}
{isOpen && filtered.length === 0 && inputValue && (
<div className="absolute top-full left-0 right-0 mt-1.5 z-50 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg overflow-hidden">
{isOpen && filtered.length === 0 && inputValue && createPortal(
<div className="z-[9999] bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg overflow-hidden" style={{ position: 'fixed', ...dropdownStyle }}>
<p className="px-3 py-2.5 text-sm text-neutral-400 dark:text-neutral-500">Aucun résultat</p>
</div>
</div>,
document.body
)}
</div>