/*
 * Copyright 2024 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { __assign } from "tslib";
import classNames from "classnames";
import * as React from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { useUID } from "react-uid";
import { Classes, mergeRefs } from "../../common";
import { OVERLAY_CHILD_REF_AND_REFS_MUTEX, OVERLAY_CHILD_REQUIRES_KEY, OVERLAY_WITH_MULTIPLE_CHILDREN_REQUIRES_CHILD_REFS, } from "../../common/errors";
import { DISPLAYNAME_PREFIX } from "../../common/props";
import { ensureElement, getActiveElement, getRef, isEmptyString, isNodeEnv, isReactElement, setRef, } from "../../common/utils";
import { hasDOMEnvironment } from "../../common/utils/domUtils";
import { useOverlayStack } from "../../hooks/overlays/useOverlayStack";
import { usePrevious } from "../../hooks/usePrevious";
import { getKeyboardFocusableElements } from "../overlay/overlayUtils";
import { Portal } from "../portal/portal";
export var OVERLAY2_DEFAULT_PROPS = {
    autoFocus: true,
    backdropProps: {},
    canEscapeKeyClose: true,
    canOutsideClickClose: true,
    enforceFocus: true,
    hasBackdrop: true,
    isOpen: false,
    lazy: hasDOMEnvironment(),
    shouldReturnFocusOnClose: true,
    transitionDuration: 300,
    transitionName: Classes.OVERLAY,
    usePortal: true,
};
/**
 * Overlay2 component.
 *
 * @see https://blueprintjs.com/docs/#core/components/overlay2
 */
