UNPKG

@ionic/core

Version:
3,104 lines • 146 kB
/*!
 * (C) Ionic http://ionicframework.com - MIT License
 */
import { r as registerInstance, d as createEvent, a as printIonWarning, w as writeTask, i as config, h, f as Host, g as getElement } from './index-BpRUsN_W.js';
import { i as isIonContent, f as findRefresherInContent, a as findClosestIonContent, d as disableContentScrollY, r as resetContentScrollY, b as findIonContent, p as printIonContentErrorMsg } from './index-TkLga3ma.js';
import { C as CoreDelegate, a as attachComponent, d as detachComponent } from './framework-delegate-CDjM1vRH.js';
import { w as win, o as onCustomPropertyChange, g as getElementRoot, e as clamp, r as raf, b as inheritAttributes, h as hasLazyBuild } from './helpers-BJqKF1pr.js';
import { c as createLockController } from './lock-controller-B-hirT0v.js';
import { g as getCapacitor } from './capacitor-CHJaJ9aX.js';
import { G as GESTURE, O as OVERLAY_GESTURE_PRIORITY, F as FOCUS_TRAP_DISABLE_CLASS, n as getOverlaySizeType, d as createTriggerController, B as BACKDROP, h as prepareOverlay, r as restoreRootFocusTrapAccessibility, j as cleanupRootFocusTrapAccessibility, k as setOverlayId, e as present, f as dismiss, g as eventMethod } from './overlays-DmDY-El7.js';
import { g as getClassMap } from './theme-byZM6qHV.js';
import { e as deepReady, w as waitForMount } from './index-z9vdZN7I.js';
import { b as getIonMode } from './ionic-global-Cep6oYzK.js';
import { KEYBOARD_DID_OPEN } from './keyboard-B4Y-Gph8.js';
import { c as createAnimation } from './animation-DmtJpz89.js';
import { g as getTimeGivenProgression } from './cubic-bezier-hHmYLOfE.js';
import { createGesture } from './index-BmLuEdV7.js';
import './dir-Dojwmvde.js';
import './gesture-controller-B_gJaBk0.js';
import './keyboard-Cmqp5iZD.js';

var Style;
(function (Style) {
    Style["Dark"] = "DARK";
    Style["Light"] = "LIGHT";
    Style["Default"] = "DEFAULT";
})(Style || (Style = {}));
const StatusBar = {
    getEngine() {
        const capacitor = getCapacitor();
        if (capacitor?.isPluginAvailable('StatusBar')) {
            return capacitor.Plugins.StatusBar;
        }
        return undefined;
    },
    setStyle(options) {
        const engine = this.getEngine();
        if (!engine) {
            return;
        }
        engine.setStyle(options);
    },
    getStyle: async function () {
        const engine = this.getEngine();
        if (!engine) {
            return Style.Default;
        }
        const { style } = await engine.getInfo();
        return style;
    },
};

/**
 * Use y = mx + b to
 * figure out the backdrop value
 * at a particular x coordinate. This
 * is useful when the backdrop does
 * not begin to fade in until after
 * the 0 breakpoint.
 */
const getBackdropValueForSheet = (x, backdropBreakpoint) => {
    /**
     * We will use these points:
     * (backdropBreakpoint, 0)
     * (maxBreakpoint, 1)
     * We know that at the beginning breakpoint,
     * the backdrop will be hidden. We also
     * know that at the maxBreakpoint, the backdrop
     * must be fully visible. maxBreakpoint should
     * always be 1 even if the maximum value
     * of the breakpoints array is not 1 since
     * the animation runs from a progress of 0
     * to a progress of 1.
     * m = (y2 - y1) / (x2 - x1)
     *
     * This is simplified from:
     * m = (1 - 0) / (maxBreakpoint - backdropBreakpoint)
     *
     * If the backdropBreakpoint is 1, we return 0 as the
     * backdrop is completely hidden.
     *
     */
    if (backdropBreakpoint === 1) {
        return 0;
    }
    const slope = 1 / (1 - backdropBreakpoint);
    /**
     * From here, compute b which is
     * the backdrop opacity if the offset
     * is 0. If the backdrop does not
     * begin to fade in until after the
     * 0 breakpoint, this b value will be
     * negative. This is fine as we never pass
     * b directly into the animation keyframes.
     * b = y - mx
     * Use a known point: (backdropBreakpoint, 0)
     * This is simplified from:
     * b = 0 - (backdropBreakpoint * slope)
     */
    const b = -(backdropBreakpoint * slope);
    /**
     * Finally, we can now determine the
     * backdrop offset given an arbitrary
     * gesture offset.
     */
    return x * slope + b;
};
/**
 * The tablet/desktop card modal activates
 * when the window width is >= 768.
 * At that point, the presenting element
 * is not transformed, so we do not need to
 * adjust the status bar color.
 *
 */
const setCardStatusBarDark = () => {
    if (!win || win.innerWidth >= 768) {
        return;
    }
    StatusBar.setStyle({ style: Style.Dark });
};
const setCardStatusBarDefault = (defaultStyle = Style.Default) => {
    if (!win || win.innerWidth >= 768) {
        return;
    }
    StatusBar.setStyle({ style: defaultStyle });
};
/**
 * Calls back when the modal's resolved `--height` changes.
 */
const onModalHeightChange = (hostEl, callback) => {
    return onCustomPropertyChange(hostEl, '--height', () => callback());
};

/**
 * Swiping is only possible when the content is scrolled to the top, so that we
 * do not interfere with scrolling, and never on content with a refresher.
 *
 * Note: We cannot solve the refresher case with gesture priority as the iOS
 * native refresh gesture uses a scroll listener in addition to a gesture.
 *
 * Note: Do not use `getScrollElement` here because we need this to be a
 * synchronous operation, and `getScrollElement` is asynchronous.
 */
const canSwipeOnContent = (contentEl) => {
    const scrollEl = isIonContent(contentEl) ? getElementRoot(contentEl).querySelector('.inner-scroll') : contentEl;
    const hasRefresherInContent = !!findRefresherInContent(contentEl);
    return !hasRefresherInContent && scrollEl.scrollTop === 0;
};
const handleCanDismiss = async (el, animation) => {
    /**
     * If canDismiss is not a function
     * then we can return early. If canDismiss is `true`,
     * then canDismissBlocksGesture is `false` as canDismiss
     * will never interrupt the gesture. As a result,
     * this code block is never reached. If canDismiss is `false`,
     * then we never dismiss.
     */
    if (typeof el.canDismiss !== 'function') {
        return;
    }
    /**
     * Run the canDismiss callback.
     * If the function returns `true`,
     * then we can proceed with dismiss.
     */
    const shouldDismiss = await el.canDismiss(undefined, GESTURE);
    if (!shouldDismiss) {
        return;
    }
    /**
     * If canDismiss resolved after the snap
     * back animation finished, we can
     * dismiss immediately.
     *
     * If canDismiss resolved before the snap
     * back animation finished, we need to
     * wait until the snap back animation is
     * done before dismissing.
     */
    if (animation.isRunning()) {
        animation.onFinish(() => {
            el.dismiss(undefined, 'handler');
        }, { oneTimeCallback: true });
    }
    else {
        el.dismiss(undefined, 'handler');
    }
};
/**
 * This function lets us simulate a realistic spring-like animation
 * when swiping down on the modal.
 * There are two forces that we need to use to compute the spring physics:
 *
 * 1. Stiffness, k: This is a measure of resistance applied a spring.
 * 2. Dampening, c: This value has the effect of reducing or preventing oscillation.
 *
 * Using these two values, we can calculate the Spring Force and the Dampening Force
 * to compute the total force applied to a spring.
 *
 * Spring Force: This force pulls a spring back into its equilibrium position.
 * Hooke's Law tells us that that spring force (FS) = kX.
 * k is the stiffness of a spring, and X is the displacement of the spring from its
 * equilibrium position. In this case, it is the amount by which the free end
 * of a spring was displaced (stretched/pushed) from its "relaxed" position.
 *
 * Dampening Force: This force slows down motion. Without it, a spring would oscillate forever.
 * The dampening force, FD, can be found via this formula: FD = -cv
 * where c the dampening value and v is velocity.
 *
 * Therefore, the resulting force that is exerted on the block is:
 * F = FS + FD = -kX - cv
 *
 * Newton's 2nd Law tells us that F = ma:
 * ma = -kX - cv.
 *
 * For Ionic's purposes, we can assume that m = 1:
 * a = -kX - cv
 *
 * Imagine a block attached to the end of a spring. At equilibrium
 * the block is at position x = 1.
 * Pressing on the block moves it to position x = 0;
 * So, to calculate the displacement, we need to take the
 * current position and subtract the previous position from it.
 * X = x - x0 = 0 - 1 = -1.
 *
 * For Ionic's purposes, we are only pushing on the spring modal
 * so we have a max position of 1.
 * As a result, we can expand displacement to this formula:
 * X = x - 1
 *
 * a = -k(x - 1) - cv
 *
 * We can represent the motion of something as a function of time: f(t) = x.
 * The derivative of position gives us the velocity: f'(t)
 * The derivative of the velocity gives us the acceleration: f''(t)
 *
 * We can substitute the formula above with these values:
 *
 * f"(t) = -k * (f(t) - 1) - c * f'(t)
 *
 * This is called a differential equation.
 *
 * We know that at t = 0, we are at x = 0 because the modal does not move: f(0) = 0
 * This means our velocity is also zero: f'(0) = 0.
 *
 * We can cheat a bit and plug the formula into Wolfram Alpha.
 * However, we need to pick stiffness and dampening values:
 * k = 0.57
 * c = 15
 *
 * I picked these as they are fairly close to native iOS's spring effect
 * with the modal.
 *
 * What we plug in is this: f(0) = 0; f'(0) = 0; f''(t) = -0.57(f(t) - 1) - 15f'(t)
 *
 * The result is a formula that lets us calculate the acceleration
 * for a given time t.
 * Note: This is the approximate form of the solution. Wolfram Alpha will
 * give you a complex differential equation too.
 */
const calculateSpringStep = (t) => {
    return 0.00255275 * 2.71828 ** (-14.9619 * t) - 1.00255 * 2.71828 ** (-0.0380968 * t) + 1;
};

