@ionic/core 8.7.17-dev.11767895575.16ea7cef → 8.7.17-dev.11767897190.1ef0f479

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 (42) hide show
  1. package/components/content.js +96 -8
  2. package/components/ion-tab-bar.js +3 -23
  3. package/components/modal.js +213 -12
  4. package/components/popover.js +83 -11
  5. package/dist/cjs/ion-app_8.cjs.entry.js +107 -20
  6. package/dist/cjs/ion-modal.cjs.entry.js +213 -12
  7. package/dist/cjs/ion-popover.cjs.entry.js +83 -11
  8. package/dist/cjs/ion-tab-bar_2.cjs.entry.js +3 -23
  9. package/dist/collection/components/content/content.css +10 -0
  10. package/dist/collection/components/content/content.js +94 -6
  11. package/dist/collection/components/modal/gestures/sheet.js +3 -1
  12. package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
  13. package/dist/collection/components/modal/modal.ios.css +0 -4
  14. package/dist/collection/components/modal/modal.js +205 -7
  15. package/dist/collection/components/modal/modal.md.css +0 -4
  16. package/dist/collection/components/popover/animations/ios.enter.js +21 -5
  17. package/dist/collection/components/popover/animations/md.enter.js +30 -5
  18. package/dist/collection/components/popover/utils.js +32 -1
  19. package/dist/collection/components/tab-bar/tab-bar.js +3 -23
  20. package/dist/docs.json +1 -1
  21. package/dist/esm/ion-app_8.entry.js +96 -9
  22. package/dist/esm/ion-modal.entry.js +213 -12
  23. package/dist/esm/ion-popover.entry.js +83 -11
  24. package/dist/esm/ion-tab-bar_2.entry.js +3 -23
  25. package/dist/ionic/ionic.esm.js +1 -1
  26. package/dist/ionic/p-7268efa5.entry.js +4 -0
  27. package/dist/ionic/p-968a55d1.entry.js +4 -0
  28. package/dist/ionic/p-d9fd799f.entry.js +4 -0
  29. package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
  30. package/dist/types/components/content/content.d.ts +24 -0
  31. package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
  32. package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
  33. package/dist/types/components/modal/modal.d.ts +45 -0
  34. package/dist/types/components/popover/utils.d.ts +2 -0
  35. package/dist/types/components/tab-bar/tab-bar.d.ts +0 -1
  36. package/hydrate/index.js +385 -52
  37. package/hydrate/index.mjs +385 -52
  38. package/package.json +1 -1
  39. package/dist/ionic/p-172a579f.entry.js +0 -4
  40. package/dist/ionic/p-732b2fd6.entry.js +0 -4
  41. package/dist/ionic/p-91840a80.entry.js +0 -4
  42. package/dist/ionic/p-f9061316.entry.js +0 -4
@@ -4,6 +4,7 @@
4
4
  'use strict';
5
5
 
6
6
  var index$3 = require('./index-D6Wc6v08.js');
7
+ var index = require('./index-DkNv4J_i.js');
7
8
  var index$2 = require('./index-CO6eryBo.js');
8
9
  var frameworkDelegate = require('./framework-delegate-DMJRBuDi.js');
9
10
  var helpers = require('./helpers-DrTqNghc.js');
@@ -17,7 +18,6 @@ var keyboard = require('./keyboard-hHzlEQpk.js');
17
18
  var animation = require('./animation-Bt3H9L1C.js');
18
19
  var cubicBezier = require('./cubic-bezier-DAjy1V-e.js');
19
20
  var index$1 = require('./index-CAvQ7Tka.js');
20
- var index = require('./index-DkNv4J_i.js');
21
21
  require('./hardware-back-button-VCK4V3mG.js');
22
22
  require('./gesture-controller-dtqlP_q4.js');
23
23
  require('./keyboard-UuAS4D_9.js');
