@ionic/core 8.7.16-dev.11767111768.1552e96d → 8.7.16-dev.11767365495.141917e9

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
@@ -21533,7 +21533,7 @@ const calculateSpringStep = (t) => {
21533
21533
  const SwipeToCloseDefaults = {
21534
21534
  MIN_PRESENTING_SCALE: 0.915,
21535
21535
  };
21536
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
21536
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
21537
21537
  /**
21538
21538
  * The step value at which a card modal
21539
21539
  * is eligible for dismissing via gesture.
@@ -21690,6 +21690,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
21690
21690
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
21691
21691
  const clampedStep = clamp(0.0001, processedStep, maxStep);
21692
21692
  animation.progressStep(clampedStep);
21693
+ // Notify modal of position change for safe-area updates
21694
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
21693
21695
  /**
21694
21696
  * When swiping down half way, the status bar style
21695
21697
  * should be reset to its default value.
@@ -22233,7 +22235,7 @@ const mdLeaveAnimation$2 = (baseEl, opts) => {
22233
22235
  return baseAnimation;
22234
22236
  };
22235
22237
 
22236
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
22238
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
22237
22239
  // Defaults for the sheet swipe animation
22238
22240
  const defaultBackdrop = [
22239
22241
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -22564,6 +22566,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22564
22566
  : step;
22565
22567
  offset = clamp(0.0001, processedStep, maxStep);
22566
22568
  animation.progressStep(offset);
22569
+ // Notify modal of position change for safe-area updates
22570
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
22567
22571
  };
22568
22572
  const onEnd = (detail) => {
22569
22573
  /**
@@ -22758,9 +22762,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22758
22762
  };
22759
22763
  };
22760
22764
 
22761
- 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}";
22765
+ 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}";
22762
22766
 
22763
- 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}";
22767
+ 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}";
22764
22768
 
22765
22769
  // TODO(FW-2832): types
22766
22770
  /**
@@ -22793,6 +22797,8 @@ class Modal {
22793
22797
  this.inline = false;
22794
22798
  // Whether or not modal is being dismissed via gesture
22795
22799
  this.gestureAnimationDismissing = false;
22800
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22801
+ this.skipSafeAreaCoordinateDetection = false;
22796
22802
  this.presented = false;
22797
22803
  /** @internal */
22798
22804
  this.hasController = false;
@@ -22983,7 +22989,9 @@ class Modal {
22983
22989
  }
22984
22990
  }
22985
22991
  onWindowResize() {
22986
- // Only handle resize for iOS card modals when no custom animations are provided
22992
+ // Update safe-area overrides for all modal types on resize
22993
+ this.updateSafeAreaOverrides();
22994
+ // Only handle view transition for iOS card modals when no custom animations are provided
22987
22995
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
22988
22996
  return;
22989
22997
  }