// Defaults for the card swipe animation
const SwipeToCloseDefaults = {
    MIN_PRESENTING_SCALE: 0.915,
};
const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onDragStart, onDragMove, onDragEnd) => {
    /**
     * The step value at which a card modal
     * is eligible for dismissing via gesture.
     */
    const DISMISS_THRESHOLD = 0.5;
    const height = el.offsetHeight;
    let isOpen = false;
    let canDismissBlocksGesture = false;
    let contentEl = null;
    const canDismissMaxStep = 0.2;
    let initialScrollY = true;
    let lastStep = 0;
    const getScrollY = () => {
        if (contentEl && isIonContent(contentEl)) {
            return contentEl.scrollY;
            /**
             * Custom scroll containers are intended to be
             * used with virtual scrolling, so we assume
             * there is scrolling in this case.
             */
        }
        else {
            return true;
        }
    };
    const canStart = (detail) => {
        const target = detail.event.target;
        if (target === null || !target.closest) {
            return true;
        }
        /**
         * We cannot assume that the `ion-content`
         * target will remain consistent between
         * swipes. For example, when using
         * ion-nav within a card modal it is
         * possible to swipe, push a view, and then
         * swipe again. The target content will not
         * be the same between swipes.
         */
        contentEl = findClosestIonContent(target);
        if (contentEl) {
            return canSwipeOnContent(contentEl);
        }
        /**
         * Card should be swipeable on all
         * parts of the modal except for the footer.
         */
        const footer = target.closest('ion-footer');
        if (footer === null) {
            return true;
        }
        return false;
    };
    const onStart = (detail) => {
        const { deltaY } = detail;
        /**
         * Firefox automatically selects the header text during drag
         * due to the focusable wrapper (tabindex="-1"). Remove any
         * selection that may have occurred.
         */
        window.getSelection()?.removeAllRanges();
        /**
         * Get the initial scrollY value so
         * that we can correctly reset the scrollY
         * prop when the gesture ends.
         */
        initialScrollY = getScrollY();
        /**
         * If canDismiss is anything other than `true`
         * then users should be able to swipe down
         * until a threshold is hit. At that point,
         * the card modal should not proceed any further.
         * TODO (FW-937)
         * Remove undefined check
         */
        canDismissBlocksGesture = el.canDismiss !== undefined && el.canDismiss !== true;
        /**
         * If we are pulling down, then
         * it is possible we are pulling on the
         * content. We do not want scrolling to
         * happen at the same time as the gesture.
         */
        if (deltaY > 0 && contentEl) {
            disableContentScrollY(contentEl);
        }
        animation.progressStart(true, isOpen ? 1 : 0);
        onDragStart();
    };
    const onMove = (detail) => {
        const { deltaY } = detail;
        /**
         * If we are pulling down, then
         * it is possible we are pulling on the
         * content. We do not want scrolling to
         * happen at the same time as the gesture.
         */
        if (deltaY > 0 && contentEl) {
            disableContentScrollY(contentEl);
        }
        /**
         * If we are swiping on the content
         * then the swipe gesture should only
         * happen if we are pulling down.
         *
         * However, if we pull up and
         * then down such that the scroll position
         * returns to 0, we should be able to swipe
         * the card.
         */
        const step = detail.deltaY / height;
        /**
         * Check if user is swiping down and
         * if we have a canDismiss value that
         * should block the gesture from
         * proceeding,
         */
        const isAttemptingDismissWithCanDismiss = step >= 0 && canDismissBlocksGesture;
        /**
         * If we are blocking the gesture from dismissing,
         * set the max step value so that the sheet cannot be
         * completely hidden.
         */
        const maxStep = isAttemptingDismissWithCanDismiss ? canDismissMaxStep : 0.9999;
        /**
         * If we are blocking the gesture from
         * dismissing, calculate the spring modifier value
         * this will be added to the starting breakpoint
         * value to give the gesture a spring-like feeling.
         * Note that the starting breakpoint is always 0,
         * so we omit adding 0 to the result.
         */
        const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
        const clampedStep = clamp(0.0001, processedStep, maxStep);
        animation.progressStep(clampedStep);
        /**
         * When swiping down half way, the status bar style
         * should be reset to its default value.
         *
         * We track lastStep so that we do not fire these
         * functions on every onMove, only when the user has
         * crossed a certain threshold.
         */
        if (clampedStep >= DISMISS_THRESHOLD && lastStep < DISMISS_THRESHOLD) {
            setCardStatusBarDefault(statusBarStyle);
            /**
             * However, if we swipe back up, then the
             * status bar style should be set to have light
             * text on a dark background.
             */
        }
        else if (clampedStep < DISMISS_THRESHOLD && lastStep >= DISMISS_THRESHOLD) {
            setCardStatusBarDark();
        }
        lastStep = clampedStep;
        const eventDetail = {
            currentY: detail.currentY,
            deltaY: detail.deltaY,
            velocityY: detail.velocityY,
            progress: calculateProgress(el, detail.deltaY),
        };
        onDragMove(eventDetail);
    };
    const onEnd = (detail) => {
        const velocity = detail.velocityY;
        const step = detail.deltaY / height;
        const isAttemptingDismissWithCanDismiss = step >= 0 && canDismissBlocksGesture;
        const maxStep = isAttemptingDismissWithCanDismiss ? canDismissMaxStep : 0.9999;
        const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
        const clampedStep = clamp(0.0001, processedStep, maxStep);
        const threshold = (detail.deltaY + velocity * 1000) / height;
        /**
         * If canDismiss blocks
         * the swipe gesture, then the
         * animation can never complete until
         * canDismiss is checked.
         */
        const shouldComplete = !isAttemptingDismissWithCanDismiss && threshold >= DISMISS_THRESHOLD;
        let newStepValue = shouldComplete ? -1e-3 : 0.001;
        if (!shouldComplete) {
            animation.easing('cubic-bezier(1, 0, 0.68, 0.28)');
            newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], clampedStep)[0];
        }
        else {
            animation.easing('cubic-bezier(0.32, 0.72, 0, 1)');
            newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], clampedStep)[0];
        }
        const duration = shouldComplete
            ? computeDuration(step * height, velocity)
            : computeDuration((1 - clampedStep) * height, velocity);
        isOpen = shouldComplete;
        gesture.enable(false);
        if (contentEl) {
            resetContentScrollY(contentEl, initialScrollY);
        }
        animation
            .onFinish(() => {
            if (!shouldComplete) {
                gesture.enable(true);
            }
        })
            .progressEnd(shouldComplete ? 1 : 0, newStepValue, duration);
        /**
         * If the canDismiss value blocked the gesture
         * from proceeding, then we should ignore whatever
         * shouldComplete is. Whether or not the modal
         * animation should complete is now determined by
         * canDismiss.
         *
         * If the user swiped >25% of the way
         * to the max step, then we should
         * check canDismiss. 25% was chosen
         * to avoid accidental swipes.
         */
        const isAttemptingCanDismiss = isAttemptingDismissWithCanDismiss && clampedStep > maxStep / 4;
        if (isAttemptingCanDismiss) {
            handleCanDismiss(el, animation);
        }
        else if (shouldComplete) {
            onDismiss();
        }
        /**
         * `shouldComplete` is not enough on its own, since it is always `false`
         * when canDismiss blocks the gesture. `canDismiss: false` blocks it and
         * never dismisses, because `handleCanDismiss` returns early when
         * canDismiss is not a function. A canDismiss function dismisses through
         * `handleCanDismiss`, which awaits the callback, so the outcome is not
         * known when this event is emitted.
         */
        const isDismissing = isAttemptingDismissWithCanDismiss
            ? isAttemptingCanDismiss && typeof el.canDismiss === 'function'
            : shouldComplete;
        const eventDetail = {
            currentY: detail.currentY,
            deltaY: detail.deltaY,
            velocityY: detail.velocityY,
            progress: calculateProgress(el, detail.deltaY),
            isDismissing,
        };
        onDragEnd(eventDetail);
    };
    const gesture = createGesture({
        el,
        gestureName: 'modalSwipeToClose',
        gesturePriority: OVERLAY_GESTURE_PRIORITY,
        direction: 'y',
        threshold: 10,
        canStart,
        onStart,
        onMove,
        onEnd,
    });
    return gesture;
};
const computeDuration = (remaining, velocity) => {
    return clamp(400, remaining / Math.abs(velocity * 1.1), 500);
};
/**
 * Calculates the progress of the swipe gesture.
 *
 * The progress is a value between 0 and 1 that represents how far
 * the swipe has progressed towards closing the modal.
 *
 * A value closer to 1 means the modal is closer to being opened,
 * while a value closer to 0 means the modal is closer to being closed.
 *
 * @param el The modal
 * @param deltaY The change in Y position (positive when dragging down, negative when dragging up)
 * @returns The progress of the swipe gesture
 */
const calculateProgress = (el, deltaY) => {
    const windowHeight = window.innerHeight;
    // Position when fully open
    const modalTop = el.getBoundingClientRect().top;
    /**
     * The distance between the top of the modal and the bottom of the screen
     * is the total distance the modal needs to travel to be fully closed.
     */
    const totalDistance = windowHeight - modalTop;
    /**
     * The pull percentage is how far the user has swiped compared to the total
     * distance needed to close the modal.
     */
    const pullPercentage = deltaY / totalDistance;
    /**
     * The progress is the inverse of the pull percentage because
     * when the user starts swiping up, the progress should be close to 1,
     * and when the user has swiped all the way down, the progress should be
     * close to 0.
     */
    const progress = 1 - pullPercentage;
    // Round to the nearest thousandth to avoid returning very small decimal
    const roundedProgress = Math.round(progress * 1000) / 1000;
    return Math.max(0, Math.min(1, roundedProgress));
};

const createSheetEnterAnimation = (opts) => {
    const { currentBreakpoint, backdropBreakpoint, expandToScroll } = opts;
    /**
     * If the backdropBreakpoint is undefined, then the backdrop
     * should always fade in. If the backdropBreakpoint came before the
     * current breakpoint, then the backdrop should be fading in.
     */
    const shouldShowBackdrop = backdropBreakpoint === undefined || backdropBreakpoint < currentBreakpoint;
    const initialBackdrop = shouldShowBackdrop ? `calc(var(--backdrop-opacity) * ${currentBreakpoint})` : '0';
    const backdropAnimation = createAnimation('backdropAnimation').fromTo('opacity', 0, initialBackdrop);
    if (shouldShowBackdrop) {
        backdropAnimation
            .beforeStyles({
            'pointer-events': 'none',
        })
            .afterClearStyles(['pointer-events']);
    }
    const wrapperAnimation = createAnimation('wrapperAnimation').keyframes([
        { offset: 0, opacity: 1, transform: 'translateY(100%)' },
        { offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint * 100}%)` },
    ]);
    /**
     * This allows the content to be scrollable at any breakpoint.
     */
    const contentAnimation = !expandToScroll
        ? createAnimation('contentAnimation').keyframes([
            { offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint) * 100}%` },
            { offset: 1, opacity: 1, maxHeight: `${currentBreakpoint * 100}%` },
        ])
        : undefined;
    return { wrapperAnimation, backdropAnimation, contentAnimation };
};
const createSheetLeaveAnimation = (opts) => {
    const { currentBreakpoint, backdropBreakpoint } = opts;
    /**
     * Backdrop does not always fade in from 0 to 1 if backdropBreakpoint
     * is defined, so we need to account for that offset by figuring out
     * what the current backdrop value should be.
     */
    const backdropValue = `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(currentBreakpoint, backdropBreakpoint)})`;
    const defaultBackdrop = [
        { offset: 0, opacity: backdropValue },
        { offset: 1, opacity: 0 },
    ];
    const customBackdrop = [
        { offset: 0, opacity: backdropValue },
        { offset: backdropBreakpoint, opacity: 0 },
        { offset: 1, opacity: 0 },
    ];
    const backdropAnimation = createAnimation('backdropAnimation').keyframes(backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop);
    const wrapperAnimation = createAnimation('wrapperAnimation').keyframes([
        { offset: 0, opacity: 1, transform: `translateY(${100 - currentBreakpoint * 100}%)` },
        { offset: 1, opacity: 1, transform: `translateY(100%)` },
    ]);
    return { wrapperAnimation, backdropAnimation };
};

const createEnterAnimation$1 = () => {
    const backdropAnimation = createAnimation()
        .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
        .beforeStyles({
        'pointer-events': 'none',
    })
        .afterClearStyles(['pointer-events']);
    const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
    return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
};
/**
 * iOS Modal Enter Animation for the Card presentation style
 */
