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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hydrate/index.js CHANGED
@@ -21581,7 +21581,7 @@ const calculateSpringStep = (t) => {
21581
21581
  const SwipeToCloseDefaults = {
21582
21582
  MIN_PRESENTING_SCALE: 0.915,
21583
21583
  };
21584
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
21584
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
21585
21585
  /**
21586
21586
  * The step value at which a card modal
21587
21587
  * is eligible for dismissing via gesture.
@@ -21738,6 +21738,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
21738
21738
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
21739
21739
  const clampedStep = clamp(0.0001, processedStep, maxStep);
21740
21740
  animation.progressStep(clampedStep);
21741
+ // Notify modal of position change for safe-area updates
21742
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
21741
21743
  /**
21742
21744
  * When swiping down half way, the status bar style
21743
21745
  * should be reset to its default value.
@@ -22281,7 +22283,7 @@ const mdLeaveAnimation$2 = (baseEl, opts) => {
22281
22283
  return baseAnimation;
22282
22284
  };
22283
22285
 
22284
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
22286
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
22285
22287
  // Defaults for the sheet swipe animation
22286
22288
  const defaultBackdrop = [
22287
22289
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -22612,6 +22614,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22612
22614
  : step;
22613
22615
  offset = clamp(0.0001, processedStep, maxStep);
22614
22616
  animation.progressStep(offset);
22617
+ // Notify modal of position change for safe-area updates
22618
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
22615
22619
  };
22616
22620
  const onEnd = (detail) => {
22617
22621
  /**
@@ -22806,9 +22810,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22806
22810
  };
22807
22811
  };
22808
22812
 
22809
- 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}";
22813
+ 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}";
22810
22814
 
22811
- 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}";
22815
+ 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}";
22812
22816
 
22813
22817
  // TODO(FW-2832): types
22814
22818
  /**
@@ -22841,6 +22845,10 @@ class Modal {
22841
22845
  this.inline = false;
22842
22846
  // Whether or not modal is being dismissed via gesture
22843
22847
  this.gestureAnimationDismissing = false;
22848
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22849
+ this.skipSafeAreaCoordinateDetection = false;
22850
+ // Track previous safe-area state to avoid redundant DOM writes
22851
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22844
22852
  this.presented = false;
22845
22853
  /** @internal */
22846
22854
  this.hasController = false;
@@ -23031,7 +23039,10 @@ class Modal {
23031
23039
  }
23032
23040
  }
23033
23041
  onWindowResize() {
23034
- // Only handle resize for iOS card modals when no custom animations are provided
23042
+ // Invalidate safe-area cache on resize (device rotation may change values)
23043
+ this.cachedSafeAreas = undefined;
23044
+ this.updateSafeAreaOverrides();
23045
+ // Only handle view transition for iOS card modals when no custom animations are provided
23035
23046
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
23036
23047
  return;
23037
23048
  }
@@ -23054,6 +23065,8 @@ class Modal {
23054
23065
  this.triggerController.removeClickListener();
23055
23066
  this.cleanupViewTransitionListener();
23056
23067
  this.cleanupParentRemovalObserver();
23068
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
23069
+ this.resetSafeAreaState();
23057
23070
  }
