import { jsx as _jsx } from "react/jsx-runtime";
// (C) 2026 GoodData Corporation
import { createContext, useCallback, useContext, useId, useMemo, useRef, useState, } from "react";
import { FloatingFocusManager, FloatingOverlay, FloatingPortal, useDismiss, useFloating, useInteractions, } from "@floating-ui/react";
import { ConditionalScopedThemeProvider } from "@gooddata/sdk-ui-theme-provider";
import { useOverlayZIndexWithRegister } from "../../Overlay/OverlayContext.js";
import { bem } from "../@utils/bem.js";
const { b, e } = bem("gd-ui-kit-modal-dialog");
const UiDialogContext = createContext(null);
/**
 * @internal
 */
export function useUiDialogContext() {
    return useContext(UiDialogContext);
}
/**
 * Modal dialog — portal, dimmed backdrop, card chrome (radius, shadow, fill,
 * padding), focus management, dismiss-on-outside / Esc, and the dialog
 * landmark (`role="dialog"`, `aria-modal="true"`). Compose `UiDialogHeader`,
 * an optional `UiDialogBody`, and `UiDialogFooter` as children.
 *
 * @internal
 */
export function UiModalDialog({ children, isOpen, onClose, width, accessibilityConfig, dataTestId, }) {
    if (!isOpen) {
        return null;
    }
    return (_jsx(OpenModalDialog, { onClose: onClose, width: width, accessibilityConfig: accessibilityConfig, dataTestId: dataTestId, children: children }));
}
/**
 * Inner body — only mounted while the modal is open so the z-index slot is
 * reserved for the open lifetime only. Splitting this out matters because
 * `useOverlayZIndexWithRegister` registers in a mount effect.
 */
function OpenModalDialog({ children, onClose, width = 540, accessibilityConfig, dataTestId, }) {
    const zIndex = useOverlayZIndexWithRegister();
    const titleId = useId();
    const descriptionId = useId();
    // Ref-counted so multiple UiDialogBody siblings + dynamic mount/unmount
    // (multi-step cards, conditional descriptions) keep `aria-describedby` in
    // sync with whether a description element is actually rendered.
    const [bodyCount, setBodyCount] = useState(0);
    const registerBody = useCallback(() => {
        setBodyCount((n) => n + 1);
        return () => setBodyCount((n) => n - 1);
    }, []);
    const hasBody = bodyCount > 0;
    const { refs, context } = useFloating({
        open: true,
        onOpenChange: (open) => {
            if (!open) {
                onClose();
            }
        },
    });
    // Prefer a deliberate `autoFocus` target (e.g. `UiConfirmDialogCard`'s
    // Cancel button) over `FloatingFocusManager`'s default of first-tabbable
    // — which would land focus on the header close icon and break the safe
    // initial keyboard target for confirm dialogs. We resolve the target via
    // a callback ref on the floating element so the lookup happens before
    // `FloatingFocusManager` reads `initialFocusRef.current` on mount.
    // When no `[autofocus]` child exists we leave the ref null and pass
    // `undefined` to `FloatingFocusManager` so it uses its default
    // first-tabbable behavior — passing a ref whose `.current` is `null`
    // makes it focus the dialog card itself instead of the first action.
    const initialFocusRef = useRef(null);
    const [hasAutofocusTarget, setHasAutofocusTarget] = useState(false);
    const setFloatingRef = useCallback((node) => {
        refs.setFloating(node);
        const target = node?.querySelector("[autofocus]") ?? null;
        initialFocusRef.current = target;
        setHasAutofocusTarget(target !== null);
    }, [refs]);
    const dismiss = useDismiss(context, {
        outsidePressEvent: "mousedown",
        // Nested kit floating popups (dropdowns, menus, autocomplete listbox)
        // portal their bodies outside our `refs.floating`, so a click on a
        // popup option looks like a backdrop press. Suppress dismiss when
        // the press lands inside any kit floating element (canonical kit
        // marker) or inside another floating-ui portal sibling.
        outsidePress: (event) => {
            const target = event.target;
            if (target?.closest("[data-gd-floating-element]")) {
                return false;
            }
            const pressedPortal = target?.closest("[data-floating-ui-portal]");
            const ownPortal = refs.floating.current?.closest("[data-floating-ui-portal]");
            return !(pressedPortal && pressedPortal !== ownPortal);
        },
        // Escape is handled locally on the card (see `onKeyDown` below) so
        // that pressing Escape inside a portaled child popover dismisses the
        // popover only — not the modal underneath. floating-ui's default
        // document-level Escape listener would fire on the modal first.
        escapeKey: false,
    });
    const { getFloatingProps } = useInteractions([dismiss]);
    const onCardKeyDown = useCallback((event) => {
        if (event.key !== "Escape" || event.defaultPrevented) {
            return;
        }
        // React events bubble through the JSX parent chain, so a keydown
        // inside a portaled child popover still reaches this handler.
        // Only act when the original target sits inside the card's DOM
        // subtree — otherwise the inner popover handles its own dismiss.
        const card = refs.floating.current;
        if (card && event.target instanceof Node && !card.contains(event.target)) {
            return;
        }
        event.stopPropagation();
        onClose();
    }, [onClose, refs]);
    const ariaLabelledBy = accessibilityConfig?.ariaLabelledBy ?? (accessibilityConfig?.ariaLabel ? undefined : titleId);
    const dialogContextValue = useMemo(() => ({ titleId, descriptionId, registerBody }), [titleId, descriptionId, registerBody]);
    // Nested kit popovers/menus (UiPopover, UiTooltip, dropdowns) portal
    // out of the modal card into sibling `[data-floating-ui-portal]` nodes.
    // FloatingFocusManager defaults to `modal=true`, which treats everything
    // outside `refs.floating` as inaccessible and blocks Tab into those
    // portals. Surface them as inside elements so keyboard navigation can
    // reach the menus while the modal trap remains in effect.
    //
    // Narrow scope: snapshot the portals present at modal mount and treat
    // only later-added portals as nested. Without the snapshot, foreign
    // floating UI mounted elsewhere on the page (toasts, persistent
    // pickers) would be considered inside the trap and Tab could escape
    // the modal.
    const portalsAtMountRef = useRef(null);
    if (portalsAtMountRef.current === null) {
        portalsAtMountRef.current = new Set(Array.from(document.querySelectorAll("[data-floating-ui-portal]")));
    }
    const getInsideElements = useCallback(() => {
        const snapshot = portalsAtMountRef.current ?? new Set();
        return Array.from(document.querySelectorAll("[data-floating-ui-portal]")).filter((node) => !snapshot.has(node) && !node.contains(refs.floating.current));
    }, [refs]);
    return (_jsx(FloatingPortal, { children: _jsx(ConditionalScopedThemeProvider, { children: _jsx(FloatingOverlay, { lockScroll: true, className: b(), "data-testid": dataTestId, style: { zIndex }, children: _jsx(FloatingFocusManager, { context: context, initialFocus: hasAutofocusTarget ? initialFocusRef : undefined, getInsideElements: getInsideElements, children: _jsx("div", { ref: setFloatingRef, className: e("card"), style: { width }, role: "dialog", "aria-modal": "true", "aria-label": accessibilityConfig?.ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": hasBody ? descriptionId : undefined, ...getFloatingProps(), onKeyDown: onCardKeyDown, children: _jsx(UiDialogContext.Provider, { value: dialogContextValue, children: children }) }) }) }) }) }));
}
