@ionic/core 8.7.18-dev.11769546281.18b0f3ba → 8.7.18-dev.11769790854.11895f8f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, writeTask, h, Host } from '@stencil/core/internal/client';
5
+ import { w as win } from './index9.js';
5
6
  import { a as findClosestIonContent, i as isIonContent, d as disableContentScrollY, r as resetContentScrollY, f as findIonContent, p as printIonContentErrorMsg } from './index8.js';
6
7
  import { C as CoreDelegate, a as attachComponent, d as detachComponent } from './framework-delegate.js';
7
8
  import { f as clamp, g as getElementRoot, r as raf, d as inheritAttributes, k as hasLazyBuild } from './helpers.js';
@@ -16,7 +17,6 @@ import { KEYBOARD_DID_OPEN } from './keyboard.js';
16
17
  import { c as createAnimation } from './animation.js';
17
18
  import { g as getTimeGivenProgression } from './cubic-bezier.js';
18
19
  import { createGesture } from './index3.js';
19
- import { w as win } from './index9.js';
20
20
  import { d as defineCustomElement$1 } from './backdrop.js';
21
21
 
22
22
  var Style;
@@ -246,7 +246,7 @@ const calculateSpringStep = (t) => {
246
246
  const SwipeToCloseDefaults = {
247
247
  MIN_PRESENTING_SCALE: 0.915,
248
248
  };
249
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
249
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
250
250
  /**
251
251
  * The step value at which a card modal
252
252
  * is eligible for dismissing via gesture.
@@ -403,6 +403,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
403
403
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
404
404
  const clampedStep = clamp(0.0001, processedStep, maxStep);
405
405
  animation.progressStep(clampedStep);
406
+ // Notify modal of position change for safe-area updates
407
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
406
408
  /**
407
409
  * When swiping down half way, the status bar style
408
410
  * should be reset to its default value.
@@ -946,7 +948,7 @@ const mdLeaveAnimation = (baseEl, opts) => {
946
948
  return baseAnimation;
947
949
  };
948
950
 
949
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
951
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
950
952
  // Defaults for the sheet swipe animation
951
953
  const defaultBackdrop = [
952
954
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -1277,6 +1279,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1277
1279
  : step;
1278
1280
  offset = clamp(0.0001, processedStep, maxStep);
1279
1281
  animation.progressStep(offset);
1282
+ // Notify modal of position change for safe-area updates
1283
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
1280
1284
  };
1281
1285
  const onEnd = (detail) => {
1282
1286
  /**
@@ -1471,9 +1475,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1471
1475
  };
1472
1476
  };
1473
1477
 
1474
- const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--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-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@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-safe-area-top) + 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}";
1478
+ const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--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-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-safe-area-top) + 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}";
1475
1479
 
1476
- const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--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-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@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-safe-area-top) + 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}";
1480
+ const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--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-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-safe-area-top) + 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}";
1477
1481
 
1478
1482
  const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1479
1483
  constructor(registerHost) {
@@ -1500,6 +1504,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1500
1504
  this.inline = false;
1501
1505
  // Whether or not modal is being dismissed via gesture
1502
1506
  this.gestureAnimationDismissing = false;
1507
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1508
+ this.skipSafeAreaCoordinateDetection = false;
1509
+ // Track previous safe-area state to avoid redundant DOM writes
1510
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1503
1511
  this.presented = false;
1504
1512
  /** @internal */
1505
1513
  this.hasController = false;
@@ -1690,7 +1698,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1690
1698
  }
1691
1699
  }
1692
1700
  onWindowResize() {
1693
- // Only handle resize for iOS card modals when no custom animations are provided
1701
+ // Invalidate safe-area cache on resize (device rotation may change values)
1702
+ this.cachedSafeAreas = undefined;
1703
+ this.updateSafeAreaOverrides();
1704
+ // Only handle view transition for iOS card modals when no custom animations are provided
1694
1705
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1695
1706
  return;
1696
1707
  }
@@ -1713,6 +1724,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1713
1724
  this.triggerController.removeClickListener();
1714
1725
  this.cleanupViewTransitionListener();
1715
1726
  this.cleanupParentRemovalObserver();
1727
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
1728
+ this.resetSafeAreaState();
1716
1729
  }
