@ionic/core 8.7.17-dev.11767636336.113b441d → 8.7.17-dev.11767641184.14a165bc

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 +144 -11
  3. package/components/popover.js +60 -10
  4. package/dist/cjs/ion-modal.cjs.entry.js +144 -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 +136 -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 +144 -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-7268efa5.entry.js +4 -0
  23. package/dist/ionic/p-fedca459.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 +29 -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 +207 -38
  30. package/hydrate/index.mjs +207 -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
@@ -247,7 +247,7 @@ const calculateSpringStep = (t) => {
247
247
  const SwipeToCloseDefaults = {
248
248
  MIN_PRESENTING_SCALE: 0.915,
249
249
  };
250
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
250
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
251
251
  /**
252
252
  * The step value at which a card modal
253
253
  * is eligible for dismissing via gesture.
@@ -404,6 +404,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
404
404
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
405
405
  const clampedStep = clamp(0.0001, processedStep, maxStep);
406
406
  animation.progressStep(clampedStep);
407
+ // Notify modal of position change for safe-area updates
408
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
407
409
  /**
408
410
  * When swiping down half way, the status bar style
409
411
  * should be reset to its default value.
@@ -947,7 +949,7 @@ const mdLeaveAnimation = (baseEl, opts) => {
947
949
  return baseAnimation;
948
950
  };
949
951
 
950
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
952
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
951
953
  // Defaults for the sheet swipe animation
952
954
  const defaultBackdrop = [
953
955
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -1278,6 +1280,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1278
1280
  : step;
1279
1281
  offset = clamp(0.0001, processedStep, maxStep);
1280
1282
  animation.progressStep(offset);
1283
+ // Notify modal of position change for safe-area updates
1284
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
1281
1285
  };
1282
1286
  const onEnd = (detail) => {
1283
1287
  /**
@@ -1472,9 +1476,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1472
1476
  };
1473
1477
  };
1474
1478
 
1475
- 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}";
1479
+ 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}";
1476
1480
 
1477
- 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}";
1481
+ 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}";
1478
1482
 
1479
1483
  const Modal = class {
1480
1484
  constructor(hostRef) {
@@ -1497,6 +1501,8 @@ const Modal = class {
1497
1501
  this.inline = false;
1498
1502
  // Whether or not modal is being dismissed via gesture
1499
1503
  this.gestureAnimationDismissing = false;
1504
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1505
+ this.skipSafeAreaCoordinateDetection = false;
1500
1506
  this.presented = false;
1501
1507
  /** @internal */
1502
1508
  this.hasController = false;
@@ -1687,7 +1693,9 @@ const Modal = class {
1687
1693
  }
1688
1694
  }
1689
1695
  onWindowResize() {
1690
- // Only handle resize for iOS card modals when no custom animations are provided
1696
+ // Update safe-area overrides for all modal types on resize
1697
+ this.updateSafeAreaOverrides();
1698
+ // Only handle view transition for iOS card modals when no custom animations are provided
1691
1699
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1692
1700
  return;
1693
1701
  }