@@ -249,7 +249,7 @@ const calculateSpringStep = (t) => {
249
249
  const SwipeToCloseDefaults = {
250
250
  MIN_PRESENTING_SCALE: 0.915,
251
251
  };
252
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
252
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
253
253
  /**
254
254
  * The step value at which a card modal
255
255
  * is eligible for dismissing via gesture.
@@ -406,6 +406,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
406
406
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
407
407
  const clampedStep = helpers.clamp(0.0001, processedStep, maxStep);
408
408
  animation.progressStep(clampedStep);
409
+ // Notify modal of position change for safe-area updates
410
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
409
411
  /**
410
412
  * When swiping down half way, the status bar style
411
413
  * should be reset to its default value.
@@ -949,7 +951,7 @@ const mdLeaveAnimation = (baseEl, opts) => {
949
951
  return baseAnimation;
950
952
  };
951
953
 
952
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
954
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
953
955
  // Defaults for the sheet swipe animation
954
956
  const defaultBackdrop = [
955
957
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -1280,6 +1282,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1280
1282
  : step;
1281
1283
  offset = helpers.clamp(0.0001, processedStep, maxStep);
1282
1284
  animation.progressStep(offset);
1285
+ // Notify modal of position change for safe-area updates
1286
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
1283
1287
  };
1284
1288
  const onEnd = (detail) => {
1285
1289
  /**
@@ -1474,9 +1478,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1474
1478
  };
1475
1479
  };
1476
1480
 
1477
- 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}";
1481
+ 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}";
1478
1482
 
1479
- 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}";
1483
+ 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}";
1480
1484
 
1481
1485
  const Modal = class {
1482
1486
  constructor(hostRef) {
@@ -1499,6 +1503,10 @@ const Modal = class {
1499
1503
  this.inline = false;
1500
1504
  // Whether or not modal is being dismissed via gesture
1501
1505
  this.gestureAnimationDismissing = false;
1506
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1507
+ this.skipSafeAreaCoordinateDetection = false;
1508
+ // Track previous safe-area state to avoid redundant DOM writes
1509
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1502
1510
  this.presented = false;
1503
1511
  /** @internal */
1504
1512
  this.hasController = false;
@@ -1689,7 +1697,10 @@ const Modal = class {
1689
1697
  }
1690
1698
  }