23058
23071
  componentWillLoad() {
23059
23072
  var _a;
@@ -23213,6 +23226,8 @@ class Modal {
23213
23226
  else if (!this.keepContentsMounted) {
23214
23227
  await waitForMount();
23215
23228
  }
23229
+ // Predict safe-area needs based on modal configuration to avoid visual snap
23230
+ this.setInitialSafeAreaOverrides(presentingElement);
23216
23231
  writeTask(() => this.el.classList.add('show-modal'));
23217
23232
  const hasCardModal = presentingElement !== undefined;
23218
23233
  /**
@@ -23274,6 +23289,8 @@ class Modal {
23274
23289
  else if (hasCardModal) {
23275
23290
  this.initSwipeToClose();
23276
23291
  }
23292
+ // Now that animation is complete, update safe-area based on actual position
23293
+ this.updateSafeAreaOverrides();
23277
23294
  // Initialize view transition listener for iOS card modals
23278
23295
  this.initViewTransitionListener();
23279
23296
  // Initialize parent removal observer
@@ -23325,7 +23342,7 @@ class Modal {
23325
23342
  await this.dismiss(undefined, GESTURE);
23326
23343
  this.gestureAnimationDismissing = false;
23327
23344
  });
23328
- });
23345
+ }, () => this.updateSafeAreaOverrides());
23329
23346
  this.gesture.enable(true);
23330
23347
  }
23331
23348
  initSheetGesture() {
@@ -23346,7 +23363,8 @@ class Modal {
23346
23363
  this.currentBreakpoint = breakpoint;
23347
23364
  this.ionBreakpointDidChange.emit({ breakpoint });
23348
23365
  }
23349
- });
23366
+ this.updateSafeAreaOverrides();
23367
+ }, () => this.updateSafeAreaOverrides());
23350
23368
  this.gesture = gesture;
23351
23369
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
23352
23370
  this.gesture.enable(true);
@@ -23424,6 +23442,194 @@ class Modal {
23424
23442
  // Clear the cached reference
23425
23443
  this.cachedPageParent = undefined;
23426
23444
  }
23445
+ /**
23446
+ * Sets initial safe-area overrides based on modal configuration before
23447
+ * the modal becomes visible. This predicts whether the modal will touch
23448
+ * screen edges to avoid a visual snap after animation completes.
23449
+ */
23450
+ setInitialSafeAreaOverrides(presentingElement) {
23451
+ const style = this.el.style;
23452
+ const mode = getIonMode$1(this);
23453
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23454
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
23455
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
23456
+ const isTablet = window.innerWidth >= 768;
23457
+ // Sheet modals always touch bottom edge, never top/left/right
23458
+ if (isSheetModal) {
23459
+ style.setProperty('--ion-safe-area-top', '0px');
23460
+ style.setProperty('--ion-safe-area-left', '0px');
23461
+ style.setProperty('--ion-safe-area-right', '0px');
23462
+ return;
23463
+ }
23464
+ // Card modals have rounded top corners
23465
+ if (isCardModal) {
23466
+ style.setProperty('--ion-safe-area-top', '0px');
23467
+ if (isTablet) {
23468
+ // On tablets, card modals are inset from all edges
23469
+ this.zeroAllSafeAreas();
23470
+ }
23471
+ else {
23472
+ // On phones, card modals still extend to the bottom edge
23473
+ style.setProperty('--ion-safe-area-left', '0px');
23474
+ style.setProperty('--ion-safe-area-right', '0px');
23475
+ this.applyFullscreenSafeArea();
23476
+ }
23477
+ return;
23478
+ }
23479
+ // Check if modal is fullscreen via CSS custom properties
23480
+ // This applies to both phone and tablet sizes - custom modals may have
23481
+ // non-fullscreen dimensions even on phones (e.g., --height: 70%)
23482
+ const computedStyle = getComputedStyle(this.el);
23483
+ const width = computedStyle.getPropertyValue('--width').trim();
23484
+ const height = computedStyle.getPropertyValue('--height').trim();
23485
+ const isFullscreen = width === '100%' && height === '100%';
23486
+ if (isFullscreen) {
23487
+ this.applyFullscreenSafeArea();
23488
+ }
23489
+ else if (isTablet) {
23490
+ // Centered dialog on tablet doesn't touch edges
23491
+ this.zeroAllSafeAreas();
23492
+ }
23493
+ else ;
23494
+ }
23495
+ /**
23496
+ * Applies safe-area handling for fullscreen modals.
23497
+ * Adds wrapper padding when no footer is present to prevent
23498
+ * content from overlapping system navigation areas.
23499
+ */
23500
+ applyFullscreenSafeArea() {
23501
+ this.skipSafeAreaCoordinateDetection = true;
23502
+ this.updateFooterPadding();
23503
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23504
+ // Use subtree:true to support wrapped footers in framework components
23505
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
23506
+ if (!this.footerObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
23507
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23508
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23509
+ }
23510
+ }
23511
+ /**
23512
+ * Updates wrapper and shadow padding based on footer presence.
23513
+ * Called initially and when footer is dynamically added/removed.
23514
+ * Both elements must be styled identically to prevent visual mismatches.
23515
+ */
23516
+ updateFooterPadding() {
23517
+ if (!this.wrapperEl)
23518
+ return;
23519
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
23520
+ // Apply to both wrapper and shadow to keep them in sync
23521
+ const elements = [this.wrapperEl, this.shadowEl].filter(Boolean);
23522
+ if (hasFooter) {
23523
+ elements.forEach((el) => {
23524
+ el.style.removeProperty('padding-bottom');
23525
+ el.style.removeProperty('box-sizing');
23526
+ });
23527
+ }
23528
+ else {
23529
+ elements.forEach((el) => {
23530
+ el.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23531
+ el.style.setProperty('box-sizing', 'border-box');
23532
+ });
23533
+ }
23534
+ }
23535
+ /**
23536
+ * Sets all safe-area CSS variables to 0px for modals that
23537
+ * don't touch screen edges.
23538
+ */
23539
+ zeroAllSafeAreas() {
23540
+ const style = this.el.style;
23541
+ style.setProperty('--ion-safe-area-top', '0px');
23542
+ style.setProperty('--ion-safe-area-bottom', '0px');
23543
+ style.setProperty('--ion-safe-area-left', '0px');
23544
+ style.setProperty('--ion-safe-area-right', '0px');
23545
+ }
23546
+ /**
23547
+ * Resets all safe-area related state and styles.
23548
+ * Called during dismiss and disconnectedCallback to ensure clean state
23549
+ * for re-presentation of inline modals.
23550
+ */
23551
+ resetSafeAreaState() {
23552
+ var _a;
23553
+ this.skipSafeAreaCoordinateDetection = false;
23554
+ this.cachedSafeAreas = undefined;
23555
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23556
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23557
+ this.footerObserver = undefined;
23558
+ // Clear wrapper and shadow styles that may have been set for safe-area handling
23559
+ [this.wrapperEl, this.shadowEl].forEach((el) => {
23560
+ if (el) {
23561
+ el.style.removeProperty('padding-bottom');
23562
+ el.style.removeProperty('box-sizing');
23563
+ }
23564
+ });
23565
+ // Clear safe-area CSS variable overrides
23566
+ const style = this.el.style;
23567
+ style.removeProperty('--ion-safe-area-top');
23568
+ style.removeProperty('--ion-safe-area-bottom');
23569
+ style.removeProperty('--ion-safe-area-left');
23570
+ style.removeProperty('--ion-safe-area-right');
23571
+ }
23572
+ /**
23573
+ * Gets the root safe-area values from the document element.
23574
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23575
+ */
23576
+ getSafeAreaValues() {
23577
+ if (!this.cachedSafeAreas) {
23578
+ const rootStyle = getComputedStyle(document.documentElement);
23579
+ this.cachedSafeAreas = {
23580
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23581
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23582
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23583
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23584
+ };
23585
+ }
23586
+ return this.cachedSafeAreas;
23587
+ }
23588
+ /**
23589
+ * Updates safe-area CSS variable overrides based on whether the modal
23590
+ * extends into each safe-area region. Called after animation
23591
+ * and during gestures to handle dynamic position changes.
23592
+ *
23593
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23594
+ */
23595
+ updateSafeAreaOverrides() {
23596
+ if (this.skipSafeAreaCoordinateDetection) {
23597
+ return;
23598
+ }
23599
+ const wrapper = this.wrapperEl;
23600
+ if (!wrapper) {
23601
+ return;
23602
+ }
23603
+ const rect = wrapper.getBoundingClientRect();
23604
+ const safeAreas = this.getSafeAreaValues();
23605
+ const extendsIntoTop = rect.top < safeAreas.top;
23606
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23607
+ const extendsIntoLeft = rect.left < safeAreas.left;
23608
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23609
+ // Only update DOM when state actually changes
23610
+ const prev = this.prevSafeAreaState;
23611
+ const style = this.el.style;
23612
+ if (extendsIntoTop !== prev.top) {
23613
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23614
+ prev.top = extendsIntoTop;
23615
+ }
23616
+ if (extendsIntoBottom !== prev.bottom) {
23617
+ extendsIntoBottom
23618
+ ? style.removeProperty('--ion-safe-area-bottom')
23619
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23620
+ prev.bottom = extendsIntoBottom;
23621
+ }
23622
+ if (extendsIntoLeft !== prev.left) {
23623
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23624
+ prev.left = extendsIntoLeft;
23625
+ }
23626
+ if (extendsIntoRight !== prev.right) {
23627
+ extendsIntoRight
23628
+ ? style.removeProperty('--ion-safe-area-right')
23629
+ : style.setProperty('--ion-safe-area-right', '0px');
23630
+ prev.right = extendsIntoRight;
23631
+ }
23632
+ }
23427
23633
  sheetOnDismiss() {
23428
23634
  /**
23429
23635
  * While the gesture animation is finishing
@@ -23516,6 +23722,8 @@ class Modal {
23516
23722
  }
23517
23723
  this.currentBreakpoint = undefined;
23518
23724
  this.animation = undefined;
23725
+ // Reset safe-area state for potential re-presentation
23726
+ this.resetSafeAreaState();
23519
23727
  unlock();
23520
23728
  return dismissed;
23521
23729
  }
@@ -23765,20 +23973,20 @@ class Modal {
23765
23973
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23766
23974
  const isHandleCycle = handleBehavior === 'cycle';
23767
23975
  const isSheetModalWithHandle = isSheetModal && showHandle;
23768
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
23976
+ return (hAsync(Host, Object.assign({ key: '8f42c71ec9c9270b4218ca7eec57ca998871ac07', "no-router": true,
23769
23977
  // Allow the modal to be navigable when the handle is focusable
23770
23978
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23771
23979
  zIndex: `${20000 + this.overlayIndex}`,
23772
- }, 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 }), hAsync("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
23980
+ }, 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 }), hAsync("ion-backdrop", { key: '2f7b08b019c66cbfceacafa9a68b91f8da1f4084', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '6b0d1ae90e8483332f74371be5dcbd0a7b2661ba', class: "modal-shadow", ref: (el) => (this.shadowEl = el) }), hAsync("div", Object.assign({ key: 'c96f671fb25e5de45231ea65ac52500547b2e262',
23773
23981
  /*
23774
23982
  role and aria-modal must be used on the
23775
23983
  same element. They must also be set inside the
23776
23984
  shadow DOM otherwise ion-button will not be highlighted
23777
23985
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23778
23986
  */
23779
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
23987
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'e07419b01a588c0ba62ed9607c843cac7bd26337', class: "modal-handle",
23780
23988
  // Prevents the handle from receiving keyboard focus when it does not cycle
23781
- 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) })), hAsync("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
23989
+ 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) })), hAsync("slot", { key: '65f35079df8eb8b6cd262e3a08297866715ce7ea', onSlotchange: this.onSlotChange }))));
23782
23990
  }