const iosEnterAnimation = (baseEl, opts) => {
    const { presentingEl, currentBreakpoint, expandToScroll } = opts;
    const root = getElementRoot(baseEl);
    const { wrapperAnimation, backdropAnimation, contentAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation$1();
    backdropAnimation.addElement(root.querySelector('ion-backdrop'));
    wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')).beforeStyles({ opacity: 1 });
    // The content animation is only added if scrolling is enabled for
    // all the breakpoints.
    !expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page'));
    const baseAnimation = createAnimation('entering-base')
        .addElement(baseEl)
        .easing('cubic-bezier(0.32,0.72,0,1)')
        .duration(500)
        .addAnimation([wrapperAnimation]);
    if (contentAnimation) {
        baseAnimation.addAnimation(contentAnimation);
    }
    if (presentingEl) {
        const isPortrait = window.innerWidth < 768;
        const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
        const presentingElRoot = getElementRoot(presentingEl);
        const presentingAnimation = createAnimation().beforeStyles({
            transform: 'translateY(0)',
            'transform-origin': 'top center',
            overflow: 'hidden',
        });
        const bodyEl = document.body;
        if (isPortrait) {
            /**
             * Fallback for browsers that does not support `max()` (ex: Firefox)
             * No need to worry about statusbar padding since engines like Gecko
             * are not used as the engine for standalone Cordova/Capacitor apps
             */
            const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
            const modalTransform = hasCardModal ? '-10px' : transformOffset;
            const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
            const finalTransform = `translateY(${modalTransform}) scale(${toPresentingScale})`;
            presentingAnimation
                .afterStyles({
                transform: finalTransform,
            })
                .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
                .addElement(presentingEl)
                .keyframes([
                { offset: 0, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' },
                { offset: 1, filter: 'contrast(0.85)', transform: finalTransform, borderRadius: '10px 10px 0 0' },
            ]);
            baseAnimation.addAnimation(presentingAnimation);
        }
        else {
            baseAnimation.addAnimation(backdropAnimation);
            if (!hasCardModal) {
                wrapperAnimation.fromTo('opacity', '0', '1');
            }
            else {
                const toPresentingScale = hasCardModal ? SwipeToCloseDefaults.MIN_PRESENTING_SCALE : 1;
                const finalTransform = `translateY(-10px) scale(${toPresentingScale})`;
                presentingAnimation
                    .afterStyles({
                    transform: finalTransform,
                })
                    .addElement(presentingElRoot.querySelector('.modal-wrapper'))
                    .keyframes([
                    { offset: 0, filter: 'contrast(1)', transform: 'translateY(0) scale(1)' },
                    { offset: 1, filter: 'contrast(0.85)', transform: finalTransform },
                ]);
                const shadowAnimation = createAnimation()
                    .afterStyles({
                    transform: finalTransform,
                })
                    .addElement(presentingElRoot.querySelector('.modal-shadow'))
                    .keyframes([
                    { offset: 0, opacity: '1', transform: 'translateY(0) scale(1)' },
                    { offset: 1, opacity: '0', transform: finalTransform },
                ]);
                baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
            }
        }
    }
    else {
        baseAnimation.addAnimation(backdropAnimation);
    }
    return baseAnimation;
};

const createLeaveAnimation$1 = () => {
    const backdropAnimation = createAnimation().fromTo('opacity', 'var(--backdrop-opacity)', 0);
    const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');
    return { backdropAnimation, wrapperAnimation };
};
/**
 * iOS Modal Leave Animation
 */
const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
    const { presentingEl, currentBreakpoint } = opts;
    const root = getElementRoot(baseEl);
    const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation$1();
    backdropAnimation.addElement(root.querySelector('ion-backdrop'));
    wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')).beforeStyles({ opacity: 1 });
    const baseAnimation = createAnimation('leaving-base')
        .addElement(baseEl)
        .easing('cubic-bezier(0.32,0.72,0,1)')
        .duration(duration)
        .addAnimation(wrapperAnimation);
    if (presentingEl) {
        const isPortrait = window.innerWidth < 768;
        const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
        const presentingElRoot = getElementRoot(presentingEl);
        const presentingAnimation = createAnimation()
            .beforeClearStyles(['transform'])
            .afterClearStyles(['transform'])
            .onFinish((currentStep) => {
            // only reset background color if this is the last card-style modal
            if (currentStep !== 1) {
                return;
            }
            presentingEl.style.setProperty('overflow', '');
            const numModals = Array.from(bodyEl.querySelectorAll('ion-modal:not(.overlay-hidden)')).filter((m) => m.presentingElement !== undefined).length;
            if (numModals <= 1) {
                bodyEl.style.setProperty('background-color', '');
            }
        });
        const bodyEl = document.body;
        if (isPortrait) {
            const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
            const modalTransform = hasCardModal ? '-10px' : transformOffset;
            const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
            const finalTransform = `translateY(${modalTransform}) scale(${toPresentingScale})`;
            presentingAnimation.addElement(presentingEl).keyframes([
                { offset: 0, filter: 'contrast(0.85)', transform: finalTransform, borderRadius: '10px 10px 0 0' },
                { offset: 1, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' },
            ]);
            baseAnimation.addAnimation(presentingAnimation);
        }
        else {
            baseAnimation.addAnimation(backdropAnimation);
            if (!hasCardModal) {
                wrapperAnimation.fromTo('opacity', '1', '0');
            }
            else {
                const toPresentingScale = hasCardModal ? SwipeToCloseDefaults.MIN_PRESENTING_SCALE : 1;
                const finalTransform = `translateY(-10px) scale(${toPresentingScale})`;
                presentingAnimation
                    .addElement(presentingElRoot.querySelector('.modal-wrapper'))
                    .afterStyles({
                    transform: 'translate3d(0, 0, 0)',
                })
                    .keyframes([
                    { offset: 0, filter: 'contrast(0.85)', transform: finalTransform },
                    { offset: 1, filter: 'contrast(1)', transform: 'translateY(0) scale(1)' },
                ]);
                const shadowAnimation = createAnimation()
                    .addElement(presentingElRoot.querySelector('.modal-shadow'))
                    .afterStyles({
                    transform: 'translateY(0) scale(1)',
                })
                    .keyframes([
                    { offset: 0, opacity: '0', transform: finalTransform },
                    { offset: 1, opacity: '1', transform: 'translateY(0) scale(1)' },
                ]);
                baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
            }
        }
    }
    else {
        baseAnimation.addAnimation(backdropAnimation);
    }
    return baseAnimation;
};

/**
 * Transition animation from portrait view to landscape view
 * This handles the case where a card modal is open in portrait view
 * and the user switches to landscape view
 */
const portraitToLandscapeTransition = (baseEl, opts, duration = 300) => {
    const { presentingEl } = opts;
    if (!presentingEl) {
        // No transition needed for non-card modals
        return createAnimation('portrait-to-landscape-transition');
    }
    const presentingElIsCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
    const presentingElRoot = getElementRoot(presentingEl);
    const bodyEl = document.body;
    const baseAnimation = createAnimation('portrait-to-landscape-transition')
        .addElement(baseEl)
        .easing('cubic-bezier(0.32,0.72,0,1)')
        .duration(duration);
    const presentingAnimation = createAnimation().beforeStyles({
        transform: 'translateY(0)',
        'transform-origin': 'top center',
        overflow: 'hidden',
    });
    if (!presentingElIsCardModal) {
        // The presenting element is not a card modal, so we do not
        // need to care about layering and modal-specific styles.
        const root = getElementRoot(baseEl);
        const wrapperAnimation = createAnimation()
            .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
            .fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
        const backdropAnimation = createAnimation()
            .addElement(root.querySelector('ion-backdrop'))
            .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
        // Animate presentingEl from portrait state back to normal
        const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
        const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
        const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
        presentingAnimation
            .addElement(presentingEl)
            .afterStyles({
            transform: 'translateY(0px) scale(1)',
            'border-radius': '0px',
        })
            .beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
            .fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
            .fromTo('filter', 'contrast(0.85)', 'contrast(1)')
            .fromTo('border-radius', '10px 10px 0 0', '0px');
        baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
    }
    else {
        // The presenting element is a card modal, so we do
        // need to care about layering and modal-specific styles.
        const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
        const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
        const toTransform = `translateY(0px) scale(1)`;
        presentingAnimation
            .addElement(presentingEl)
            .afterStyles({
            transform: toTransform,
        })
            .fromTo('transform', fromTransform, toTransform)
            .fromTo('filter', 'contrast(0.85)', 'contrast(1)');
        const shadowAnimation = createAnimation()
            .addElement(presentingElRoot.querySelector('.modal-shadow'))
            .afterStyles({
            transform: toTransform,
            opacity: '0',
        })
            .fromTo('transform', fromTransform, toTransform);
        baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
    }
    return baseAnimation;
};
/**
 * Transition animation from landscape view to portrait view
 * This handles the case where a card modal is open in landscape view
 * and the user switches to portrait view
 */
const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
    const { presentingEl } = opts;
    if (!presentingEl) {
        // No transition needed for non-card modals
        return createAnimation('landscape-to-portrait-transition');
    }
    const presentingElIsCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
    const presentingElRoot = getElementRoot(presentingEl);
    const bodyEl = document.body;
    const baseAnimation = createAnimation('landscape-to-portrait-transition')
        .addElement(baseEl)
        .easing('cubic-bezier(0.32,0.72,0,1)')
        .duration(duration);
    const presentingAnimation = createAnimation().beforeStyles({
        transform: 'translateY(0)',
        'transform-origin': 'top center',
        overflow: 'hidden',
    });
    if (!presentingElIsCardModal) {
        // The presenting element is not a card modal, so we do not
        // need to care about layering and modal-specific styles.
        const root = getElementRoot(baseEl);
        const wrapperAnimation = createAnimation()
            .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
            .fromTo('opacity', '1', '1'); // Keep wrapper visible
        const backdropAnimation = createAnimation()
            .addElement(root.querySelector('ion-backdrop'))
            .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
        // Animate presentingEl from normal state to portrait state
        const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
        const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
        const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
        presentingAnimation
            .addElement(presentingEl)
            .afterStyles({
            transform: toTransform,
        })
            .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
            .keyframes([
            { offset: 0, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '0px' },
            { offset: 0.2, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '10px 10px 0 0' },
            { offset: 1, transform: toTransform, filter: 'contrast(0.85)', borderRadius: '10px 10px 0 0' },
        ]);
        baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
    }
    else {
        // The presenting element is also a card modal, so we need
        // to handle layering and modal-specific styles.
        const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
        const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
        const toTransform = `translateY(0) scale(1)`;
        presentingAnimation
            .addElement(presentingEl)
            .afterStyles({
            transform: toTransform,
        })
            .fromTo('transform', fromTransform, toTransform);
        const shadowAnimation = createAnimation()
            .addElement(presentingElRoot.querySelector('.modal-shadow'))
            .afterStyles({
            transform: toTransform,
            opacity: '0',
        })
            .fromTo('transform', fromTransform, toTransform);
        baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
    }
    return baseAnimation;
};

const createEnterAnimation = () => {
    const backdropAnimation = createAnimation()
        .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
        .beforeStyles({
        'pointer-events': 'none',
    })
        .afterClearStyles(['pointer-events']);
    const wrapperAnimation = createAnimation().keyframes([
        { offset: 0, opacity: 0.01, transform: 'translateY(40px)' },
        { offset: 1, opacity: 1, transform: `translateY(0px)` },
    ]);
    return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
};
/**
 * Md Modal Enter Animation
 */
const mdEnterAnimation = (baseEl, opts) => {
    const { currentBreakpoint, expandToScroll } = opts;
    const root = getElementRoot(baseEl);
    const { wrapperAnimation, backdropAnimation, contentAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
    backdropAnimation.addElement(root.querySelector('ion-backdrop'));
    wrapperAnimation.addElement(root.querySelector('.modal-wrapper'));
    // The content animation is only added if scrolling is enabled for
    // all the breakpoints.
    !expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page'));
    const baseAnimation = createAnimation()
        .addElement(baseEl)
        .easing('cubic-bezier(0.36,0.66,0.04,1)')
        .duration(280)
        .addAnimation([backdropAnimation, wrapperAnimation]);
    if (contentAnimation) {
        baseAnimation.addAnimation(contentAnimation);
    }
    return baseAnimation;
};

const createLeaveAnimation = () => {
    const backdropAnimation = createAnimation().fromTo('opacity', 'var(--backdrop-opacity)', 0);
    const wrapperAnimation = createAnimation().keyframes([
        { offset: 0, opacity: 0.99, transform: `translateY(0px)` },
        { offset: 1, opacity: 0, transform: 'translateY(40px)' },
    ]);
    return { backdropAnimation, wrapperAnimation };
};
/**
 * Md Modal Leave Animation
 */
const mdLeaveAnimation = (baseEl, opts) => {
    const { currentBreakpoint } = opts;
    const root = getElementRoot(baseEl);
    const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
    backdropAnimation.addElement(root.querySelector('ion-backdrop'));
    wrapperAnimation.addElement(root.querySelector('.modal-wrapper'));
    const baseAnimation = createAnimation()
        .easing('cubic-bezier(0.47,0,0.745,0.715)')
        .duration(200)
        .addAnimation([backdropAnimation, wrapperAnimation]);
    return baseAnimation;
};

/** How far ahead, in milliseconds, a drag's velocity is projected when snapping. */
const FLICK_PROJECTION_MS = 350;
const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onDragStart, onDragMove, onDragEnd) => {
    // Defaults for the sheet swipe animation
    const defaultBackdrop = [
        { offset: 0, opacity: 'var(--backdrop-opacity)' },
        { offset: 1, opacity: 0.01 },
    ];
    const customBackdrop = [
        { offset: 0, opacity: 'var(--backdrop-opacity)' },
        { offset: 1 - backdropBreakpoint, opacity: 0 },
        { offset: 1, opacity: 0 },
    ];
    const SheetDefaults = {
        WRAPPER_KEYFRAMES: [
            { offset: 0, transform: 'translateY(0%)' },
            { offset: 1, transform: 'translateY(100%)' },
        ],
        BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
        CONTENT_KEYFRAMES: [
            { offset: 0, maxHeight: '100%' },
            { offset: 1, maxHeight: '0%' },
        ],
    };
    const contentEl = baseEl.querySelector('ion-content');
    // Cache the initial value so the gesture restores it instead of forcing scrolling on.
    const initialContentScrollY = contentEl?.scrollY ?? true;
    /**
     * The height of the sheet the user actually sees. This comes from the wrapper
     * rather than the host element, which is stretched to the full viewport no
     * matter how tall the sheet itself is.
     */
    const height = wrapperEl.clientHeight;
    let currentBreakpoint = initialBreakpoint;
    let offset = 0;
    let canDismissBlocksGesture = false;
    let cachedScrollEl = null;
    let cachedFooterEls = null;
    let cachedFooterYPosition = null;
    let currentFooterState = null;
    const canDismissMaxStep = 0.95;
    const maxBreakpoint = breakpoints[breakpoints.length - 1];
    const minBreakpoint = breakpoints[0];
    const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
    const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
    const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation');
    const enableBackdrop = () => {
        // Respect explicit opt-out of focus trapping/backdrop interactions
        // If focusTrap is false or showBackdrop is false, do not enable the backdrop or re-enable focus trap
        const el = baseEl;
        if (el.focusTrap === false || el.showBackdrop === false) {
            return;
        }
        baseEl.style.setProperty('pointer-events', 'auto');
        backdropEl.style.setProperty('pointer-events', 'auto');
        /**
         * When the backdrop is enabled, elements such
         * as inputs should not be focusable outside
         * the sheet.
         */
        baseEl.classList.remove(FOCUS_TRAP_DISABLE_CLASS);
    };
    const disableBackdrop = () => {
        baseEl.style.setProperty('pointer-events', 'none');
        backdropEl.style.setProperty('pointer-events', 'none');
        /**
         * When the backdrop is enabled, elements such
         * as inputs should not be focusable outside
         * the sheet.
         * Adding this class disables focus trapping
         * for the sheet temporarily.
         */
        baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
    };
    /**
     * Toggles the footer to an absolute position while moving to prevent
     * it from shaking while the sheet is being dragged.
     * @param newPosition Whether the footer is in a moving or stationary position.
     */
    const swapFooterPosition = (newPosition) => {
        if (!cachedFooterEls) {
            cachedFooterEls = Array.from(baseEl.querySelectorAll('ion-footer'));
            if (!cachedFooterEls.length) {
                return;
            }
        }
        const page = baseEl.querySelector('.ion-page');
        currentFooterState = newPosition;
        if (newPosition === 'stationary') {
            cachedFooterEls.forEach((cachedFooterEl) => {
                // Reset positioning styles to allow normal document flow
                cachedFooterEl.classList.remove('modal-footer-moving');
                cachedFooterEl.style.removeProperty('position');
                cachedFooterEl.style.removeProperty('width');
                cachedFooterEl.style.removeProperty('height');
                cachedFooterEl.style.removeProperty('top');
                cachedFooterEl.style.removeProperty('left');
                page?.style.removeProperty('padding-bottom');
                // Move to page
                page?.appendChild(cachedFooterEl);
            });
        }
        else {
            let footerHeights = 0;
            cachedFooterEls.forEach((cachedFooterEl, index) => {
                // Get both the footer and document body positions
                const cachedFooterElRect = cachedFooterEl.getBoundingClientRect();
                const bodyRect = document.body.getBoundingClientRect();
                // Calculate the total height of all footers
                // so we can add padding to the page element
                footerHeights += cachedFooterEl.clientHeight;
                // Calculate absolute position relative to body
                // We need to subtract the body's offsetTop to get true position within document.body
                const absoluteTop = cachedFooterElRect.top - bodyRect.top;
                const absoluteLeft = cachedFooterElRect.left - bodyRect.left;
                // Capture the footer's current dimensions and store them in CSS variables for
                // later use when applying absolute positioning.
                cachedFooterEl.style.setProperty('--pinned-width', `${cachedFooterEl.clientWidth}px`);
                cachedFooterEl.style.setProperty('--pinned-height', `${cachedFooterEl.clientHeight}px`);
                cachedFooterEl.style.setProperty('--pinned-top', `${absoluteTop}px`);
                cachedFooterEl.style.setProperty('--pinned-left', `${absoluteLeft}px`);
                // Only cache the first footer's Y position
                // This is used to determine if the sheet has been moved below the footer
                // and needs to be swapped back to stationary so it collapses correctly.
                if (index === 0) {
                    cachedFooterYPosition = absoluteTop;
                    // If there's a header, we need to combine the header height with the footer position
                    // because the header moves with the drag handle, so when it starts overlapping the footer,
                    // we need to account for that.
                    const header = baseEl.querySelector('ion-header');
                    if (header) {
                        cachedFooterYPosition -= header.clientHeight;
                    }
                }
            });
            // Apply the pinning of styles after we've calculated everything
            // so that we don't cause layouts to shift while calculating the footer positions.
            // Otherwise, with multiple footers we'll end up capturing the wrong positions.
            cachedFooterEls.forEach((cachedFooterEl) => {
                // Add padding to the parent element to prevent content from being hidden
                // when the footer is positioned absolutely. This has to be done before we
                // make the footer absolutely positioned or we may accidentally cause the
                // sheet to scroll.
                page?.style.setProperty('padding-bottom', `${footerHeights}px`);
                // Apply positioning styles to keep footer at bottom
                cachedFooterEl.classList.add('modal-footer-moving');
                // Apply our preserved styles to pin the footer
                cachedFooterEl.style.setProperty('position', 'absolute');
                cachedFooterEl.style.setProperty('width', 'var(--pinned-width)');
                cachedFooterEl.style.setProperty('height', 'var(--pinned-height)');
                cachedFooterEl.style.setProperty('top', 'var(--pinned-top)');
                cachedFooterEl.style.setProperty('left', 'var(--pinned-left)');
                // Move the element to the body when everything else is done
                document.body.appendChild(cachedFooterEl);
            });
        }
    };
    /**
     * After the entering animation completes,
     * we need to set the animation to go from
     * offset 0 to offset 1 so that users can
     * swipe in any direction. We then set the
     * animation offset to the current breakpoint
     * so there is no flickering.
     */
    if (wrapperAnimation && backdropAnimation) {
        wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
        backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
        contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
        animation.progressStart(true, 1 - currentBreakpoint);
        /**
         * If backdrop is not enabled, then content
         * behind modal should be clickable. To do this, we need
         * to remove pointer-events from ion-modal as a whole.
         * ion-backdrop and .modal-wrapper always have pointer-events: auto
         * applied, so the modal content can still be interacted with.
         */
        const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint &&
            baseEl.focusTrap !== false &&
            baseEl.showBackdrop !== false;
        if (shouldEnableBackdrop) {
            enableBackdrop();
        }
        else {
            disableBackdrop();
        }
    }
    if (contentEl && currentBreakpoint !== maxBreakpoint && expandToScroll) {
        contentEl.scrollY = false;
    }
    const canStart = (detail) => {
        /**
         * We cannot assume that the `ion-content` target will remain consistent between swipes.
         * For example, when using ion-nav within a modal it is possible to swipe, push a view,
         * and then swipe again. The target content will not be the same between swipes.
         */
        const contentEl = findClosestIonContent(detail.event.target);
        currentBreakpoint = getCurrentBreakpoint();
        /**
         * Upwards swipes on the content cannot move the sheet anyway, so this only
         * blocks swiping the sheet down from the content.
         */
        if (contentEl && (!expandToScroll || currentBreakpoint === 1)) {
            return canSwipeOnContent(contentEl);
        }
        return true;
    };
    const onStart = (detail) => {
        /**
         * Firefox automatically selects the header text during drag
         * due to the focusable wrapper (tabindex="-1"). Remove any
         * selection that may have occurred.
         */
        window.getSelection()?.removeAllRanges();
        /**
         * If canDismiss is anything other than `true`
         * then users should be able to swipe down
         * until a threshold is hit. At that point,
         * the card modal should not proceed any further.
         *
         * canDismiss is never fired via gesture if there is
         * no 0 breakpoint. However, it can be fired if the user
         * presses Esc or the hardware back button.
         * TODO (FW-937)
         * Remove undefined check
         */
        canDismissBlocksGesture = baseEl.canDismiss !== undefined && baseEl.canDismiss !== true && minBreakpoint === 0;
        /**
         * Cache the scroll element reference when the gesture starts,
         * this allows us to avoid querying the DOM for the target in onMove,
         * which would impact performance significantly.
         */
        if (!expandToScroll) {
            const targetEl = findClosestIonContent(detail.event.target);
            cachedScrollEl =
                targetEl && isIonContent(targetEl) ? getElementRoot(targetEl).querySelector('.inner-scroll') : targetEl;
        }
        /**
         * If expandToScroll is disabled, we need to swap
         * the footer position to moving so that it doesn't shake
         * while the sheet is being dragged.
         */
        if (!expandToScroll) {
            swapFooterPosition('moving');
        }
        /**
         * If we are pulling down, then it is possible we are pulling on the content.
         * We do not want scrolling to happen at the same time as the gesture.
         */
        if (detail.deltaY > 0 && contentEl) {
            contentEl.scrollY = false;
        }
        raf(() => {
            /**
             * Dismisses the open keyboard when the sheet drag gesture is started.
             * Sets the focus onto the modal element.
             */
            baseEl.focus();
        });
        animation.progressStart(true, 1 - currentBreakpoint);
        onDragStart();
    };
    const onMove = (detail) => {
        /**
         * If `expandToScroll` is disabled, we need to see if we're currently below
         * the footer element and the footer is in a stationary position. If so,
         * we need to make the stationary the original position so that the footer
         * collapses with the sheet.
         */
        if (!expandToScroll && cachedFooterYPosition !== null && currentFooterState !== null) {
            // Check if we need to swap the footer position
            if (detail.currentY >= cachedFooterYPosition && currentFooterState === 'moving') {
                swapFooterPosition('stationary');
            }
            else if (detail.currentY < cachedFooterYPosition && currentFooterState === 'stationary') {
                swapFooterPosition('moving');
            }
        }
        /**
         * If `expandToScroll` is disabled, and an upwards swipe gesture is done within
         * the scrollable content, we should not allow the swipe gesture to continue.
         */
        if (!expandToScroll && detail.deltaY <= 0 && cachedScrollEl) {
            return;
        }
        /**
         * If we are pulling down, then it is possible we are pulling on the content.
         * We do not want scrolling to happen at the same time as the gesture.
         * This accounts for when the user scrolls down, scrolls all the way up, and then
         * pulls down again such that the modal should start to move.
         */
        if (detail.deltaY > 0 && contentEl) {
            contentEl.scrollY = false;
        }
        /**
         * Given the change in gesture position on the Y axis,
         * compute where the offset of the animation should be
         * relative to where the user dragged.
         */
        const initialStep = 1 - currentBreakpoint;
        const secondToLastBreakpoint = breakpoints.length > 1 ? 1 - breakpoints[1] : undefined;
        const step = initialStep + detail.deltaY / height;
        const isAttemptingDismissWithCanDismiss = secondToLastBreakpoint !== undefined && step >= secondToLastBreakpoint && canDismissBlocksGesture;
        /**
         * If we are blocking the gesture from dismissing,
         * set the max step value so that the sheet cannot be
         * completely hidden.
         */
        const maxStep = isAttemptingDismissWithCanDismiss ? canDismissMaxStep : 0.9999;
        /**
         * If we are blocking the gesture from
         * dismissing, calculate the spring modifier value
         * this will be added to the starting breakpoint
         * value to give the gesture a spring-like feeling.
         * Note that when isAttemptingDismissWithCanDismiss is true,
         * the modifier is always added to the breakpoint that
         * appears right after the 0 breakpoint.
         *
         * Note that this modifier is essentially the progression
         * between secondToLastBreakpoint and maxStep which is
         * why we subtract secondToLastBreakpoint. This lets us get
         * the result as a value from 0 to 1.
         */
        const processedStep = isAttemptingDismissWithCanDismiss && secondToLastBreakpoint !== undefined
            ? secondToLastBreakpoint +
                calculateSpringStep((step - secondToLastBreakpoint) / (maxStep - secondToLastBreakpoint))
            : step;
        offset = clamp(0.0001, processedStep, maxStep);
        animation.progressStep(offset);
        const snapBreakpoint = calculateSnapBreakpoint(detail.deltaY, detail.velocityY);
        const eventDetail = {
            currentY: detail.currentY,
            deltaY: detail.deltaY,
            velocityY: detail.velocityY,
            progress: calculateEventProgress(detail.deltaY),
            snapBreakpoint: snapBreakpoint,
        };
        onDragMove(eventDetail);
    };
    const onEnd = (detail) => {
        const snapBreakpoint = calculateSnapBreakpoint(detail.deltaY, detail.velocityY);
        /**
         * `snapBreakpoint === 0` is not enough on its own. `canDismiss: false`
         * snaps to 0 but never dismisses, since `handleCanDismiss` returns early
         * when canDismiss is not a function. A canDismiss function dismisses
         * through `handleCanDismiss`, which awaits the callback, so the outcome
         * is not known when this event is emitted.
         */
        const shouldPreventDismiss = canDismissBlocksGesture && snapBreakpoint === 0;
        const isDismissing = shouldPreventDismiss ? typeof baseEl.canDismiss === 'function' : snapBreakpoint === 0;
        const eventDetail = {
            currentY: detail.currentY,
            deltaY: detail.deltaY,
            velocityY: detail.velocityY,
            progress: calculateEventProgress(detail.deltaY),
            snapBreakpoint,
            isDismissing,
        };
        /**
         * If expandToScroll is disabled, we should not allow the moveSheetToBreakpoint
         * function to be called if the user is trying to swipe content upwards and the content
         * is not scrolled to the top.
         */
        if (!expandToScroll && detail.deltaY <= 0 && cachedScrollEl && cachedScrollEl.scrollTop > 0) {
            /**
             * If expand to scroll is disabled, we need to make sure we swap the footer position
             * back to stationary so that it will collapse correctly if the modal is dismissed without
             * dragging (e.g. through a dismiss button).
             * This can cause issues if the user has a modal with content that can be dragged, as we'll
             * swap to moving on drag and if we don't swap back here then the footer will get stuck.
             */
            swapFooterPosition('stationary');
            /**
             * The sheet does not move in this case, so the gesture
             * never dismisses the modal.
             */
            onDragEnd({ ...eventDetail, isDismissing: false });
            return;
        }
        moveSheetToBreakpoint({
            breakpoint: snapBreakpoint,
            breakpointOffset: offset,
            canDismiss: canDismissBlocksGesture,
            /**
             * The swipe is user-driven, so we should
             * always animate when the gesture ends.
             */
            animated: true,
        });
        onDragEnd(eventDetail);
    };
    const moveSheetToBreakpoint = (options) => {
        const { breakpoint, canDismiss, breakpointOffset, animated } = options;
        /**
         * canDismiss should only prevent snapping
         * when users are trying to dismiss. If canDismiss
         * is present but the user is trying to swipe upwards,
         * we should allow that to happen,
         */
        const shouldPreventDismiss = canDismiss && breakpoint === 0;
        const snapToBreakpoint = shouldPreventDismiss ? currentBreakpoint : breakpoint;
        const shouldRemainOpen = snapToBreakpoint !== 0;
        currentBreakpoint = 0;
        /**
         * Update the animation so that it plays from
         * the last offset to the closest snap point.
         */
        if (wrapperAnimation && backdropAnimation) {
            wrapperAnimation.keyframes([
                { offset: 0, transform: `translateY(${breakpointOffset * 100}%)` },
                { offset: 1, transform: `translateY(${(1 - snapToBreakpoint) * 100}%)` },
            ]);
            backdropAnimation.keyframes([
                {
                    offset: 0,
                    opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(1 - breakpointOffset, backdropBreakpoint)})`,
                },
                {
                    offset: 1,
                    opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(snapToBreakpoint, backdropBreakpoint)})`,
                },
            ]);
            if (contentAnimation) {
                /**
                 * The modal content should scroll at any breakpoint when expandToScroll
                 * is disabled. In order to do this, the content needs to be completely
                 * viewable so scrolling can access everything. Otherwise, the default
                 * behavior would show the content off the screen and only allow
                 * scrolling when the sheet is fully expanded.
                 */
                contentAnimation.keyframes([
                    { offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
                    { offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
                ]);
            }
            animation.progressStep(0);
        }
        /**
         * Gesture should remain disabled until the
         * snapping animation completes.
         */
        gesture.enable(false);
        if (shouldPreventDismiss) {
            handleCanDismiss(baseEl, animation);
        }
        else if (!shouldRemainOpen) {
            onDismiss();
        }
        /**
         * Restores the content's scroll setting immediately if the sheet is about to
         * fully expand or if it allows scrolling at any breakpoint. Without this, there would
         * be a ~500ms delay while the modal animation completes, causing a
         * noticeable lag. Native iOS allows scrolling as soon as the gesture is
         * released, so we align with that behavior.
         */
        if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !expandToScroll)) {
            contentEl.scrollY = initialContentScrollY;
        }
        /**
         * If expandToScroll is disabled and we're animating
         * to close the sheet, we need to swap
         * the footer position to stationary so that it
         * will collapse correctly. We cannot just always swap
         * here or it'll be jittery while animating movement.
         */
        if (!expandToScroll && snapToBreakpoint === 0) {
            swapFooterPosition('stationary');
        }
        return new Promise((resolve) => {
            animation
                .onFinish(() => {
                if (shouldRemainOpen) {
                    /**
                     * If expandToScroll is disabled, we need to swap
                     * the footer position to stationary so that it
                     * will act as it would by default.
                     */
                    if (!expandToScroll) {
                        swapFooterPosition('stationary');
                    }
                    /**
                     * Once the snapping animation completes,
                     * we need to reset the animation to go
                     * from 0 to 1 so users can swipe in any direction.
                     * We then set the animation offset to the current
                     * breakpoint so that it starts at the snapped position.
                     */
                    if (wrapperAnimation && backdropAnimation) {
                        raf(() => {
                            wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
                            backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
                            contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
                            animation.progressStart(true, 1 - snapToBreakpoint);
                            currentBreakpoint = snapToBreakpoint;
                            onBreakpointChange(currentBreakpoint);
                            /**
                             * Backdrop should become enabled
                             * after the backdropBreakpoint value
                             */
                            const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint &&
                                baseEl.focusTrap !== false &&
                                baseEl.showBackdrop !== false;
                            if (shouldEnableBackdrop) {
                                enableBackdrop();
                            }
                            else {
                                disableBackdrop();
                            }
                            gesture.enable(true);
                            resolve();
                        });
                    }
                    else {
                        gesture.enable(true);
                        resolve();
                    }
                }
                else {
                    resolve();
                }
                /**
                 * This must be a one time callback
                 * otherwise a new callback will
                 * be added every time onEnd runs.
                 */
            }, { oneTimeCallback: true })
                .progressEnd(1, 0, animated ? 500 : 0);
        });
    };
    /**
     * Decides which breakpoint the sheet should settle on for the current drag.
     *
     * @param deltaY The change in Y position since the gesture started
     * @param velocityY The velocity in pixels per millisecond
     * @returns The snap breakpoint value
     */
    const calculateSnapBreakpoint = (deltaY, velocityY) => {
        // Project the drag forward so a quick flick counts.
        const currentProgress = calculateProgress(deltaY + velocityY * FLICK_PROJECTION_MS);
        /**
         * Find and return the defined breakpoint that is closest to the
         * current progress.
         */
        const snapBreakpoint = breakpoints.reduce((a, b) => {
            return Math.abs(b - currentProgress) < Math.abs(a - currentProgress) ? b : a;
        });
        return snapBreakpoint;
    };
    /**
     * Calculates how open the sheet is part way through a gesture, on the same
     * 0 to 1 scale as the breakpoints: 1 is fully open and 0 is fully closed.
     *
     * @param deltaY The change in Y position since the gesture started
     * @returns The progress of the sheet gesture
     */
    const calculateProgress = (deltaY) => {
        // Mirrors the animation step in onMove, so the snap matches where the sheet is drawn.
        const progress = currentBreakpoint - deltaY / height;
        // Round to the nearest thousandth to avoid returning very small decimal
        const roundedProgress = Math.round(progress * 1000) / 1000;
        return clamp(0, roundedProgress, 1);
    };
    /**
     * Scales the progress from the lowest to the highest breakpoint, so the
     * drag events report 0 at the lowest breakpoint and 1 at the highest.
     */
    const calculateEventProgress = (deltaY) => {
        const progress = (calculateProgress(deltaY) - minBreakpoint) / (maxBreakpoint - minBreakpoint);
        return clamp(0, progress, 1);
    };
    const gesture = createGesture({
        el: wrapperEl,
        gestureName: 'modalSheet',
        gesturePriority: 40,
        direction: 'y',
        threshold: 10,
        canStart,
        onStart,
        onMove,
        onEnd,
    });
    /**
     * Puts the content back the way the app declared it. A sheet can dismiss
     * without going through moveSheetToBreakpoint, and an inline modal reuses
     * the same element on the next present.
     */
    const resetContentScroll = () => {
        if (contentEl) {
            contentEl.scrollY = initialContentScrollY;
        }
    };
    return {
        gesture,
        moveSheetToBreakpoint,
        resetContentScroll,
    };
};