1691
1699
  onWindowResize() {
1692
- // Only handle resize for iOS card modals when no custom animations are provided
1700
+ // Invalidate safe-area cache on resize (device rotation may change values)
1701
+ this.cachedSafeAreas = undefined;
1702
+ this.updateSafeAreaOverrides();
1703
+ // Only handle view transition for iOS card modals when no custom animations are provided
1693
1704
  if (ionicGlobal.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1694
1705
  return;
1695
1706
  }
@@ -1712,6 +1723,8 @@ const Modal = class {
1712
1723
  this.triggerController.removeClickListener();
1713
1724
  this.cleanupViewTransitionListener();
1714
1725
  this.cleanupParentRemovalObserver();
1726
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
1727
+ this.resetSafeAreaState();
1715
1728
  }
1716
1729
  componentWillLoad() {
1717
1730
  var _a;
@@ -1871,6 +1884,8 @@ const Modal = class {
1871
1884
  else if (!this.keepContentsMounted) {
1872
1885
  await index$4.waitForMount();
1873
1886
  }
1887
+ // Predict safe-area needs based on modal configuration to avoid visual snap
1888
+ this.setInitialSafeAreaOverrides(presentingElement);
1874
1889
  index$3.writeTask(() => this.el.classList.add('show-modal'));
1875
1890
  const hasCardModal = presentingElement !== undefined;
1876
1891
  /**
@@ -1932,6 +1947,8 @@ const Modal = class {
1932
1947
  else if (hasCardModal) {
1933
1948
  this.initSwipeToClose();
1934
1949
  }
1950
+ // Now that animation is complete, update safe-area based on actual position
1951
+ this.updateSafeAreaOverrides();
1935
1952
  // Initialize view transition listener for iOS card modals
1936
1953
  this.initViewTransitionListener();
1937
1954
  // Initialize parent removal observer
@@ -1983,7 +2000,7 @@ const Modal = class {
1983
2000
  await this.dismiss(undefined, overlays.GESTURE);
1984
2001
  this.gestureAnimationDismissing = false;
1985
2002
  });
1986
- });
2003
+ }, () => this.updateSafeAreaOverrides());
1987
2004
  this.gesture.enable(true);
1988
2005
  }
1989
2006
  initSheetGesture() {
@@ -2004,7 +2021,8 @@ const Modal = class {
2004
2021
  this.currentBreakpoint = breakpoint;
2005
2022
  this.ionBreakpointDidChange.emit({ breakpoint });
2006
2023
  }
2007
- });
2024
+ this.updateSafeAreaOverrides();
2025
+ }, () => this.updateSafeAreaOverrides());
2008
2026
  this.gesture = gesture;
2009
2027
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
2010
2028
  this.gesture.enable(true);
@@ -2082,6 +2100,187 @@ const Modal = class {
2082
2100
  // Clear the cached reference
2083
2101
  this.cachedPageParent = undefined;
2084
2102
  }
2103
+ /**
2104
+ * Sets initial safe-area overrides based on modal configuration before
2105
+ * the modal becomes visible. This predicts whether the modal will touch
2106
+ * screen edges to avoid a visual snap after animation completes.
2107
+ */
2108
+ setInitialSafeAreaOverrides(presentingElement) {
2109
+ const style = this.el.style;
2110
+ const mode = ionicGlobal.getIonMode(this);
2111
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2112
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
2113
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
2114
+ const isTablet = window.innerWidth >= 768;
2115
+ // Sheet modals always touch bottom edge, never top/left/right
2116
+ if (isSheetModal) {
2117
+ style.setProperty('--ion-safe-area-top', '0px');
2118
+ style.setProperty('--ion-safe-area-left', '0px');
2119
+ style.setProperty('--ion-safe-area-right', '0px');
2120
+ return;
2121
+ }
2122
+ // Card modals have rounded top corners
2123
+ if (isCardModal) {
2124
+ style.setProperty('--ion-safe-area-top', '0px');
2125
+ if (isTablet) {
2126
+ // On tablets, card modals are inset from all edges
2127
+ this.zeroAllSafeAreas();
2128
+ }
2129
+ else {
2130
+ // On phones, card modals still extend to the bottom edge
2131
+ style.setProperty('--ion-safe-area-left', '0px');
2132
+ style.setProperty('--ion-safe-area-right', '0px');
2133
+ this.applyFullscreenSafeArea();
2134
+ }
2135
+ return;
2136
+ }
2137
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
2138
+ if (!isTablet) {
2139
+ this.applyFullscreenSafeArea();
2140
+ return;
2141
+ }
2142
+ // Check if tablet modal is fullscreen via CSS custom properties
2143
+ const computedStyle = getComputedStyle(this.el);
2144
+ const width = computedStyle.getPropertyValue('--width').trim();
2145
+ const height = computedStyle.getPropertyValue('--height').trim();
2146
+ const isFullscreen = width === '100%' && height === '100%';
2147
+ if (isFullscreen) {
2148
+ this.applyFullscreenSafeArea();
2149
+ }
2150
+ else {
2151
+ // Centered dialog doesn't touch edges
2152
+ this.zeroAllSafeAreas();
2153
+ }
2154
+ }
2155
+ /**
2156
+ * Applies safe-area handling for fullscreen modals.
2157
+ * Adds wrapper padding when no footer is present to prevent
2158
+ * content from overlapping system navigation areas.
2159
+ */
2160
+ applyFullscreenSafeArea() {
2161
+ this.skipSafeAreaCoordinateDetection = true;
2162
+ this.updateFooterPadding();
2163
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2164
+ // Use subtree:true to support wrapped footers in framework components
2165
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
2166
+ if (!this.footerObserver && index.win !== undefined && 'MutationObserver' in index.win) {
2167
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2168
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2169
+ }
2170
+ }
2171
+ /**
2172
+ * Updates wrapper padding based on footer presence.
2173
+ * Called initially and when footer is dynamically added/removed.
2174
+ */
2175
+ updateFooterPadding() {
2176
+ if (!this.wrapperEl)
2177
+ return;
2178
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
2179
+ if (hasFooter) {
2180
+ this.wrapperEl.style.removeProperty('padding-bottom');
2181
+ this.wrapperEl.style.removeProperty('box-sizing');
2182
+ }
2183
+ else {
2184
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2185
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2186
+ }
2187
+ }
2188
+ /**
2189
+ * Sets all safe-area CSS variables to 0px for modals that
2190
+ * don't touch screen edges.
2191
+ */
2192
+ zeroAllSafeAreas() {
2193
+ const style = this.el.style;
2194
+ style.setProperty('--ion-safe-area-top', '0px');
2195
+ style.setProperty('--ion-safe-area-bottom', '0px');
2196
+ style.setProperty('--ion-safe-area-left', '0px');
2197
+ style.setProperty('--ion-safe-area-right', '0px');
2198
+ }
2199
+ /**
2200
+ * Resets all safe-area related state and styles.
2201
+ * Called during dismiss and disconnectedCallback to ensure clean state
2202
+ * for re-presentation of inline modals.
2203
+ */
2204
+ resetSafeAreaState() {
2205
+ var _a;
2206
+ this.skipSafeAreaCoordinateDetection = false;
2207
+ this.cachedSafeAreas = undefined;
2208
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2209
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2210
+ this.footerObserver = undefined;
2211
+ // Clear wrapper styles that may have been set for safe-area handling
2212
+ if (this.wrapperEl) {
2213
+ this.wrapperEl.style.removeProperty('padding-bottom');
2214
+ this.wrapperEl.style.removeProperty('box-sizing');
2215
+ }
2216
+ // Clear safe-area CSS variable overrides
2217
+ const style = this.el.style;
2218
+ style.removeProperty('--ion-safe-area-top');
2219
+ style.removeProperty('--ion-safe-area-bottom');
2220
+ style.removeProperty('--ion-safe-area-left');
2221
+ style.removeProperty('--ion-safe-area-right');
2222
+ }
2223
+ /**
2224
+ * Gets the root safe-area values from the document element.
2225
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2226
+ */
2227
+ getSafeAreaValues() {
2228
+ if (!this.cachedSafeAreas) {
2229
+ const rootStyle = getComputedStyle(document.documentElement);
2230
+ this.cachedSafeAreas = {
2231
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2232
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2233
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2234
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2235
+ };
2236
+ }
2237
+ return this.cachedSafeAreas;
2238
+ }
2239
+ /**
2240
+ * Updates safe-area CSS variable overrides based on whether the modal
2241
+ * extends into each safe-area region. Called after animation
2242
+ * and during gestures to handle dynamic position changes.
2243
+ *
2244
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2245
+ */
2246
+ updateSafeAreaOverrides() {
2247
+ if (this.skipSafeAreaCoordinateDetection) {
2248
+ return;
2249
+ }
2250
+ const wrapper = this.wrapperEl;
2251
+ if (!wrapper) {
2252
+ return;
2253
+ }
2254
+ const rect = wrapper.getBoundingClientRect();
2255
+ const safeAreas = this.getSafeAreaValues();
2256
+ const extendsIntoTop = rect.top < safeAreas.top;
2257
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2258
+ const extendsIntoLeft = rect.left < safeAreas.left;
2259
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2260
+ // Only update DOM when state actually changes
2261
+ const prev = this.prevSafeAreaState;
2262
+ const style = this.el.style;
2263
+ if (extendsIntoTop !== prev.top) {
2264
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2265
+ prev.top = extendsIntoTop;
2266
+ }
2267
+ if (extendsIntoBottom !== prev.bottom) {
2268
+ extendsIntoBottom
2269
+ ? style.removeProperty('--ion-safe-area-bottom')
2270
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2271
+ prev.bottom = extendsIntoBottom;
2272
+ }
2273
+ if (extendsIntoLeft !== prev.left) {
2274
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2275
+ prev.left = extendsIntoLeft;
2276
+ }
2277
+ if (extendsIntoRight !== prev.right) {
2278
+ extendsIntoRight
2279
+ ? style.removeProperty('--ion-safe-area-right')
2280
+ : style.setProperty('--ion-safe-area-right', '0px');
2281
+ prev.right = extendsIntoRight;
2282
+ }
2283
+ }
2085
2284
  sheetOnDismiss() {
2086
2285
  /**
2087
2286
  * While the gesture animation is finishing
@@ -2174,6 +2373,8 @@ const Modal = class {
2174
2373
  }
2175
2374
  this.currentBreakpoint = undefined;
2176
2375
  this.animation = undefined;
2376
+ // Reset safe-area state for potential re-presentation
2377
+ this.resetSafeAreaState();
2177
2378
  unlock();
2178
2379
  return dismissed;
2179
2380
  }
@@ -2423,20 +2624,20 @@ const Modal = class {
2423
2624
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2424
2625
  const isHandleCycle = handleBehavior === 'cycle';
2425
2626
  const isSheetModalWithHandle = isSheetModal && showHandle;
2426
- return (index$3.h(index$3.Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2627
+ return (index$3.h(index$3.Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
2427
2628
  // Allow the modal to be navigable when the handle is focusable
2428
2629
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2429
2630
  zIndex: `${20000 + this.overlayIndex}`,
2430
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
2631
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
2431
2632
  /*
2432
2633
  role and aria-modal must be used on the
2433
2634
  same element. They must also be set inside the
2434
2635
  shadow DOM otherwise ion-button will not be highlighted
2435
2636
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2436
2637
  */
2437
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
2638
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
2438
2639
  // Prevents the handle from receiving keyboard focus when it does not cycle
2439
- 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) })), index$3.h("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
2640
+ 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) })), index$3.h("slot", { key: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
2440
2641
  }
2441
2642
  get el() { return index$3.getElement(this); }
2442
2643
  static get watchers() { return {
@@ -660,6 +660,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
660
660
  let bottom;
661
661
  let originX = contentOriginX;
662
662
  let originY = contentOriginY;
663
+ let checkSafeAreaTop = false;
664
+ let checkSafeAreaBottom = false;
663
665
  let checkSafeAreaLeft = false;
664
666
  let checkSafeAreaRight = false;
665
667
  const triggerTop = triggerCoordinates
@@ -704,10 +706,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
704
706
  * We chose 12 here so that the popover position looks a bit nicer as
705
707
  * it is not right up against the edge of the screen.
706
708
  */
707
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
709
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
708
710
  arrowTop = top + contentHeight;
709
711
  originY = 'bottom';
710
712
  addPopoverBottomClass = true;
713
+ /**
714
+ * If the popover is positioned near the top edge, account for safe area.
715
+ * This ensures the popover doesn't overlap with status bars or notches.
716
+ */
717
+ if (top <= bodyPadding + safeAreaMargin) {
718
+ checkSafeAreaTop = true;
719
+ top = bodyPadding;
720
+ }
711
721
  /**
712
722
  * If not enough room for popover to appear
713
723
  * above trigger, then cut it off.
@@ -715,14 +725,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
715
725
  }
716
726
  else {
717
727
  bottom = bodyPadding;
728
+ /**
729
+ * When the popover is pinned to the bottom, account for safe area.
730
+ * This ensures the popover doesn't overlap with home indicators
731
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
732
+ */
733
+ checkSafeAreaBottom = true;
718
734
  }
719
735
  }
736
+ /**
737
+ * Final check: If the popover extends into any safe-area region,
738
+ * ensure the corresponding flag is set regardless of side.
739
+ * This handles cases where a side-positioned popover (left/right)
740
+ * still needs bottom safe-area padding because it extends into that region.
741
+ */
742
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
743
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
744
+ checkSafeAreaBottom = true;
745
+ }
746
+ if (top < safeAreaMargin) {
747
+ checkSafeAreaTop = true;
748
+ }
720
749
  return {
721
750
  top,
722
751
  left,
723
752
  bottom,
724
753
  originX,
725
754
  originY,
755
+ checkSafeAreaTop,
756
+ checkSafeAreaBottom,
726
757
  checkSafeAreaLeft,
727
758
  checkSafeAreaRight,
728
759
  arrowTop,
@@ -783,7 +814,7 @@ const iosEnterAnimation = (baseEl, opts) => {
783
814
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
784
815
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
785
816
  const margin = size === 'cover' ? 0 : 25;
786
- 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);
817
+ 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);
787
818
  const baseAnimation = animation.createAnimation();
