fix: use Portal for hover tooltips to prevent table container overflow clipping

This commit is contained in:
Christian Vidal Wolf
2026-04-27 14:50:13 +02:00
parent 557cb537a9
commit 601a02117f
+56 -14
View File
@@ -53,6 +53,58 @@ function findCol(headers: string[], ...keywords: string[]): number {
); );
} }
function NoteTooltip({ note, isAnna, children }: { note: string, isAnna?: boolean, children: React.ReactNode }) {
const [show, setShow] = useState(false);
const [pos, setPos] = useState({ top: 0, left: 0 });
const ref = useRef<HTMLDivElement>(null);
if (!note) {
return <>{children}</>;
}
const handleMouseEnter = () => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setPos({
top: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX + rect.width / 2
});
setShow(true);
}
};
useEffect(() => {
const handleScroll = () => setShow(false);
if (show) {
window.addEventListener('scroll', handleScroll, true);
}
return () => window.removeEventListener('scroll', handleScroll, true);
}, [show]);
return (
<div
className="relative flex items-center"
ref={ref}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setShow(false)}
>
{children}
{show && createPortal(
<div
style={{ top: pos.top, left: pos.left, transform: 'translateX(-50%)' }}
className={cn(
"absolute w-48 p-2 border rounded text-xs text-white shadow-xl z-[99999] pointer-events-none animate-in fade-in zoom-in-95 duration-100",
isAnna ? "bg-pink-900/95 border-pink-500/50" : "bg-amber-900/95 border-amber-500/50"
)}
>
{note}
</div>,
document.body
)}
</div>
);
}
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) { export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses }: PricingViewProps) {
const COLUMNS = useColumns(); const COLUMNS = useColumns();
const [filterMode, setFilterMode] = usePersistentState<FilterMode>('pricing-filterMode', 'all_errors'); const [filterMode, setFilterMode] = usePersistentState<FilterMode>('pricing-filterMode', 'all_errors');
@@ -1928,7 +1980,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onChange={(e) => handleToggleCheck(dataIndex, e.target.checked)} onChange={(e) => handleToggleCheck(dataIndex, e.target.checked)}
className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-slate-800 cursor-pointer" className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-slate-800 cursor-pointer"
/> />
<div className="relative group"> <NoteTooltip note={note}>
<button <button
onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })} onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })}
className={cn( className={cn(
@@ -1940,12 +1992,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
> >
<MessageSquare className={cn("w-3.5 h-3.5", note && "fill-amber-400/20")} /> <MessageSquare className={cn("w-3.5 h-3.5", note && "fill-amber-400/20")} />
</button> </button>
{note && ( </NoteTooltip>
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 w-48 p-2 bg-amber-900/90 border border-amber-500/50 rounded text-xs text-white shadow-lg z-[1000] opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
{note}
</div>
)}
</div>
</div> </div>
</td> </td>
@@ -1962,7 +2009,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleAnnaCheck(dataIndex, e.target.checked)} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleAnnaCheck(dataIndex, e.target.checked)}
className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-pink-600 focus:ring-pink-500 focus:ring-offset-slate-800 cursor-pointer" className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-pink-600 focus:ring-pink-500 focus:ring-offset-slate-800 cursor-pointer"
/> />
<div className="relative group"> <NoteTooltip note={annaNote} isAnna>
<button <button
onClick={() => setAnnaEditor({ rowIndex: dataIndex, text: annaNote })} onClick={() => setAnnaEditor({ rowIndex: dataIndex, text: annaNote })}
className={cn( className={cn(
@@ -1974,12 +2021,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
> >
<MessageSquare className={cn("w-3.5 h-3.5", annaNote && "fill-pink-400/20")} /> <MessageSquare className={cn("w-3.5 h-3.5", annaNote && "fill-pink-400/20")} />
</button> </button>
{annaNote && ( </NoteTooltip>
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 w-48 p-2 bg-pink-900/90 border border-pink-500/50 rounded text-xs text-white shadow-lg z-[1000] opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
{annaNote}
</div>
)}
</div>
</div> </div>
</td> </td>
); );