const EDGE_THRESHOLD = 5;
/**
 * Cache for resolved root safe-area-top value, invalidated once per frame.
 */
let cachedRootSafeAreaTop = null;
let cacheInvalidationScheduled = false;
/**
 * Resolves the current root --ion-safe-area-top value to pixels.
 * Uses a temporary element because getComputedStyle on :root returns
 * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
 * rather than a resolved number.
 *
 * Results are cached for the current frame to avoid repeated reflows.
 */
const getRootSafeAreaTop = () => {
    if (cachedRootSafeAreaTop !== null) {
        return cachedRootSafeAreaTop;
    }
    const doc = win?.document;
    if (!doc?.body) {
        return 0;
    }
    const el = doc.createElement('div');
    el.style.cssText =
        'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
    doc.body.appendChild(el);
    const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
    el.remove();
    cachedRootSafeAreaTop = value;
    if (!cacheInvalidationScheduled) {
        cacheInvalidationScheduled = true;
        raf(() => {
            cachedRootSafeAreaTop = null;
            cacheInvalidationScheduled = false;
        });
    }
    return value;
};
/**
 * Calls back when the resolved root `--ion-safe-area-top` changes. The value
 * the caller already applied is passed as the baseline, so a change between
 * that read and the observer starting is still reported.
 */
