"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DismissableLayer = void 0;
const react_1 = __importStar(require("react"));
const helpers_1 = require("../../helpers");
const hooks_1 = require("../../hooks");
const Slot_1 = require("../slot/Slot");
const sort_layers_1 = require("./util/sort-layers");
const useEscapeKeydown_1 = require("./util/useEscapeKeydown");
const useFocusOutside_1 = require("./util/useFocusOutside");
const usePointerDownOutside_1 = require("./util/usePointerDownOutside");
const usePointerUpOutside_1 = require("./util/usePointerUpOutside");
const BranchedLayerContext = react_1.default.createContext(null);
/* ------------------------ DismissableLayerInternal ------------------------ */
const CONTEXT_UPDATE_EVENT = "dismissableLayer.update";
let originalBodyPointerEvents;
const DismissableLayerContext = react_1.default.createContext({
    layers: new Set(),
    branchedLayers: new Map(),
    layersWithOutsidePointerEventsDisabled: new Set(),
});
const DismissableLayer = (0, react_1.forwardRef)((_a, forwardedRef) => {
    var { children, disableOutsidePointerEvents, onDismiss, onInteractOutside, onEscapeKeyDown, onFocusOutside, onPointerDownOutside, onPointerUpOutside, enablePointerUpOutside = false, safeZone, asChild, enabled = true } = _a, restProps = __rest(_a, ["children", "disableOutsidePointerEvents", "onDismiss", "onInteractOutside", "onEscapeKeyDown", "onFocusOutside", "onPointerDownOutside", "onPointerUpOutside", "enablePointerUpOutside", "safeZone", "asChild", "enabled"]);
    const context = (0, react_1.useContext)(DismissableLayerContext);
    const triggerPointerDownRef = (0, react_1.useRef)(false);
    const dismissedWithPointerRef = (0, react_1.useRef)(false);
    const timeout = (0, hooks_1.useTimeout)();
    const [, forceRerender] = (0, react_1.useState)({});
    const [node, setNode] = react_1.default.useState(null);
    const mergedRefs = (0, hooks_1.useMergeRefs)(forwardedRef, setNode);
    const ownerDoc = (0, helpers_1.ownerDocument)(node);
    /* Layer handling */
    const layers = (0, sort_layers_1.getSortedLayers)(context.layers, context.branchedLayers);
    const highestLayerWithOutsidePointerEventsDisabledIndex = findHighestLayerIndex(layers, context.layersWithOutsidePointerEventsDisabled);
    const index = node ? layers.indexOf(node) : -1;
    const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0;
    const shouldEnablePointerEvents = highestLayerWithOutsidePointerEventsDisabledIndex === -1 ||
        index >= highestLayerWithOutsidePointerEventsDisabledIndex;
    /**
     * We want to prevent the Layer from closing when the trigger/anchor element or its child elements are interacted with.
     * To achieve this, we check if the event target is the trigger/anchor or a child. If it is, we prevent default event behavior.
     */
    function handleOutsideEvent(event) {
        if (!(safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor)) {
            return;
        }
        const eventType = event.detail.originalEvent.type;
        const target = event.target;
        const targetIsAnchor = safeZone.anchor.contains(target) || target === safeZone.anchor;
        if (targetIsAnchor) {
            event.preventDefault();
        }
        /**
         * If the target is inside a custom element, event.target will be that element on pointerdown.
         * Therefore, checking anchor.contains(target) etc. won't work in that case.
         */
        if (eventType === "pointerdown" &&
            triggerPointerDownRef.current === true) {
            event.preventDefault();
        }
        triggerPointerDownRef.current = false;
    }
    const pointerDownOutside = (0, usePointerDownOutside_1.usePointerDownOutside)((event) => {
        if (!shouldEnablePointerEvents) {
            return;
        }
        dismissedWithPointerRef.current = true;
        /**
         * We reset dismissedWithPointerRef on the next tick.
         * This allows pointerDown to run its effect, without re-running them here right after.
         */
        timeout.start(0, () => {
            dismissedWithPointerRef.current = false;
        });
        /**
         * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault.
         */
        onPointerDownOutside === null || onPointerDownOutside === void 0 ? void 0 : onPointerDownOutside(event);
        onInteractOutside === null || onInteractOutside === void 0 ? void 0 : onInteractOutside(event);
        /**
         * Add safeZone to prevent closing when interacting with trigger/anchor or its children.
         */
        safeZone && handleOutsideEvent(event);
        if (!event.defaultPrevented && onDismiss) {
            onDismiss(event);
        }
    }, ownerDoc, enabled);
    const pointerUpOutside = (0, usePointerUpOutside_1.usePointerUpOutside)((event) => {
        if (!shouldEnablePointerEvents || !enablePointerUpOutside) {
            return;
        }
        /**
         * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
         */
        onPointerUpOutside === null || onPointerUpOutside === void 0 ? void 0 : onPointerUpOutside(event);
        onInteractOutside === null || onInteractOutside === void 0 ? void 0 : onInteractOutside(event);
        /**
         * Add safeZone to prevent closing when interacting with trigger/anchor or its children.
         */
        safeZone && handleOutsideEvent(event);
        /**
         * Both `onPointerUpOutside`, `onInteractOutside` and `handleOutsideEvent`
         * are able to preventDefault the event, thus stopping call for `onDismiss`.
         */
        if (!event.defaultPrevented && onDismiss) {
            onDismiss(event);
        }
    }, ownerDoc, enabled);
    const focusOutside = (0, useFocusOutside_1.useFocusOutside)((event) => {
        if (dismissedWithPointerRef.current) {
            return;
        }
        /**
         * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault.
         */
        onFocusOutside === null || onFocusOutside === void 0 ? void 0 : onFocusOutside(event);
        onInteractOutside === null || onInteractOutside === void 0 ? void 0 : onInteractOutside(event);
        /**
         * Add safeZone to prevent closing when interacting with trigger/anchor or its children.
         */
        safeZone && handleOutsideEvent(event);
        if (!event.defaultPrevented && onDismiss) {
            onDismiss(event);
        }
    }, ownerDoc, enabled);
    (0, useEscapeKeydown_1.useEscapeKeydown)((event) => {
        /**
         * The deepest nested element will always be last in the descendants list.
         * This allows us to only close the highest layer when pressing escape.
         */
        const isHighestLayer = index === context.layers.size - 1;
        if (!isHighestLayer) {
            return;
        }
        /**
         * We call this before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
         */
        onEscapeKeyDown === null || onEscapeKeyDown === void 0 ? void 0 : onEscapeKeyDown(event);
        /**
         * `onEscapeKeyDown` is able to preventDefault the event, thus stopping call for `onDismiss`.
         * We want to `preventDefault` the escape-event to avoid sideeffect from other elements on screen
         */
        if (!event.defaultPrevented && onDismiss) {
            onDismiss(event);
            /**
             * Preventing after dismiss allows us to check if user prevents default on the escape event
             * to avoid side effects on other elements after this layer has been dismissed.
             */
            event.preventDefault();
        }
    }, ownerDoc, enabled);
    (0, react_1.useEffect)(() => {
        if (!(safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor)) {
            return;
        }
        const handlePointerDown = () => {
            triggerPointerDownRef.current = true;
        };
        const handlePointerEnd = () => {
            triggerPointerDownRef.current = false;
        };
        const anchor = safeZone.anchor;
        anchor.addEventListener("pointerdown", handlePointerDown, {
            capture: true,
        });
        anchor.addEventListener("pointerup", handlePointerEnd);
        anchor.addEventListener("pointerleave", handlePointerEnd);
        anchor.addEventListener("pointercancel", handlePointerEnd);
        return () => {
            anchor.removeEventListener("pointerdown", handlePointerDown, {
                capture: true,
            });
            anchor.removeEventListener("pointerup", handlePointerEnd);
            anchor.removeEventListener("pointerleave", handlePointerEnd);
            anchor.removeEventListener("pointercancel", handlePointerEnd);
        };
    }, [safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor]);
    /**
     * Handles registering `layers` and `layersWithOutsidePointerEventsDisabled`.
     */
    (0, react_1.useEffect)(() => {
        if (!node || !enabled) {
            return;
        }
        if (disableOutsidePointerEvents) {
            if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
                originalBodyPointerEvents = ownerDoc.body.style.pointerEvents;
                ownerDoc.body.style.pointerEvents = "none";
            }
            context.layersWithOutsidePointerEventsDisabled.add(node);
        }
        context.layers.add(node);
        dispatchUpdate();
        return () => {
            if (disableOutsidePointerEvents &&
                context.layersWithOutsidePointerEventsDisabled.size === 1) {
                ownerDoc.body.style.pointerEvents = originalBodyPointerEvents;
            }
        };
    }, [node, enabled, disableOutsidePointerEvents, context, ownerDoc]);
    /**
     * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect
     * because a change to `disableOutsidePointerEvents` would remove this layer from the stack
     * and add it to the end again so the layering order wouldn't be creation order.
     * We only want them to be removed from context stacks when unmounted.
     *
     * We depend on `enabled` to clean up when the layer is disabled.
     */
    // biome-ignore lint/correctness/useExhaustiveDependencies: We need to clean up after enabled changes.
    (0, react_1.useEffect)(() => {
        return () => {
            if (!node) {
                return;
            }
            if (context.layers.has(node) ||
                context.layersWithOutsidePointerEventsDisabled.has(node)) {
                context.layers.delete(node);
                context.layersWithOutsidePointerEventsDisabled.delete(node);
                dispatchUpdate();
            }
        };
    }, [node, context, enabled]);
    const parentBranchedLayer = (0, react_1.useContext)(BranchedLayerContext);
    /**
     * Handles registering and unregistering branched (nested) layers.
     * When this layer has a parent, we register it as a child of the parent.
     */
    (0, react_1.useEffect)(() => {
        if (!node ||
            !enabled ||
            !parentBranchedLayer ||
            node === parentBranchedLayer) {
            return;
        }
        if (!context.branchedLayers.has(parentBranchedLayer)) {
            context.branchedLayers.set(parentBranchedLayer, new Set());
        }
        const branchedChildren = context.branchedLayers.get(parentBranchedLayer);
        branchedChildren.add(node);
        dispatchUpdate();
        return () => {
            // Remove this node from the parent's children
            branchedChildren.delete(node);
            // If the parent has no more children, remove the parent from branchedLayers
            if (branchedChildren.size === 0) {
                context.branchedLayers.delete(parentBranchedLayer);
            }
            dispatchUpdate();
        };
    }, [node, enabled, parentBranchedLayer, context]);
    /**
     * Synchronizes layer state across all mounted `DismissableLayer` instances.
     * All layers re-render on every context change to recalculate their position and pointer-events.
     */
    (0, react_1.useEffect)(() => {
        const handleUpdate = () => forceRerender({});
        document.addEventListener(CONTEXT_UPDATE_EVENT, handleUpdate);
        return () => document.removeEventListener(CONTEXT_UPDATE_EVENT, handleUpdate);
    }, []);
    const Comp = asChild ? Slot_1.Slot : "div";
    return (react_1.default.createElement(BranchedLayerContext.Provider, { value: node },
        react_1.default.createElement(Comp, Object.assign({}, restProps, { ref: mergedRefs, style: Object.assign({ pointerEvents: isBodyPointerEventsDisabled
                    ? shouldEnablePointerEvents
                        ? "auto"
                        : "none"
                    : undefined }, restProps.style), onFocusCapture: (0, helpers_1.composeEventHandlers)(restProps.onFocusCapture, focusOutside.onFocusCapture), onBlurCapture: (0, helpers_1.composeEventHandlers)(restProps.onBlurCapture, focusOutside.onBlurCapture), onPointerDownCapture: (0, helpers_1.composeEventHandlers)(restProps.onPointerDownCapture, () => {
                pointerDownOutside.onPointerDownCapture();
                pointerUpOutside.onPointerDownCapture();
                focusOutside.onPointerDownCapture();
            }), onPointerUpCapture: (0, helpers_1.composeEventHandlers)(restProps.onPointerUpCapture, pointerUpOutside.onPointerUpCapture) }), children)));
});
exports.DismissableLayer = DismissableLayer;
/**
 * Dispatches a custom event to inform all `DismissableLayer` components to update.
 */
function dispatchUpdate() {
    const event = new CustomEvent(CONTEXT_UPDATE_EVENT);
    document.dispatchEvent(event);
}
/**
 * Returns the index of the last layer that is found in the given subset.
 * Returns -1 if no layers are found.
 */
function findHighestLayerIndex(orderedLayers, layersWithOutsidePointerEventsDisabled) {
    for (let i = orderedLayers.length - 1; i >= 0; i -= 1) {
        if (layersWithOutsidePointerEventsDisabled.has(orderedLayers[i])) {
            return i;
        }
    }
    return -1;
}
//# sourceMappingURL=DismissableLayer.js.map