@ionic/core 8.7.18-dev.11770318664.15f47a5e → 8.7.18-dev.11770916502.174efd7f

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 +326 -17
  3. package/components/popover.js +152 -30
  4. package/dist/cjs/ion-modal.cjs.entry.js +326 -17
  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 +6 -5
  8. package/dist/collection/components/modal/modal.js +164 -15
  9. package/dist/collection/components/modal/modal.md.css +6 -5
  10. package/dist/collection/components/modal/safe-area-utils.js +165 -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 +326 -17
  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-3e69999d.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 +35 -0
  25. package/dist/types/components/modal/safe-area-utils.d.ts +66 -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 +481 -79
  32. package/hydrate/index.mjs +481 -79
  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,170 @@ 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
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
1491
+ */
1492
+ let cachedRootSafeAreaTop = null;
1493
+ let cacheInvalidationScheduled = false;
1494
+ /**
1495
+ * Determines if the current viewport meets the CSS media query conditions
1496
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
1497
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
1498
+ */
1499
+ const isCenteredDialogViewport = () => {
1500
+ if (!index.win)
1501
+ return false;
1502
+ return index.win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
1503
+ .matches;
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
+ };
1537
+ /**
1538
+ * Returns the initial safe-area configuration based on modal type.
1539
+ * This is called before animation starts and uses configuration-based prediction.
1540
+ *
1541
+ * @param context - Modal context information
1542
+ * @returns SafeAreaConfig with initial safe-area values
1543
+ */
1544
+ const getInitialSafeAreaConfig = (context) => {
1545
+ const { isSheetModal, isCardModal } = context;
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.
1551
+ if (isSheetModal) {
1552
+ return {
1553
+ top: '0px',
1554
+ bottom: 'inherit',
1555
+ left: '0px',
1556
+ right: '0px',
1557
+ };
1558
+ }
1559
+ // Card modals need safe-area for height calculation.
1560
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
1561
+ if (isCardModal) {
1562
+ return {
1563
+ top: 'inherit',
1564
+ bottom: 'inherit',
1565
+ left: '0px',
1566
+ right: '0px',
1567
+ };
1568
+ }
1569
+ // On viewports that meet the centered dialog media query breakpoints,
1570
+ // regular modals render as centered dialogs (not fullscreen), so they
1571
+ // don't touch any screen edges and don't need safe-area insets.
1572
+ if (isCenteredDialogViewport()) {
1573
+ return {
1574
+ top: '0px',
1575
+ bottom: '0px',
1576
+ left: '0px',
1577
+ right: '0px',
1578
+ };
1579
+ }
1580
+ // Fullscreen modals on phone - inherit all safe areas
1581
+ return {
1582
+ top: 'inherit',
1583
+ bottom: 'inherit',
1584
+ left: 'inherit',
1585
+ right: 'inherit',
1586
+ };
1587
+ };
1588
+ /**
1589
+ * Returns safe-area configuration based on actual modal position.
1590
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
1591
+ *
1592
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
1593
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
1594
+ * prediction instead. Regular modals use coordinate detection which works reliably
1595
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
1596
+ * once a reliable detection mechanism is available.
1597
+ *
1598
+ * @param wrapperEl - The modal wrapper element to measure
1599
+ * @returns SafeAreaConfig based on position
1600
+ */
1601
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
1602
+ var _a, _b;
1603
+ const rect = wrapperEl.getBoundingClientRect();
1604
+ const vh = (_a = index.win === null || index.win === void 0 ? void 0 : index.win.innerHeight) !== null && _a !== void 0 ? _a : 0;
1605
+ const vw = (_b = index.win === null || index.win === void 0 ? void 0 : index.win.innerWidth) !== null && _b !== void 0 ? _b : 0;
1606
+ // Only apply safe-area to sides where modal overlaps with screen edge
1607
+ return {
1608
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
1609
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
1610
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
1611
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
1612
+ };
1613
+ };
1614
+ /**
1615
+ * Applies safe-area CSS custom property overrides to the modal host element.
1616
+ *
1617
+ * @param hostEl - The modal host element (ion-modal)
1618
+ * @param config - Safe-area configuration to apply
1619
+ */
1620
+ const applySafeAreaOverrides = (hostEl, config) => {
1621
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
1622
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
1623
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
1624
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
1625
+ };
1626
+ /**
1627
+ * Clears safe-area CSS custom property overrides from the modal host element.
1628
+ *
1629
+ * @param hostEl - The modal host element (ion-modal)
1630
+ */
1631
+ const clearSafeAreaOverrides = (hostEl) => {
1632
+ hostEl.style.removeProperty('--ion-safe-area-top');
1633
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
1634
+ hostEl.style.removeProperty('--ion-safe-area-left');
1635
+ hostEl.style.removeProperty('--ion-safe-area-right');
1636
+ };
1637
+
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}";
1478
1639
 
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}";
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}";
1480
1641
 