788
819
  const backdropAnimation = animation.createAnimation();
789
820
  const contentAnimation = animation.createAnimation();
@@ -813,19 +844,35 @@ const iosEnterAnimation = (baseEl, opts) => {
813
844
  if (addPopoverBottomClass) {
814
845
  baseEl.classList.add('popover-bottom');
815
846
  }
816
- if (bottom !== undefined) {
817
- contentEl.style.setProperty('bottom', `${bottom}px`);
818
- }
847
+ /**
848
+ * Safe area CSS variable adjustments.
849
+ * When the popover is positioned near an edge, we add the corresponding
850
+ * safe-area inset to ensure the popover doesn't overlap with system UI
851
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
852
+ */
853
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
854
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
819
855
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
820
856
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
857
+ let topValue = `${top}px`;
858
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
821
859
  let leftValue = `${left}px`;
860
+ if (checkSafeAreaTop) {
861
+ topValue = `${top}px${safeAreaTop}`;
862
+ }
863
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
864
+ bottomValue = `${bottom}px${safeAreaBottom}`;
865
+ }
822
866
  if (checkSafeAreaLeft) {
823
867
  leftValue = `${left}px${safeAreaLeft}`;
824
868
  }
825
869
  if (checkSafeAreaRight) {
826
870
  leftValue = `${left}px${safeAreaRight}`;
827
871
  }
