@ionic/core 8.7.18-dev.11770318664.15f47a5e → 8.7.18-dev.11770674094.18396f54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/components/ion-refresher.js +3 -32
  2. package/components/modal.js +248 -13
  3. package/components/popover.js +152 -30
  4. package/dist/cjs/ion-modal.cjs.entry.js +248 -13
  5. package/dist/cjs/ion-popover.cjs.entry.js +152 -30
  6. package/dist/cjs/ion-refresher_2.cjs.entry.js +3 -32
  7. package/dist/collection/components/modal/modal.ios.css +0 -4
  8. package/dist/collection/components/modal/modal.js +127 -11
  9. package/dist/collection/components/modal/modal.md.css +0 -4
  10. package/dist/collection/components/modal/safe-area-utils.js +123 -0
  11. package/dist/collection/components/popover/animations/ios.enter.js +33 -8
  12. package/dist/collection/components/popover/animations/md.enter.js +26 -6
  13. package/dist/collection/components/popover/utils.js +96 -19
  14. package/dist/collection/components/refresher/refresher.js +4 -70
  15. package/dist/docs.json +4 -49
  16. package/dist/esm/ion-modal.entry.js +248 -13
  17. package/dist/esm/ion-popover.entry.js +152 -30
  18. package/dist/esm/ion-refresher_2.entry.js +3 -32
  19. package/dist/html.html-data.json +1 -1
  20. package/dist/ionic/ionic.esm.js +1 -1
  21. package/dist/ionic/p-36d78f36.entry.js +4 -0
  22. package/dist/ionic/p-639dd543.entry.js +4 -0
  23. package/dist/ionic/p-cc3446d5.entry.js +4 -0
  24. package/dist/types/components/modal/modal.d.ts +23 -0
  25. package/dist/types/components/modal/safe-area-utils.d.ts +55 -0
  26. package/dist/types/components/popover/utils.d.ts +23 -1
  27. package/dist/types/components/refresher/refresher-interface.d.ts +0 -7
  28. package/dist/types/components/refresher/refresher.d.ts +3 -16
  29. package/dist/types/components.d.ts +5 -16
  30. package/dist/types/interface.d.ts +1 -1
  31. package/hydrate/index.js +403 -75
  32. package/hydrate/index.mjs +403 -75
  33. package/package.json +1 -1
  34. package/dist/ionic/p-90f2d3ff.entry.js +0 -4
  35. package/dist/ionic/p-91840a80.entry.js +0 -4
  36. package/dist/ionic/p-f9061316.entry.js +0 -4
@@ -1474,9 +1474,129 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1474
1474
  };
1475
1475
  };
1476
1476
 