23783
23991
  get el() { return getElement(this); }
23784
23992
  static get watchers() { return {
@@ -27356,6 +27564,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27356
27564
  let bottom;
27357
27565
  let originX = contentOriginX;
27358
27566
  let originY = contentOriginY;
27567
+ let checkSafeAreaTop = false;
27568
+ let checkSafeAreaBottom = false;
27359
27569
  let checkSafeAreaLeft = false;
27360
27570
  let checkSafeAreaRight = false;
27361
27571
  const triggerTop = triggerCoordinates
@@ -27400,10 +27610,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27400
27610
  * We chose 12 here so that the popover position looks a bit nicer as
27401
27611
  * it is not right up against the edge of the screen.
27402
27612
  */
27403
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27613
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27404
27614
  arrowTop = top + contentHeight;
27405
27615
  originY = 'bottom';
27406
27616
  addPopoverBottomClass = true;
27617
+ /**
27618
+ * If the popover is positioned near the top edge, account for safe area.
27619
+ * This ensures the popover doesn't overlap with status bars or notches.
27620
+ */
27621
+ if (top <= bodyPadding + safeAreaMargin) {
27622
+ checkSafeAreaTop = true;
27623
+ top = bodyPadding;
27624
+ }
27407
27625
  /**
27408
27626
  * If not enough room for popover to appear
27409
27627
  * above trigger, then cut it off.
@@ -27411,14 +27629,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27411
27629
  }
27412
27630
  else {
27413
27631
  bottom = bodyPadding;
27632
+ /**
27633
+ * When the popover is pinned to the bottom, account for safe area.
27634
+ * This ensures the popover doesn't overlap with home indicators
27635
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
27636
+ */
27637
+ checkSafeAreaBottom = true;
27414
27638
  }
27415
27639
  }