828
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
872
+ if (bottomValue !== undefined) {
873
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
874
+ }
875
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
829
876
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
830
877
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
831
878
  if (arrowEl !== null) {
@@ -901,7 +948,32 @@ const mdEnterAnimation = (baseEl, opts) => {
901
948
  };
902
949
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
903
950
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
904
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
951
+ 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);
952
+ /**
953
+ * Safe area CSS variable adjustments.
954
+ * When the popover is positioned near an edge, we add the corresponding
955
+ * safe-area inset to ensure the popover doesn't overlap with system UI
956
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
957
+ */
958
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
959
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
960
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
961
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
962
+ let topValue = `${top}px`;
963
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
964
+ let leftValue = `${left}px`;
965
+ if (checkSafeAreaTop) {
966
+ topValue = `${top}px${safeAreaTop}`;
967
+ }
968
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
969
+ bottomValue = `${bottom}px${safeAreaBottom}`;
970
+ }
971
+ if (checkSafeAreaLeft) {
972
+ leftValue = `${left}px${safeAreaLeft}`;
973
+ }
974
+ if (checkSafeAreaRight) {
975
+ leftValue = `${left}px${safeAreaRight}`;
976
+ }
905
977
  const baseAnimation = animation.createAnimation();
906
978
  const backdropAnimation = animation.createAnimation();