1717
1730
  componentWillLoad() {
1718
1731
  var _a;
@@ -1872,6 +1885,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1872
1885
  else if (!this.keepContentsMounted) {
1873
1886
  await waitForMount();
1874
1887
  }
1888
+ // Predict safe-area needs based on modal configuration to avoid visual snap
1889
+ this.setInitialSafeAreaOverrides(presentingElement);
1875
1890
  writeTask(() => this.el.classList.add('show-modal'));
1876
1891
  const hasCardModal = presentingElement !== undefined;
1877
1892
  /**
@@ -1933,6 +1948,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1933
1948
  else if (hasCardModal) {
1934
1949
  this.initSwipeToClose();
1935
1950
  }
1951
+ // Now that animation is complete, update safe-area based on actual position
1952
+ this.updateSafeAreaOverrides();
1936
1953
  // Initialize view transition listener for iOS card modals
1937
1954
  this.initViewTransitionListener();
1938
1955
  // Initialize parent removal observer
@@ -1984,7 +2001,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1984
2001
  await this.dismiss(undefined, GESTURE);
1985
2002
  this.gestureAnimationDismissing = false;
1986
2003
  });
1987
- });
2004
+ }, () => this.updateSafeAreaOverrides());
1988
2005
  this.gesture.enable(true);
1989
2006
  }
1990
2007
  initSheetGesture() {
@@ -2005,7 +2022,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2005
2022
  this.currentBreakpoint = breakpoint;
2006
2023
  this.ionBreakpointDidChange.emit({ breakpoint });
2007
2024
  }
2008
- });
2025
+ this.updateSafeAreaOverrides();
2026
+ }, () => this.updateSafeAreaOverrides());
2009
2027
  this.gesture = gesture;
2010
2028
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
2011
2029
  this.gesture.enable(true);
@@ -2083,6 +2101,194 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2083
2101
  // Clear the cached reference
2084
2102
  this.cachedPageParent = undefined;
2085
2103
  }
2104
+ /**
2105
+ * Sets initial safe-area overrides based on modal configuration before
2106
+ * the modal becomes visible. This predicts whether the modal will touch
2107
+ * screen edges to avoid a visual snap after animation completes.
2108
+ */
2109
+ setInitialSafeAreaOverrides(presentingElement) {
2110
+ const style = this.el.style;
2111
+ const mode = getIonMode(this);
2112
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2113
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
2114
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
2115
+ const isTablet = window.innerWidth >= 768;
2116
+ // Sheet modals always touch bottom edge, never top/left/right
2117
+ if (isSheetModal) {
2118
+ style.setProperty('--ion-safe-area-top', '0px');
2119
+ style.setProperty('--ion-safe-area-left', '0px');
2120
+ style.setProperty('--ion-safe-area-right', '0px');
2121
+ return;
2122
+ }
2123
+ // Card modals have rounded top corners
2124
+ if (isCardModal) {
2125
+ style.setProperty('--ion-safe-area-top', '0px');
2126
+ if (isTablet) {
2127
+ // On tablets, card modals are inset from all edges
2128
+ this.zeroAllSafeAreas();
2129
+ }
2130
+ else {
2131
+ // On phones, card modals still extend to the bottom edge
2132
+ style.setProperty('--ion-safe-area-left', '0px');
2133
+ style.setProperty('--ion-safe-area-right', '0px');
2134
+ this.applyFullscreenSafeArea();
2135
+ }
2136
+ return;
2137
+ }
2138
+ // Check if modal is fullscreen via CSS custom properties
2139
+ // This applies to both phone and tablet sizes - custom modals may have
2140
+ // non-fullscreen dimensions even on phones (e.g., --height: 70%)
2141
+ const computedStyle = getComputedStyle(this.el);
2142
+ const width = computedStyle.getPropertyValue('--width').trim();
2143
+ const height = computedStyle.getPropertyValue('--height').trim();
2144
+ const isFullscreen = width === '100%' && height === '100%';
2145
+ if (isFullscreen) {
2146
+ this.applyFullscreenSafeArea();
2147
+ }
2148
+ else if (isTablet) {
2149
+ // Centered dialog on tablet doesn't touch edges
2150
+ this.zeroAllSafeAreas();
2151
+ }
2152
+ else ;
2153
+ }
2154
+ /**
2155
+ * Applies safe-area handling for fullscreen modals.
2156
+ * Adds wrapper padding when no footer is present to prevent
2157
+ * content from overlapping system navigation areas.
2158
+ */
2159
+ applyFullscreenSafeArea() {
2160
+ this.skipSafeAreaCoordinateDetection = true;
2161
+ this.updateFooterPadding();
2162
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2163
+ // Use subtree:true to support wrapped footers in framework components
2164
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
2165
+ if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
2166
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2167
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2168
+ }
2169
+ }
2170
+ /**
2171
+ * Updates wrapper and shadow padding based on footer presence.
2172
+ * Called initially and when footer is dynamically added/removed.
2173
+ * Both elements must be styled identically to prevent visual mismatches.
2174
+ */
2175
+ updateFooterPadding() {
2176
+ if (!this.wrapperEl)
2177
+ return;
2178
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
2179
+ // Apply to both wrapper and shadow to keep them in sync
2180
+ const elements = [this.wrapperEl, this.shadowEl].filter(Boolean);
2181
+ if (hasFooter) {
2182
+ elements.forEach((el) => {
2183
+ el.style.removeProperty('padding-bottom');
2184
+ el.style.removeProperty('box-sizing');
2185
+ });
2186
+ }
2187
+ else {
2188
+ elements.forEach((el) => {
2189
+ el.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2190
+ el.style.setProperty('box-sizing', 'border-box');
2191
+ });
2192
+ }
2193
+ }
2194
+ /**
2195
+ * Sets all safe-area CSS variables to 0px for modals that
2196
+ * don't touch screen edges.
2197
+ */
2198
+ zeroAllSafeAreas() {
2199
+ const style = this.el.style;
2200
+ style.setProperty('--ion-safe-area-top', '0px');
2201
+ style.setProperty('--ion-safe-area-bottom', '0px');
2202
+ style.setProperty('--ion-safe-area-left', '0px');
2203
+ style.setProperty('--ion-safe-area-right', '0px');
2204
+ }
2205
+ /**
2206
+ * Resets all safe-area related state and styles.
2207
+ * Called during dismiss and disconnectedCallback to ensure clean state
2208
+ * for re-presentation of inline modals.
2209
+ */
2210
+ resetSafeAreaState() {
2211
+ var _a;
2212
+ this.skipSafeAreaCoordinateDetection = false;
2213
+ this.cachedSafeAreas = undefined;
2214
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2215
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2216
+ this.footerObserver = undefined;
2217
+ // Clear wrapper and shadow styles that may have been set for safe-area handling
2218
+ [this.wrapperEl, this.shadowEl].forEach((el) => {
2219
+ if (el) {
2220
+ el.style.removeProperty('padding-bottom');
2221
+ el.style.removeProperty('box-sizing');
2222
+ }
2223
+ });
2224
+ // Clear safe-area CSS variable overrides
2225
+ const style = this.el.style;
2226
+ style.removeProperty('--ion-safe-area-top');
2227
+ style.removeProperty('--ion-safe-area-bottom');
2228
+ style.removeProperty('--ion-safe-area-left');
2229
+ style.removeProperty('--ion-safe-area-right');
2230
+ }
2231
+ /**
2232
+ * Gets the root safe-area values from the document element.
2233
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2234
+ */
2235
+ getSafeAreaValues() {
2236
+ if (!this.cachedSafeAreas) {
2237
+ const rootStyle = getComputedStyle(document.documentElement);
2238
+ this.cachedSafeAreas = {
2239
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2240
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2241
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2242
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2243
+ };
2244
+ }
2245
+ return this.cachedSafeAreas;
2246
+ }
2247
+ /**
2248
+ * Updates safe-area CSS variable overrides based on whether the modal
2249
+ * extends into each safe-area region. Called after animation
2250
+ * and during gestures to handle dynamic position changes.
2251
+ *
2252
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2253
+ */
2254
+ updateSafeAreaOverrides() {
2255
+ if (this.skipSafeAreaCoordinateDetection) {
2256
+ return;
2257
+ }
2258
+ const wrapper = this.wrapperEl;
2259
+ if (!wrapper) {
2260
+ return;
2261
+ }
2262
+ const rect = wrapper.getBoundingClientRect();
2263
+ const safeAreas = this.getSafeAreaValues();
2264
+ const extendsIntoTop = rect.top < safeAreas.top;
2265
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2266
+ const extendsIntoLeft = rect.left < safeAreas.left;
2267
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2268
+ // Only update DOM when state actually changes
2269
+ const prev = this.prevSafeAreaState;
2270
+ const style = this.el.style;
2271
+ if (extendsIntoTop !== prev.top) {
2272
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2273
+ prev.top = extendsIntoTop;
2274
+ }
2275
+ if (extendsIntoBottom !== prev.bottom) {
2276
+ extendsIntoBottom
2277
+ ? style.removeProperty('--ion-safe-area-bottom')
2278
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2279
+ prev.bottom = extendsIntoBottom;
2280
+ }
2281
+ if (extendsIntoLeft !== prev.left) {
2282
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2283
+ prev.left = extendsIntoLeft;
2284
+ }
2285
+ if (extendsIntoRight !== prev.right) {
2286
+ extendsIntoRight
2287
+ ? style.removeProperty('--ion-safe-area-right')
2288
+ : style.setProperty('--ion-safe-area-right', '0px');
2289
+ prev.right = extendsIntoRight;
2290
+ }
2291
+ }
2086
2292
  sheetOnDismiss() {
2087
2293
  /**
2088
2294
  * While the gesture animation is finishing
@@ -2175,6 +2381,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2175
2381
  }
2176
2382
  this.currentBreakpoint = undefined;
2177
2383
  this.animation = undefined;
2384
+ // Reset safe-area state for potential re-presentation
2385
+ this.resetSafeAreaState();
2178
2386
  unlock();
2179
2387
  return dismissed;
2180
2388
  }
@@ -2424,20 +2632,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2424
2632
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2425
2633
  const isHandleCycle = handleBehavior === 'cycle';
2426
2634
  const isSheetModalWithHandle = isSheetModal && showHandle;
2427
- return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2635
+ return (h(Host, Object.assign({ key: '8f42c71ec9c9270b4218ca7eec57ca998871ac07', "no-router": true,
2428
2636
  // Allow the modal to be navigable when the handle is focusable
2429
2637
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2430
2638
  zIndex: `${20000 + this.overlayIndex}`,
2431
- }, class: Object.assign({ [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: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
2639
+ }, class: Object.assign({ [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: '2f7b08b019c66cbfceacafa9a68b91f8da1f4084', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '6b0d1ae90e8483332f74371be5dcbd0a7b2661ba', class: "modal-shadow", ref: (el) => (this.shadowEl = el) }), h("div", Object.assign({ key: 'c96f671fb25e5de45231ea65ac52500547b2e262',
2432
2640
  /*
2433
2641
  role and aria-modal must be used on the
2434
2642
  same element. They must also be set inside the
2435
2643
  shadow DOM otherwise ion-button will not be highlighted
2436
2644
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2437
2645
  */
2438
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
2646
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'e07419b01a588c0ba62ed9607c843cac7bd26337', class: "modal-handle",
2439
2647
  // Prevents the handle from receiving keyboard focus when it does not cycle
2440
- 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: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
2648
+ 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: '65f35079df8eb8b6cd262e3a08297866715ce7ea', onSlotchange: this.onSlotChange }))));
2441
2649
  }
