@ionic/core 8.7.17-dev.11767636336.113b441d → 8.7.17-dev.11767639327.19c404a6

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.
Files changed (34) hide show
  1. package/components/ion-tab-bar.js +3 -17
  2. package/components/modal.js +127 -11
  3. package/components/popover.js +60 -10
  4. package/dist/cjs/ion-modal.cjs.entry.js +127 -11
  5. package/dist/cjs/ion-popover.cjs.entry.js +60 -10
  6. package/dist/cjs/ion-tab-bar_2.cjs.entry.js +3 -17
  7. package/dist/collection/components/modal/gestures/sheet.js +3 -1
  8. package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
  9. package/dist/collection/components/modal/modal.ios.css +0 -4
  10. package/dist/collection/components/modal/modal.js +119 -7
  11. package/dist/collection/components/modal/modal.md.css +0 -4
  12. package/dist/collection/components/popover/animations/ios.enter.js +21 -5
  13. package/dist/collection/components/popover/animations/md.enter.js +20 -4
  14. package/dist/collection/components/popover/utils.js +19 -1
  15. package/dist/collection/components/tab-bar/tab-bar.js +3 -17
  16. package/dist/docs.json +1 -1
  17. package/dist/esm/ion-modal.entry.js +127 -11
  18. package/dist/esm/ion-popover.entry.js +60 -10
  19. package/dist/esm/ion-tab-bar_2.entry.js +3 -17
  20. package/dist/ionic/ionic.esm.js +1 -1
  21. package/dist/ionic/p-301ad219.entry.js +4 -0
  22. package/dist/ionic/p-71fa7adc.entry.js +4 -0
  23. package/dist/ionic/p-7268efa5.entry.js +4 -0
  24. package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
  25. package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
  26. package/dist/types/components/modal/modal.d.ts +24 -0
  27. package/dist/types/components/popover/utils.d.ts +2 -0
  28. package/dist/types/components/tab-bar/tab-bar.d.ts +0 -1
  29. package/hydrate/index.js +190 -38
  30. package/hydrate/index.mjs +190 -38
  31. package/package.json +1 -1
  32. package/dist/ionic/p-0d0eb444.entry.js +0 -4
  33. package/dist/ionic/p-91840a80.entry.js +0 -4
  34. package/dist/ionic/p-f9061316.entry.js +0 -4
@@ -21,7 +21,6 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
21
21
  this.ionTabBarLoaded = createEvent(this, "ionTabBarLoaded", 7);
22
22
  this.keyboardCtrl = null;
23
23
  this.didLoad = false;
24
- this.isComponentConnected = false;
25
24
  this.keyboardVisible = false;
26
25
  /**
27
26
  * If `true`, the tab bar will be translucent.
@@ -56,8 +55,7 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
56
55
  }
57
56
  }
58
57
  async connectedCallback() {
59
- this.isComponentConnected = true;
60
- const keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
58
+ this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
61
59
  /**
62
60
  * If the keyboard is hiding, then we need to wait
63
61
  * for the webview to resize. Otherwise, the tab bar
@@ -68,33 +66,21 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
68
66
  }
69
67
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
70
68
  });
71
- /**
72
- * Destroy the keyboard controller if the component was
73
- * disconnected during async initialization to prevent memory leaks.
74
- */
75
- if (this.isComponentConnected) {
76
- this.keyboardCtrl = keyboardCtrl;
77
- }
78
- else {
79
- keyboardCtrl.destroy();
80
- }
81
69
  }
82
70
  disconnectedCallback() {
83
- this.isComponentConnected = false;
84
71
  if (this.keyboardCtrl) {
85
72
  this.keyboardCtrl.destroy();
86
- this.keyboardCtrl = null;
87
73
  }
88
74
  }