27640
+ /**
27641
+ * Final check: If the popover extends into any safe-area region,
27642
+ * ensure the corresponding flag is set regardless of side.
27643
+ * This handles cases where a side-positioned popover (left/right)
27644
+ * still needs bottom safe-area padding because it extends into that region.
27645
+ */
27646
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
27647
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
27648
+ checkSafeAreaBottom = true;
27649
+ }
27650
+ if (top < safeAreaMargin) {
27651
+ checkSafeAreaTop = true;
27652
+ }
27416
27653
  return {
27417
27654
  top,
27418
27655
  left,
27419
27656
  bottom,
27420
27657
  originX,
27421
27658
  originY,
27659
+ checkSafeAreaTop,
27660
+ checkSafeAreaBottom,
27422
27661
  checkSafeAreaLeft,
27423
27662
  checkSafeAreaRight,
27424
27663
  arrowTop,
@@ -27479,7 +27718,7 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27479
27718
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27480
27719
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27481
27720
  const margin = size === 'cover' ? 0 : 25;
27482
- 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);
27721
+ 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);
27483
27722
  const baseAnimation = createAnimation();
27484
27723
  const backdropAnimation = createAnimation();
27485
27724
  const contentAnimation = createAnimation();
@@ -27509,19 +27748,35 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27509
27748
  if (addPopoverBottomClass) {
27510
27749
  baseEl.classList.add('popover-bottom');
27511
27750
  }