1481
1642
  const Modal = class {
1482
1643
  constructor(hostRef) {
@@ -1689,13 +1850,31 @@ const Modal = class {
1689
1850
  }
1690
1851
  }
1691
1852
  onWindowResize() {
1692
- // Only handle resize for iOS card modals when no custom animations are provided
1693
- if (ionicGlobal.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1853
+ if (!this.presented)
1694
1854
  return;
1695
- }
1696
1855
  clearTimeout(this.resizeTimeout);
1697
1856
  this.resizeTimeout = setTimeout(() => {
1698
- 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
+ }
1699
1878
  }, 50); // Debounce to avoid excessive calls during active resizing
1700
1879
  }
1701
1880
  breakpointsChanged(breakpoints) {
@@ -1712,6 +1891,9 @@ const Modal = class {
1712
1891
  this.triggerController.removeClickListener();
1713
1892
  this.cleanupViewTransitionListener();
1714
1893
  this.cleanupParentRemovalObserver();
1894
+ // Also called in dismiss() — intentional dual cleanup covers both
1895
+ // dismiss-then-remove and direct DOM removal without dismiss.
1896
+ this.cleanupSafeAreaOverrides();
1715
1897
  }
1716
1898
  componentWillLoad() {
1717
1899
  var _a;
@@ -1872,6 +2054,11 @@ const Modal = class {
1872
2054
  await index$4.waitForMount();
1873
2055
  }
1874
2056
  index$3.writeTask(() => this.el.classList.add('show-modal'));
2057
+ // Recalculate isSheetModal before safe-area setup because framework
2058
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
2059
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2060
+ // Set initial safe-area overrides before animation
2061
+ this.setInitialSafeAreaOverrides();
1875
2062
  const hasCardModal = presentingElement !== undefined;
1876
2063
  /**
1877
2064
  * We need to change the status bar at the
@@ -1889,6 +2076,10 @@ const Modal = class {
1889
2076
  backdropBreakpoint: this.backdropBreakpoint,
1890
2077
  expandToScroll: this.expandToScroll,
1891
2078
  });
2079
+ // Update safe-area based on actual position after animation
2080
+ this.updateSafeAreaOverrides();
2081
+ // Apply fullscreen safe-area padding if needed
2082
+ this.applyFullscreenSafeArea();
1892
2083
  /* tslint:disable-next-line */
1893
2084
  if (typeof window !== 'undefined') {
1894
2085
  /**
@@ -1920,13 +2111,7 @@ const Modal = class {
1920
2111
  };
1921
2112
  window.addEventListener(keyboard.KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
1922
2113
  }
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) {
2114
+ if (this.isSheetModal) {
1930
2115
  this.initSheetGesture();
1931
2116
  }
1932
2117
  else if (hasCardModal) {
@@ -2116,6 +2301,9 @@ const Modal = class {
2116
2301
  if (this.gestureAnimationDismissing && role !== overlays.GESTURE) {
2117
2302
  return false;
2118
2303
  }
2304
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
2305
+ clearTimeout(this.resizeTimeout);
2306
+ this.resizeTimeout = undefined;
2119
2307
  /**
2120
2308
  * Because the canDismiss check below is async,
2121
2309
  * we need to claim a lock before the check happens,
@@ -2170,6 +2358,7 @@ const Modal = class {
2170
2358
  }
2171
2359
  this.cleanupViewTransitionListener();
2172
2360
  this.cleanupParentRemovalObserver();
2361
+ this.cleanupSafeAreaOverrides();
2173
2362
  this.cleanupChildRoutePassthrough();
2174
2363
  }
2175
2364
  this.currentBreakpoint = undefined;
@@ -2290,6 +2479,9 @@ const Modal = class {
2290
2479
  this.viewTransitionAnimation = transitionAnimation;
2291
2480
  transitionAnimation.play().then(() => {
2292
2481
  this.viewTransitionAnimation = undefined;
2482
+ // Wait for a layout pass after the transition so getBoundingClientRect()
2483
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
2484
+ helpers.raf(() => this.updateSafeAreaOverrides());
2293
2485
  // After orientation transition, recreate the swipe-to-close gesture
2294
2486
  // with updated animation that reflects the new presenting element state
2295
2487
  this.reinitSwipeToClose();
@@ -2416,6 +2608,123 @@ const Modal = class {
2416
2608
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2417
2609
  this.parentRemovalObserver = undefined;
2418
2610
  }
2611
+ /**
2612
+ * Creates the context object for safe-area utilities.
2613
+ */
2614
+ getSafeAreaContext() {
2615
+ return {
2616
+ isSheetModal: this.isSheetModal,
2617
+ isCardModal: this.presentingElement !== undefined && ionicGlobal.getIonMode(this) === 'ios',
2618
+ presentingElement: this.presentingElement,
2619
+ breakpoints: this.breakpoints,
2620
+ currentBreakpoint: this.currentBreakpoint,
2621
+ };
2622
+ }
2623
+ /**
2624
+ * Sets initial safe-area overrides before modal animation.
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).
2632
+ */
2633
+ setInitialSafeAreaOverrides() {
2634
+ const context = this.getSafeAreaContext();
2635
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
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`);
2650
+ }
2651
+ /**
2652
+ * Updates safe-area overrides during dynamic state changes.
2653
+ * Called after animations, during gestures, and on orientation changes.
2654
+ */
2655
+ updateSafeAreaOverrides() {
2656
+ const { wrapperEl, el } = this;
2657
+ const context = this.getSafeAreaContext();
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)
2661
+ return;
2662
+ // Card modals have fixed safe-area requirements set by initial prediction.
2663
+ if (context.isCardModal)
2664
+ return;
2665
+ // wrapperEl is required for position-based detection below
2666
+ if (!wrapperEl)
2667
+ return;
2668
+ // Regular modals: use position-based detection to correctly handle both
2669
+ // fullscreen modals and centered dialogs with custom dimensions.
2670
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
2671
+ applySafeAreaOverrides(el, safeAreaConfig);
2672
+ }
2673
+ /**
2674
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
2675
+ * content from overlapping system navigation bar.
2676
+ */
2677
+ applyFullscreenSafeArea() {
2678
+ const { wrapperEl, el } = this;
2679
+ if (!wrapperEl)
2680
+ return;
2681
+ const context = this.getSafeAreaContext();
2682
+ if (context.isSheetModal || context.isCardModal)
2683
+ return;
2684
+ // Check for standard Ionic layout children (ion-content, ion-footer),
2685
+ // searching one level deep for wrapped components (e.g.,
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.
2689
+ let hasContent = false;
2690
+ let hasFooter = false;
2691
+ for (const child of Array.from(el.children)) {
2692
+ if (child.tagName === 'ION-CONTENT')
2693
+ hasContent = true;
2694
+ if (child.tagName === 'ION-FOOTER')
2695
+ hasFooter = true;
2696
+ for (const grandchild of Array.from(child.children)) {
2697
+ if (grandchild.tagName === 'ION-CONTENT')
2698
+ hasContent = true;
2699
+ if (grandchild.tagName === 'ION-FOOTER')
2700
+ hasFooter = true;
2701
+ }
2702
+ }
2703
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
2704
+ // but no ion-footer). Custom modals with raw HTML are fully
2705
+ // developer-controlled and should not be modified.
2706
+ if (!hasContent || hasFooter)
2707
+ return;
2708
+ // Reduce wrapper height by safe-area and add equivalent padding so the
2709
+ // total visual size stays the same but the flex content area shrinks.
2710
+ // Using height + padding instead of box-sizing: border-box avoids
2711
+ // breaking custom modals that set --border-width (border-box would
2712
+ // include the border inside the height, changing the layout).
2713
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
2714
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2715
+ }
2716
+ /**
2717
+ * Clears all safe-area overrides and padding from wrapper.
2718
+ */
2719
+ cleanupSafeAreaOverrides() {
2720
+ clearSafeAreaOverrides(this.el);
2721
+ // Remove internal sheet offset property
2722
+ this.el.style.removeProperty('--ion-modal-offset-top');
2723
+ if (this.wrapperEl) {
2724
+ this.wrapperEl.style.removeProperty('height');
2725
+ this.wrapperEl.style.removeProperty('padding-bottom');
2726
+ }
2727
+ }
2419
2728
  render() {
2420
2729
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
2421
2730
  const showHandle = handle !== false && isSheetModal;
@@ -2423,20 +2732,20 @@ const Modal = class {
2423
2732
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2424
2733
  const isHandleCycle = handleBehavior === 'cycle';
2425
2734
  const isSheetModalWithHandle = isSheetModal && showHandle;
2426
- return (index$3.h(index$3.Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2735
+ return (index$3.h(index$3.Host, Object.assign({ key: '038ae26c2cb040043c7d0c217a4de6a7ae909235', "no-router": true,
2427
2736
  // Allow the modal to be navigable when the handle is focusable
2428
2737
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2429
2738
  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',
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',
2431
2740
  /*
2432
2741
  role and aria-modal must be used on the
2433
2742
  same element. They must also be set inside the
2434
2743
  shadow DOM otherwise ion-button will not be highlighted
2435
2744
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2436
2745
  */
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",
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",
2438
2747
  // 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 }))));
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 }))));
2440
2749
  }
2441
2750
  get el() { return index$3.getElement(this); }
2442
2751
  static get watchers() { return {