@ionic/core 8.7.18-dev.11770674094.18396f54 → 8.7.18-dev.11771020096.1ca03a6d

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.
@@ -1483,6 +1483,11 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1483
1483
  const MODAL_INSET_MIN_WIDTH = 768;
1484
1484
  const MODAL_INSET_MIN_HEIGHT = 600;
1485
1485
  const EDGE_THRESHOLD = 5;
1486
+ /**
1487
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
1488
+ */
1489
+ let cachedRootSafeAreaTop = null;
1490
+ let cacheInvalidationScheduled = false;
1486
1491
  /**
1487
1492
  * Determines if the current viewport meets the CSS media query conditions
1488
1493
  * that cause regular modals to render as centered dialogs instead of fullscreen.
@@ -1494,6 +1499,38 @@ const isCenteredDialogViewport = () => {
1494
1499
  return win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
1495
1500
  .matches;
1496
1501
  };
1502
+ /**
1503
+ * Resolves the current root --ion-safe-area-top value to pixels.
1504
+ * Uses a temporary element because getComputedStyle on :root returns
1505
+ * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
1506
+ * rather than a resolved number.
1507
+ *
1508
+ * Results are cached for the current frame to avoid repeated reflows.
1509
+ */
1510
+ const getRootSafeAreaTop = () => {
1511
+ if (cachedRootSafeAreaTop !== null) {
1512
+ return cachedRootSafeAreaTop;
1513
+ }
1514
+ const doc = win === null || win === void 0 ? void 0 : win.document;
1515
+ if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
1516
+ return 0;
1517
+ }
1518
+ const el = doc.createElement('div');
1519
+ el.style.cssText =
1520
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
1521
+ doc.body.appendChild(el);
1522
+ const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
1523
+ el.remove();
1524
+ cachedRootSafeAreaTop = value;
1525
+ if (!cacheInvalidationScheduled) {
1526
+ cacheInvalidationScheduled = true;
1527
+ raf(() => {
1528
+ cachedRootSafeAreaTop = null;
1529
+ cacheInvalidationScheduled = false;
1530
+ });
1531
+ }
1532
+ return value;
1533
+ };
1497
1534
  /**
1498
1535
  * Returns the initial safe-area configuration based on modal type.
1499
1536
  * This is called before animation starts and uses configuration-based prediction.
@@ -1503,10 +1540,14 @@ const isCenteredDialogViewport = () => {
1503
1540
  */
1504
1541
  const getInitialSafeAreaConfig = (context) => {
1505
1542
  const { isSheetModal, isCardModal } = context;
1506
- // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
1543
+ // Sheet modals always zero top safe-area. The sheet height offset from the
1544
+ // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
1545
+ // the resolved root value, so --ion-safe-area-top is never needed for
1546
+ // height calculation. Keeping it at 0px prevents header content from
1547
+ // getting double-offset padding.
1507
1548
  if (isSheetModal) {
1508
1549
  return {
1509
- top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
1550
+ top: '0px',
1510
1551
  bottom: 'inherit',
1511
1552
  left: '0px',
1512
1553
  right: '0px',
@@ -1591,9 +1632,9 @@ const clearSafeAreaOverrides = (hostEl) => {
1591
1632
  hostEl.style.removeProperty('--ion-safe-area-right');
1592
1633
  };
1593
1634
 
1594
- 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}";
1635
+ 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-modal-offset-top, 0px) + 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}";
1595
1636
 
1596
- 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}";
1637
+ 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-modal-offset-top, 0px) + 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}";
1597
1638
 
1598
1639
  const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1599
1640
  constructor(registerHost) {
@@ -1810,13 +1851,31 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1810
1851
  }
1811
1852
  }
1812
1853
  onWindowResize() {
1813
- // Only handle resize for iOS card modals when no custom animations are provided
1814
- if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1854
+ if (!this.presented)
1815
1855
  return;
1816
- }
1817
1856
  clearTimeout(this.resizeTimeout);
