"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.FocusBoundary = void 0;
const react_1 = __importStar(require("react"));
const utils_external_1 = require("../../../utils-external");
const helpers_1 = require("../../helpers");
const focus_1 = require("../../helpers/focus");
const hooks_1 = require("../../hooks");
const useValueAsRef_1 = require("../../hooks/useValueAsRef");
const Slot_1 = require("../slot/Slot");
const FocusBoundary = (0, react_1.forwardRef)((_a, forwardedRef) => {
    var { loop = false, trapped = false, initialFocus = true, returnFocus = true, modal = false } = _a, restProps = __rest(_a, ["loop", "trapped", "initialFocus", "returnFocus", "modal"]);
    const initialFocusRef = (0, useValueAsRef_1.useValueAsRef)(initialFocus);
    const returnFocusRef = (0, useValueAsRef_1.useValueAsRef)(returnFocus);
    const lastFocusedElementRef = (0, react_1.useRef)(null);
    const [container, setContainer] = (0, react_1.useState)(null);
    const mergedRefs = (0, hooks_1.useMergeRefs)(forwardedRef, setContainer);
    const focusBoundary = (0, react_1.useRef)({
        paused: false,
        pause() {
            this.paused = true;
        },
        resume() {
            this.paused = false;
        },
    }).current;
    /* Handles trapped state */
    (0, react_1.useEffect)(() => {
        if (!trapped || !container) {
            return;
        }
        function handleFocusIn(event) {
            if (focusBoundary.paused || container === null) {
                return;
            }
            const target = event.target;
            if (container.contains(target)) {
                lastFocusedElementRef.current = target;
            }
            else {
                (0, focus_1.focusElement)(lastFocusedElementRef.current, { select: true });
            }
        }
        function handleFocusOut(event) {
            if (focusBoundary.paused || container === null) {
                return;
            }
            const relatedTarget = event.relatedTarget;
            /*
             * `focusout` event with a `null` `relatedTarget` will happen in a few known cases:
             * 1. When the user switches app/tabs/windows/the browser itself loses focus.
             * 2. In Google Chrome, when the focused element is removed from the DOM.
             * 3. When clicking on an element that cannot receive focus.
             *
             * We let the browser do its thing here because:
             * 1. The browser already keeps a memory of what's focused for when the page gets refocused.
             * 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it
             * throws the CPU to 100%, so we avoid doing anything for this reason here too.
             */
            if (relatedTarget === null) {
                return;
            }
            /*
             * If the focus has moved to an element outside the container, we move focus to the last valid focused element inside.
             * This makes sure to "trap" focus inside the container.
             * We handle focus on focusout instead of focusin to avoid elements recieving focusin events
             * when they are not supposed to (like when clicking on elements outside the container
             */
            if (!container.contains(relatedTarget)) {
                (0, focus_1.focusElement)(lastFocusedElementRef.current, { select: true });
            }
        }
        /**
         * When the currently focused element is removed from the DOM, browsers move focus
         * to the document.body. In this case, we move focus to the container
         * to keep focus trapped correctly instead.
         */
        const handleMutations = (mutations) => {
            if (document.activeElement !== document.body) {
                return;
            }
            if (mutations.some((mutation) => mutation.removedNodes.length > 0)) {
                (0, focus_1.focusElement)(container);
            }
        };
        document.addEventListener("focusin", handleFocusIn);
        document.addEventListener("focusout", handleFocusOut);
        const observer = new MutationObserver(handleMutations);
        observer.observe(container, { childList: true, subtree: true });
        return () => {
            document.removeEventListener("focusin", handleFocusIn);
            document.removeEventListener("focusout", handleFocusOut);
            observer.disconnect();
        };
    }, [trapped, container, focusBoundary.paused]);
    /* Adds element to focus-stack */
    (0, react_1.useEffect)(() => {
        if (!container) {
            return;
        }
        const ownerDoc = (0, helpers_1.ownerDocument)(container);
        const activeElement = ownerDoc.activeElement;
        const closestContainer = activeElement === null || activeElement === void 0 ? void 0 : activeElement.closest("[data-focus-boundary]");
        addPreviouslyFocusedElement(ownerDoc.activeElement, closestContainer);
        focusBoundarysStack.add(focusBoundary);
        return () => {
            setTimeout(() => {
                focusBoundarysStack.remove(focusBoundary);
            }, 0);
        };
    }, [container, focusBoundary]);
    /**
     * On unmount, we need to clean up previously focused elements associated with this container
     * This makes sure we don't accidentally try to focus elements that are no longer relevant
     * or will be removed from the DOM.
     */
    (0, react_1.useEffect)(() => {
        return () => {
            container && deleteContainerAndPreviouslyFocusedElements(container);
        };
    }, [container]);
    (0, react_1.useEffect)(() => {
        if (!container || !modal) {
            return;
        }
        return (0, helpers_1.hideNonTargetElements)([container]);
    }, [container, modal]);
    /* Handles mount focus */
    (0, utils_external_1.useClientLayoutEffect)(() => {
        if (!container || initialFocusRef.current === false) {
            return;
        }
        const ownerDoc = (0, helpers_1.ownerDocument)(container);
        const previouslyFocusedElement = ownerDoc.activeElement;
        queueMicrotask(() => {
            const focusableElements = (0, focus_1.getTabbableCandidates)(container);
            const initialFocusValueOrFn = initialFocusRef.current;
            const resolvedInitialFocus = typeof initialFocusValueOrFn === "function"
                ? initialFocusValueOrFn()
                : initialFocusValueOrFn;
            if (resolvedInitialFocus === undefined ||
                resolvedInitialFocus === false) {
                return;
            }
            let elToFocus;
            const fallbackelements = focusableElements[0] || container;
            /* `null` should fallback to default behavior in case of an empty ref. */
            if (resolvedInitialFocus === true || resolvedInitialFocus === null) {
                elToFocus = fallbackelements;
            }
            else {
                elToFocus = (0, helpers_1.resolveRef)(resolvedInitialFocus) || fallbackelements;
            }
            const focusAlreadyInsideFloatingEl = container.contains(previouslyFocusedElement);
            if (focusAlreadyInsideFloatingEl) {
                return;
            }
            (0, focus_1.focusElement)(elToFocus, {
                preventScroll: elToFocus === container,
                sync: false,
            });
        });
    }, [container, initialFocusRef]);
    /* Handles unmount focus */
    (0, utils_external_1.useClientLayoutEffect)(() => {
        if (!container) {
            return;
        }
        const ownerDoc = (0, helpers_1.ownerDocument)(container);
        function getReturnElement() {
            const resolvedReturnFocusValueOrFn = returnFocusRef.current;
            let resolvedReturnFocusValue = typeof resolvedReturnFocusValueOrFn === "function"
                ? resolvedReturnFocusValueOrFn()
                : resolvedReturnFocusValueOrFn;
            if (resolvedReturnFocusValue === undefined ||
                resolvedReturnFocusValue === false) {
                return null;
            }
            /* `null` should fallback to default behavior in case of an empty ref. */
            if (resolvedReturnFocusValue === null) {
                resolvedReturnFocusValue = true;
            }
            if (typeof resolvedReturnFocusValue === "boolean") {
                const el = getPreviouslyFocusedElement();
                return (el === null || el === void 0 ? void 0 : el.isConnected) ? el : ownerDoc.body;
            }
            const fallback = getPreviouslyFocusedElement() || ownerDoc.body;
            return (0, helpers_1.resolveRef)(resolvedReturnFocusValue) || fallback;
        }
        return () => {
            const returnElement = getReturnElement();
            const activeEl = ownerDoc.activeElement;
            queueMicrotask(() => {
                if (
                // eslint-disable-next-line react-hooks/exhaustive-deps
                returnFocusRef.current &&
                    returnElement &&
                    returnElement !== activeEl) {
                    returnElement.focus({ preventScroll: true });
                }
            });
        };
    }, [container, returnFocusRef]);
    /* Takes care of looping focus */
    const handleKeyDown = (0, react_1.useCallback)((event) => {
        if ((!loop && !trapped) || focusBoundary.paused) {
            return;
        }
        const isTabKey = event.key === "Tab" &&
            !event.altKey &&
            !event.ctrlKey &&
            !event.metaKey;
        const focusedElement = document.activeElement;
        if (isTabKey && focusedElement) {
            const containerTarget = event.currentTarget;
            const [first, last] = getTabbableEdges(containerTarget);
            /* We can only wrap focus if we have tabbable edges */
            if (!(first && last)) {
                /*
                 * No need to do anything if active element is the expected focus-target
                 * Case: No tabbable elements, focus should stay on container. If we don't preventDefault, the container will lose focus
                 * and potentially lose controll of focus to browser (like focusing address bar).
                 */
                if (focusedElement === containerTarget) {
                    event.preventDefault();
                }
                return;
            }
            /**
             * Since we are either trapped + looping, or one of them we will do nothing when trapped and focus first element when looping.
             */
            if (!event.shiftKey && focusedElement === last) {
                event.preventDefault();
                if (loop) {
                    (0, focus_1.focusElement)(first, { select: true });
                }
            }
            else if (event.shiftKey && focusedElement === first) {
                event.preventDefault();
                if (loop) {
                    (0, focus_1.focusElement)(last, { select: true });
                }
            }
        }
    }, [loop, trapped, focusBoundary.paused]);
    return (react_1.default.createElement(Slot_1.Slot, Object.assign({ tabIndex: -1 }, restProps, { ref: mergedRefs, onKeyDown: handleKeyDown, "data-focus-boundary": true })));
});
exports.FocusBoundary = FocusBoundary;
/* ---------------------------- FocusBoundary utils ---------------------------- */
/**
 * Returns the first and last tabbable elements inside a container as a tuple.
 */