2442
2650
  get el() { return this; }
2443
2651
  static get watchers() { return {
@@ -657,6 +657,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
657
657
  let bottom;
658
658
  let originX = contentOriginX;
659
659
  let originY = contentOriginY;
660
+ let checkSafeAreaTop = false;
661
+ let checkSafeAreaBottom = false;
660
662
  let checkSafeAreaLeft = false;
661
663
  let checkSafeAreaRight = false;
662
664
  const triggerTop = triggerCoordinates
@@ -701,10 +703,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
701
703
  * We chose 12 here so that the popover position looks a bit nicer as
702
704
  * it is not right up against the edge of the screen.
703
705
  */
704
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
706
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
705
707
  arrowTop = top + contentHeight;
706
708
  originY = 'bottom';
707
709
  addPopoverBottomClass = true;
710
+ /**
711
+ * If the popover is positioned near the top edge, account for safe area.
712
+ * This ensures the popover doesn't overlap with status bars or notches.
713
+ */
714
+ if (top <= bodyPadding + safeAreaMargin) {
715
+ checkSafeAreaTop = true;
716
+ top = bodyPadding;
717
+ }
708
718
  /**
709
719
  * If not enough room for popover to appear
710
720
  * above trigger, then cut it off.
@@ -712,14 +722,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
712
722
  }
713
723
  else {
714
724
  bottom = bodyPadding;
725
+ /**
726
+ * When the popover is pinned to the bottom, account for safe area.
727
+ * This ensures the popover doesn't overlap with home indicators
728
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
729
+ */
730
+ checkSafeAreaBottom = true;
715
731
  }
716
732
  }
