"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useScrollLock = useScrollLock;
const utils_external_1 = require("../../utils-external");
const helpers_1 = require("../helpers");
const useTimeout_1 = require("./useTimeout");
let originalHtmlStyles = {};
let originalBodyStyles = {};
let originalHtmlScrollBehavior = "";
function supportsStableScrollbarGutter(referenceElement) {
    const supported = typeof CSS !== "undefined" &&
        CSS.supports &&
        CSS.supports("scrollbar-gutter", "stable");
    if (!supported || typeof document === "undefined") {
        return false;
    }
    /*
     * We need to do additional checks since the scenario:
     * - Scrollbar is edited with `::-webkit-scrollbar`
     * - MacOS setting: Show scroll bars -> Automatically based on mouse or tracked
     * Causes the calculation of scrollbar width to be incorrect, and thus the scrollbar gutter to not work as intended.
     */
    const doc = (0, helpers_1.ownerDocument)(referenceElement);
    const html = doc.documentElement;
    const body = doc.body;
    const scrollContainer = isOverflowElement(html) ? html : body;
    const originalScrollContainerOverflowY = scrollContainer.style.overflowY;
    const originalHtmlStyleGutter = html.style.scrollbarGutter;
    html.style.scrollbarGutter = "stable";
    scrollContainer.style.overflowY = "scroll";
    const before = scrollContainer.offsetWidth;
    scrollContainer.style.overflowY = "hidden";
    const after = scrollContainer.offsetWidth;
    scrollContainer.style.overflowY = originalScrollContainerOverflowY;
    html.style.scrollbarGutter = originalHtmlStyleGutter;
    return before === after;
}
function hasInsetScrollbars(referenceElement) {
    if (typeof document === "undefined") {
        return false;
    }
    const doc = (0, helpers_1.ownerDocument)(referenceElement);
    const win = (0, helpers_1.ownerWindow)(doc);
    return win.innerWidth - doc.documentElement.clientWidth > 0;
}
function preventScrollBasic(referenceElement) {
    const doc = (0, helpers_1.ownerDocument)(referenceElement);
    const html = doc.documentElement;
    const body = doc.body;
    /**
     * If an `overflow` style is present on <html>, we need to lock it, because a lock on <body>
     * won't have any effect.
     * But if <body> has an `overflow` style (like `overflow-x: hidden`), we need to lock it
     * instead, as sticky elements shift otherwise.
     */
    const elementToLock = isOverflowElement(html) ? html : body;
    const originalOverflow = elementToLock.style.overflow;
    elementToLock.style.overflow = "hidden";
    return () => {
        elementToLock.style.overflow = originalOverflow;
    };
}
function preventScrollStandard(referenceElement) {
    var _a, _b;
    const doc = (0, helpers_1.ownerDocument)(referenceElement);
    const html = doc.documentElement;
    const body = doc.body;
    const win = (0, helpers_1.ownerWindow)(html);
    let scrollTop = 0;
    let scrollLeft = 0;
    let resizeRaf = 0;
    /* Pinch-zoom in Safari causes a shift. Just don't lock scroll if there's any pinch-zoom. */
    if (helpers_1.isWebKit && ((_b = (_a = win.visualViewport) === null || _a === void 0 ? void 0 : _a.scale) !== null && _b !== void 0 ? _b : 1) !== 1) {
        return () => { };
    }
    /**
     * Locks the scroll by applying styles to Html and Body element.
     * Reads the DOM first, then writes to avoid layout thrashing.
     */
    function lockScroll() {
        /* DOM reads: */
        const htmlStyles = win.getComputedStyle(html);
        const bodyStyles = win.getComputedStyle(body);
        const htmlScrollbarGutterValue = htmlStyles.scrollbarGutter || "";
        const hasBothEdges = htmlScrollbarGutterValue.includes("both-edges");
        const scrollbarGutterValue = hasBothEdges ? "stable both-edges" : "stable";
        scrollTop = html.scrollTop;
        scrollLeft = html.scrollLeft;
        originalHtmlStyles = {
            scrollbarGutter: html.style.scrollbarGutter,
            overflowY: html.style.overflowY,
            overflowX: html.style.overflowX,
        };
        originalHtmlScrollBehavior = html.style.scrollBehavior;
        originalBodyStyles = {
            position: body.style.position,
            height: body.style.height,
            width: body.style.width,
            boxSizing: body.style.boxSizing,
            overflowY: body.style.overflowY,
            overflowX: body.style.overflowX,
            scrollBehavior: body.style.scrollBehavior,
        };
        const isScrollableY = html.scrollHeight > html.clientHeight;
        const isScrollableX = html.scrollWidth > html.clientWidth;
        const hasConstantOverflowY = htmlStyles.overflowY === "scroll" || bodyStyles.overflowY === "scroll";
        const hasConstantOverflowX = htmlStyles.overflowX === "scroll" || bodyStyles.overflowX === "scroll";
        /* Values can be negative in Firefox */
        const scrollbarWidth = Math.max(0, win.innerWidth - html.clientWidth);
        const scrollbarHeight = Math.max(0, win.innerHeight - html.clientHeight);
        /*
         * Avoid shift due to <body> margin. NB: This does cause elements to be clipped
         * with whitespace.
         */
        const marginY = parseFloat(bodyStyles.marginTop) + parseFloat(bodyStyles.marginBottom);
        const marginX = parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight);
        /**
         * Check support for stable scrollbar gutter to avoid layout shift when scrollbars appear/disappear.
         */
        const supportsScrollbarGutter = supportsStableScrollbarGutter(referenceElement);
        /*
         * DOM writes:
         * Do not read the DOM past this point!
         */
        if (supportsScrollbarGutter) {
            const elementToLock = isOverflowElement(html) ? html : body;
            html.style.scrollbarGutter = scrollbarGutterValue;
            elementToLock.style.overflowY = "hidden";
            elementToLock.style.overflowX = "hidden";
            return;
        }
        Object.assign(html.style, {
            scrollbarGutter: "stable",
            overflowY: isScrollableY || hasConstantOverflowY ? "scroll" : "hidden",
            overflowX: isScrollableX || hasConstantOverflowX ? "scroll" : "hidden",
        });
        Object.assign(body.style, {
            /*
             * Keeps existing positioned children in place (e.g. fixed headers).
             */
            position: "relative",
            /**
             *  Limits height to the viewport minus margins/scrollbar compensation to stop vertical overflow from reappearing.
             */
            height: marginY || scrollbarHeight
                ? `calc(100dvh - ${marginY + scrollbarHeight}px)`
                : "100dvh",
            /**
             * Mirrors height-logic for width.
             */
            width: marginX || scrollbarWidth
                ? `calc(100vw - ${marginX + scrollbarWidth}px)`
                : "100vw",
            /**
             * Ensures the adjusted dimensions include padding/border, matching the measured values.
             */
            boxSizing: "border-box",
            /**
             * Blocks scrollable overflow.
             */
            overflow: "hidden",
            /**
             * Removes smooth-scrolling so immediate position restores occur without animation.
             */
            scrollBehavior: "unset",
        });
        body.scrollTop = scrollTop;
        body.scrollLeft = scrollLeft;
        html.setAttribute("data-aksel-scroll-locked", "");
        html.style.scrollBehavior = "unset";
    }
    /**
     * Restores the original scroll position and styles to Html and Body element.
     */
    function cleanup() {
        Object.assign(html.style, originalHtmlStyles);
        Object.assign(body.style, originalBodyStyles);
        html.scrollTop = scrollTop;
        html.scrollLeft = scrollLeft;
        html.removeAttribute("data-aksel-scroll-locked");
        html.style.scrollBehavior = originalHtmlScrollBehavior;
    }
    /**
     * On resize, restore original styles, then re-apply scroll lock next frame.
     */
    function handleResize() {
        cleanup();
        if (resizeRaf) {
            cancelAnimationFrame(resizeRaf);
        }
        /**
         * Wait until next frame to re-apply scroll lock ensuring layout has settled after resize.
         */
        resizeRaf = requestAnimationFrame(lockScroll);
    }
    lockScroll();
    win.addEventListener("resize", handleResize);
    return () => {
        if (resizeRaf) {
            cancelAnimationFrame(resizeRaf);
        }
        cleanup();
        win.removeEventListener("resize", handleResize);
    };
}
class ScrollLocker {
    constructor() {
        this.lockCount = 0;
        this.restore = null;
        this.timeoutLock = useTimeout_1.Timeout.create();
        this.timeoutUnlock = useTimeout_1.Timeout.create();
        /**
         * Releases a lock
         * - If last lock, unlock document-scroll.
         * - If not last lock, do nothing.
         */
        this.release = () => {
            this.lockCount -= 1;
            if (this.lockCount === 0 && this.restore) {
                this.timeoutUnlock.start(0, this.unlock);
            }
        };
        this.unlock = () => {
            var _a;
            if (this.lockCount === 0 && this.restore) {
                (_a = this.restore) === null || _a === void 0 ? void 0 : _a.call(this);
                this.restore = null;
            }
        };
    }
    /**
     * Acquires a new lock
     * - If first lock, lock document-scroll.
     * - If not first lock, do nothing.
     */
    acquire(referenceElement) {
        this.lockCount += 1;
        if (this.lockCount === 1 && this.restore === null) {
            /*
             * Delay locking to avoid layout thrashing when multiple locks/unlocks are requested in quick succession.
             */
            this.timeoutLock.start(0, () => this.lock(referenceElement));
        }
        return this.release;
    }
    lock(referenceElement) {
        if (this.lockCount === 0 || this.restore !== null) {
            return;
        }
        const doc = (0, helpers_1.ownerDocument)(referenceElement);
        const html = doc.documentElement;
        const htmlOverflowY = (0, helpers_1.ownerWindow)(html).getComputedStyle(html).overflowY;
        /* If the site author already hid overflow on <html>, respect it and bail out. */
        if (htmlOverflowY === "hidden" || htmlOverflowY === "clip") {
            this.restore = () => { };
            return;
        }
        const shouldUseBasicLock = helpers_1.isIOS || !hasInsetScrollbars(referenceElement);
        /**
         * On iOS, the standard scroll locking method does not work properly if the navbar is collapsed.
         * The following must be researched extensively before activating standard scroll locking on iOS:
         * - Textboxes must scroll into view when focused, and not cause a glitchy scroll animation.
         * - The navbar must not force itself into view and cause layout shift.
         * - Scroll containers must not flicker upon closing a popup when it has an exit animation.
         */
        this.restore = shouldUseBasicLock
            ? preventScrollBasic(referenceElement)
            : preventScrollStandard(referenceElement);
    }
}
const SCROLL_LOCKER = new ScrollLocker();
/**
 * Locks the scroll of the document when enabled.
 * @param enabled - Whether to enable the scroll lock.
 */