89
75
  render() {
90
76
  const { color, translucent, keyboardVisible } = this;
91
77
  const mode = getIonMode(this);
92
78
  const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
93
- return (h(Host, { key: '1f721132a86fd0db988bf9af0fbc1e8e6fa6b7da', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses(color, {
79
+ return (h(Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses(color, {
94
80
  [mode]: true,
95
81
  'tab-bar-translucent': translucent,
96
82
  'tab-bar-hidden': shouldHide,
97
- }) }, h("slot", { key: '828838764307f150c64e9bbbbbafbdee6fa8e9f2' })));
83
+ }) }, h("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
98
84
  }
99
85
  get el() { return this; }
100
86
  static get watchers() { return {
@@ -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,8 @@ 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;
1503
1509
  this.presented = false;
1504
1510
  /** @internal */
1505
1511
  this.hasController = false;
@@ -1690,7 +1696,9 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1690
1696
  }
1691
1697
  }
1692
1698
  onWindowResize() {
1693
- // Only handle resize for iOS card modals when no custom animations are provided
1699
+ // Update safe-area overrides for all modal types on resize
1700
+ this.updateSafeAreaOverrides();
1701
+ // Only handle view transition for iOS card modals when no custom animations are provided
1694
1702
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1695
1703
  return;
1696
1704
  }
@@ -1872,6 +1880,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1872
1880
  else if (!this.keepContentsMounted) {
1873
1881
  await waitForMount();
1874
1882
  }
1883
+ // Predict safe-area needs based on modal configuration to avoid visual snap
1884
+ this.setInitialSafeAreaOverrides(presentingElement);
1875
1885
  writeTask(() => this.el.classList.add('show-modal'));
1876
1886
  const hasCardModal = presentingElement !== undefined;
1877
1887
  /**
@@ -1933,6 +1943,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1933
1943
  else if (hasCardModal) {
1934
1944
  this.initSwipeToClose();
1935
1945
  }
1946
+ // Now that animation is complete, update safe-area based on actual position
1947
+ this.updateSafeAreaOverrides();
1936
1948
  // Initialize view transition listener for iOS card modals
1937
1949
  this.initViewTransitionListener();
1938
1950
  // Initialize parent removal observer
@@ -1984,7 +1996,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1984
1996
  await this.dismiss(undefined, GESTURE);
1985
1997
  this.gestureAnimationDismissing = false;
1986
1998
  });
1987
- });
1999
+ }, () => this.updateSafeAreaOverrides());
1988
2000
  this.gesture.enable(true);
1989
2001
  }
1990
2002
  initSheetGesture() {
@@ -2005,7 +2017,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2005
2017
  this.currentBreakpoint = breakpoint;
2006
2018
  this.ionBreakpointDidChange.emit({ breakpoint });
2007
2019
  }
2008
- });
2020
+ this.updateSafeAreaOverrides();
2021
+ }, () => this.updateSafeAreaOverrides());
2009
2022
  this.gesture = gesture;
2010
2023
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
2011
2024
  this.gesture.enable(true);
@@ -2083,6 +2096,107 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2083
2096
  // Clear the cached reference
2084
2097
  this.cachedPageParent = undefined;
2085
2098
  }
2099
+ /**
2100
+ * Sets initial safe-area overrides based on modal configuration before
2101
+ * the modal becomes visible. This predicts whether the modal will touch
2102
+ * screen edges to avoid a visual snap after animation completes.
2103
+ */
2104
+ setInitialSafeAreaOverrides(presentingElement) {
2105
+ const style = this.el.style;
2106
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2107
+ const isCardModal = presentingElement !== undefined;
2108
+ const isTablet = window.innerWidth >= 768;
2109
+ // Sheet modals always touch bottom edge, never top/left/right
2110
+ if (isSheetModal) {
2111
+ style.setProperty('--ion-safe-area-top', '0px');
2112
+ style.setProperty('--ion-safe-area-left', '0px');
2113
+ style.setProperty('--ion-safe-area-right', '0px');
2114
+ return;
2115
+ }
2116
+ // Card modals have rounded top corners
2117
+ if (isCardModal) {
2118
+ style.setProperty('--ion-safe-area-top', '0px');
2119
+ if (isTablet) {
2120
+ // On tablets, card modals are inset from all edges
2121
+ this.zeroAllSafeAreas();
2122
+ }
2123
+ else {
2124
+ // On phones, card modals still extend to the bottom edge
2125
+ style.setProperty('--ion-safe-area-left', '0px');
2126
+ style.setProperty('--ion-safe-area-right', '0px');
2127
+ this.applyFullscreenSafeArea();
2128
+ }
2129
+ return;
2130
+ }
2131
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
2132
+ if (!isTablet) {
2133
+ this.applyFullscreenSafeArea();
2134
+ return;
2135
+ }
2136
+ // Check if tablet modal is fullscreen via CSS custom properties
2137
+ const computedStyle = getComputedStyle(this.el);
2138
+ const width = computedStyle.getPropertyValue('--width').trim();
2139
+ const height = computedStyle.getPropertyValue('--height').trim();
2140
+ const isFullscreen = width === '100%' && height === '100%';
2141
+ if (isFullscreen) {
2142
+ this.applyFullscreenSafeArea();
2143
+ }
2144
+ else {
2145
+ // Centered dialog doesn't touch edges
2146
+ this.zeroAllSafeAreas();
2147
+ }
2148
+ }
2149
+ /**
2150
+ * Applies safe-area handling for fullscreen modals.
2151
+ * Adds wrapper padding when no footer is present to prevent
2152
+ * content from overlapping system navigation areas.
2153
+ */
2154
+ applyFullscreenSafeArea() {
2155
+ this.skipSafeAreaCoordinateDetection = true;
2156
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
2157
+ if (!hasFooter && this.wrapperEl) {
2158
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2159
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Sets all safe-area CSS variables to 0px for modals that
2164
+ * don't touch screen edges.
2165
+ */
2166
+ zeroAllSafeAreas() {
2167
+ const style = this.el.style;
2168
+ style.setProperty('--ion-safe-area-top', '0px');
2169
+ style.setProperty('--ion-safe-area-bottom', '0px');
2170
+ style.setProperty('--ion-safe-area-left', '0px');
2171
+ style.setProperty('--ion-safe-area-right', '0px');
2172
+ }
2173
+ /**
2174
+ * Updates safe-area CSS variable overrides based on whether the modal
2175
+ * is touching each edge of the viewport. Called after animation
2176
+ * and during gestures to handle dynamic position changes.
2177
+ */
2178
+ updateSafeAreaOverrides() {
2179
+ if (this.skipSafeAreaCoordinateDetection) {
2180
+ return;
2181
+ }
2182
+ const wrapper = this.wrapperEl;
2183
+ if (!wrapper) {
2184
+ return;
2185
+ }
2186
+ const rect = wrapper.getBoundingClientRect();
2187
+ const threshold = 2;
2188
+ const touchingTop = rect.top <= threshold;
2189
+ const touchingBottom = rect.bottom >= window.innerHeight - threshold;
2190
+ const touchingLeft = rect.left <= threshold;
2191
+ const touchingRight = rect.right >= window.innerWidth - threshold;
2192
+ const style = this.el.style;
2193
+ touchingTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2194
+ touchingBottom
2195
+ ? style.removeProperty('--ion-safe-area-bottom')
2196
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2197
+ touchingLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2198
+ touchingRight ? style.removeProperty('--ion-safe-area-right') : style.setProperty('--ion-safe-area-right', '0px');
2199
+ }
2086
2200
  sheetOnDismiss() {
2087
2201
  /**
2088
2202
  * While the gesture animation is finishing
@@ -2175,6 +2289,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2175
2289
  }
2176
2290
  this.currentBreakpoint = undefined;
2177
2291
  this.animation = undefined;
2292
+ // Reset safe-area detection flag for potential re-presentation
2293
+ this.skipSafeAreaCoordinateDetection = false;
2178
2294
  unlock();
2179
2295
  return dismissed;
2180
2296
  }
@@ -2424,20 +2540,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2424
2540
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2425
2541
  const isHandleCycle = handleBehavior === 'cycle';
2426
2542
  const isSheetModalWithHandle = isSheetModal && showHandle;
2427
- return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2543
+ return (h(Host, Object.assign({ key: '6642b8a32dad449f26ac4ed60814d69afc97bf30', "no-router": true,
2428
2544
  // Allow the modal to be navigable when the handle is focusable
2429
2545
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2430
2546
  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',
2547
+ }, 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: '8adf79ea72c0f622190ff366980621067c35795b', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '34f14fcd4970c04aee3b7a5305f9f220ff377f37', class: "modal-shadow" }), h("div", Object.assign({ key: 'a3a8e3df6a1cfe061bbd99503655015ca8cd416d',
2432
2548
  /*
2433
2549
  role and aria-modal must be used on the
2434
2550
  same element. They must also be set inside the
2435
2551
  shadow DOM otherwise ion-button will not be highlighted
2436
2552
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2437
2553
  */
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",
2554
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'a4ad2f6def8c20a36a9731553dfa5a98392151ef', class: "modal-handle",
2439
2555
  // 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 }))));
2556
+ 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: '46fb6d94814dae4a9bcdf99387d57a17c6a6a95c', onSlotchange: this.onSlotChange }))));
2441
2557
  }