@@ -1869,6 +1877,8 @@ const Modal = class {
1869
1877
  else if (!this.keepContentsMounted) {
1870
1878
  await waitForMount();
1871
1879
  }
1880
+ // Predict safe-area needs based on modal configuration to avoid visual snap
1881
+ this.setInitialSafeAreaOverrides(presentingElement);
1872
1882
  writeTask(() => this.el.classList.add('show-modal'));
1873
1883
  const hasCardModal = presentingElement !== undefined;
1874
1884
  /**
@@ -1930,6 +1940,8 @@ const Modal = class {
1930
1940
  else if (hasCardModal) {
1931
1941
  this.initSwipeToClose();
1932
1942
  }
1943
+ // Now that animation is complete, update safe-area based on actual position
1944
+ this.updateSafeAreaOverrides();
1933
1945
  // Initialize view transition listener for iOS card modals
1934
1946
  this.initViewTransitionListener();
1935
1947
  // Initialize parent removal observer
@@ -1981,7 +1993,7 @@ const Modal = class {
1981
1993
  await this.dismiss(undefined, GESTURE);
1982
1994
  this.gestureAnimationDismissing = false;
1983
1995
  });
1984
- });
1996
+ }, () => this.updateSafeAreaOverrides());
1985
1997
  this.gesture.enable(true);
1986
1998
  }
1987
1999
  initSheetGesture() {
@@ -2002,7 +2014,8 @@ const Modal = class {
2002
2014
  this.currentBreakpoint = breakpoint;
2003
2015
  this.ionBreakpointDidChange.emit({ breakpoint });
2004
2016
  }
2005
- });
2017
+ this.updateSafeAreaOverrides();
2018
+ }, () => this.updateSafeAreaOverrides());
2006
2019
  this.gesture = gesture;
2007
2020
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
2008
2021
  this.gesture.enable(true);
@@ -2080,6 +2093,124 @@ const Modal = class {
2080
2093
  // Clear the cached reference
2081
2094
  this.cachedPageParent = undefined;
2082
2095
  }
2096
+ /**
2097
+ * Sets initial safe-area overrides based on modal configuration before
2098
+ * the modal becomes visible. This predicts whether the modal will touch
2099
+ * screen edges to avoid a visual snap after animation completes.
2100
+ */
2101
+ setInitialSafeAreaOverrides(presentingElement) {
2102
+ const style = this.el.style;
2103
+ const mode = getIonMode(this);
2104
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2105
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
2106
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
2107
+ const isTablet = window.innerWidth >= 768;
2108
+ // Sheet modals always touch bottom edge, never top/left/right
2109
+ if (isSheetModal) {
2110
+ style.setProperty('--ion-safe-area-top', '0px');
2111
+ style.setProperty('--ion-safe-area-left', '0px');
2112
+ style.setProperty('--ion-safe-area-right', '0px');
2113
+ return;
2114
+ }
2115
+ // Card modals have rounded top corners
2116
+ if (isCardModal) {
2117
+ style.setProperty('--ion-safe-area-top', '0px');
2118
+ if (isTablet) {
2119
+ // On tablets, card modals are inset from all edges
2120
+ this.zeroAllSafeAreas();
2121
+ }
2122
+ else {
2123
+ // On phones, card modals still extend to the bottom edge
2124
+ style.setProperty('--ion-safe-area-left', '0px');
2125
+ style.setProperty('--ion-safe-area-right', '0px');
2126
+ this.applyFullscreenSafeArea();
2127
+ }
2128
+ return;
2129
+ }
2130
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
2131
+ if (!isTablet) {
2132
+ this.applyFullscreenSafeArea();
2133
+ return;
2134
+ }
2135
+ // Check if tablet modal is fullscreen via CSS custom properties
2136
+ const computedStyle = getComputedStyle(this.el);
2137
+ const width = computedStyle.getPropertyValue('--width').trim();
2138
+ const height = computedStyle.getPropertyValue('--height').trim();
2139
+ const isFullscreen = width === '100%' && height === '100%';
2140
+ if (isFullscreen) {
2141
+ this.applyFullscreenSafeArea();
2142
+ }
2143
+ else {
2144
+ // Centered dialog doesn't touch edges
2145
+ this.zeroAllSafeAreas();
2146
+ }
2147
+ }
2148
+ /**
2149
+ * Applies safe-area handling for fullscreen modals.
2150
+ * Adds wrapper padding when no footer is present to prevent
2151
+ * content from overlapping system navigation areas.
2152
+ */
2153
+ applyFullscreenSafeArea() {
2154
+ this.skipSafeAreaCoordinateDetection = true;
2155
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
2156
+ if (!hasFooter && this.wrapperEl) {
2157
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2158
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2159
+ }
2160
+ }
2161
+ /**
2162
+ * Sets all safe-area CSS variables to 0px for modals that
2163
+ * don't touch screen edges.
2164
+ */
2165
+ zeroAllSafeAreas() {
2166
+ const style = this.el.style;
2167
+ style.setProperty('--ion-safe-area-top', '0px');
2168
+ style.setProperty('--ion-safe-area-bottom', '0px');
2169
+ style.setProperty('--ion-safe-area-left', '0px');
2170
+ style.setProperty('--ion-safe-area-right', '0px');
2171
+ }
2172
+ /**
2173
+ * Gets the root safe-area values from the document element.
2174
+ * These represent the actual device safe areas before any overlay overrides.
2175
+ */
2176
+ getRootSafeAreaValues() {
2177
+ const rootStyle = getComputedStyle(document.documentElement);
2178
+ return {
2179
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2180
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2181
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2182
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2183
+ };
2184
+ }
2185
+ /**
2186
+ * Updates safe-area CSS variable overrides based on whether the modal
2187
+ * extends into each safe-area region. Called after animation
2188
+ * and during gestures to handle dynamic position changes.
2189
+ */
2190
+ updateSafeAreaOverrides() {
2191
+ if (this.skipSafeAreaCoordinateDetection) {
2192
+ return;
2193
+ }
2194
+ const wrapper = this.wrapperEl;
2195
+ if (!wrapper) {
2196
+ return;
2197
+ }
2198
+ const rect = wrapper.getBoundingClientRect();
2199
+ const safeAreas = this.getRootSafeAreaValues();
2200
+ const extendsIntoTop = rect.top < safeAreas.top;
2201
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2202
+ const extendsIntoLeft = rect.left < safeAreas.left;
2203
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2204
+ const style = this.el.style;
2205
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2206
+ extendsIntoBottom
2207
+ ? style.removeProperty('--ion-safe-area-bottom')
2208
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2209
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2210
+ extendsIntoRight
2211
+ ? style.removeProperty('--ion-safe-area-right')
2212
+ : style.setProperty('--ion-safe-area-right', '0px');
2213
+ }
2083
2214
  sheetOnDismiss() {
2084
2215
  /**
2085
2216
  * While the gesture animation is finishing
@@ -2172,6 +2303,8 @@ const Modal = class {
2172
2303
  }
2173
2304
  this.currentBreakpoint = undefined;
2174
2305
  this.animation = undefined;
2306
+ // Reset safe-area detection flag for potential re-presentation
2307
+ this.skipSafeAreaCoordinateDetection = false;
2175
2308
  unlock();
2176
2309
  return dismissed;
2177
2310
  }
@@ -2421,20 +2554,20 @@ const Modal = class {
2421
2554
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2422
2555
  const isHandleCycle = handleBehavior === 'cycle';
2423
2556
  const isSheetModalWithHandle = isSheetModal && showHandle;
2424
- return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2557
+ return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2425
2558
  // Allow the modal to be navigable when the handle is focusable
2426
2559
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2427
2560
  zIndex: `${20000 + this.overlayIndex}`,
2428
- }, 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',
2561
+ }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), h("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
2429
2562
  /*
2430
2563
  role and aria-modal must be used on the
2431
2564
  same element. They must also be set inside the
2432
2565
  shadow DOM otherwise ion-button will not be highlighted
2433
2566
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2434
2567
  */
2435
- 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",
2568
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
2436
2569
  // Prevents the handle from receiving keyboard focus when it does not cycle
2437
- 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 }))));
2570
+ 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
2438
2571
  }