function useScrollLock(params) {
    const { enabled = true, mounted, open, referenceElement = null } = params;
    /**
     * When closing elements with "sloppy clicks" (clicks that start inside the element and ends outside),
     * animating out on WebKit browsers (mounted + not open) can cause the whole page to be selected.
     * To prevent this, we temporarily disable user-select on body while the element is animating out.
     * This bug might be fixed in newer WebKit versions.
     *
     * @see https://github.com/mui/base-ui/issues/1135
     */
    (0, utils_external_1.useClientLayoutEffect)(() => {
        if (enabled && helpers_1.isWebKit && mounted && !open) {
            const doc = (0, helpers_1.ownerDocument)(referenceElement);
            const originalUserSelect = doc.body.style.userSelect;
            const originalWebkitUserSelect = doc.body.style.webkitUserSelect;
            doc.body.style.userSelect = "none";
            doc.body.style.webkitUserSelect = "none";
            return () => {
                doc.body.style.userSelect = originalUserSelect;
                doc.body.style.webkitUserSelect = originalWebkitUserSelect;
            };
        }
        return undefined;
    }, [enabled, mounted, open, referenceElement]);
    (0, utils_external_1.useClientLayoutEffect)(() => {
        if (!enabled) {
            return undefined;
        }
        return SCROLL_LOCKER.acquire(referenceElement);
    }, [enabled, referenceElement]);
}
const invalidOverflowDisplayValues = new Set(["inline", "contents"]);
function isOverflowElement(element) {
    const { overflow, overflowX, overflowY, display } = getComputedStyle(element);
    return (/auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) &&
        !invalidOverflowDisplayValues.has(display));
}
//# sourceMappingURL=useScrollLock.js.map