2442
2558
  get el() { return this; }
2443
2559
  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,6 +722,12 @@ 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
  }
717
733
  return {
@@ -720,6 +736,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
720
736
  bottom,
721
737
  originX,
722
738
  originY,
739
+ checkSafeAreaTop,
740
+ checkSafeAreaBottom,
723
741
  checkSafeAreaLeft,
724
742
  checkSafeAreaRight,
725
743
  arrowTop,
@@ -780,7 +798,7 @@ const iosEnterAnimation = (baseEl, opts) => {
780
798
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
781
799
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
782
800
  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);
801
+ 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
802
  const baseAnimation = createAnimation();
785
803
  const backdropAnimation = createAnimation();
786
804
  const contentAnimation = createAnimation();
@@ -810,19 +828,35 @@ const iosEnterAnimation = (baseEl, opts) => {
810
828
  if (addPopoverBottomClass) {
811
829
  baseEl.classList.add('popover-bottom');
812
830
  }
813
- if (bottom !== undefined) {
814
- contentEl.style.setProperty('bottom', `${bottom}px`);
815
- }
831
+ /**
832
+ * Safe area CSS variable adjustments.
833
+ * When the popover is positioned near an edge, we add the corresponding
834
+ * safe-area inset to ensure the popover doesn't overlap with system UI
835
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
836
+ */
837
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
838
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
816
839
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
817
840
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
841
+ let topValue = `${top}px`;
842
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
818
843
  let leftValue = `${left}px`;
844
+ if (checkSafeAreaTop) {
845
+ topValue = `${top}px${safeAreaTop}`;
846
+ }
847
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
848
+ bottomValue = `${bottom}px${safeAreaBottom}`;
849
+ }
819
850
  if (checkSafeAreaLeft) {
820
851
  leftValue = `${left}px${safeAreaLeft}`;
821
852
  }
822
853
  if (checkSafeAreaRight) {
823
854
  leftValue = `${left}px${safeAreaRight}`;
824
855
  }
825
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
856
+ if (bottomValue !== undefined) {
857
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
858
+ }
859
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
826
860
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
827
861
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
828
862
  if (arrowEl !== null) {
@@ -898,7 +932,23 @@ const mdEnterAnimation = (baseEl, opts) => {
898
932
  };
899
933
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
900
934
  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);
935
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
936
+ /**
937
+ * Safe area CSS variable adjustments.
938
+ * When the popover is positioned near an edge, we add the corresponding
939
+ * safe-area inset to ensure the popover doesn't overlap with system UI
940
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
941
+ */
942
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
943
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
944
+ let topValue = `${top}px`;
945
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
946
+ if (checkSafeAreaTop) {
947
+ topValue = `${top}px${safeAreaTop}`;
948
+ }
949
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
950
+ bottomValue = `${bottom}px${safeAreaBottom}`;
951
+ }
902
952
  const baseAnimation = createAnimation();
903
953
  const backdropAnimation = createAnimation();
904
954
  const wrapperAnimation = createAnimation();
@@ -915,13 +965,13 @@ const mdEnterAnimation = (baseEl, opts) => {
915
965
  contentAnimation
916
966
  .addElement(contentEl)
917
967
  .beforeStyles({
918
- top: `calc(${top}px + var(--offset-y, 0px))`,
968
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
919
969
  left: `calc(${left}px + var(--offset-x, 0px))`,
920
970
  'transform-origin': `${originY} ${originX}`,
921
971
  })
922
972
  .beforeAddWrite(() => {
923
- if (bottom !== undefined) {
924
- contentEl.style.setProperty('bottom', `${bottom}px`);
973
+ if (bottomValue !== undefined) {
974
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
925
975
  }
926
976
  })
927
977
  .fromTo('transform', 'scale(0.8)', 'scale(1)');