1477
- const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
1477
+ /**
1478
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
1479
+ * that trigger the centered dialog layout (non-fullscreen modal).
1480
+ *
1481
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
1482
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
1483
+ * that's the threshold where the modal transitions from fullscreen to centered
1484
+ * dialog — the larger breakpoint only increases the dialog's height.
1485
+ */
1486
+ const MODAL_INSET_MIN_WIDTH = 768;
1487
+ const MODAL_INSET_MIN_HEIGHT = 600;
1488
+ const EDGE_THRESHOLD = 5;
1489
+ /**
1490
+ * Determines if the current viewport meets the CSS media query conditions
1491
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
1492
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
1493
+ */
1494
+ const isCenteredDialogViewport = () => {
1495
+ if (!index.win)
1496
+ return false;
1497
+ return index.win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
1498
+ .matches;
1499
+ };
1500
+ /**
1501
+ * Returns the initial safe-area configuration based on modal type.
1502
+ * This is called before animation starts and uses configuration-based prediction.
1503
+ *
1504
+ * @param context - Modal context information
1505
+ * @returns SafeAreaConfig with initial safe-area values
1506
+ */
1507
+ const getInitialSafeAreaConfig = (context) => {
1508
+ const { isSheetModal, isCardModal } = context;
1509
+ // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
1510
+ if (isSheetModal) {
1511
+ return {
1512
+ top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
1513
+ bottom: 'inherit',
1514
+ left: '0px',
1515
+ right: '0px',
1516
+ };
1517
+ }
1518
+ // Card modals need safe-area for height calculation.
1519
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
1520
+ if (isCardModal) {
1521
+ return {
1522
+ top: 'inherit',
1523
+ bottom: 'inherit',
1524
+ left: '0px',
1525
+ right: '0px',
1526
+ };
1527
+ }
1528
+ // On viewports that meet the centered dialog media query breakpoints,
1529
+ // regular modals render as centered dialogs (not fullscreen), so they
1530
+ // don't touch any screen edges and don't need safe-area insets.
1531
+ if (isCenteredDialogViewport()) {
1532
+ return {
1533
+ top: '0px',
1534
+ bottom: '0px',
1535
+ left: '0px',
1536
+ right: '0px',
1537
+ };
1538
+ }
1539
+ // Fullscreen modals on phone - inherit all safe areas
1540
+ return {
1541
+ top: 'inherit',
1542
+ bottom: 'inherit',
1543
+ left: 'inherit',
1544
+ right: 'inherit',
1545
+ };
1546
+ };
1547
+ /**
1548
+ * Returns safe-area configuration based on actual modal position.
1549
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
1550
+ *
1551
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
1552
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
1553
+ * prediction instead. Regular modals use coordinate detection which works reliably
1554
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
1555
+ * once a reliable detection mechanism is available.
1556
+ *
1557
+ * @param wrapperEl - The modal wrapper element to measure
1558
+ * @returns SafeAreaConfig based on position
1559
+ */
1560
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
1561
+ var _a, _b;
1562
+ const rect = wrapperEl.getBoundingClientRect();
1563
+ const vh = (_a = index.win === null || index.win === void 0 ? void 0 : index.win.innerHeight) !== null && _a !== void 0 ? _a : 0;
1564
+ const vw = (_b = index.win === null || index.win === void 0 ? void 0 : index.win.innerWidth) !== null && _b !== void 0 ? _b : 0;
1565
+ // Only apply safe-area to sides where modal overlaps with screen edge
1566
+ return {
1567
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
1568
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
1569
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
1570
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
1571
+ };
1572
+ };
1573
+ /**
1574
+ * Applies safe-area CSS custom property overrides to the modal host element.
1575
+ *
1576
+ * @param hostEl - The modal host element (ion-modal)
1577
+ * @param config - Safe-area configuration to apply
1578
+ */
1579
+ const applySafeAreaOverrides = (hostEl, config) => {
1580
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
1581
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
1582
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
1583
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
1584
+ };
1585
+ /**
1586
+ * Clears safe-area CSS custom property overrides from the modal host element.
1587
+ *
1588
+ * @param hostEl - The modal host element (ion-modal)
1589
+ */
1590
+ const clearSafeAreaOverrides = (hostEl) => {
1591
+ hostEl.style.removeProperty('--ion-safe-area-top');
1592
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
1593
+ hostEl.style.removeProperty('--ion-safe-area-left');
1594
+ hostEl.style.removeProperty('--ion-safe-area-right');
1595
+ };
1596
+
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}";
1478
1598
 
1479
- const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
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}";
1480
1600
 