2439
2572
  get el() { return getElement(this); }
2440
2573
  static get watchers() { return {
@@ -658,6 +658,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
658
658
  let bottom;
659
659
  let originX = contentOriginX;
660
660
  let originY = contentOriginY;
661
+ let checkSafeAreaTop = false;
662
+ let checkSafeAreaBottom = false;
661
663
  let checkSafeAreaLeft = false;
662
664
  let checkSafeAreaRight = false;
663
665
  const triggerTop = triggerCoordinates
@@ -702,10 +704,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
702
704
  * We chose 12 here so that the popover position looks a bit nicer as
703
705
  * it is not right up against the edge of the screen.
704
706
  */
705
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
707
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
706
708
  arrowTop = top + contentHeight;
707
709
  originY = 'bottom';
708
710
  addPopoverBottomClass = true;
711
+ /**
712
+ * If the popover is positioned near the top edge, account for safe area.
713
+ * This ensures the popover doesn't overlap with status bars or notches.
714
+ */
715
+ if (top <= bodyPadding + safeAreaMargin) {
716
+ checkSafeAreaTop = true;
717
+ top = bodyPadding;
718
+ }
709
719
  /**
710
720
  * If not enough room for popover to appear
711
721
  * above trigger, then cut it off.
@@ -713,6 +723,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
713
723
  }
714
724
  else {
715
725
  bottom = bodyPadding;
726
+ /**
727
+ * When the popover is pinned to the bottom, account for safe area.
728
+ * This ensures the popover doesn't overlap with home indicators
729
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
730
+ */
731
+ checkSafeAreaBottom = true;
716
732
  }
717
733
  }