const onRootSafeAreaTopChange = (callback) => {
    return onCustomPropertyChange(win?.document?.body, '--ion-safe-area-top', callback, getRootSafeAreaTop());
};
/**
 * Determines which viewport axes the modal spans so safe-area requirements
 * can be predicted independently for each axis.
 *
 * A modal that spans an axis reaches both edges on that axis and needs the
 * corresponding safe-area insets. A modal that does not span an axis reaches
 * neither edge on that axis.
 *
 * When both `--width` and `--height` are `fullscreen`, coverage can be
 * determined directly. Otherwise, coverage is based on the rendered wrapper,
 * including cases where content sizing or `--max-height` causes the modal
 * to reach the viewport.
 */
const getModalCoveredAxes = (hostEl) => {
    const styles = getComputedStyle(hostEl);
    const width = getOverlaySizeType(styles.getPropertyValue('--width'));
    const height = getOverlaySizeType(styles.getPropertyValue('--height'));
    if (width === 'fullscreen' && height === 'fullscreen') {
        return { vertical: true, horizontal: true };
    }
    return measureCoveredAxes(hostEl);
};
/**
 * Measures the modal wrapper to determine whether it spans the viewport
 * on each axis.
 *
 * The wrapper has no box while the modal is hidden, so `overlay-hidden`
 * is temporarily removed to allow the wrapper to be measured. The class
 * is restored in the same task before the browser can paint.
 *
 * Only the wrapper's size is measured. Its position is affected by the
 * enter animation, which initially translates it by its own height, while
 * the translation does not affect its measured size.
 */
const measureCoveredAxes = (hostEl) => {
    const wrapperEl = hostEl.shadowRoot?.querySelector('.modal-wrapper');
    if (wrapperEl == null || win === undefined) {
        return { vertical: false, horizontal: false };
    }
    const wasHidden = hostEl.classList.contains('overlay-hidden');
    if (wasHidden) {
        hostEl.classList.remove('overlay-hidden');
    }
    const { width, height } = wrapperEl.getBoundingClientRect();
    if (wasHidden) {
        hostEl.classList.add('overlay-hidden');
    }
    return {
        vertical: height >= win.innerHeight - EDGE_THRESHOLD,
        horizontal: width >= win.innerWidth - EDGE_THRESHOLD,
    };
};
/**
 * Returns the initial safe-area configuration based on modal type.
 * This is called before animation starts and uses configuration-based prediction.
 *
 * @param context - Modal context information
 * @returns SafeAreaConfig with initial safe-area values
 */
const getInitialSafeAreaConfig = (context) => {
    const { isSheetModal, isCardModal } = context;
    // Sheet modals always zero top safe-area. The sheet height offset from the
    // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
    // the resolved root value, so --ion-safe-area-top is never needed for
    // height calculation. Keeping it at 0px prevents header content from
    // getting double-offset padding.
    if (isSheetModal) {
        return {
            top: '0px',
            bottom: 'inherit',
            left: '0px',
            right: '0px',
        };
    }
    // Card modals need safe-area for height calculation.
    // Note: isCardModal is already gated on mode === 'ios' by the caller.
    if (isCardModal) {
        return {
            top: 'inherit',
            bottom: 'inherit',
            left: '0px',
            right: '0px',
        };
    }
    /**
     * Each axis is evaluated independently because a modal can span one axis
     * without spanning the other. This allows the initial safe-area configuration
     * to match the modal's expected dimensions and avoids correcting an incorrect
     * pair of insets after presentation.
     *
     * A modal can span the horizontal axis while remaining inset vertically, or
     * span the vertical axis while remaining inset horizontally. Wide viewports
     * can also render regular modals as centered dialogs, while content-sized
     * modals may still be clamped to the viewport.
     */
    const { vertical, horizontal } = context.coveredAxes ?? { vertical: true, horizontal: true };
    return {
        top: vertical ? 'inherit' : '0px',
        bottom: vertical ? 'inherit' : '0px',
        left: horizontal ? 'inherit' : '0px',
        right: horizontal ? 'inherit' : '0px',
    };
};
/**
 * Returns safe-area configuration based on actual modal position.
 * Detects which edges the modal overlaps with and only applies safe-area to those edges.
 *
 * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
 * inconsistent values. Sheet and card modals avoid this by using configuration-based
 * prediction instead. Regular modals use coordinate detection which works reliably
 * on web and iOS; Android edge-to-edge may need a configuration-based fallback
 * once a reliable detection mechanism is available.
 *
 * @param wrapperEl - The modal wrapper element to measure
 * @returns SafeAreaConfig based on position
 */
const getPositionBasedSafeAreaConfig = (wrapperEl) => {
    const rect = wrapperEl.getBoundingClientRect();
    const vh = win?.innerHeight ?? 0;
    const vw = win?.innerWidth ?? 0;
    // Only apply safe-area to sides where modal overlaps with screen edge
    return {
        top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
        bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
        left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
        right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
    };
};
/**
 * Applies safe-area CSS custom property overrides to the modal host element.
 *
 * @param hostEl - The modal host element (ion-modal)
 * @param config - Safe-area configuration to apply
 */
const applySafeAreaOverrides = (hostEl, config) => {
    hostEl.style.setProperty('--ion-safe-area-top', config.top);
    hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
    hostEl.style.setProperty('--ion-safe-area-left', config.left);
    hostEl.style.setProperty('--ion-safe-area-right', config.right);
};
/**
 * Clears safe-area CSS custom property overrides from the modal host element.
 *
 * @param hostEl - The modal host element (ion-modal)
 */
const clearSafeAreaOverrides = (hostEl) => {
    hostEl.style.removeProperty('--ion-safe-area-top');
    hostEl.style.removeProperty('--ion-safe-area-bottom');
    hostEl.style.removeProperty('--ion-safe-area-left');
    hostEl.style.removeProperty('--ion-safe-area-right');
};

const modalIosCss = () => `:host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:100%;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-wrapper{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;outline:none}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:""}:host(.modal-sheet){--height:calc(100% - (var(--ion-modal-offset-top, 0px) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0,  100%,  0);transform:translate3d(0,  100%,  0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}`;

const modalMdCss = () => `:host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:100%;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-wrapper{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;outline:none}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:""}:host(.modal-sheet){--height:calc(100% - (var(--ion-modal-offset-top, 0px) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0,  40px,  0);transform:translate3d(0,  40px,  0);opacity:0.01}`;