@@ -23165,6 +23173,8 @@ class Modal {
23165
23173
  else if (!this.keepContentsMounted) {
23166
23174
  await waitForMount();
23167
23175
  }
23176
+ // Predict safe-area needs based on modal configuration to avoid visual snap
23177
+ this.setInitialSafeAreaOverrides(presentingElement);
23168
23178
  writeTask(() => this.el.classList.add('show-modal'));
23169
23179
  const hasCardModal = presentingElement !== undefined;
23170
23180
  /**
@@ -23226,6 +23236,8 @@ class Modal {
23226
23236
  else if (hasCardModal) {
23227
23237
  this.initSwipeToClose();
23228
23238
  }
23239
+ // Now that animation is complete, update safe-area based on actual position
23240
+ this.updateSafeAreaOverrides();
23229
23241
  // Initialize view transition listener for iOS card modals
23230
23242
  this.initViewTransitionListener();
23231
23243
  // Initialize parent removal observer
@@ -23277,7 +23289,7 @@ class Modal {
23277
23289
  await this.dismiss(undefined, GESTURE);
23278
23290
  this.gestureAnimationDismissing = false;
23279
23291
  });
23280
- });
23292
+ }, () => this.updateSafeAreaOverrides());
23281
23293
  this.gesture.enable(true);
23282
23294
  }
23283
23295
  initSheetGesture() {
@@ -23298,7 +23310,8 @@ class Modal {
23298
23310
  this.currentBreakpoint = breakpoint;
23299
23311
  this.ionBreakpointDidChange.emit({ breakpoint });
23300
23312
  }
23301
- });
23313
+ this.updateSafeAreaOverrides();
23314
+ }, () => this.updateSafeAreaOverrides());
23302
23315
  this.gesture = gesture;
23303
23316
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
23304
23317
  this.gesture.enable(true);
@@ -23376,6 +23389,107 @@ class Modal {
23376
23389
  // Clear the cached reference
23377
23390
  this.cachedPageParent = undefined;
23378
23391
  }
23392
+ /**
23393
+ * Sets initial safe-area overrides based on modal configuration before
23394
+ * the modal becomes visible. This predicts whether the modal will touch
23395
+ * screen edges to avoid a visual snap after animation completes.
23396
+ */
23397
+ setInitialSafeAreaOverrides(presentingElement) {
23398
+ const style = this.el.style;
23399
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23400
+ const isCardModal = presentingElement !== undefined;
23401
+ const isTablet = window.innerWidth >= 768;
23402
+ // Sheet modals always touch bottom edge, never top/left/right
23403
+ if (isSheetModal) {
23404
+ style.setProperty('--ion-safe-area-top', '0px');
23405
+ style.setProperty('--ion-safe-area-left', '0px');
23406
+ style.setProperty('--ion-safe-area-right', '0px');
23407
+ return;
23408
+ }
23409
+ // Card modals have rounded top corners
23410
+ if (isCardModal) {
23411
+ style.setProperty('--ion-safe-area-top', '0px');
23412
+ if (isTablet) {
23413
+ // On tablets, card modals are inset from all edges
23414
+ this.zeroAllSafeAreas();
23415
+ }
23416
+ else {
23417
+ // On phones, card modals still extend to the bottom edge
23418
+ style.setProperty('--ion-safe-area-left', '0px');
23419
+ style.setProperty('--ion-safe-area-right', '0px');
23420
+ this.applyFullscreenSafeArea();
23421
+ }
23422
+ return;
23423
+ }
23424
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
23425
+ if (!isTablet) {
23426
+ this.applyFullscreenSafeArea();
23427
+ return;
23428
+ }
23429
+ // Check if tablet modal is fullscreen via CSS custom properties
23430
+ const computedStyle = getComputedStyle(this.el);
23431
+ const width = computedStyle.getPropertyValue('--width').trim();
23432
+ const height = computedStyle.getPropertyValue('--height').trim();
23433
+ const isFullscreen = width === '100%' && height === '100%';
23434
+ if (isFullscreen) {
23435
+ this.applyFullscreenSafeArea();
23436
+ }
23437
+ else {
23438
+ // Centered dialog doesn't touch edges
23439
+ this.zeroAllSafeAreas();
23440
+ }
23441
+ }
23442
+ /**
23443
+ * Applies safe-area handling for fullscreen modals.
23444
+ * Adds wrapper padding when no footer is present to prevent
23445
+ * content from overlapping system navigation areas.
23446
+ */
23447
+ applyFullscreenSafeArea() {
23448
+ this.skipSafeAreaCoordinateDetection = true;
23449
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
23450
+ if (!hasFooter && this.wrapperEl) {
23451
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23452
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23453
+ }
23454
+ }
23455
+ /**
23456
+ * Sets all safe-area CSS variables to 0px for modals that
23457
+ * don't touch screen edges.
23458
+ */
23459
+ zeroAllSafeAreas() {
23460
+ const style = this.el.style;
23461
+ style.setProperty('--ion-safe-area-top', '0px');
23462
+ style.setProperty('--ion-safe-area-bottom', '0px');
23463
+ style.setProperty('--ion-safe-area-left', '0px');
23464
+ style.setProperty('--ion-safe-area-right', '0px');
23465
+ }
23466
+ /**
23467
+ * Updates safe-area CSS variable overrides based on whether the modal
23468
+ * is touching each edge of the viewport. Called after animation
23469
+ * and during gestures to handle dynamic position changes.
23470
+ */
23471
+ updateSafeAreaOverrides() {
23472
+ if (this.skipSafeAreaCoordinateDetection) {
23473
+ return;
23474
+ }
23475
+ const wrapper = this.wrapperEl;
23476
+ if (!wrapper) {
23477
+ return;
23478
+ }
23479
+ const rect = wrapper.getBoundingClientRect();
23480
+ const threshold = 2;
23481
+ const touchingTop = rect.top <= threshold;
23482
+ const touchingBottom = rect.bottom >= window.innerHeight - threshold;
23483
+ const touchingLeft = rect.left <= threshold;
23484
+ const touchingRight = rect.right >= window.innerWidth - threshold;
23485
+ const style = this.el.style;
23486
+ touchingTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23487
+ touchingBottom
23488
+ ? style.removeProperty('--ion-safe-area-bottom')
23489
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23490
+ touchingLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23491
+ touchingRight ? style.removeProperty('--ion-safe-area-right') : style.setProperty('--ion-safe-area-right', '0px');
23492
+ }
23379
23493
  sheetOnDismiss() {
23380
23494
  /**
23381
23495
  * While the gesture animation is finishing
@@ -23468,6 +23582,8 @@ class Modal {
23468
23582
  }
23469
23583
  this.currentBreakpoint = undefined;
23470
23584
  this.animation = undefined;
23585
+ // Reset safe-area detection flag for potential re-presentation
23586
+ this.skipSafeAreaCoordinateDetection = false;
23471
23587
  unlock();
23472
23588
  return dismissed;
23473
23589
  }
@@ -23549,6 +23665,10 @@ class Modal {
23549
23665
  this.currentViewIsPortrait = window.innerWidth < 768;
23550
23666
  }
23551
23667
  handleViewTransition() {
23668
+ // Only run view transitions when the modal is presented
23669
+ if (!this.presented) {
23670
+ return;
23671
+ }
23552
23672
  const isPortrait = window.innerWidth < 768;
23553
23673
  // Only transition if view state actually changed
23554
23674
  if (this.currentViewIsPortrait === isPortrait) {
@@ -23713,20 +23833,20 @@ class Modal {
23713
23833
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23714
23834
  const isHandleCycle = handleBehavior === 'cycle';
23715
23835
  const isSheetModalWithHandle = isSheetModal && showHandle;
23716
- return (hAsync(Host, Object.assign({ key: '9a75095a13de0cfc96f1fa69fd92777d25da8daa', "no-router": true,
23836
+ return (hAsync(Host, Object.assign({ key: '6642b8a32dad449f26ac4ed60814d69afc97bf30', "no-router": true,
23717
23837
  // Allow the modal to be navigable when the handle is focusable
23718
23838
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23719
23839
  zIndex: `${20000 + this.overlayIndex}`,
23720
- }, 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: 'd02612d8063ef20f59f173ff47795f71cdaaf63e', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '708761b70a93e34c08faae079569f444c7416a4c', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'a72226ff1a98229f9bfd9207b98fc57e02baa430',
23840
+ }, 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: '8adf79ea72c0f622190ff366980621067c35795b', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '34f14fcd4970c04aee3b7a5305f9f220ff377f37', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'a3a8e3df6a1cfe061bbd99503655015ca8cd416d',
23721
23841
  /*
23722
23842
  role and aria-modal must be used on the
23723
23843
  same element. They must also be set inside the
23724
23844
  shadow DOM otherwise ion-button will not be highlighted
23725
23845
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23726
23846
  */
23727
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '0547f32323882660221385d84d492929caa77c6b', class: "modal-handle",
23847
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'a4ad2f6def8c20a36a9731553dfa5a98392151ef', class: "modal-handle",
23728
23848
  // Prevents the handle from receiving keyboard focus when it does not cycle
23729
- 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: 'fccbd64518b6fa22f9e874deb6b4ba55d8d89c3b', onSlotchange: this.onSlotChange }))));
23849
+ 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: '46fb6d94814dae4a9bcdf99387d57a17c6a6a95c', onSlotchange: this.onSlotChange }))));
23730
23850
  }