27512
- if (bottom !== undefined) {
27513
- contentEl.style.setProperty('bottom', `${bottom}px`);
27514
- }
27751
+ /**
27752
+ * Safe area CSS variable adjustments.
27753
+ * When the popover is positioned near an edge, we add the corresponding
27754
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27755
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27756
+ */
27757
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27758
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27515
27759
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27516
27760
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27761
+ let topValue = `${top}px`;
27762
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27517
27763
  let leftValue = `${left}px`;
27764
+ if (checkSafeAreaTop) {
27765
+ topValue = `${top}px${safeAreaTop}`;
27766
+ }
27767
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27768
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27769
+ }
27518
27770
  if (checkSafeAreaLeft) {
27519
27771
  leftValue = `${left}px${safeAreaLeft}`;
27520
27772
  }
27521
27773
  if (checkSafeAreaRight) {
27522
27774
  leftValue = `${left}px${safeAreaRight}`;
27523
27775
  }
27524
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27776
+ if (bottomValue !== undefined) {
27777
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27778
+ }
27779
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27525
27780
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27526
27781
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27527
27782
  if (arrowEl !== null) {
@@ -27597,7 +27852,32 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27597
27852
  };
27598
27853
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27599
27854
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27600
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
27855
+ 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);
27856
+ /**
27857
+ * Safe area CSS variable adjustments.
27858
+ * When the popover is positioned near an edge, we add the corresponding
27859
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27860
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27861
+ */
27862
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27863
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27864
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27865
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27866
+ let topValue = `${top}px`;
27867
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27868
+ let leftValue = `${left}px`;
27869
+ if (checkSafeAreaTop) {
27870
+ topValue = `${top}px${safeAreaTop}`;
27871
+ }
27872
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27873
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27874
+ }
27875
+ if (checkSafeAreaLeft) {
27876
+ leftValue = `${left}px${safeAreaLeft}`;
27877
+ }
27878
+ if (checkSafeAreaRight) {
27879
+ leftValue = `${left}px${safeAreaRight}`;
27880
+ }
27601
27881
  const baseAnimation = createAnimation();
27602
27882
  const backdropAnimation = createAnimation();
27603
27883
  const wrapperAnimation = createAnimation();
@@ -27614,13 +27894,13 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27614
27894
  contentAnimation
27615
27895
  .addElement(contentEl)
27616
27896
  .beforeStyles({
27617
- top: `calc(${top}px + var(--offset-y, 0px))`,
27618
- left: `calc(${left}px + var(--offset-x, 0px))`,
27897
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27898
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27619
27899
  'transform-origin': `${originY} ${originX}`,
27620
27900
  })
27621
27901
  .beforeAddWrite(() => {
27622
- if (bottom !== undefined) {
27623
- contentEl.style.setProperty('bottom', `${bottom}px`);
27902
+ if (bottomValue !== undefined) {
27903
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27624
27904
  }
27625
27905
  })
27626
27906
  .fromTo('transform', 'scale(0.8)', 'scale(1)');