1818
1857
  this.resizeTimeout = setTimeout(() => {
1819
- this.handleViewTransition();
1858
+ const context = this.getSafeAreaContext();
1859
+ // iOS card modals: handle portrait/landscape view transitions
1860
+ if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
1861
+ this.handleViewTransition();
1862
+ }
1863
+ // Sheet modals: re-compute the internal offset property since safe-area
1864
+ // values may change on device rotation (e.g., portrait notch vs landscape).
1865
+ if (context.isSheetModal) {
1866
+ this.updateSheetOffsetTop();
1867
+ }
1868
+ // Regular (non-sheet, non-card) modals: update safe-area overrides
1869
+ // since the viewport may have crossed the centered-dialog breakpoint.
1870
+ if (!context.isSheetModal && !context.isCardModal) {
1871
+ this.updateSafeAreaOverrides();
1872
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
1873
+ if (this.wrapperEl) {
1874
+ this.wrapperEl.style.removeProperty('height');
1875
+ this.wrapperEl.style.removeProperty('padding-bottom');
1876
+ }
1877
+ this.applyFullscreenSafeArea();
1878
+ }
1820
1879
  }, 50); // Debounce to avoid excessive calls during active resizing
1821
1880
  }
1822
1881
  breakpointsChanged(breakpoints) {
@@ -2130,8 +2189,6 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2130
2189
  if (this.currentBreakpoint !== breakpoint) {
2131
2190
  this.currentBreakpoint = breakpoint;
2132
2191
  this.ionBreakpointDidChange.emit({ breakpoint });
2133
- // Update safe-area overrides based on new position
2134
- this.updateSafeAreaOverrides();
2135
2192
  }
2136
2193
  });
2137
2194
  this.gesture = gesture;
@@ -2245,6 +2302,9 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2245
2302
  if (this.gestureAnimationDismissing && role !== GESTURE) {
2246
2303
  return false;
2247
2304
  }
2305
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
2306
+ clearTimeout(this.resizeTimeout);
2307
+ this.resizeTimeout = undefined;
2248
2308
  /**
2249
2309
  * Because the canDismiss check below is async,
2250
2310
  * we need to claim a lock before the check happens,
@@ -2564,11 +2624,30 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2564
2624
  /**
2565
2625
  * Sets initial safe-area overrides before modal animation.
2566
2626
  * Called in present() before animation starts.
2627
+ *
2628
+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
2629
+ * (an internal property) instead of --ion-safe-area-top. We resolve the
2630
+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
2631
+ * the height calculation from --ion-safe-area-top (which is zeroed for
2632
+ * sheets to prevent header content from getting double-offset padding).
2567
2633
  */