23731
23851
  get el() { return getElement(this); }
23732
23852
  static get watchers() { return {
@@ -27304,6 +27424,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27304
27424
  let bottom;
27305
27425
  let originX = contentOriginX;
27306
27426
  let originY = contentOriginY;
27427
+ let checkSafeAreaTop = false;
27428
+ let checkSafeAreaBottom = false;
27307
27429
  let checkSafeAreaLeft = false;
27308
27430
  let checkSafeAreaRight = false;
27309
27431
  const triggerTop = triggerCoordinates
@@ -27348,10 +27470,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27348
27470
  * We chose 12 here so that the popover position looks a bit nicer as
27349
27471
  * it is not right up against the edge of the screen.
27350
27472
  */
27351
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27473
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27352
27474
  arrowTop = top + contentHeight;
27353
27475
  originY = 'bottom';
27354
27476
  addPopoverBottomClass = true;
27477
+ /**
27478
+ * If the popover is positioned near the top edge, account for safe area.
27479
+ * This ensures the popover doesn't overlap with status bars or notches.
27480
+ */
27481
+ if (top <= bodyPadding + safeAreaMargin) {
27482
+ checkSafeAreaTop = true;
27483
+ top = bodyPadding;
27484
+ }
27355
27485
  /**
27356
27486
  * If not enough room for popover to appear
27357
27487
  * above trigger, then cut it off.
@@ -27359,6 +27489,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27359
27489
  }
27360
27490
  else {
27361
27491
  bottom = bodyPadding;
27492
+ /**
27493
+ * When the popover is pinned to the bottom, account for safe area.
27494
+ * This ensures the popover doesn't overlap with home indicators
27495
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
27496
+ */
27497
+ checkSafeAreaBottom = true;
27362
27498
  }
27363
27499
  }
