import React, { useCallback } from 'react'; import { Dialog } from '@base-ui/react/dialog'; export const ANNOTATION_SELECTORS = [ '.annotation-toolbar', '[data-comment-popover="true"]', '[data-floating-picker="true"]', // Transient hidden textarea created by the legacy clipboard fallback // (packages/ui/utils/clipboard.ts). It briefly steals focus during copy; // without this guard the focus-out close reason would dismiss the popout // whenever a copy button inside it falls back to execCommand. '[data-clipboard-fallback="true"]', ]; interface PopoutDialogProps { open: boolean; onClose: () => void; title: string; container?: HTMLElement | null; className?: string; children: React.ReactNode; /** Extra data attributes to spread onto the dialog popup */ dataAttributes?: Record; } export const PopoutDialog: React.FC = ({ open, onClose, title, container, className, children, dataAttributes, }) => { const handleBackdropClick = useCallback( (e: React.MouseEvent) => { const target = e.target as Element; if (ANNOTATION_SELECTORS.some((sel) => target.closest(sel))) return; onClose(); }, [onClose], ); const handleOpenChange = useCallback( (next: boolean, eventDetails: Dialog.Root.ChangeEventDetails) => { if (next) return; // Interactions with annotation UI (toolbars/popovers portal outside the // dialog) must not dismiss the popout — the Radix version guarded this // via onInteractOutside preventDefault; Base UI expresses it as // cancelling the outside-press/focus-out close reason. if (eventDetails.reason === 'outside-press' || eventDetails.reason === 'focus-out') { // For focus events the annotation UI may sit on either side of the // event (target = element losing focus, relatedTarget = gaining), so // guard both. const event = eventDetails.event as Event | undefined; const candidates = [ event?.target as Element | null, (event as FocusEvent | undefined)?.relatedTarget as Element | null, ]; if ( candidates.some( (el) => el instanceof Element && ANNOTATION_SELECTORS.some((sel) => el.closest(sel)), ) ) { eventDetails.cancel(); return; } } onClose(); }, [onClose], ); return ( {/* Non-modal dialogs render no library backdrop, so we use a plain div for the dark scrim + blur while keeping annotation toolbars (which portal outside the dialog) interactive. */}