2568
2634
  setInitialSafeAreaOverrides() {
2569
2635
  const context = this.getSafeAreaContext();
2570
2636
  const safeAreaConfig = getInitialSafeAreaConfig(context);
2571
2637
  applySafeAreaOverrides(this.el, safeAreaConfig);
2638
+ // Set the internal offset property with the resolved root safe-area-top value
2639
+ if (context.isSheetModal) {
2640
+ this.updateSheetOffsetTop();
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Resolves the current root --ion-safe-area-top value and sets the
2645
+ * internal --ion-modal-offset-top property on the host element.
2646
+ * Called on present and on resize (e.g., device rotation changes safe-area).
2647
+ */
2648
+ updateSheetOffsetTop() {
2649
+ const safeAreaTop = getRootSafeAreaTop();
2650
+ this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
2572
2651
  }
2573
2652
  /**
2574
2653
  * Updates safe-area overrides during dynamic state changes.
@@ -2577,19 +2656,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2577
2656
  updateSafeAreaOverrides() {
2578
2657
  const { wrapperEl, el } = this;
2579
2658
  const context = this.getSafeAreaContext();
2580
- // Sheet modals: the wrapper extends beyond the viewport and is translated
2581
- // via breakpoint gestures, making getBoundingClientRect unreliable for
2582
- // edge detection. Instead, use breakpoint value to determine top safe-area.
2583
- if (context.isSheetModal) {
2584
- const needsTopSafeArea = context.currentBreakpoint === 1;
2585
- applySafeAreaOverrides(el, {
2586
- top: needsTopSafeArea ? 'inherit' : '0px',
2587
- bottom: 'inherit',
2588
- left: '0px',
2589
- right: '0px',
2590
- });
2659
+ // Sheet modals: safe-area is fully determined at presentation time
2660
+ // (top is always 0px, height is frozen). Nothing to update.
2661
+ if (context.isSheetModal)
2591
2662
  return;
2592
- }
2593
2663
  // Card modals have fixed safe-area requirements set by initial prediction.
2594
2664
  if (context.isCardModal)
2595
2665
  return;
@@ -2615,6 +2685,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2615
2685
  // Check for standard Ionic layout children (ion-content, ion-footer),
2616
2686
  // searching one level deep for wrapped components (e.g.,
2617
2687
  // <app-footer><ion-footer>...</ion-footer></app-footer>).
2688
+ // Note: uses a manual loop instead of querySelector(':scope > ...') because
2689
+ // Stencil's mock-doc (used in spec tests) does not support :scope.
2618
2690
  let hasContent = false;
2619
2691
  let hasFooter = false;
2620
2692
  for (const child of Array.from(el.children)) {
@@ -2647,6 +2719,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2647
2719
  */
2648
2720
  cleanupSafeAreaOverrides() {
2649
2721
  clearSafeAreaOverrides(this.el);
2722
+ // Remove internal sheet offset property
2723
+ this.el.style.removeProperty('--ion-modal-offset-top');
2650
2724
  if (this.wrapperEl) {
2651
2725
  this.wrapperEl.style.removeProperty('height');
2652
2726
  this.wrapperEl.style.removeProperty('padding-bottom');
@@ -2659,20 +2733,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2659
2733
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2660
2734
  const isHandleCycle = handleBehavior === 'cycle';
2661
2735
  const isSheetModalWithHandle = isSheetModal && showHandle;
2662
- return (h(Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
2736
+ return (h(Host, Object.assign({ key: '038ae26c2cb040043c7d0c217a4de6a7ae909235', "no-router": true,
2663
2737
  // Allow the modal to be navigable when the handle is focusable
2664
2738
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2665
2739
  zIndex: `${20000 + this.overlayIndex}`,
2666
- }, 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: '6866fb2a265d40c761ed6bfb661a217948c571ef', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '00788de9267f1c064f16aa55f35250de16522ae6', class: "modal-shadow" }), h("div", Object.assign({ key: 'a575e1755bd25a7996cdb04b014a91d538e7bc94',
2740
+ }, 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: '6bab796467b370e79b24ce17c5a3d51adb07ee8c', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'b9a19e86a2c7f079c88ef7a914c7c890aa601887', class: "modal-shadow" }), h("div", Object.assign({ key: 'f4315c34aa78be4e3540361755964be2ddf43265',
2667
2741
  /*
2668
2742
  role and aria-modal must be used on the
2669
2743
  same element. They must also be set inside the
2670
2744
  shadow DOM otherwise ion-button will not be highlighted
2671
2745
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2672
2746
  */
2673
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b99c64717037e9241479ea2f5bb84e79a466d918', class: "modal-handle",
2747
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '3f7792bd65e0f8634e62bf27f9edcb636a03030a', class: "modal-handle",
2674
2748
  // Prevents the handle from receiving keyboard focus when it does not cycle
2675
- 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: 'ee672afd537b281aeeb73a55d61ceeb5dfae212a', onSlotchange: this.onSlotChange }))));
2749
+ 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: '001e5b01831e240bd98f75ff62446b90b64d8394', onSlotchange: this.onSlotChange }))));
2676
2750
  }
2677
2751
  get el() { return this; }