function getTabbableEdges(container) {
    const candidates = (0, focus_1.getTabbableCandidates)(container, { omitLinks: false });
    return [
        findFirstVisible(candidates, container),
        findFirstVisible(candidates.reverse(), container),
    ];
}
/**
 * Returns the first visible element in a list.
 * NOTE: Only checks visibility up to the `container`.
 */
function findFirstVisible(elements, container) {
    for (const element of elements) {
        if (!isHidden(element, { upTo: container })) {
            return element;
        }
    }
}
function isHidden(node, { upTo }) {
    if (getComputedStyle(node).visibility === "hidden") {
        return true;
    }
    while (node) {
        /* we stop at `upTo` */
        if (upTo !== undefined && node === upTo) {
            return false;
        }
        if (getComputedStyle(node).display === "none") {
            return true;
        }
        node = node.parentElement;
    }
    return false;
}
const focusBoundarysStack = createFocusBoundarysStack();
function createFocusBoundarysStack() {
    /* A stack of focus-boundaries, with the active one at the top */
    let stack = [];
    return {
        add(focusBoundary) {
            /* Pause the currently active focus-boundary (at the top of the stack) */
            const activeFocusBoundary = stack[0];
            if (focusBoundary !== activeFocusBoundary) {
                activeFocusBoundary === null || activeFocusBoundary === void 0 ? void 0 : activeFocusBoundary.pause();
            }
            /* remove in case it already exists (because we'll re-add it at the top of the stack) */
            stack = arrayRemove(stack, focusBoundary);
            stack.unshift(focusBoundary);
        },
        remove(focusBoundary) {
            var _a;
            stack = arrayRemove(stack, focusBoundary);
            (_a = stack[0]) === null || _a === void 0 ? void 0 : _a.resume();
        },
    };
}
function arrayRemove(array, item) {
    const updatedArray = [...array];
    const index = updatedArray.indexOf(item);
    if (index !== -1) {
        updatedArray.splice(index, 1);
    }
    return updatedArray;
}
const LIST_LIMIT = 10;
let previouslyFocusedElements = [];
const focusedElementsByContainer = new WeakMap();
function clearDisconnectedPreviouslyFocusedElements() {
    previouslyFocusedElements = previouslyFocusedElements.filter((el) => el.isConnected);
}
/**
 * Removes "will be" unmounted elements from previouslyFocusedElements,
 * and deletes the container from focusedElementsByContainer.
 */
function deleteContainerAndPreviouslyFocusedElements(container) {
    const nestedElements = focusedElementsByContainer.get(container) || [];
    previouslyFocusedElements = previouslyFocusedElements.filter((el) => {
        return !nestedElements.includes(el);
    });
    focusedElementsByContainer.delete(container);
}
function addPreviouslyFocusedElement(element, container) {
    clearDisconnectedPreviouslyFocusedElements();
    if (element && (element === null || element === void 0 ? void 0 : element.nodeName) !== "BODY") {
        previouslyFocusedElements.push(element);
        if (container) {
            const nestedElements = focusedElementsByContainer.get(container) || [];
            nestedElements.push(element);
            focusedElementsByContainer.set(container, nestedElements);
        }
        if (previouslyFocusedElements.length > LIST_LIMIT) {
            previouslyFocusedElements = previouslyFocusedElements.slice(-LIST_LIMIT);
        }
    }
}
function getPreviouslyFocusedElement() {
    clearDisconnectedPreviouslyFocusedElements();
    return previouslyFocusedElements[previouslyFocusedElements.length - 1];
}
//# sourceMappingURL=FocusBoundary.js.map