"use client"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Info } from "lucide-react"; // Hilfetext-Bubble. // // Der Text wird über ein PORTAL an den gehängt und `fixed` positioniert. Vorher lag er // als `absolute` Kind im Fluss -- in einem scrollenden Dialog (Verteil-Dialoge, Assistent) // schnitt dessen `overflow` die Box ab, teilweise nach wenigen Zeilen. Fix positioniert // bezieht sich auf den Viewport und entkommt damit jedem Overflow-Container. // // Zusätzlich: Die Box klappt nach OBEN, wenn unten kein Platz mehr ist, und wird an den // Bildschirmrändern geklemmt, damit sie nie halb ausserhalb steht. const WIDTH = 256; // entspricht w-64 const MARGIN = 8; export function InfoBubble({ text }: { text: string }) { const [open, setOpen] = useState(false); const [pos, setPos] = useState<{ top: number; left: number } | null>(null); const btnRef = useRef(null); const boxRef = useRef(null); // Position aus der Lage des Knopfes berechnen -- erst wenn geöffnet, und erneut, sobald die // tatsächliche Höhe der Box bekannt ist (für das Hochklappen). useEffect(() => { if (!open) { // eslint-disable-next-line react-hooks/set-state-in-effect -- Position gilt nur im offenen Zustand setPos(null); return; } const place = () => { const btn = btnRef.current; if (!btn) return; const r = btn.getBoundingClientRect(); const height = boxRef.current?.offsetHeight ?? 0; const spaceBelow = window.innerHeight - r.bottom; const above = height > 0 && spaceBelow < height + MARGIN && r.top > height + MARGIN; const left = Math.min( Math.max(MARGIN, r.left + r.width / 2 - WIDTH / 2), Math.max(MARGIN, window.innerWidth - WIDTH - MARGIN) ); setPos({ top: above ? r.top - height - 6 : r.bottom + 6, left }); }; place(); // Nach dem ersten Zeichnen erneut -- dann steht die Höhe fest. const raf = requestAnimationFrame(place); window.addEventListener("scroll", place, true); window.addEventListener("resize", place); return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", place, true); window.removeEventListener("resize", place); }; }, [open]); return ( {open && typeof document !== "undefined" && createPortal(
{text}
, document.body )}
); }