27364
27500
  return {
@@ -27367,6 +27503,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27367
27503
  bottom,
27368
27504
  originX,
27369
27505
  originY,
27506
+ checkSafeAreaTop,
27507
+ checkSafeAreaBottom,
27370
27508
  checkSafeAreaLeft,
27371
27509
  checkSafeAreaRight,
27372
27510
  arrowTop,
@@ -27427,7 +27565,7 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27427
27565
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27428
27566
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27429
27567
  const margin = size === 'cover' ? 0 : 25;
27430
- 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);
27568
+ 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);
27431
27569
  const baseAnimation = createAnimation();
27432
27570
  const backdropAnimation = createAnimation();
27433
27571
  const contentAnimation = createAnimation();
@@ -27457,19 +27595,35 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27457
27595
  if (addPopoverBottomClass) {
27458
27596
  baseEl.classList.add('popover-bottom');
27459
27597
  }
27460
- if (bottom !== undefined) {
27461
- contentEl.style.setProperty('bottom', `${bottom}px`);
27462
- }
27598
+ /**
27599
+ * Safe area CSS variable adjustments.
27600
+ * When the popover is positioned near an edge, we add the corresponding
27601
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27602
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27603
+ */
27604
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27605
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27463
27606
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27464
27607
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27608
+ let topValue = `${top}px`;
27609
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27465
27610
  let leftValue = `${left}px`;
27611
+ if (checkSafeAreaTop) {
27612
+ topValue = `${top}px${safeAreaTop}`;
27613
+ }
27614
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27615
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27616
+ }
27466
27617
  if (checkSafeAreaLeft) {
27467
27618
  leftValue = `${left}px${safeAreaLeft}`;
27468
27619
  }
27469
27620
  if (checkSafeAreaRight) {
27470
27621
  leftValue = `${left}px${safeAreaRight}`;
27471
27622
  }
27472
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27623
+ if (bottomValue !== undefined) {
27624
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27625
+ }
27626
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27473
27627
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27474
27628
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27475
27629
  if (arrowEl !== null) {
@@ -27545,7 +27699,23 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27545
27699
  };
27546
27700
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27547
27701
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27548
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
27702
+ 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);
27703
+ /**
27704
+ * Safe area CSS variable adjustments.
27705
+ * When the popover is positioned near an edge, we add the corresponding
27706
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27707
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27708
+ */
27709
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27710
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27711
+ let topValue = `${top}px`;
27712
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27713
+ if (checkSafeAreaTop) {
27714
+ topValue = `${top}px${safeAreaTop}`;
27715
+ }
27716
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27717
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27718
+ }
27549
27719
  const baseAnimation = createAnimation();
27550
27720
  const backdropAnimation = createAnimation();
27551
27721
  const wrapperAnimation = createAnimation();
@@ -27562,13 +27732,13 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27562
27732
  contentAnimation
27563
27733
  .addElement(contentEl)
27564
27734
  .beforeStyles({
27565
- top: `calc(${top}px + var(--offset-y, 0px))`,
27735
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27566
27736
  left: `calc(${left}px + var(--offset-x, 0px))`,
27567
27737
  'transform-origin': `${originY} ${originX}`,
27568
27738
  })
27569
27739
  .beforeAddWrite(() => {
27570
- if (bottom !== undefined) {
27571
- contentEl.style.setProperty('bottom', `${bottom}px`);
27740
+ if (bottomValue !== undefined) {
27741
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27572
27742
  }
27573
27743
  })
27574
27744
  .fromTo('transform', 'scale(0.8)', 'scale(1)');