718
734
  return {
@@ -721,6 +737,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
721
737
  bottom,
722
738
  originX,
723
739
  originY,
740
+ checkSafeAreaTop,
741
+ checkSafeAreaBottom,
724
742
  checkSafeAreaLeft,
725
743
  checkSafeAreaRight,
726
744
  arrowTop,
@@ -781,7 +799,7 @@ const iosEnterAnimation = (baseEl, opts) => {
781
799
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
782
800
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
783
801
  const margin = size === 'cover' ? 0 : 25;
784
- 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);
802
+ 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);
785
803
  const baseAnimation = createAnimation();
786
804
  const backdropAnimation = createAnimation();
787
805
  const contentAnimation = createAnimation();
@@ -811,19 +829,35 @@ const iosEnterAnimation = (baseEl, opts) => {
811
829
  if (addPopoverBottomClass) {
812
830
  baseEl.classList.add('popover-bottom');
813
831
  }
814
- if (bottom !== undefined) {
815
- contentEl.style.setProperty('bottom', `${bottom}px`);
816
- }
832
+ /**
833
+ * Safe area CSS variable adjustments.
834
+ * When the popover is positioned near an edge, we add the corresponding
835
+ * safe-area inset to ensure the popover doesn't overlap with system UI
836
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
837
+ */
838
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
839
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
817
840
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
818
841
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
842
+ let topValue = `${top}px`;
843
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
819
844
  let leftValue = `${left}px`;
845
+ if (checkSafeAreaTop) {
846
+ topValue = `${top}px${safeAreaTop}`;
847
+ }
848
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
849
+ bottomValue = `${bottom}px${safeAreaBottom}`;
850
+ }
820
851
  if (checkSafeAreaLeft) {
821
852
  leftValue = `${left}px${safeAreaLeft}`;
822
853
  }
823
854
  if (checkSafeAreaRight) {
824
855
  leftValue = `${left}px${safeAreaRight}`;
825
856
  }
826
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
857
+ if (bottomValue !== undefined) {
858
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
859
+ }
860
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
827
861
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
828
862
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
829
863
  if (arrowEl !== null) {
@@ -899,7 +933,23 @@ const mdEnterAnimation = (baseEl, opts) => {
899
933
  };
900
934
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
901
935
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
902
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
936
+ 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);
937
+ /**
938
+ * Safe area CSS variable adjustments.
939
+ * When the popover is positioned near an edge, we add the corresponding
940
+ * safe-area inset to ensure the popover doesn't overlap with system UI
941
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
942
+ */
943
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
944
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
945
+ let topValue = `${top}px`;
946
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
947
+ if (checkSafeAreaTop) {
948
+ topValue = `${top}px${safeAreaTop}`;
949
+ }
950
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
951
+ bottomValue = `${bottom}px${safeAreaBottom}`;
952
+ }
903
953
  const baseAnimation = createAnimation();
904
954
  const backdropAnimation = createAnimation();
905
955
  const wrapperAnimation = createAnimation();
@@ -916,13 +966,13 @@ const mdEnterAnimation = (baseEl, opts) => {
916
966
  contentAnimation
917
967
  .addElement(contentEl)
918
968
  .beforeStyles({
919
- top: `calc(${top}px + var(--offset-y, 0px))`,
969
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
920
970
  left: `calc(${left}px + var(--offset-x, 0px))`,
921
971
  'transform-origin': `${originY} ${originX}`,
922
972
  })
923
973
  .beforeAddWrite(() => {
924
- if (bottom !== undefined) {
925
- contentEl.style.setProperty('bottom', `${bottom}px`);
974
+ if (bottomValue !== undefined) {
975
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
926
976
  }
927
977
  })
928
978
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -21,7 +21,6 @@ const TabBar = class {
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 = class {
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 = class {
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 getElement(this); }
100
86
  static get watchers() { return {