const Modal = class {
    constructor(hostRef) {
        registerInstance(this, hostRef);
        this.didPresent = createEvent(this, "ionModalDidPresent", 7);
        this.willPresent = createEvent(this, "ionModalWillPresent", 7);
        this.willDismiss = createEvent(this, "ionModalWillDismiss", 7);
        this.didDismiss = createEvent(this, "ionModalDidDismiss", 7);
        this.ionBreakpointDidChange = createEvent(this, "ionBreakpointDidChange", 7);
        this.didPresentShorthand = createEvent(this, "didPresent", 7);
        this.willPresentShorthand = createEvent(this, "willPresent", 7);
        this.willDismissShorthand = createEvent(this, "willDismiss", 7);
        this.didDismissShorthand = createEvent(this, "didDismiss", 7);
        this.ionMount = createEvent(this, "ionMount", 7);
        this.ionDragStart = createEvent(this, "ionDragStart", 7);
        this.ionDragMove = createEvent(this, "ionDragMove", 7);
        this.ionDragEnd = createEvent(this, "ionDragEnd", 7);
        this.lockController = createLockController();
        this.triggerController = createTriggerController();
        this.coreDelegate = CoreDelegate();
        this.isSheetModal = false;
        this.inheritedAttributes = {};
        this.inline = false;
        // Whether or not modal is being dismissed via gesture
        this.gestureAnimationDismissing = false;
        // True from the first safe-area write in `present()` until the enter
        // animation settles. A position-based read in that window is not the rest position.
        this.isPresenting = false;
        this.presented = false;
        /** @internal */
        this.hasController = false;
        /**
         * If `true`, the keyboard will be automatically dismissed when the overlay is presented.
         */
        this.keyboardClose = true;
        /**
         * Controls whether scrolling or dragging within the sheet modal expands
         * it to a larger breakpoint. This only takes effect when `breakpoints`
         * and `initialBreakpoint` are set.
         *
         * If `true`, scrolling or dragging anywhere in the modal will first expand
         * it to the next breakpoint. Once fully expanded, scrolling will affect the
         * content.
         * If `false`, scrolling will always affect the content. The modal will
         * only expand when dragging the header or handle. The modal will close when
         * dragging the header or handle. It can also be closed when dragging the
         * content, but only if the content is scrolled to the top.
         */
        this.expandToScroll = true;
        /**
         * A decimal value between 0 and 1 that indicates the
         * point after which the backdrop will begin to fade in
         * when using a sheet modal. Prior to this point, the
         * backdrop will be hidden and the content underneath
         * the sheet can be interacted with. This value is exclusive
         * meaning the backdrop will become active after the value
         * specified.
         */
        this.backdropBreakpoint = 0;
        /**
         * The interaction behavior for the sheet modal when the handle is pressed.
         *
         * Handle behavior is unavailable when the `handle` property is set to
         * `false` or when the `breakpoints` property is not set (using a
         * fullscreen or card modal).
         *
         * Set to `"cycle"` to make the handle focusable and let the sheet modal
         * cycle between available breakpoints when pressed. This keeps the sheet
         * operable with assistive technology.
         *
         * Set to `"none"` to make the handle purely decorative when pressed and
         * removed from the tab order.
         *
         * Defaults to `"cycle"`.
         */
        this.handleBehavior = 'cycle';
        /**
         * If `true`, the modal will be dismissed when the backdrop is clicked.
         */
        this.backdropDismiss = true;
        /**
         * If `true`, a backdrop will be displayed behind the modal.
         * This property controls whether or not the backdrop
         * darkens the screen when the modal is presented.
         * It does not control whether or not the backdrop
         * is active or present in the DOM.
         */
        this.showBackdrop = true;
        /**
         * If `true`, the modal will animate.
         */
        this.animated = true;
        /**
         * If `true`, the modal will open. If `false`, the modal will close.
         * Use this if you need finer grained control over presentation, otherwise
         * just use the modalController or the `trigger` property.
         * Note: `isOpen` will not automatically be set back to `false` when
         * the modal dismisses. You will need to do that in your code.
         */
        this.isOpen = false;
        /**
         * If `true`, the component passed into `ion-modal` will
         * automatically be mounted when the modal is created. The
         * component will remain mounted even when the modal is dismissed.
         * However, the component will be destroyed when the modal is
         * destroyed. This property is not reactive and should only be
         * used when initially creating a modal.
         *
         * Note: This feature only applies to inline modals in JavaScript
         * frameworks such as Angular, React, and Vue.
         */
        this.keepContentsMounted = false;
        /**
         * If `true`, focus will not be allowed to move outside of this overlay.
         * If `false`, focus will be allowed to move outside of the overlay.
         *
         * In most scenarios this property should remain set to `true`. Setting
         * this property to `false` can cause severe accessibility issues as users
         * relying on assistive technologies may be able to move focus into
         * a confusing state. We recommend only setting this to `false` when
         * absolutely necessary.
         *
         * Developers may want to consider disabling focus trapping if this
         * overlay presents a non-Ionic overlay from a 3rd party library.
         * Developers would disable focus trapping on the Ionic overlay
         * when presenting the 3rd party overlay and then re-enable
         * focus trapping when dismissing the 3rd party overlay and moving
         * focus back to the Ionic overlay.
         */
        this.focusTrap = true;
        /**
         * Determines whether or not a modal can dismiss
         * when calling the `dismiss` method.
         *
         * If the value is `true` or the value's function returns `true`, the modal will close when trying to dismiss.
         * If the value is `false` or the value's function returns `false`, the modal will not close when trying to dismiss.
         *
         * See https://ionicframework.com/docs/troubleshooting/runtime#accessing-this
         * if you need to access `this` from within the callback.
         */
        this.canDismiss = true;
        this.onHandleClick = () => {
            const { sheetTransition, handleBehavior } = this;
            if (handleBehavior !== 'cycle' || sheetTransition !== undefined) {
                /**
                 * The sheet modal should not advance to the next breakpoint
                 * if the handle behavior is not `cycle` or if the handle
                 * is clicked while the sheet is moving to a breakpoint.
                 */
                return;
            }
            this.moveToNextBreakpoint();
        };
        this.onBackdropTap = () => {
            const { sheetTransition } = this;
            if (sheetTransition !== undefined) {
                /**
                 * When the handle is double clicked at the largest breakpoint,
                 * it will start to move to the first breakpoint. While transitioning,
                 * the backdrop will often receive the second click. We prevent the
                 * backdrop from dismissing the modal while moving between breakpoints.
                 */
                return;
            }
            this.dismiss(undefined, BACKDROP);
        };
        this.onLifecycle = (modalEvent) => {
            const el = this.usersElement;
            const name = LIFECYCLE_MAP[modalEvent.type];
            if (el && name) {
                const ev = new CustomEvent(name, {
                    bubbles: false,
                    cancelable: false,
                    detail: modalEvent.detail,
                });
                el.dispatchEvent(ev);
            }
        };
        /**
         * When the modal receives focus directly, pass focus to the handle
         * if it exists and is focusable, otherwise let the focus trap handle it.
         */
        this.onModalFocus = (ev) => {
            const { dragHandleEl, el } = this;
            /**
             * Shadow DOM focus is retargeted to the host, so `ev.target === el` is also
             * true when a shadow child (the dialog wrapper present() focuses) is focused.
             * shadowRoot's activeElement is null only when the host was focused directly,
             * so redirect to the handle only then and leave present()'s wrapper focus intact.
             */
            if (ev.target === el && el.shadowRoot?.activeElement == null && dragHandleEl && dragHandleEl.tabIndex !== -1) {
                dragHandleEl.focus();
            }
        };
        /**
         * When the slot changes, we need to find all the modals in the slot
         * and set the data-parent-ion-modal attribute on them so we can find them
         * and dismiss them when we get dismissed.
         * We need to do it this way because when a modal is opened, it's moved to
         * the end of the body and is no longer an actual child of the modal.
         */
        this.onSlotChange = ({ target }) => {
            const slot = target;
            slot.assignedElements().forEach((el) => {
                el.querySelectorAll('ion-modal').forEach((childModal) => {
                    // We don't need to write to the DOM if the modal is already tagged
                    // If this is a deeply nested modal, this effect should cascade so we don't
                    // need to worry about another modal claiming the same child.
                    if (childModal.getAttribute('data-parent-ion-modal') === null) {
                        childModal.setAttribute('data-parent-ion-modal', this.el.id);
                    }
                });
            });
        };
    }
    onIsOpenChange(newValue, oldValue) {
        if (newValue === true && oldValue === false) {
            this.present();
        }
        else if (newValue === false && oldValue === true) {
            this.dismiss();
        }
    }
    triggerChanged() {
        const { trigger, el, triggerController } = this;
        if (trigger) {
            triggerController.addClickListener(el, trigger);
        }
    }
    onWindowResize() {
        if (!this.presented)
            return;
        clearTimeout(this.resizeTimeout);
        this.resizeTimeout = setTimeout(() => {
            const context = this.getSafeAreaContext();
            // iOS card modals: handle portrait/landscape view transitions
            if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
                this.handleViewTransition();
            }
            // Sheet modals: re-compute the internal offset property since safe-area
            // values may change on device rotation (e.g., portrait notch vs landscape).
            if (context.isSheetModal) {
                this.updateSheetOffsetTop();
            }
            // Regular (non-sheet, non-card) modals: update safe-area overrides
            // since the viewport may have crossed the centered-dialog breakpoint.
            if (!context.isSheetModal && !context.isCardModal) {
                this.updateSafeAreaOverrides();
                // Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
                const { contentEl, hasFooter } = this.findContentAndFooter();
                this.clearContentSafeAreaPadding(contentEl);
                this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
            }
        }, 50); // Debounce to avoid excessive calls during active resizing
    }
    breakpointsChanged(breakpoints) {
        if (breakpoints !== undefined) {
            this.sortedBreakpoints = breakpoints.sort((a, b) => a - b);
        }
    }
    connectedCallback() {
        const { el } = this;
        prepareOverlay(el);
        this.triggerChanged();
        // A disconnect tears down state this presentation still needs and cannot
        // tell a re-insert from a removal, so put it back.
        if (this.presented || this.isPresenting) {
            this.restoreSafeAreaOverrides();
        }
        // A relocation from `willPresent` reaches these before `present()` has
        // applied the lock, so they apply it instead. Both are idempotent.
        if (this.presented) {
            restoreRootFocusTrapAccessibility(el);
            this.initParentRemovalObserver();
        }
    }
    disconnectedCallback() {
        this.triggerController.removeClickListener();
        this.cleanupViewTransitionListener();
        this.cleanupParentRemovalObserver();
        // Also called in dismiss() — intentional dual cleanup covers both
        // dismiss-then-remove and direct DOM removal without dismiss.
        this.cleanupSafeAreaOverrides();
        // Clean up aria-hidden if removed without dismiss() being called
        if (this.presented) {
            cleanupRootFocusTrapAccessibility();
        }
    }
    componentWillLoad() {
        const { breakpoints, initialBreakpoint, el, htmlAttributes } = this;
        const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
        const attributesToInherit = ['aria-label', 'role'];
        this.inheritedAttributes = inheritAttributes(el, attributesToInherit);
        // Cache original parent before modal gets moved to body during presentation
        if (el.parentNode) {
            this.cachedOriginalParent = el.parentNode;
        }
        /**
         * When using a controller modal you can set attributes
         * using the htmlAttributes property. Since the above attributes
         * need to be inherited inside of the modal, we need to look
         * and see if these attributes are being set via htmlAttributes.
         *
         * We could alternatively move this to componentDidLoad to simplify the work
         * here, but we'd then need to make inheritedAttributes a State variable,
         * thus causing another render to always happen after the first render.
         */
        if (htmlAttributes !== undefined) {
            attributesToInherit.forEach((attribute) => {
                const attributeValue = htmlAttributes[attribute];
                if (attributeValue) {
                    /**
                     * If an attribute we need to inherit was
                     * set using htmlAttributes then add it to
                     * inheritedAttributes and remove it from htmlAttributes.
                     * This ensures the attribute is inherited and not
                     * set on the host.
                     *
                     * In this case, if an inherited attribute is set
                     * on the host element and using htmlAttributes then
                     * htmlAttributes wins, but that's not a pattern that we recommend.
                     * The only time you'd need htmlAttributes is when using modalController.
                     */
                    this.inheritedAttributes = {
                        ...this.inheritedAttributes,
                        [attribute]: htmlAttributes[attribute],
                    };
                    delete htmlAttributes[attribute];
                }
            });
        }
        if (isSheetModal) {
            this.currentBreakpoint = this.initialBreakpoint;
        }
        if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
            printIonWarning('[ion-modal] - Your breakpoints array must include the initialBreakpoint value.');
        }
        if (!this.htmlAttributes?.id) {
            setOverlayId(this.el);
        }
    }
    componentDidLoad() {
        /**
         * If modal was rendered with isOpen="true"
         * then we should open modal immediately.
         */
        if (this.isOpen === true) {
            raf(() => this.present());
        }
        this.breakpointsChanged(this.breakpoints);
        /**
         * When binding values in frameworks such as Angular
         * it is possible for the value to be set after the Web Component
         * initializes but before the value watcher is set up in Stencil.
         * As a result, the watcher callback may not be fired.
         * We work around this by manually calling the watcher
         * callback when the component has loaded and the watcher
         * is configured.
         */
        this.triggerChanged();
    }
    /**
     * Determines whether or not an overlay
     * is being used inline or via a controller/JS
     * and returns the correct delegate.
     * By default, subsequent calls to getDelegate
     * will use a cached version of the delegate.
     * This is useful for calling dismiss after
     * present so that the correct delegate is given.
     */
    getDelegate(force = false) {
        if (this.workingDelegate && !force) {
            return {
                delegate: this.workingDelegate,
                inline: this.inline,
            };
        }
        /**
         * If using overlay inline
         * we potentially need to use the coreDelegate
         * so that this works in vanilla JS apps.
         * If a developer has presented this component
         * via a controller, then we can assume
         * the component is already in the
         * correct place.
         */
        const parentEl = this.el.parentNode;
        const inline = (this.inline = parentEl !== null && !this.hasController);
        const delegate = (this.workingDelegate = inline ? this.delegate || this.coreDelegate : this.delegate);
        return { inline, delegate };
    }
    /**
     * Determines whether or not the
     * modal is allowed to dismiss based
     * on the state of the canDismiss prop.
     */
    async checkCanDismiss(data, role) {
        const { canDismiss } = this;
        if (typeof canDismiss === 'function') {
            return canDismiss(data, role);
        }
        return canDismiss;
    }
    /**
     * Present the modal overlay after it has been created.
     */
    async present() {
        const unlock = await this.lockController.lock();
        if (this.presented) {
            unlock();
            return;
        }
        const { presentingElement, el } = this;
        /**
         * If the modal is presented multiple times (inline modals), we
         * need to reset the current breakpoint to the initial breakpoint.
         */
        this.currentBreakpoint = this.initialBreakpoint;
        const { inline, delegate } = this.getDelegate(true);
        /**
         * Emit ionMount so JS Frameworks have an opportunity
         * to add the child component to the DOM. The child
         * component will be assigned to this.usersElement below.
         */
        this.ionMount.emit();
        this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
        /**
         * When using the lazy loaded build of Stencil, we need to wait
         * for every Stencil component instance to be ready before presenting
         * otherwise there can be a flash of unstyled content. With the
         * custom elements bundle we need to wait for the JS framework
         * mount the inner contents of the overlay otherwise WebKit may
         * get the transition incorrect.
         */
        if (hasLazyBuild(el)) {
            await deepReady(this.usersElement);
            /**
             * If keepContentsMounted="true" then the
             * JS Framework has already mounted the inner
             * contents so there is no need to wait.
             * Otherwise, we need to wait for the JS
             * Framework to mount the inner contents
             * of this component.
             */
        }
        else if (!this.keepContentsMounted) {
            await waitForMount();
        }
        writeTask(() => this.el.classList.add('show-modal'));
        // Recalculate isSheetModal before safe-area setup because framework
        // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
        this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
        this.isPresenting = true;
        const hasCardModal = presentingElement !== undefined;
        // Wrapped so a throw cannot strand `isPresenting`, which would suppress
        // the position-based correction for the life of this instance.
        try {
            // Set initial safe-area overrides before animation
            this.setInitialSafeAreaOverrides();
            /**
             * We need to change the status bar at the
             * start of the animation so that it completes
             * by the time the card animation is done.
             */
            if (hasCardModal && getIonMode(this) === 'ios') {
                // Cache the original status bar color before the modal is presented
                this.statusBarStyle = await StatusBar.getStyle();
                setCardStatusBarDark();
            }
            await present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, {
                presentingEl: presentingElement,
                currentBreakpoint: this.initialBreakpoint,
                backdropBreakpoint: this.backdropBreakpoint,
                expandToScroll: this.expandToScroll,
            });
        }
        finally {
            this.isPresenting = false;
        }
        // Update safe-area based on actual position after animation
        this.updateSafeAreaOverrides();
        // Apply fullscreen safe-area padding if needed
        this.applyFullscreenSafeArea();
        /* tslint:disable-next-line */
        if (typeof window !== 'undefined') {
            /**
             * This needs to be setup before any
             * non-transition async work so it can be dereferenced
             * in the dismiss method. The dismiss method
             * only waits for the entering transition
             * to finish. It does not wait for all of the `present`
             * method to resolve.
             */
            this.keyboardOpenCallback = () => {
                if (this.gesture) {
                    /**
                     * When the native keyboard is opened and the webview
                     * is resized, the gesture implementation will become unresponsive
                     * and enter a free-scroll mode.
                     *
                     * When the keyboard is opened, we disable the gesture for
                     * a single frame and re-enable once the contents have repositioned
                     * from the keyboard placement.
                     */
                    this.gesture.enable(false);
                    raf(() => {
                        if (this.gesture) {
                            this.gesture.enable(true);
                        }
                    });
                }
            };
            window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
        }
        if (this.isSheetModal) {
            this.initSheetGesture();
        }
        else if (hasCardModal) {
            this.initSwipeToClose();
        }
        // Initialize view transition listener for iOS card modals
        this.initViewTransitionListener();
        // Initialize parent removal observer
        this.initParentRemovalObserver();
        unlock();
    }
    initSwipeToClose() {
        if (getIonMode(this) !== 'ios') {
            return;
        }
        const { el } = this;
        // All of the elements needed for the swipe gesture
        // should be in the DOM and referenced by now, except
        // for the presenting el
        const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
        const ani = (this.animation = animationBuilder(el, {
            presentingEl: this.presentingElement,
            expandToScroll: this.expandToScroll,
        }));
        const contentEl = findIonContent(el);
        if (!contentEl) {
            printIonContentErrorMsg(el);
            return;
        }
        const statusBarStyle = this.statusBarStyle ?? Style.Default;
        this.gesture = createSwipeToCloseGesture(el, ani, statusBarStyle, () => this.cardOnDismiss(), () => this.onDragStart(), (detail) => this.onDragMove(detail), (detail) => this.onDragEnd(detail));
        this.gesture.enable(true);
    }
    initSheetGesture() {
        const { wrapperEl, initialBreakpoint, backdropBreakpoint } = this;
        if (!wrapperEl || initialBreakpoint === undefined) {
            return;
        }
        const animationBuilder = this.enterAnimation || config.get('modalEnter', iosEnterAnimation);
        const ani = (this.animation = animationBuilder(this.el, {
            presentingEl: this.presentingElement,
            currentBreakpoint: initialBreakpoint,
            backdropBreakpoint,
            expandToScroll: this.expandToScroll,
        }));
        ani.progressStart(true, 1);
        const { gesture, moveSheetToBreakpoint, resetContentScroll } = createSheetGesture(this.el, this.backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, ani, this.sortedBreakpoints, this.expandToScroll, () => this.currentBreakpoint ?? 0, () => this.sheetOnDismiss(), (breakpoint) => {
            if (this.currentBreakpoint !== breakpoint) {
                this.currentBreakpoint = breakpoint;
                this.ionBreakpointDidChange.emit({ breakpoint });
            }
        }, () => this.onDragStart(), (detail) => this.onDragMove(detail), (detail) => this.onDragEnd(detail));
        this.gesture = gesture;
        this.moveSheetToBreakpoint = moveSheetToBreakpoint;
        this.resetSheetContentScroll = resetContentScroll;
        this.gesture.enable(true);
        /**
         * When backdrop interaction is allowed, nested router outlets from child routes
         * may block pointer events to parent content. Apply passthrough styles only when
         * the modal was the sole content of a child route page.
         * See https://github.com/ionic-team/ionic-framework/issues/30700
         */
        const backdropNotBlocking = this.showBackdrop === false || this.focusTrap === false || backdropBreakpoint > 0;
        if (backdropNotBlocking) {
            this.setupChildRoutePassthrough();
        }
    }
    /**
     * For sheet modals that allow background interaction, sets up pointer-events
     * passthrough on child route page wrappers and nested router outlets.
     */
    setupChildRoutePassthrough() {
        // Cache the page parent for cleanup
        this.cachedPageParent = this.getOriginalPageParent();
        const pageParent = this.cachedPageParent;
        // Skip ion-app (controller modals) and pages with visible sibling content next to the modal
        if (!pageParent || pageParent.tagName === 'ION-APP') {
            return;
        }
        const hasVisibleContent = Array.from(pageParent.children).some((child) => child !== this.el &&
            !(child instanceof HTMLElement && window.getComputedStyle(child).display === 'none') &&
            child.tagName !== 'TEMPLATE' &&
            child.tagName !== 'SLOT' &&
            !(child.nodeType === Node.TEXT_NODE && !child.textContent?.trim()));
        if (hasVisibleContent) {
            return;
        }
        // Child route case: page only contained the modal
        pageParent.classList.add('ion-page-overlay-passthrough');
        // Also make nested router outlets passthrough
        const routerOutlet = pageParent.parentElement;
        if (routerOutlet?.tagName === 'ION-ROUTER-OUTLET' && routerOutlet.parentElement?.tagName !== 'ION-APP') {
            routerOutlet.style.setProperty('pointer-events', 'none');
            routerOutlet.setAttribute('data-overlay-passthrough', 'true');
        }
    }
    /**
     * Finds the ion-page ancestor of the modal's original parent location.
     */
    getOriginalPageParent() {
        if (!this.cachedOriginalParent) {
            return null;
        }
        let pageParent = this.cachedOriginalParent;
        while (pageParent && !pageParent.classList.contains('ion-page')) {
            pageParent = pageParent.parentElement;
        }
        return pageParent;
    }
    /**
     * Removes passthrough styles added by setupChildRoutePassthrough.
     */
    cleanupChildRoutePassthrough() {
        const pageParent = this.cachedPageParent;
        if (!pageParent) {
            return;
        }
        pageParent.classList.remove('ion-page-overlay-passthrough');
        const routerOutlet = pageParent.parentElement;
        if (routerOutlet?.hasAttribute('data-overlay-passthrough')) {
            routerOutlet.style.removeProperty('pointer-events');
            routerOutlet.removeAttribute('data-overlay-passthrough');
        }
        // Clear the cached reference
        this.cachedPageParent = undefined;
    }
    sheetOnDismiss() {
        /**
         * While the gesture animation is finishing
         * it is possible for a user to tap the backdrop.
         * This would result in the dismiss animation
         * being played again. Typically this is avoided
         * by setting `presented = false` on the overlay
         * component; however, we cannot do that here as
         * that would prevent the element from being
         * removed from the DOM.
         */
        this.gestureAnimationDismissing = true;
        this.animation.onFinish(async () => {
            this.currentBreakpoint = 0;
            this.ionBreakpointDidChange.emit({ breakpoint: this.currentBreakpoint });
            await this.dismiss(undefined, GESTURE);
            this.gestureAnimationDismissing = false;
        });
    }
    cardOnDismiss() {
        /**
         * While the gesture animation is finishing
         * it is possible for a user to tap the backdrop.
         * This would result in the dismiss animation
         * being played again. Typically this is avoided
         * by setting `presented = false` on the overlay
         * component; however, we cannot do that here as
         * that would prevent the element from being
         * removed from the DOM.
         */
        this.gestureAnimationDismissing = true;
        /**
         * Reset the status bar style as the dismiss animation
         * starts otherwise the status bar will be the wrong
         * color for the duration of the dismiss animation.
         * The dismiss method does this as well, but
         * in this case it's only called once the animation
         * has finished.
         */
        setCardStatusBarDefault(this.statusBarStyle);
        this.animation.onFinish(async () => {
            await this.dismiss(undefined, GESTURE);
            this.gestureAnimationDismissing = false;
        });
    }
    /**
     * Dismiss the modal overlay after it has been presented.
     * This is a no-op if the overlay has not been presented yet. If you want
     * to remove an overlay from the DOM that was never presented, use the
     * [remove](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove) method.
     *
     * @param data Any data to emit in the dismiss events.
     * @param role The role of the element that is dismissing the modal.
     * For example, `cancel` or `backdrop`.
     */
    async dismiss(data, role) {
        if (this.gestureAnimationDismissing && role !== GESTURE) {
            return false;
        }
        // Cancel any pending resize timeout to prevent stale updates during dismiss
        clearTimeout(this.resizeTimeout);
        this.resizeTimeout = undefined;
        /**
         * Because the canDismiss check below is async,
         * we need to claim a lock before the check happens,
         * in case the dismiss transition does run.
         */
        const unlock = await this.lockController.lock();
        /**
         * Dismiss all child modals. This is especially important in
         * Angular and React because it's possible to lose control of a child
         * modal when the parent modal is dismissed.
         */
        await this.dismissNestedModals();
        /**
         * If a canDismiss handler is responsible
         * for calling the dismiss method, we should
         * not run the canDismiss check again.
         */
        if (role !== 'handler' && !(await this.checkCanDismiss(data, role))) {
            unlock();
            return false;
        }
        const { presentingElement } = this;
        /**
         * We need to start the status bar change
         * before the animation so that the change
         * finishes when the dismiss animation does.
         */
        const hasCardModal = presentingElement !== undefined;
        if (hasCardModal && getIonMode(this) === 'ios') {
            setCardStatusBarDefault(this.statusBarStyle);
        }
        /* tslint:disable-next-line */
        if (typeof window !== 'undefined' && this.keyboardOpenCallback) {
            window.removeEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
            this.keyboardOpenCallback = undefined;
        }
        const dismissed = await dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation, {
            presentingEl: presentingElement,
            currentBreakpoint: this.currentBreakpoint ?? this.initialBreakpoint,
            backdropBreakpoint: this.backdropBreakpoint,
            expandToScroll: this.expandToScroll,
        });
        if (dismissed) {
            const { delegate } = this.getDelegate();
            await detachComponent(delegate, this.usersElement);
            writeTask(() => this.el.classList.remove('show-modal'));
            if (this.animation) {
                this.animation.destroy();
            }
            if (this.gesture) {
                this.gesture.destroy();
            }
            /**
             * The sheet gesture turns content scrolling off while the sheet sits below
             * the top breakpoint. Inline modals reuse the same content on the next
             * present, so hand it back before the gesture goes away.
             */
            this.resetSheetContentScroll?.();
            this.resetSheetContentScroll = undefined;
            this.cleanupViewTransitionListener();
            this.cleanupParentRemovalObserver();
            this.cleanupSafeAreaOverrides();
            this.cleanupChildRoutePassthrough();
        }
        this.currentBreakpoint = undefined;
        this.animation = undefined;
        unlock();
        return dismissed;
    }
    /**
     * Returns a promise that resolves when the modal did dismiss.
     */
    onDidDismiss() {
        return eventMethod(this.el, 'ionModalDidDismiss');
    }
    /**
     * Returns a promise that resolves when the modal will dismiss.
     */
    onWillDismiss() {
        return eventMethod(this.el, 'ionModalWillDismiss');
    }
    /**
     * Move a sheet style modal to a specific breakpoint.
     *
     * @param breakpoint The breakpoint value to move the sheet modal to.
     * Must be a value defined in your `breakpoints` array.
     */
    async setCurrentBreakpoint(breakpoint) {
        if (!this.isSheetModal) {
            printIonWarning('[ion-modal] - setCurrentBreakpoint is only supported on sheet modals.');
            return;
        }
        if (!this.breakpoints.includes(breakpoint)) {
            printIonWarning(`[ion-modal] - Attempted to set invalid breakpoint value ${breakpoint}. Please double check that the breakpoint value is part of your defined breakpoints.`);
            return;
        }
        const { currentBreakpoint, moveSheetToBreakpoint, canDismiss, breakpoints, animated } = this;
        if (currentBreakpoint === breakpoint) {
            return;
        }
        if (moveSheetToBreakpoint) {
            this.sheetTransition = moveSheetToBreakpoint({
                breakpoint,
                breakpointOffset: 1 - currentBreakpoint,
                canDismiss: canDismiss !== undefined && canDismiss !== true && breakpoints[0] === 0,
                animated,
            });
            await this.sheetTransition;
            this.sheetTransition = undefined;
        }
    }
    /**
     * Returns the current breakpoint of a sheet style modal
     */
    async getCurrentBreakpoint() {
        return this.currentBreakpoint;
    }
    async moveToNextBreakpoint() {
        const { breakpoints, currentBreakpoint } = this;
        if (!breakpoints || currentBreakpoint == null) {
            /**
             * If the modal does not have breakpoints and/or the current
             * breakpoint is not set, we can't move to the next breakpoint.
             */
            return false;
        }
        const allowedBreakpoints = breakpoints.filter((b) => b !== 0);
        const currentBreakpointIndex = allowedBreakpoints.indexOf(currentBreakpoint);
        const nextBreakpointIndex = (currentBreakpointIndex + 1) % allowedBreakpoints.length;
        const nextBreakpoint = allowedBreakpoints[nextBreakpointIndex];
        /**
         * Sets the current breakpoint to the next available breakpoint.
         * If the current breakpoint is the last breakpoint, we set the current
         * breakpoint to the first non-zero breakpoint to avoid dismissing the sheet.
         */
        await this.setCurrentBreakpoint(nextBreakpoint);
        return true;
    }
    initViewTransitionListener() {
        // Only enable for iOS card modals when no custom animations are provided
        if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
            return;
        }
        // Set initial view state
        this.currentViewIsPortrait = window.innerWidth < 768;
    }
    handleViewTransition() {
        // Only run view transitions when the modal is presented
        if (!this.presented) {
            return;
        }
        const isPortrait = window.innerWidth < 768;
        // Only transition if view state actually changed
        if (this.currentViewIsPortrait === isPortrait) {
            return;
        }
        // Cancel any ongoing transition animation
        if (this.viewTransitionAnimation) {
            this.viewTransitionAnimation.destroy();
            this.viewTransitionAnimation = undefined;
        }
        const { presentingElement } = this;
        if (!presentingElement) {
            return;
        }
        // Create transition animation
        let transitionAnimation;
        if (this.currentViewIsPortrait && !isPortrait) {
            // Portrait to landscape transition
            transitionAnimation = portraitToLandscapeTransition(this.el, {
                presentingEl: presentingElement});
        }
        else {
            // Landscape to portrait transition
            transitionAnimation = landscapeToPortraitTransition(this.el, {
                presentingEl: presentingElement});
        }
        // Update state and play animation
        this.currentViewIsPortrait = isPortrait;
        this.viewTransitionAnimation = transitionAnimation;
        transitionAnimation.play().then(() => {
            this.viewTransitionAnimation = undefined;
            // Wait for a layout pass after the transition so getBoundingClientRect()
            // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
            raf(() => this.updateSafeAreaOverrides());
            // After orientation transition, recreate the swipe-to-close gesture
            // with updated animation that reflects the new presenting element state
            this.reinitSwipeToClose();
        });
    }
    cleanupViewTransitionListener() {
        // Clear any pending resize timeout
        if (this.resizeTimeout) {
            clearTimeout(this.resizeTimeout);
            this.resizeTimeout = undefined;
        }
        if (this.viewTransitionAnimation) {
            this.viewTransitionAnimation.destroy();
            this.viewTransitionAnimation = undefined;
        }
    }
    reinitSwipeToClose() {
        // Only reinitialize if we have a presenting element and are on iOS
        if (getIonMode(this) !== 'ios' || !this.presentingElement) {
            return;
        }
        // Clean up existing gesture and animation
        if (this.gesture) {
            this.gesture.destroy();
            this.gesture = undefined;
        }
        if (this.animation) {
            // Properly end the progress-based animation at initial state before destroying
            // to avoid leaving modal in intermediate swipe position
            this.animation.progressEnd(0, 0, 0);
            this.animation.destroy();
            this.animation = undefined;
        }
        // Force the modal back to the correct position or it could end up
        // in a weird state after destroying the animation
        raf(() => {
            this.ensureCorrectModalPosition();
            this.initSwipeToClose();
        });
    }
    ensureCorrectModalPosition() {
        const { el, presentingElement } = this;
        const root = getElementRoot(el);
        const wrapperEl = root.querySelector('.modal-wrapper');
        if (wrapperEl) {
            wrapperEl.style.transform = 'translateY(0vh)';
            wrapperEl.style.opacity = '1';
        }
        if (presentingElement?.tagName === 'ION-MODAL') {
            const isPortrait = window.innerWidth < 768;
            if (isPortrait) {
                const transformOffset = !CSS.supports('width', 'max(0px, 1px)')
                    ? '30px'
                    : 'max(30px, var(--ion-safe-area-top))';
                const scale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
                presentingElement.style.transform = `translateY(${transformOffset}) scale(${scale})`;
            }
            else {
                presentingElement.style.transform = 'translateY(0px) scale(1)';
            }
        }
    }
    async dismissNestedModals() {
        const nestedModals = document.querySelectorAll(`ion-modal[data-parent-ion-modal="${this.el.id}"]`);
        nestedModals?.forEach(async (modal) => {
            await modal.dismiss(undefined, 'parent-dismissed');
        });
    }
    initParentRemovalObserver() {
        if (typeof MutationObserver === 'undefined') {
            return;
        }
        // Only observe if we have a cached parent and are in browser environment
        if (typeof window === 'undefined' || !this.cachedOriginalParent) {
            return;
        }
        // Don't observe document or fragment nodes as they can't be "removed"
        if (this.cachedOriginalParent.nodeType === Node.DOCUMENT_NODE ||
            this.cachedOriginalParent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
            return;
        }
        /**
         * Don't observe for controller-based modals or when the parent is the
         * app root (document.body or ion-app). These parents won't be removed,
         * and observing document.body with subtree: true causes performance
         * issues with frameworks like Angular during change detection.
         */
        if (this.hasController ||
            this.cachedOriginalParent === document.body ||
            this.cachedOriginalParent.tagName === 'ION-APP') {
            return;
        }
        // Both `present()` and a reconnect call this, so drop any existing
        // observer. Below the guards, so a call that bails cannot leave none.
        this.cleanupParentRemovalObserver();
        this.parentRemovalObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
                    // Check if our cached original parent was removed
                    const cachedParentWasRemoved = Array.from(mutation.removedNodes).some((node) => {
                        const isDirectMatch = node === this.cachedOriginalParent;
                        const isContainedMatch = this.cachedOriginalParent
                            ? node.contains?.(this.cachedOriginalParent)
                            : false;
                        return isDirectMatch || isContainedMatch;
                    });
                    // Also check if parent is no longer connected to DOM
                    const cachedParentDisconnected = this.cachedOriginalParent && !this.cachedOriginalParent.isConnected;
                    if (cachedParentWasRemoved || cachedParentDisconnected) {
                        this.dismiss(undefined, 'parent-removed');
                        // Release the reference to the cached original parent
                        // so we don't have a memory leak
                        this.cachedOriginalParent = undefined;
                    }
                }
            });
        });
        // Observe document body with subtree to catch removals at any level
        this.parentRemovalObserver.observe(document.body, {
            childList: true,
            subtree: true,
        });
    }
    cleanupParentRemovalObserver() {
        this.parentRemovalObserver?.disconnect();
        this.parentRemovalObserver = undefined;
    }
    onDragStart() {
        this.ionDragStart.emit();
    }
    onDragMove(detail) {
        this.ionDragMove.emit(detail);
    }
    onDragEnd(detail) {
        this.ionDragEnd.emit(detail);
    }
    /**
     * Creates the context object for safe-area utilities.
     *
     * `coveredAxes` is only set by `setInitialSafeAreaOverrides()`
     * because it is only read by `getInitialSafeAreaConfig()`. Other callers
     * (resize handler, post-animation update, fullscreen-padding apply) would
     * pay a `getComputedStyle()` cost for a value they never consult.
     */
    getSafeAreaContext() {
        return {
            isSheetModal: this.isSheetModal,
            isCardModal: this.presentingElement !== undefined && getIonMode(this) === 'ios',
            presentingElement: this.presentingElement,
            breakpoints: this.breakpoints,
            currentBreakpoint: this.currentBreakpoint,
        };
    }
    /**
     * Keeps the content's sizing in sync with `--height`. The content reads the
     * property to determine whether it should size itself to its content, and
     * changes to `--height` on an ancestor or the root can change that behavior
     * without changing the modal itself.
     */
    watchHeightForContent() {
        /**
         * A sheet's height comes from its breakpoints, so its content never sizes
         * itself to `--height`. Watching it would cause the drag to recalculate on
         * every frame.
         */
        if (this.isSheetModal) {
            return;
        }
        this.unsubscribeHeightChange?.();
        this.unsubscribeHeightChange = onModalHeightChange(this.el, () => {
            this.el.querySelectorAll('ion-content').forEach((contentEl) => contentEl.recalculateDimensions());
        });
    }
    /**
     * Sets initial safe-area overrides before modal animation.
     * Called in present() before animation starts.
     *
     * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
     * (an internal property) instead of --ion-safe-area-top. We resolve the
     * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
     * the height calculation from --ion-safe-area-top (which is zeroed for
     * sheets to prevent header content from getting double-offset padding).
     */
    setInitialSafeAreaOverrides() {
        const context = {
            ...this.getSafeAreaContext(),
            coveredAxes: getModalCoveredAxes(this.el),
        };
        const safeAreaConfig = getInitialSafeAreaConfig(context);
        applySafeAreaOverrides(this.el, safeAreaConfig);
        this.watchHeightForContent();
        // Set the internal offset property with the resolved root safe-area-top value
        if (context.isSheetModal) {
            this.updateSheetOffsetTop();
            // A restore after a DOM move runs this a second time for the same
            // modal, so drop the previous subscription rather than stacking one.
            this.unsubscribeRootSafeAreaTop?.();
            this.unsubscribeRootSafeAreaTop = onRootSafeAreaTopChange((safeAreaTop) => this.updateSheetOffsetTop(safeAreaTop));
        }
    }
    /**
     * Sets the internal --ion-modal-offset-top property on the host element,
     * resolving the current root --ion-safe-area-top when no value is given.
     * Called on present, on resize (e.g., device rotation changes safe-area),
     * and whenever the root safe-area value itself changes.
     */
    updateSheetOffsetTop(safeAreaTop) {
        const value = safeAreaTop ?? getRootSafeAreaTop();
        this.el.style.setProperty('--ion-modal-offset-top', `${value}px`);
    }
    /**
     * Updates safe-area overrides during dynamic state changes.
     * Called after animations, during gestures, and on orientation changes.
     */
    updateSafeAreaOverrides() {
        const { wrapperEl, el } = this;
        const context = this.getSafeAreaContext();
        // Sheet modals: safe-area is fully determined at presentation time
        // (top is always 0px, height is frozen). Nothing to update.
        if (context.isSheetModal)
            return;
        // Card modals have fixed safe-area requirements set by initial prediction.
        if (context.isCardModal)
            return;
        // wrapperEl is required for position-based detection below
        if (!wrapperEl)
            return;
        // Regular modals: use position-based detection to correctly handle both
        // fullscreen modals and centered dialogs with custom dimensions.
        const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
        applySafeAreaOverrides(el, safeAreaConfig);
    }
    /**
     * Applies safe-area-bottom scroll padding to ion-content inside
     * fullscreen modals that have no ion-footer. This prevents content
     * from being hidden behind the system navigation bar while keeping
     * the modal background edge-to-edge (no visible gap).
     */
    applyFullscreenSafeArea() {
        const context = this.getSafeAreaContext();
        if (context.isSheetModal || context.isCardModal)
            return;
        const { contentEl, hasFooter } = this.findContentAndFooter();
        this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
    }
    /**
     * Sets --internal-content-safe-area-padding-bottom on the given ion-content
     * when no footer is present, so ion-content's .inner-scroll includes
     * safe-area-bottom in its scroll padding. This keeps the modal background
     * edge-to-edge while ensuring content scrolls clear of the system nav bar.
     *
     * --internal-content-safe-area-padding-bottom is an internal CSS property used
     * only by this code path. It is not part of ion-content's public API and
     * should not be set by consumers. The default of 0px makes it a no-op
     * when unset, which is the expected state for ion-content used outside of
     * a fullscreen modal without a footer.
     */
    applyFullscreenSafeAreaTo(contentEl, hasFooter) {
        // Only apply for standard Ionic layouts (has ion-content but no
        // ion-footer). When a footer is present it handles its own safe-area
        // padding. Custom modals with raw HTML are developer-controlled.
        if (!contentEl || hasFooter)
            return;
        contentEl.style.setProperty('--internal-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
    }
    /**
     * Removes the internal --internal-content-safe-area-padding-bottom property
     * from an already-located ion-content. Callers do their own
     * findContentAndFooter() so they can also read hasFooter if needed.
     */
    clearContentSafeAreaPadding(contentEl) {
        if (!contentEl)
            return;
        contentEl.style.removeProperty('--internal-content-safe-area-padding-bottom');
    }
    /**
     * Finds ion-content and ion-footer among direct children and one level of
     * grandchildren (for wrapped components like <app-footer><ion-footer>).
     *
     * Intentionally does NOT use findIonContent() or querySelector() because
     * those search the full subtree and would match ion-content inside nested
     * routes/pages. We only want direct slot children (+ one wrapper level).
     *
     * Uses a manual loop instead of querySelector(':scope > ...') because
     * Stencil's mock-doc (used in spec tests) does not support :scope.
     */
    findContentAndFooter() {
        let contentEl = null;
        let hasFooter = false;
        for (const child of Array.from(this.el.children)) {
            if (child.tagName === 'ION-CONTENT')
                contentEl = child;
            if (child.tagName === 'ION-FOOTER')
                hasFooter = true;
            for (const grandchild of Array.from(child.children)) {
                if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
                    contentEl = grandchild;
                if (grandchild.tagName === 'ION-FOOTER')
                    hasFooter = true;
            }
        }
        return { contentEl, hasFooter };
    }
    /**
     * Clears all safe-area overrides and padding.
     */
    cleanupSafeAreaOverrides() {
        clearSafeAreaOverrides(this.el);
        this.unsubscribeRootSafeAreaTop?.();
        this.unsubscribeRootSafeAreaTop = undefined;
        this.unsubscribeHeightChange?.();
        this.unsubscribeHeightChange = undefined;
        // Remove internal sheet offset property
        this.el.style.removeProperty('--ion-modal-offset-top');
        const { contentEl } = this.findContentAndFooter();
        this.clearContentSafeAreaPadding(contentEl);
    }
    /**
     * Re-applies what `cleanupSafeAreaOverrides()` removed for a modal moved
     * while presented. Anything needing no measurement goes on synchronously so
     * the modal is never painted without safe-area. The position-based
     * correction waits a frame for the layout pass it reads.
     */
    restoreSafeAreaOverrides() {
        this.setInitialSafeAreaOverrides();
        this.applyFullscreenSafeArea();
        raf(() => {
            // A dismiss or another move can land first, and a read before the enter
            // animation settles is not the rest position. `present()` redoes it.
            if (!this.presented || !this.el.isConnected || this.isPresenting) {
                return;
            }
            // A hidden subtree carries `display: none`, so there is no box and the
            // read would write the safe-area for a modal touching no edge. The
            // prediction applied above holds until the modal is shown again.
            const { width, height } = this.el.getBoundingClientRect();
            if (width === 0 && height === 0) {
                return;
            }
            this.updateSafeAreaOverrides();
        });
    }
    render() {
        const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
        const showHandle = handle !== false && isSheetModal;
        const mode = getIonMode(this);
        const isCardModal = presentingElement !== undefined && mode === 'ios';
        const isHandleCycle = handleBehavior === 'cycle';
        const isSheetModalWithHandle = isSheetModal && showHandle;
        return (h(Host, { key: '17a95bd7722bac2344bea041aff97db17f55c0cf', "no-router": true,
            // Allow the modal to be navigable when the handle is focusable
            tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1, ...htmlAttributes, style: {
                zIndex: `${20000 + this.overlayIndex}`,
            }, class: {
                [mode]: true,
                ['modal-default']: !isCardModal && !isSheetModal,
                [`modal-card`]: isCardModal,
                [`modal-sheet`]: isSheetModal,
                [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
                'overlay-hidden': true,
                [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
                ...getClassMap(this.cssClass),
            }, onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }, h("ion-backdrop", { key: '9dab1ef4771c0d847ba2be8672afbaa2748517f3', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '64af7ed1c5f552b61fc4b9387b926acc76c8a99a', class: "modal-shadow" }), h("div", { key: '7e76d77d5b2e8f0467bb793facf5aa86f8d01bbe',
            /*
              role and aria-modal must be used on the
              same element. They must also be set inside the
              shadow DOM otherwise ion-button will not be highlighted
              when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
  
              tabIndex={-1} is required so present() can move focus to this
              element (which carries the dialog role) instead of the role-less
              host. role="dialog" alone does not make an element focusable, so
              without the tabindex focus() would be a no-op and screen readers
              may not properly announce the dialog and its content when it opens.
            */
            role: "dialog", ...inheritedAttributes, "aria-modal": "true", tabIndex: -1, class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }, showHandle && (h("button", { key: '5efc0bb0a59adcc594ac48da9fd90e57908a7ffb', class: "modal-handle",
            // Prevents the handle from receiving keyboard focus when it does not cycle
            tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: '6175fcd0b39c086b4e9eb9e96c4d631c2a86774b', onSlotchange: this.onSlotChange }))));
    }
    get el() { return getElement(this); }
    static get watchers() { return {
        "isOpen": [{
                "onIsOpenChange": 0
            }],
        "trigger": [{
                "triggerChanged": 0
            }],
        "breakpoints": [{
                "breakpointsChanged": 0
            }]
    }; }
};
const LIFECYCLE_MAP = {
    ionModalDidPresent: 'ionViewDidEnter',
    ionModalWillPresent: 'ionViewWillEnter',
    ionModalWillDismiss: 'ionViewWillLeave',
    ionModalDidDismiss: 'ionViewDidLeave',
};
Modal.style = {
    ios: modalIosCss(),
    md: modalMdCss()
};

export { Modal as ion_modal };