2678
2752
  static get watchers() { return {
@@ -1486,6 +1486,11 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1486
1486
  const MODAL_INSET_MIN_WIDTH = 768;
1487
1487
  const MODAL_INSET_MIN_HEIGHT = 600;
1488
1488
  const EDGE_THRESHOLD = 5;
1489
+ /**
1490
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
1491
+ */
1492
+ let cachedRootSafeAreaTop = null;
1493
+ let cacheInvalidationScheduled = false;
1489
1494
  /**
1490
1495
  * Determines if the current viewport meets the CSS media query conditions
1491
1496
  * that cause regular modals to render as centered dialogs instead of fullscreen.
@@ -1497,6 +1502,38 @@ const isCenteredDialogViewport = () => {
1497
1502
  return index.win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
1498
1503
  .matches;
1499
1504
  };
1505
+ /**
1506
+ * Resolves the current root --ion-safe-area-top value to pixels.
1507
+ * Uses a temporary element because getComputedStyle on :root returns
1508
+ * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
1509
+ * rather than a resolved number.
1510
+ *
1511
+ * Results are cached for the current frame to avoid repeated reflows.
1512
+ */
1513
+ const getRootSafeAreaTop = () => {
1514
+ if (cachedRootSafeAreaTop !== null) {
1515
+ return cachedRootSafeAreaTop;
1516
+ }
1517
+ const doc = index.win === null || index.win === void 0 ? void 0 : index.win.document;
1518
+ if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
1519
+ return 0;
1520
+ }
1521
+ const el = doc.createElement('div');
1522
+ el.style.cssText =
1523
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
1524
+ doc.body.appendChild(el);
1525
+ const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
1526
+ el.remove();
1527
+ cachedRootSafeAreaTop = value;
1528
+ if (!cacheInvalidationScheduled) {
1529
+ cacheInvalidationScheduled = true;
1530
+ helpers.raf(() => {
1531
+ cachedRootSafeAreaTop = null;
1532
+ cacheInvalidationScheduled = false;
1533
+ });
1534
+ }
1535
+ return value;
1536
+ };
1500
1537
  /**
1501
1538
  * Returns the initial safe-area configuration based on modal type.
1502
1539
  * This is called before animation starts and uses configuration-based prediction.
@@ -1506,10 +1543,14 @@ const isCenteredDialogViewport = () => {
1506
1543
  */
1507
1544
  const getInitialSafeAreaConfig = (context) => {
1508
1545
  const { isSheetModal, isCardModal } = context;
1509
- // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
1546
+ // Sheet modals always zero top safe-area. The sheet height offset from the
1547
+ // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
1548
+ // the resolved root value, so --ion-safe-area-top is never needed for
1549
+ // height calculation. Keeping it at 0px prevents header content from
1550
+ // getting double-offset padding.
1510
1551
  if (isSheetModal) {
1511
1552
  return {
1512
- top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
1553
+ top: '0px',
1513
1554
  bottom: 'inherit',
1514
1555
  left: '0px',
1515
1556
  right: '0px',
@@ -1594,9 +1635,9 @@ const clearSafeAreaOverrides = (hostEl) => {
1594
1635
  hostEl.style.removeProperty('--ion-safe-area-right');
1595
1636
  };
1596
1637
 
1597
- 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}";
1638
+ 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-modal-offset-top, 0px) + 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}";
1598
1639
 
1599
- 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}";
1640
+ 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-modal-offset-top, 0px) + 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}";
1600
1641
 
1601
1642
  const Modal = class {
1602
1643
  constructor(hostRef) {
@@ -1809,13 +1850,31 @@ const Modal = class {
1809
1850
  }
1810
1851
  }
1811
1852
  onWindowResize() {
1812
- // Only handle resize for iOS card modals when no custom animations are provided
1813
- if (ionicGlobal.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1853
+ if (!this.presented)
1814
1854
  return;
1815
- }
1816
1855
  clearTimeout(this.resizeTimeout);
1817
1856
  this.resizeTimeout = setTimeout(() => {
1818
- this.handleViewTransition();
1857
+ const context = this.getSafeAreaContext();
1858
+ // iOS card modals: handle portrait/landscape view transitions
1859
+ if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
1860
+ this.handleViewTransition();
1861
+ }
1862
+ // Sheet modals: re-compute the internal offset property since safe-area
1863
+ // values may change on device rotation (e.g., portrait notch vs landscape).
1864
+ if (context.isSheetModal) {
1865
+ this.updateSheetOffsetTop();
1866
+ }
1867
+ // Regular (non-sheet, non-card) modals: update safe-area overrides
1868
+ // since the viewport may have crossed the centered-dialog breakpoint.
1869
+ if (!context.isSheetModal && !context.isCardModal) {
1870
+ this.updateSafeAreaOverrides();
1871
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
1872
+ if (this.wrapperEl) {
1873
+ this.wrapperEl.style.removeProperty('height');
1874
+ this.wrapperEl.style.removeProperty('padding-bottom');
1875
+ }
1876
+ this.applyFullscreenSafeArea();
1877
+ }
1819
1878
  }, 50); // Debounce to avoid excessive calls during active resizing
1820
1879
  }