export var Overlay2 = React.forwardRef(function (props, forwardedRef) {
    var _a;
    var _b, _c;
    var autoFocus = props.autoFocus, backdropClassName = props.backdropClassName, backdropProps = props.backdropProps, canEscapeKeyClose = props.canEscapeKeyClose, canOutsideClickClose = props.canOutsideClickClose, childRef = props.childRef, childRefs = props.childRefs, children = props.children, className = props.className, enforceFocus = props.enforceFocus, hasBackdrop = props.hasBackdrop, isOpen = props.isOpen, lazy = props.lazy, onClose = props.onClose, onClosed = props.onClosed, onClosing = props.onClosing, onOpened = props.onOpened, onOpening = props.onOpening, portalClassName = props.portalClassName, portalContainer = props.portalContainer, shouldReturnFocusOnClose = props.shouldReturnFocusOnClose, transitionDuration = props.transitionDuration, transitionName = props.transitionName, usePortal = props.usePortal;
    useOverlay2Validation(props);
    var _d = useOverlayStack(), closeOverlay = _d.closeOverlay, getLastOpened = _d.getLastOpened, getThisOverlayAndDescendants = _d.getThisOverlayAndDescendants, openOverlay = _d.openOverlay;
    var _e = React.useState(false), isAutoFocusing = _e[0], setIsAutoFocusing = _e[1];
    var _f = React.useState(false), hasEverOpened = _f[0], setHasEverOpened = _f[1];
    var lastActiveElementBeforeOpened = React.useRef(null);
    /** Ref for container element, containing all children and the backdrop */
    var containerElement = React.useRef(null);
    /** Ref for backdrop element */
    var backdropElement = React.useRef(null);
    /* An empty, keyboard-focusable div at the beginning of the Overlay content */
    var startFocusTrapElement = React.useRef(null);
    /* An empty, keyboard-focusable div at the end of the Overlay content */
    var endFocusTrapElement = React.useRef(null);
    /**
     * Locally-generated DOM ref for a singleton child element.
     * This is only used iff the user does not specify the `childRef` or `childRefs` props.
     */
    var localChildRef = React.useRef(null);
    var bringFocusInsideOverlay = React.useCallback(function () {
        // always delay focus manipulation to just before repaint to prevent scroll jumping
        return requestAnimationFrame(function () {
            var _a;
            // container element may be undefined between component mounting and Portal rendering
            // activeElement may be undefined in some rare cases in IE
            var container = getRef(containerElement);
            var activeElement = getActiveElement(container);
            if (container == null || activeElement == null) {
                return;
            }
            // Overlay2 is guaranteed to be mounted here
            var isFocusOutsideModal = !container.contains(activeElement);
            if (isFocusOutsideModal) {
                (_a = getRef(startFocusTrapElement)) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
                setIsAutoFocusing(false);
            }
        });
    }, []);
    /** Unique ID for this overlay in the global stack */
    var id = useOverlay2ID();
    // N.B. use `null` here and not simply `undefined` because `useImperativeHandle` will set `null` on unmount,
    // and we need the following code to be resilient to that value.
    var instance = React.useRef(null);
    /**
     * When multiple `enforceFocus` Overlays are open, this event handler is only active for the most
     * recently opened one to avoid Overlays competing with each other for focus.
     */
    var handleDocumentFocus = React.useCallback(function (e) {
        // get the actual target even in the Shadow DOM
        // see https://github.com/palantir/blueprint/issues/4220
        var eventTarget = e.composed ? e.composedPath()[0] : e.target;
        var container = getRef(containerElement);
        if (container != null && eventTarget instanceof Node && !container.contains(eventTarget)) {
            // prevent default focus behavior (sometimes auto-scrolls the page)
            e.preventDefault();
            e.stopImmediatePropagation();
            bringFocusInsideOverlay();
        }
    }, [bringFocusInsideOverlay]);
    // N.B. this listener is only kept attached when `isOpen={true}` and `canOutsideClickClose={true}`
    var handleDocumentMousedown = React.useCallback(function (e) {
        // get the actual target even in the Shadow DOM
        // see https://github.com/palantir/blueprint/issues/4220
        var eventTarget = (e.composed ? e.composedPath()[0] : e.target);
        var thisOverlayAndDescendants = getThisOverlayAndDescendants(id);
        var isClickInThisOverlayOrDescendant = thisOverlayAndDescendants.some(function (_a) {
            var containerRef = _a.containerElement;
            // `elem` is the container of backdrop & content, so clicking directly on that container
            // should not count as being "inside" the overlay.
            var elem = getRef(containerRef);
            return (elem === null || elem === void 0 ? void 0 : elem.contains(eventTarget)) && !elem.isSameNode(eventTarget);
        });
        if (!isClickInThisOverlayOrDescendant) {
            // casting to any because this is a native event
            onClose === null || onClose === void 0 ? void 0 : onClose(e);
        }
    }, [getThisOverlayAndDescendants, id, onClose]);
    // send this instance's imperative handle to the the forwarded ref as well as our local ref
    var ref = React.useMemo(function () { return mergeRefs(forwardedRef, instance); }, [forwardedRef]);
    React.useImperativeHandle(ref, function () { return ({
        bringFocusInsideOverlay: bringFocusInsideOverlay,
        containerElement: containerElement,
        handleDocumentFocus: handleDocumentFocus,
        handleDocumentMousedown: handleDocumentMousedown,
        id: id,
        props: {
            autoFocus: autoFocus,
            enforceFocus: enforceFocus,
            hasBackdrop: hasBackdrop,
            usePortal: usePortal,
        },
    }); }, [
        autoFocus,
        bringFocusInsideOverlay,
        enforceFocus,
        handleDocumentFocus,
        handleDocumentMousedown,
        hasBackdrop,
        id,
        usePortal,
    ]);
    var handleContainerKeyDown = React.useCallback(function (e) {
        if (e.key === "Escape" && canEscapeKeyClose) {
            onClose === null || onClose === void 0 ? void 0 : onClose(e);
            // prevent other overlays from closing
            e.stopPropagation();
            // prevent browser-specific escape key behavior (Safari exits fullscreen)
            e.preventDefault();
        }
    }, [canEscapeKeyClose, onClose]);
    var overlayWillOpen = React.useCallback(function () {
        if (instance.current == null) {
            return;
        }
        var lastOpenedOverlay = getLastOpened();
        if ((lastOpenedOverlay === null || lastOpenedOverlay === void 0 ? void 0 : lastOpenedOverlay.handleDocumentFocus) !== undefined) {
            document.removeEventListener("focus", lastOpenedOverlay.handleDocumentFocus, /* useCapture */ true);
        }
        openOverlay(instance.current);
        if (autoFocus) {
            setIsAutoFocusing(true);
            bringFocusInsideOverlay();
        }
        setRef(lastActiveElementBeforeOpened, getActiveElement(getRef(containerElement)));
    }, [autoFocus, bringFocusInsideOverlay, getLastOpened, openOverlay]);
    var overlayWillClose = React.useCallback(function () {
        var _a;
        document.removeEventListener("focus", handleDocumentFocus, /* useCapture */ true);
        document.removeEventListener("mousedown", handleDocumentMousedown);
        // N.B. `instance.current` may be null at this point if we are cleaning up an open overlay during the unmount phase
        // (this is common, for example, with context menu's singleton `showContextMenu` / `hideContextMenu` imperative APIs).
        closeOverlay(id);
        var lastOpenedOverlay = getLastOpened();
        if (lastOpenedOverlay !== undefined) {
            // Only bring focus back to last overlay if it had autoFocus _and_ enforceFocus enabled.
            // If `autoFocus={false}`, it's likely that the overlay never received focus in the first place,
            // so it would be surprising for us to send it there. See https://github.com/palantir/blueprint/issues/4921
            if (lastOpenedOverlay.props.autoFocus && lastOpenedOverlay.props.enforceFocus) {
                (_a = lastOpenedOverlay.bringFocusInsideOverlay) === null || _a === void 0 ? void 0 : _a.call(lastOpenedOverlay);
                if (lastOpenedOverlay.handleDocumentFocus !== undefined) {
                    document.addEventListener("focus", lastOpenedOverlay.handleDocumentFocus, /* useCapture */ true);
                }
            }
        }
    }, [closeOverlay, getLastOpened, handleDocumentFocus, handleDocumentMousedown, id]);
    var prevIsOpen = (_b = usePrevious(isOpen)) !== null && _b !== void 0 ? _b : false;
    React.useEffect(function () {
        if (isOpen) {
            setHasEverOpened(true);
        }
        if (!prevIsOpen && isOpen) {
            // just opened
            overlayWillOpen();
        }
        if (prevIsOpen && !isOpen) {
            // just closed
            overlayWillClose();
        }
    }, [isOpen, overlayWillOpen, overlayWillClose, prevIsOpen]);
    // Important: clean up old document-level event listeners if their memoized values change (this is rare, but
    // may happen, for example, if a user forgets to use `React.useCallback` in the `props.onClose` value).
    // Otherwise, we will lose the reference to those values and create a memory leak since we won't be able
    // to successfully detach them inside overlayWillClose.
    React.useEffect(function () {
        if (!isOpen || !(canOutsideClickClose && !hasBackdrop)) {
            return;
        }
        document.addEventListener("mousedown", handleDocumentMousedown);
        return function () {
            document.removeEventListener("mousedown", handleDocumentMousedown);
        };
    }, [handleDocumentMousedown, isOpen, canOutsideClickClose, hasBackdrop]);
    React.useEffect(function () {
        if (!isOpen || !enforceFocus) {
            return;
        }
        // Focus events do not bubble, but setting useCapture allows us to listen in and execute
        // our handler before all others
        document.addEventListener("focus", handleDocumentFocus, /* useCapture */ true);
        return function () {
            document.removeEventListener("focus", handleDocumentFocus, /* useCapture */ true);
        };
    }, [handleDocumentFocus, enforceFocus, isOpen]);
    var overlayWillCloseRef = React.useRef(overlayWillClose);
    overlayWillCloseRef.current = overlayWillClose;
    React.useEffect(function () {
        // run cleanup code once on unmount, ensuring we call the most recent overlayWillClose callback
        // by storing in a ref and keeping up to date
        return function () {
            overlayWillCloseRef.current();
        };
    }, []);
    var handleTransitionExited = React.useCallback(function (node) {
        var lastActiveElement = getRef(lastActiveElementBeforeOpened);
        if (shouldReturnFocusOnClose && lastActiveElement instanceof HTMLElement) {
            lastActiveElement.focus();
        }
        onClosed === null || onClosed === void 0 ? void 0 : onClosed(node);
    }, [onClosed, shouldReturnFocusOnClose]);
    // N.B. CSSTransition requires this callback to be defined, even if it's unused.
    var handleTransitionAddEnd = React.useCallback(function () {
        // no-op
    }, []);
    /**
     * Gets the relevant DOM ref for a child element using the `childRef` or `childRefs` props (if possible).
     * This ref is necessary for `CSSTransition` to work in React 18 without relying on `ReactDOM.findDOMNode`.
     *
     * Returns `undefined` if the user did not specify either of those props. In those cases, we use the ref we
     * have locally generated and expect that the user _did not_ specify their own `ref` on the child element
     * (it will get clobbered / overriden).
     *
     * @see https://reactcommunity.org/react-transition-group/css-transition
     */
    var getUserChildRef = React.useCallback(function (child) {
        if (childRef != null) {
            return childRef;
        }
        else if (childRefs != null) {
            var key = child.key;
            if (key == null) {
                if (!isNodeEnv("production")) {
                    console.error(OVERLAY_CHILD_REQUIRES_KEY);
                }
                return undefined;
            }
            return childRefs[key];
        }
        return undefined;
    }, [childRef, childRefs]);
    var maybeRenderChild = React.useCallback(function (child) {
        if (child == null || isEmptyString(child)) {
            return null;
        }
        // decorate the child with a few injected props
        var userChildRef = getUserChildRef(child);
        var childProps = isReactElement(child) ? child.props : {};
        // if the child is a string, number, or fragment, it will be wrapped in a <span> element
        var decoratedChild = ensureElement(child, "span", {
            className: classNames(childProps.className, Classes.OVERLAY_CONTENT),
            // IMPORTANT: only inject our ref if the user didn't specify childRef or childRefs already. Otherwise,
            // we risk clobbering the user's ref (which we cannot inspect here while cloning/decorating the child).
            ref: userChildRef === undefined ? localChildRef : undefined,
            tabIndex: enforceFocus || autoFocus ? 0 : undefined,
        });
        var resolvedChildRef = userChildRef !== null && userChildRef !== void 0 ? userChildRef : localChildRef;
        return (React.createElement(CSSTransition, { addEndListener: handleTransitionAddEnd, classNames: transitionName, 
            // HACKHACK: CSSTransition types are slightly incompatible with React types here.
            // React prefers `| null` but not `| undefined` for the ref value, while
            // CSSTransition _demands_ that `| undefined` be part of the element type.
            nodeRef: resolvedChildRef, onEntered: getLifecycleCallbackWithChildRef(onOpened, resolvedChildRef), onEntering: getLifecycleCallbackWithChildRef(onOpening, resolvedChildRef), onExited: getLifecycleCallbackWithChildRef(handleTransitionExited, resolvedChildRef), onExiting: getLifecycleCallbackWithChildRef(onClosing, resolvedChildRef), timeout: transitionDuration }, decoratedChild));
    }, [
        autoFocus,
        enforceFocus,
        getUserChildRef,
        handleTransitionAddEnd,
        handleTransitionExited,
        onClosing,
        onOpened,
        onOpening,
        transitionDuration,
        transitionName,
    ]);
    var handleBackdropMouseDown = React.useCallback(function (e) {
        var _a;
        if (canOutsideClickClose) {
            onClose === null || onClose === void 0 ? void 0 : onClose(e);
        }
        if (enforceFocus) {
            bringFocusInsideOverlay();
        }
        (_a = backdropProps === null || backdropProps === void 0 ? void 0 : backdropProps.onMouseDown) === null || _a === void 0 ? void 0 : _a.call(backdropProps, e);
    }, [backdropProps, bringFocusInsideOverlay, canOutsideClickClose, enforceFocus, onClose]);
    var renderDummyElement = React.useCallback(function (key, dummyElementProps) { return (React.createElement(CSSTransition, { addEndListener: handleTransitionAddEnd, classNames: transitionName, key: key, nodeRef: dummyElementProps.ref, timeout: transitionDuration, unmountOnExit: true },
        React.createElement("div", __assign({ tabIndex: 0 }, dummyElementProps)))); }, [handleTransitionAddEnd, transitionDuration, transitionName]);
    /**
     * Ensures repeatedly pressing shift+tab keeps focus inside the Overlay. Moves focus to
     * the `endFocusTrapElement` or the first keyboard-focusable element in the Overlay (excluding
     * the `startFocusTrapElement`), depending on whether the element losing focus is inside the
     * Overlay.
     */
    var handleStartFocusTrapElementFocus = React.useCallback(function (e) {
        if (!enforceFocus || isAutoFocusing) {
            return;
        }
        // e.relatedTarget will not be defined if this was a programmatic focus event, as is the
        // case when we call this.bringFocusInsideOverlay() after a user clicked on the backdrop.
        // Otherwise, we're handling a user interaction, and we should wrap around to the last
        // element in this transition group.
        var container = getRef(containerElement);
        var endFocusTrap = getRef(endFocusTrapElement);
        if (e.relatedTarget != null &&
            (container === null || container === void 0 ? void 0 : container.contains(e.relatedTarget)) &&
            e.relatedTarget !== endFocusTrap) {
            endFocusTrap === null || endFocusTrap === void 0 ? void 0 : endFocusTrap.focus({ preventScroll: true });
        }
    }, [enforceFocus, isAutoFocusing]);
    /**
     * Wrap around to the end of the dialog if `enforceFocus` is enabled.
     */
    var handleStartFocusTrapElementKeyDown = React.useCallback(function (e) {
        var _a;
        if (!enforceFocus) {
            return;
        }
        if (e.shiftKey && e.key === "Tab") {
            var lastFocusableElement = getKeyboardFocusableElements(containerElement).pop();
            if (lastFocusableElement != null) {
                lastFocusableElement.focus();
            }
            else {
                (_a = getRef(endFocusTrapElement)) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
            }
        }
    }, [enforceFocus]);
    /**
     * Ensures repeatedly pressing tab keeps focus inside the Overlay. Moves focus to the
     * `startFocusTrapElement` or the last keyboard-focusable element in the Overlay (excluding the
     * `startFocusTrapElement`), depending on whether the element losing focus is inside the
     * Overlay.
     */
    var handleEndFocusTrapElementFocus = React.useCallback(function (e) {
        var _a;
        // No need for this.props.enforceFocus check here because this element is only rendered
        // when that prop is true.
        // During user interactions, e.relatedTarget will be defined, and we should wrap around to the
        // "start focus trap" element.
        // Otherwise, we're handling a programmatic focus event, which can only happen after a user
        // presses shift+tab from the first focusable element in the overlay.
        var startFocusTrap = getRef(startFocusTrapElement);
        if (e.relatedTarget != null &&
            ((_a = getRef(containerElement)) === null || _a === void 0 ? void 0 : _a.contains(e.relatedTarget)) &&
            e.relatedTarget !== startFocusTrap) {
            var firstFocusableElement = getKeyboardFocusableElements(containerElement).shift();
            // ensure we don't re-focus an already active element by comparing against e.relatedTarget
            if (!isAutoFocusing && firstFocusableElement != null && firstFocusableElement !== e.relatedTarget) {
                firstFocusableElement.focus();
            }
            else {
                startFocusTrap === null || startFocusTrap === void 0 ? void 0 : startFocusTrap.focus({ preventScroll: true });
            }
        }
        else {
            var lastFocusableElement = getKeyboardFocusableElements(containerElement).pop();
            if (lastFocusableElement != null) {
                lastFocusableElement.focus();
            }
            else {
                // Keeps focus within Overlay even if there are no keyboard-focusable children
                startFocusTrap === null || startFocusTrap === void 0 ? void 0 : startFocusTrap.focus({ preventScroll: true });
            }
        }
    }, [isAutoFocusing]);
    var maybeBackdrop = React.useMemo(function () {
        return hasBackdrop && isOpen ? (React.createElement(CSSTransition, { classNames: transitionName, key: "__backdrop", nodeRef: backdropElement, timeout: transitionDuration, addEndListener: handleTransitionAddEnd },
            React.createElement("div", __assign({}, backdropProps, { className: classNames(Classes.OVERLAY_BACKDROP, backdropClassName, backdropProps === null || backdropProps === void 0 ? void 0 : backdropProps.className), onMouseDown: handleBackdropMouseDown, ref: backdropElement })))) : null;
    }, [
        backdropClassName,
        backdropProps,
        handleBackdropMouseDown,
        handleTransitionAddEnd,
        hasBackdrop,
        isOpen,
        transitionDuration,
        transitionName,
    ]);
    // no reason to render anything at all if we're being truly lazy
    if (lazy && !hasEverOpened) {
        return null;
    }
    // TransitionGroup types require single array of children; does not support nested arrays.
    // So we must collapse backdrop and children into one array, and every item must be wrapped in a
    // Transition element (no ReactText allowed).
    var childrenWithTransitions = isOpen ? (_c = React.Children.map(children, maybeRenderChild)) !== null && _c !== void 0 ? _c : [] : [];
    // const maybeBackdrop = maybeRenderBackdrop();
    if (maybeBackdrop !== null) {
        childrenWithTransitions.unshift(maybeBackdrop);
    }
    if (isOpen && (autoFocus || enforceFocus) && childrenWithTransitions.length > 0) {
        childrenWithTransitions.unshift(renderDummyElement("__start", {
            className: Classes.OVERLAY_START_FOCUS_TRAP,
            onFocus: handleStartFocusTrapElementFocus,
            onKeyDown: handleStartFocusTrapElementKeyDown,
            ref: startFocusTrapElement,
        }));
        if (enforceFocus) {
            childrenWithTransitions.push(renderDummyElement("__end", {
                className: Classes.OVERLAY_END_FOCUS_TRAP,
                onFocus: handleEndFocusTrapElementFocus,
                ref: endFocusTrapElement,
            }));
        }
    }
    var transitionGroup = (React.createElement("div", { "aria-live": "polite", className: classNames(Classes.OVERLAY, (_a = {},
            _a[Classes.OVERLAY_OPEN] = isOpen,
            _a[Classes.OVERLAY_INLINE] = !usePortal,
            _a), className), onKeyDown: handleContainerKeyDown, ref: containerElement },
        React.createElement(TransitionGroup, { appear: true, component: null }, childrenWithTransitions)));
    if (usePortal) {
        return (React.createElement(Portal, { className: portalClassName, container: portalContainer }, transitionGroup));
    }
    else {
        return transitionGroup;
    }
});
// eslint-disable-next-line @typescript-eslint/no-deprecated
Overlay2.defaultProps = OVERLAY2_DEFAULT_PROPS;
Overlay2.displayName = "".concat(DISPLAYNAME_PREFIX, ".Overlay2");
function useOverlay2Validation(_a) {
    var childRef = _a.childRef, childRefs = _a.childRefs, children = _a.children;
    var numChildren = React.Children.count(children);
    React.useEffect(function () {
        if (isNodeEnv("production")) {
            return;
        }
        if (childRef != null && childRefs != null) {
            console.error(OVERLAY_CHILD_REF_AND_REFS_MUTEX);
        }
        if (numChildren > 1 && childRefs == null) {
            console.error(OVERLAY_WITH_MULTIPLE_CHILDREN_REQUIRES_CHILD_REFS);
        }
    }, [childRef, childRefs, numChildren]);
}
/**
 * Generates a unique ID for a given Overlay which persists across the component's lifecycle.
 */
function useOverlay2ID() {
    // TODO: migrate to React.useId() in React 18
    var id = useUID();
    return "".concat(Overlay2.displayName, "-").concat(id);
}
// N.B. the `onExiting` callback is not provided with the `node` argument as suggested in CSSTransition types since
// we are using the `nodeRef` prop, so we must inject it dynamically.
function getLifecycleCallbackWithChildRef(callback, childRef) {
    return function () {
        if ((childRef === null || childRef === void 0 ? void 0 : childRef.current) != null) {
            callback === null || callback === void 0 ? void 0 : callback(childRef.current);
        }
    };
}
//# sourceMappingURL=overlay2.js.map