1481
1601
  const Modal = class {
1482
1602
  constructor(hostRef) {
@@ -1712,6 +1832,9 @@ const Modal = class {
1712
1832
  this.triggerController.removeClickListener();
1713
1833
  this.cleanupViewTransitionListener();
1714
1834
  this.cleanupParentRemovalObserver();
1835
+ // Also called in dismiss() — intentional dual cleanup covers both
1836
+ // dismiss-then-remove and direct DOM removal without dismiss.
1837
+ this.cleanupSafeAreaOverrides();
1715
1838
  }
1716
1839
  componentWillLoad() {
1717
1840
  var _a;
@@ -1872,6 +1995,11 @@ const Modal = class {
1872
1995
  await index$4.waitForMount();
1873
1996
  }
1874
1997
  index$3.writeTask(() => this.el.classList.add('show-modal'));
1998
+ // Recalculate isSheetModal before safe-area setup because framework
1999
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
2000
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2001
+ // Set initial safe-area overrides before animation
2002
+ this.setInitialSafeAreaOverrides();
1875
2003
  const hasCardModal = presentingElement !== undefined;
1876
2004
  /**
1877
2005
  * We need to change the status bar at the
@@ -1889,6 +2017,10 @@ const Modal = class {
1889
2017
  backdropBreakpoint: this.backdropBreakpoint,
1890
2018
  expandToScroll: this.expandToScroll,
1891
2019
  });
2020
+ // Update safe-area based on actual position after animation
2021
+ this.updateSafeAreaOverrides();
2022
+ // Apply fullscreen safe-area padding if needed
2023
+ this.applyFullscreenSafeArea();
1892
2024
  /* tslint:disable-next-line */
1893
2025
  if (typeof window !== 'undefined') {
1894
2026
  /**
@@ -1920,13 +2052,7 @@ const Modal = class {
1920
2052
  };
1921
2053
  window.addEventListener(keyboard.KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
1922
2054
  }
1923
- /**
1924
- * Recalculate isSheetModal because framework bindings (e.g., Angular)
1925
- * may not have been applied when componentWillLoad ran.
1926
- */
1927
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
1928
- this.isSheetModal = isSheetModal;
1929
- if (isSheetModal) {
2055
+ if (this.isSheetModal) {
1930
2056
  this.initSheetGesture();
1931
2057
  }
1932
2058
  else if (hasCardModal) {
@@ -2003,6 +2129,8 @@ const Modal = class {
2003
2129
  if (this.currentBreakpoint !== breakpoint) {
2004
2130
  this.currentBreakpoint = breakpoint;
2005
2131
  this.ionBreakpointDidChange.emit({ breakpoint });
2132
+ // Update safe-area overrides based on new position
2133
+ this.updateSafeAreaOverrides();
2006
2134
  }
2007
2135
  });
2008
2136
  this.gesture = gesture;
@@ -2170,6 +2298,7 @@ const Modal = class {
2170
2298
  }
2171
2299
  this.cleanupViewTransitionListener();
2172
2300
  this.cleanupParentRemovalObserver();
2301
+ this.cleanupSafeAreaOverrides();
2173
2302
  this.cleanupChildRoutePassthrough();
2174
2303
  }
2175
2304
  this.currentBreakpoint = undefined;
@@ -2290,6 +2419,9 @@ const Modal = class {
2290
2419
  this.viewTransitionAnimation = transitionAnimation;
2291
2420
  transitionAnimation.play().then(() => {
2292
2421
  this.viewTransitionAnimation = undefined;
2422
+ // Wait for a layout pass after the transition so getBoundingClientRect()
2423
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
2424
+ helpers.raf(() => this.updateSafeAreaOverrides());
2293
2425
  // After orientation transition, recreate the swipe-to-close gesture
2294
2426
  // with updated animation that reflects the new presenting element state
2295
2427
  this.reinitSwipeToClose();
@@ -2416,6 +2548,109 @@ const Modal = class {
2416
2548
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2417
2549
  this.parentRemovalObserver = undefined;
2418
2550
  }
2551
+ /**
2552
+ * Creates the context object for safe-area utilities.
2553
+ */
2554
+ getSafeAreaContext() {
2555
+ return {
2556
+ isSheetModal: this.isSheetModal,
2557
+ isCardModal: this.presentingElement !== undefined && ionicGlobal.getIonMode(this) === 'ios',
2558
+ presentingElement: this.presentingElement,
2559
+ breakpoints: this.breakpoints,
2560
+ currentBreakpoint: this.currentBreakpoint,
2561
+ };
2562
+ }
2563
+ /**
2564
+ * Sets initial safe-area overrides before modal animation.
2565
+ * Called in present() before animation starts.
2566
+ */
2567
+ setInitialSafeAreaOverrides() {
2568
+ const context = this.getSafeAreaContext();
2569
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
2570
+ applySafeAreaOverrides(this.el, safeAreaConfig);
2571
+ }
2572
+ /**
2573
+ * Updates safe-area overrides during dynamic state changes.
2574
+ * Called after animations, during gestures, and on orientation changes.
2575
+ */
2576
+ updateSafeAreaOverrides() {
2577
+ const { wrapperEl, el } = this;
2578
+ 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
+ });
2590
+ return;
2591
+ }
2592
+ // Card modals have fixed safe-area requirements set by initial prediction.
2593
+ if (context.isCardModal)
2594
+ return;
2595
+ // wrapperEl is required for position-based detection below
2596
+ if (!wrapperEl)
2597
+ return;
2598
+ // Regular modals: use position-based detection to correctly handle both
2599
+ // fullscreen modals and centered dialogs with custom dimensions.
2600
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
2601
+ applySafeAreaOverrides(el, safeAreaConfig);
2602
+ }
2603
+ /**
2604
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
2605
+ * content from overlapping system navigation bar.
2606
+ */
2607
+ applyFullscreenSafeArea() {
2608
+ const { wrapperEl, el } = this;
2609
+ if (!wrapperEl)
2610
+ return;
2611
+ const context = this.getSafeAreaContext();
2612
+ if (context.isSheetModal || context.isCardModal)
2613
+ return;
2614
+ // Check for standard Ionic layout children (ion-content, ion-footer),
2615
+ // searching one level deep for wrapped components (e.g.,
2616
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
2617
+ let hasContent = false;
2618
+ let hasFooter = false;
2619
+ for (const child of Array.from(el.children)) {
2620
+ if (child.tagName === 'ION-CONTENT')
2621
+ hasContent = true;
2622
+ if (child.tagName === 'ION-FOOTER')
2623
+ hasFooter = true;
2624
+ for (const grandchild of Array.from(child.children)) {
2625
+ if (grandchild.tagName === 'ION-CONTENT')
2626
+ hasContent = true;
2627
+ if (grandchild.tagName === 'ION-FOOTER')
2628
+ hasFooter = true;
2629
+ }
2630
+ }
2631
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
2632
+ // but no ion-footer). Custom modals with raw HTML are fully
2633
+ // developer-controlled and should not be modified.
2634
+ if (!hasContent || hasFooter)
2635
+ return;
2636
+ // Reduce wrapper height by safe-area and add equivalent padding so the
2637
+ // total visual size stays the same but the flex content area shrinks.
2638
+ // Using height + padding instead of box-sizing: border-box avoids
2639
+ // breaking custom modals that set --border-width (border-box would
2640
+ // include the border inside the height, changing the layout).
2641
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
2642
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2643
+ }
2644
+ /**
2645
+ * Clears all safe-area overrides and padding from wrapper.
2646
+ */
2647
+ cleanupSafeAreaOverrides() {
2648
+ clearSafeAreaOverrides(this.el);
2649
+ if (this.wrapperEl) {
2650
+ this.wrapperEl.style.removeProperty('height');
2651
+ this.wrapperEl.style.removeProperty('padding-bottom');
2652
+ }
2653
+ }
2419
2654
  render() {
2420
2655
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
2421
2656
  const showHandle = handle !== false && isSheetModal;
@@ -2423,20 +2658,20 @@ const Modal = class {
2423
2658
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2424
2659
  const isHandleCycle = handleBehavior === 'cycle';
2425
2660
  const isSheetModalWithHandle = isSheetModal && showHandle;
2426
- return (index$3.h(index$3.Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2661
+ return (index$3.h(index$3.Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
2427
2662
  // Allow the modal to be navigable when the handle is focusable
2428
2663
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2429
2664
  zIndex: `${20000 + this.overlayIndex}`,
2430
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
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',
2431
2666
  /*
2432
2667
  role and aria-modal must be used on the
2433
2668
  same element. They must also be set inside the
2434
2669
  shadow DOM otherwise ion-button will not be highlighted
2435
2670
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2436
2671
  */
2437
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
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",
2438
2673
  // Prevents the handle from receiving keyboard focus when it does not cycle
2439
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), index$3.h("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
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 }))));
2440
2675
  }
2441
2676
  get el() { return index$3.getElement(this); }
2442
2677
  static get watchers() { return {