733
+ /**
734
+ * Final check: If the popover extends into any safe-area region,
735
+ * ensure the corresponding flag is set regardless of side.
736
+ * This handles cases where a side-positioned popover (left/right)
737
+ * still needs bottom safe-area padding because it extends into that region.
738
+ */
739
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
740
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
741
+ checkSafeAreaBottom = true;
742
+ }
743
+ if (top < safeAreaMargin) {
744
+ checkSafeAreaTop = true;
745
+ }
717
746
  return {
718
747
  top,
719
748
  left,
720
749
  bottom,
721
750
  originX,
722
751
  originY,
752
+ checkSafeAreaTop,
753
+ checkSafeAreaBottom,
723
754
  checkSafeAreaLeft,
724
755
  checkSafeAreaRight,
725
756
  arrowTop,
@@ -780,7 +811,7 @@ const iosEnterAnimation = (baseEl, opts) => {
780
811
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
781
812
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
782
813
  const margin = size === 'cover' ? 0 : 25;
783
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
814
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
784
815
  const baseAnimation = createAnimation();
785
816
  const backdropAnimation = createAnimation();
786
817
  const contentAnimation = createAnimation();
@@ -810,19 +841,35 @@ const iosEnterAnimation = (baseEl, opts) => {
810
841
  if (addPopoverBottomClass) {
811
842
  baseEl.classList.add('popover-bottom');
812
843
  }
813
- if (bottom !== undefined) {
814
- contentEl.style.setProperty('bottom', `${bottom}px`);
815
- }
844
+ /**
845
+ * Safe area CSS variable adjustments.
846
+ * When the popover is positioned near an edge, we add the corresponding
847
+ * safe-area inset to ensure the popover doesn't overlap with system UI
848
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
849
+ */
850
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
851
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
816
852
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
817
853
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
854
+ let topValue = `${top}px`;
855
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
818
856
  let leftValue = `${left}px`;
857
+ if (checkSafeAreaTop) {
858
+ topValue = `${top}px${safeAreaTop}`;
859
+ }
860
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
861
+ bottomValue = `${bottom}px${safeAreaBottom}`;
862
+ }
819
863
  if (checkSafeAreaLeft) {
820
864
  leftValue = `${left}px${safeAreaLeft}`;
821
865
  }
822
866
  if (checkSafeAreaRight) {
823
867
  leftValue = `${left}px${safeAreaRight}`;
824
868
  }
825
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
869
+ if (bottomValue !== undefined) {
870
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
871
+ }
872
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
826
873
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
827
874
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
828
875
  if (arrowEl !== null) {
@@ -898,7 +945,32 @@ const mdEnterAnimation = (baseEl, opts) => {
898
945
  };
899
946
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
900
947
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
901
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
948
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
949
+ /**
950
+ * Safe area CSS variable adjustments.
951
+ * When the popover is positioned near an edge, we add the corresponding
952
+ * safe-area inset to ensure the popover doesn't overlap with system UI
953
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
954
+ */
955
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
956
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
957
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
958
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
959
+ let topValue = `${top}px`;
960
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
961
+ let leftValue = `${left}px`;
962
+ if (checkSafeAreaTop) {
963
+ topValue = `${top}px${safeAreaTop}`;
964
+ }
965
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
966
+ bottomValue = `${bottom}px${safeAreaBottom}`;
967
+ }
968
+ if (checkSafeAreaLeft) {
969
+ leftValue = `${left}px${safeAreaLeft}`;
970
+ }
971
+ if (checkSafeAreaRight) {
972
+ leftValue = `${left}px${safeAreaRight}`;
973
+ }
902
974
  const baseAnimation = createAnimation();
903
975
  const backdropAnimation = createAnimation();
904
976
  const wrapperAnimation = createAnimation();
@@ -915,13 +987,13 @@ const mdEnterAnimation = (baseEl, opts) => {
915
987
  contentAnimation
916
988
  .addElement(contentEl)
917
989
  .beforeStyles({
918
- top: `calc(${top}px + var(--offset-y, 0px))`,
919
- left: `calc(${left}px + var(--offset-x, 0px))`,
990
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
991
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
920
992
  'transform-origin': `${originY} ${originX}`,
921
993
  })
922
994
  .beforeAddWrite(() => {
923
- if (bottom !== undefined) {
924
- contentEl.style.setProperty('bottom', `${bottom}px`);
995
+ if (bottomValue !== undefined) {
996
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
925
997
  }
926
998
  })
927
999
  .fromTo('transform', 'scale(0.8)', 'scale(1)');