fix: refactor tooltips to use global state to prevent crash and duplicate function error

This commit is contained in:
Christian Vidal Wolf
2026-04-27 14:54:09 +02:00
parent 601a02117f
commit d23f4e625a
+35 -56
View File
@@ -53,63 +53,12 @@ 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');
const [search, setSearch] = usePersistentState('pricing-search', ''); const [search, setSearch] = usePersistentState('pricing-search', '');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [hoveredNote, setHoveredNote] = useState<{ text: string, pos: { top: number, left: number }, isAnna: boolean } | null>(null);
const pageSize = 100; const pageSize = 100;
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({
articleNo: 100, articleNo: 100,
@@ -928,6 +877,20 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
document.body document.body
)} )}
{/* ── Tooltip Portal ── */}
{hoveredNote && createPortal(
<div
style={{ top: hoveredNote.pos.top, left: hoveredNote.pos.left, transform: 'translateX(-50%)' }}
className={cn(
"fixed 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",
hoveredNote.isAnna ? "bg-pink-900/95 border-pink-500/50" : "bg-amber-900/95 border-amber-500/50"
)}
>
{hoveredNote.text}
</div>,
document.body
)}
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
{/* ── Search bar with Dropdown ── */} {/* ── Search bar with Dropdown ── */}
<div className="flex-1 max-w-md relative" ref={searchDropdownRef}> <div className="flex-1 max-w-md relative" ref={searchDropdownRef}>
@@ -1980,9 +1943,18 @@ 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"
/> />
<NoteTooltip note={note}>
<button <button
onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })} onClick={() => setNoteEditor({ rowIndex: dataIndex, text: note })}
onMouseEnter={(e) => {
if (!note) return;
const rect = e.currentTarget.getBoundingClientRect();
setHoveredNote({
text: note,
isAnna: false,
pos: { top: rect.bottom + window.scrollY + 8, left: rect.left + window.scrollX + rect.width / 2 }
});
}}
onMouseLeave={() => setHoveredNote(null)}
className={cn( className={cn(
"p-1.5 rounded-md transition-all shrink-0", "p-1.5 rounded-md transition-all shrink-0",
note note
@@ -1992,7 +1964,6 @@ 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>
</NoteTooltip>
</div> </div>
</td> </td>
@@ -2009,9 +1980,18 @@ 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"
/> />
<NoteTooltip note={annaNote} isAnna>
<button <button
onClick={() => setAnnaEditor({ rowIndex: dataIndex, text: annaNote })} onClick={() => setAnnaEditor({ rowIndex: dataIndex, text: annaNote })}
onMouseEnter={(e) => {
if (!annaNote) return;
const rect = e.currentTarget.getBoundingClientRect();
setHoveredNote({
text: annaNote,
isAnna: true,
pos: { top: rect.bottom + window.scrollY + 8, left: rect.left + window.scrollX + rect.width / 2 }
});
}}
onMouseLeave={() => setHoveredNote(null)}
className={cn( className={cn(
"p-1.5 rounded-md transition-all shrink-0", "p-1.5 rounded-md transition-all shrink-0",
annaNote annaNote
@@ -2021,7 +2001,6 @@ 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>
</NoteTooltip>
</div> </div>
</td> </td>
); );