907
979
  const wrapperAnimation = animation.createAnimation();
@@ -918,13 +990,13 @@ const mdEnterAnimation = (baseEl, opts) => {
918
990
  contentAnimation
919
991
  .addElement(contentEl)
920
992
  .beforeStyles({
921
- top: `calc(${top}px + var(--offset-y, 0px))`,
922
- left: `calc(${left}px + var(--offset-x, 0px))`,
993
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
994
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
923
995
  'transform-origin': `${originY} ${originX}`,
924
996
  })
925
997
  .beforeAddWrite(() => {
926
- if (bottom !== undefined) {
927
- contentEl.style.setProperty('bottom', `${bottom}px`);
998
+ if (bottomValue !== undefined) {
999
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
928
1000
  }
929
1001
  })
930
1002
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -22,7 +22,6 @@ const TabBar = class {
22
22
  this.ionTabBarChanged = index.createEvent(this, "ionTabBarChanged", 7);
23
23
  this.ionTabBarLoaded = index.createEvent(this, "ionTabBarLoaded", 7);
24
24
  this.keyboardCtrl = null;
25
- this.keyboardCtrlPromise = null;
26
25
  this.didLoad = false;
27
26
  this.keyboardVisible = false;
28
27
  /**
@@ -58,7 +57,7 @@ const TabBar = class {
58
57
  }
59
58
  }
60
59
  async connectedCallback() {
61
- const promise = keyboardController.createKeyboardController(async (keyboardOpen, waitForResize) => {
60
+ this.keyboardCtrl = await keyboardController.createKeyboardController(async (keyboardOpen, waitForResize) => {
62
61
  /**
63
62
  * If the keyboard is hiding, then we need to wait
64
63
  * for the webview to resize. Otherwise, the tab bar
@@ -69,40 +68,21 @@ const TabBar = class {
69
68
  }
70
69
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
71
70
  });
72
- this.keyboardCtrlPromise = promise;
73
- const keyboardCtrl = await promise;
74
- /**
75
- * Only assign if this is still the current promise.
76
- * Otherwise, a new connectedCallback has started or
77
- * disconnectedCallback was called, so destroy this instance.
78
- */
79
- if (this.keyboardCtrlPromise === promise) {
80
- this.keyboardCtrl = keyboardCtrl;
81
- this.keyboardCtrlPromise = null;
82
- }
83
- else {
84
- keyboardCtrl.destroy();
85
- }
86
71
  }
87
72
  disconnectedCallback() {
88
- if (this.keyboardCtrlPromise) {
89
- this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
90
- this.keyboardCtrlPromise = null;
91
- }
92
73
  if (this.keyboardCtrl) {
93
74
  this.keyboardCtrl.destroy();
94
- this.keyboardCtrl = null;
95
75
  }
96
76
  }
97
77
  render() {
98
78
  const { color, translucent, keyboardVisible } = this;
99
79
  const mode = ionicGlobal.getIonMode(this);
100
80
  const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
101
- return (index.h(index.Host, { key: '9daf4e2acaff6e3ce3878cf9dd5109fb1afbbebe', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: theme.createColorClasses(color, {
81
+ return (index.h(index.Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: theme.createColorClasses(color, {
102
82
  [mode]: true,
103
83
  'tab-bar-translucent': translucent,
104
84
  'tab-bar-hidden': shouldHide,
105
- }) }, index.h("slot", { key: '1d15aa2da8501e8e7eff11ad4a491478be845c43' })));
85
+ }) }, index.h("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
106
86
  }
107
87
  get el() { return index.getElement(this); }
108
88
  static get watchers() { return {
@@ -263,6 +263,16 @@
263
263
  transform: scaleX(-1);
264
264
  }
265
265
 
266
+ :host(.safe-area-top) #background-content,
267
+ :host(.safe-area-top) .inner-scroll {
268
+ top: var(--ion-safe-area-top, 0px);
269
+ }
270
+
271
+ :host(.safe-area-bottom) #background-content,
272
+ :host(.safe-area-bottom) .inner-scroll {
273
+ bottom: var(--ion-safe-area-bottom, 0px);
274
+ }
275
+
266
276
  ::slotted([slot=fixed]) {
267
277
  position: absolute;
268
278
  /**