1821
1880
  breakpointsChanged(breakpoints) {
@@ -2129,8 +2188,6 @@ const Modal = class {
2129
2188
  if (this.currentBreakpoint !== breakpoint) {
2130
2189
  this.currentBreakpoint = breakpoint;
2131
2190
  this.ionBreakpointDidChange.emit({ breakpoint });
2132
- // Update safe-area overrides based on new position
2133
- this.updateSafeAreaOverrides();
2134
2191
  }
2135
2192
  });
2136
2193
  this.gesture = gesture;
@@ -2244,6 +2301,9 @@ const Modal = class {
2244
2301
  if (this.gestureAnimationDismissing && role !== overlays.GESTURE) {
2245
2302
  return false;
2246
2303
  }
2304
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
2305
+ clearTimeout(this.resizeTimeout);
2306
+ this.resizeTimeout = undefined;
2247
2307
  /**
2248
2308
  * Because the canDismiss check below is async,
2249
2309
  * we need to claim a lock before the check happens,
@@ -2563,11 +2623,30 @@ const Modal = class {
2563
2623
  /**
2564
2624
  * Sets initial safe-area overrides before modal animation.
2565
2625
  * Called in present() before animation starts.
2626
+ *
2627
+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
2628
+ * (an internal property) instead of --ion-safe-area-top. We resolve the
2629
+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
2630
+ * the height calculation from --ion-safe-area-top (which is zeroed for
2631
+ * sheets to prevent header content from getting double-offset padding).
2566
2632
  */
2567
2633
  setInitialSafeAreaOverrides() {
2568
2634
  const context = this.getSafeAreaContext();
2569
2635
  const safeAreaConfig = getInitialSafeAreaConfig(context);
2570
2636
  applySafeAreaOverrides(this.el, safeAreaConfig);
2637
+ // Set the internal offset property with the resolved root safe-area-top value
2638
+ if (context.isSheetModal) {
2639
+ this.updateSheetOffsetTop();
2640
+ }
2641
+ }
2642
+ /**
2643
+ * Resolves the current root --ion-safe-area-top value and sets the
2644
+ * internal --ion-modal-offset-top property on the host element.
2645
+ * Called on present and on resize (e.g., device rotation changes safe-area).
2646
+ */
2647
+ updateSheetOffsetTop() {
2648
+ const safeAreaTop = getRootSafeAreaTop();
2649
+ this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
2571
2650
  }
2572
2651
  /**
2573
2652
  * Updates safe-area overrides during dynamic state changes.
@@ -2576,19 +2655,10 @@ const Modal = class {
2576
2655
  updateSafeAreaOverrides() {
2577
2656
  const { wrapperEl, el } = this;
2578
2657
  const context = this.getSafeAreaContext();
2579
- // Sheet modals: the wrapper extends beyond the viewport and is translated
2580
- // via breakpoint gestures, making getBoundingClientRect unreliable for
2581
- // edge detection. Instead, use breakpoint value to determine top safe-area.
2582
- if (context.isSheetModal) {
2583
- const needsTopSafeArea = context.currentBreakpoint === 1;
2584
- applySafeAreaOverrides(el, {
2585
- top: needsTopSafeArea ? 'inherit' : '0px',
2586
- bottom: 'inherit',
2587
- left: '0px',
2588
- right: '0px',
2589
- });
2658
+ // Sheet modals: safe-area is fully determined at presentation time
2659
+ // (top is always 0px, height is frozen). Nothing to update.
2660
+ if (context.isSheetModal)
2590
2661
  return;
2591
- }
2592
2662
  // Card modals have fixed safe-area requirements set by initial prediction.
2593
2663
  if (context.isCardModal)
2594
2664
  return;
@@ -2614,6 +2684,8 @@ const Modal = class {
2614
2684
  // Check for standard Ionic layout children (ion-content, ion-footer),
2615
2685
  // searching one level deep for wrapped components (e.g.,
2616
2686
  // <app-footer><ion-footer>...</ion-footer></app-footer>).
2687
+ // Note: uses a manual loop instead of querySelector(':scope > ...') because
2688
+ // Stencil's mock-doc (used in spec tests) does not support :scope.
2617
2689
  let hasContent = false;
2618
2690
  let hasFooter = false;
2619
2691
  for (const child of Array.from(el.children)) {
@@ -2646,6 +2718,8 @@ const Modal = class {
2646
2718
  */
2647
2719
  cleanupSafeAreaOverrides() {
2648
2720
  clearSafeAreaOverrides(this.el);
2721
+ // Remove internal sheet offset property
2722
+ this.el.style.removeProperty('--ion-modal-offset-top');
2649
2723
  if (this.wrapperEl) {
2650
2724
  this.wrapperEl.style.removeProperty('height');
2651
2725
  this.wrapperEl.style.removeProperty('padding-bottom');
@@ -2658,20 +2732,20 @@ const Modal = class {
2658
2732
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2659
2733
  const isHandleCycle = handleBehavior === 'cycle';
2660
2734
  const isSheetModalWithHandle = isSheetModal && showHandle;
2661
- return (index$3.h(index$3.Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
2735
+ return (index$3.h(index$3.Host, Object.assign({ key: '038ae26c2cb040043c7d0c217a4de6a7ae909235', "no-router": true,
2662
2736
  // Allow the modal to be navigable when the handle is focusable
2663
2737
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2664
2738
  zIndex: `${20000 + this.overlayIndex}`,
2665
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '6866fb2a265d40c761ed6bfb661a217948c571ef', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '00788de9267f1c064f16aa55f35250de16522ae6', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'a575e1755bd25a7996cdb04b014a91d538e7bc94',
2739
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '6bab796467b370e79b24ce17c5a3d51adb07ee8c', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'b9a19e86a2c7f079c88ef7a914c7c890aa601887', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'f4315c34aa78be4e3540361755964be2ddf43265',
2666
2740
  /*
2667
2741
  role and aria-modal must be used on the
2668
2742
  same element. They must also be set inside the
2669
2743
  shadow DOM otherwise ion-button will not be highlighted
2670
2744
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2671
2745
  */
2672
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: 'b99c64717037e9241479ea2f5bb84e79a466d918', class: "modal-handle",
2746
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '3f7792bd65e0f8634e62bf27f9edcb636a03030a', class: "modal-handle",
2673
2747
  // Prevents the handle from receiving keyboard focus when it does not cycle
2674
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), index$3.h("slot", { key: 'ee672afd537b281aeeb73a55d61ceeb5dfae212a', onSlotchange: this.onSlotChange }))));
2748
+ tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), index$3.h("slot", { key: '001e5b01831e240bd98f75ff62446b90b64d8394', onSlotchange: this.onSlotChange }))));
2675
2749
  }
2676
2750
  get el() { return index$3.getElement(this); }
2677
2751
  static get watchers() { return {
@@ -186,9 +186,14 @@ ion-backdrop {
186
186
  /**
187
187
  * Ensure that the sheet modal does not
188
188
  * completely cover the content.
189
+ *
190
+ * --ion-modal-offset-top is an internal property set by modal.tsx
191
+ * with the resolved root safe-area-top pixel value. This decouples
192
+ * the height calculation from --ion-safe-area-top (which is zeroed
193
+ * for sheet modals to prevent header double-padding).
189
194
  */
190
195
  :host(.modal-sheet) {
191
- --height: calc(100% - (var(--ion-safe-area-top) + 10px));
196
+ --height: calc(100% - (var(--ion-modal-offset-top, 0px) + 10px));
192
197
  }
193
198
 
194
199
  :host(.modal-sheet) .modal-wrapper,