fix(dashboard): portal modals to .dash-root so they anchor to the viewport
A transformed/overflow panel ancestor was trapping the modal overlay's position:fixed, so the modal rendered offset inside the messenger/inbox panel and clipped (Group settings sat inside the thread pane; the compose modal's top was cut at the panel edge). Portal the overlay up to .dash-root — above those panels but still inside the scoped design-system CSS — so it centers on the viewport and the 90vh cap works. Falls back to inline render if no .dash-root is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
createContext, useCallback, useContext, useEffect, useId,
|
createContext, useCallback, useContext, useEffect, useId,
|
||||||
useRef, useState, type ReactNode,
|
useRef, useState, type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
MessageCircle, Ticket, Phone, Mail, BookOpen, Rocket, Shield, ShieldCheck,
|
MessageCircle, Ticket, Phone, Mail, BookOpen, Rocket, Shield, ShieldCheck,
|
||||||
Lock, CreditCard, User, Bell, Eye, EyeOff, Camera, Upload, Plus, Star, Send,
|
Lock, CreditCard, User, Bell, Eye, EyeOff, Camera, Upload, Plus, Star, Send,
|
||||||
@@ -221,6 +222,11 @@ export function Modal({ open, onClose, title, subtitle, icon, children, footer,
|
|||||||
children: ReactNode; footer?: ReactNode; size?: "sm" | "md" | "lg";
|
children: ReactNode; footer?: ReactNode; size?: "sm" | "md" | "lg";
|
||||||
}) {
|
}) {
|
||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
|
// Portal the overlay up to `.dash-root` so its position:fixed anchors to the viewport,
|
||||||
|
// not to a transformed/overflow panel ancestor (which would clip or offset the modal).
|
||||||
|
const [host, setHost] = useState<Element | null>(null);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => { setHost(document.querySelector(".dash-root")); setMounted(true); }, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||||
@@ -228,8 +234,8 @@ export function Modal({ open, onClose, title, subtitle, icon, children, footer,
|
|||||||
return () => document.removeEventListener("keydown", onKey);
|
return () => document.removeEventListener("keydown", onKey);
|
||||||
}, [open, onClose]);
|
}, [open, onClose]);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open || !mounted) return null;
|
||||||
return (
|
const overlay = (
|
||||||
<div className="ds-modal-overlay" onMouseDown={onClose}>
|
<div className="ds-modal-overlay" onMouseDown={onClose}>
|
||||||
<div className={`ds-modal size-${size}`} role="dialog" aria-modal="true" aria-labelledby={titleId} onMouseDown={(e) => e.stopPropagation()}>
|
<div className={`ds-modal size-${size}`} role="dialog" aria-modal="true" aria-labelledby={titleId} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
<div className="ds-modal-head">
|
<div className="ds-modal-head">
|
||||||
@@ -247,6 +253,7 @@ export function Modal({ open, onClose, title, subtitle, icon, children, footer,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
return host ? createPortal(overlay, host) : overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user