@ionic/core 8.7.18-dev.11770316772.1e7b64e5 → 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
@@ -23,8 +23,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
23
23
  this.ionRefresh = createEvent(this, "ionRefresh", 7);
24
24
  this.ionPull = createEvent(this, "ionPull", 7);
25
25
  this.ionStart = createEvent(this, "ionStart", 7);
26
- this.ionPullStart = createEvent(this, "ionPullStart", 7);
27
- this.ionPullEnd = createEvent(this, "ionPullEnd", 7);
28
26
  this.appliedStyles = false;
29
27
  this.didStart = false;
30
28
  this.progress = 0;
@@ -79,8 +77,8 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
79
77
  * than `1`. The default value is `1` which is equal to the speed of the cursor.
80
78
  * If a negative value is passed in, the factor will be `1` instead.
81
79
  *
82
- * For example, If the value passed is `1.2` and the content is dragged by
83
- * `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
80
+ * For example: If the value passed is `1.2` and the content is dragged by
81
+ * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
84
82
  * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
85
83
  * will be `8` pixels, less than the amount the cursor has moved.
86
84
  *
@@ -130,9 +128,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
130
128
  this.animations = [];
131
129
  this.progress = 0;
132
130
  this.state = 1 /* RefresherState.Inactive */;
133
- this.ionPullEnd.emit({
134
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
135
- });
136
131
  }
137
132
  async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
138
133
  this.elementToTransform = this.scrollEl;
@@ -165,7 +160,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
165
160
  if (!this.didStart) {
166
161
  this.didStart = true;
167
162
  this.ionStart.emit();
168
- this.ionPullStart.emit();
169
163
  }
170
164
  // emit "pulling" on every move
171
165
  if (this.pointerDown) {
@@ -241,7 +235,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
241
235
  this.lastVelocityY = ev.velocityY;
242
236
  },
243
237
  onEnd: () => {
244
- const hadStarted = this.didStart;
245
238
  this.pointerDown = false;
246
239
  this.didStart = false;
247
240
  if (this.needsCompletion) {
@@ -251,13 +244,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
251
244
  else if (this.didRefresh) {
252
245
  readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
253
246
  }
254
- else if (hadStarted) {
255
- /**
256
- * User started pulling but released before reaching the refresh threshold.
257
- * Emit ionPullEnd to complete the event pair.
258
- */
259
- this.ionPullEnd.emit({ reason: 'cancel' });
260
- }
261
247
  },
262
248
  });
263
249
  this.disabledChanged();
@@ -304,7 +290,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
304
290
  ev.data.animation = animation;
305
291
  animation.progressStart(false, 0);
306
292
  this.ionStart.emit();
307
- this.ionPullStart.emit();
308
293
  this.animations.push(animation);
309
294
  return;
310
295
  }
@@ -327,7 +312,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
327
312
  this.animations = [];
328
313
  this.gesture.enable(true);
329
314
  this.state = 1 /* RefresherState.Inactive */;
330
- this.ionPullEnd.emit({ reason: 'cancel' });
331
315
  });
332
316
  return;
333
317
  }
@@ -572,7 +556,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
572
556
  if (!this.didStart) {
573
557
  this.didStart = true;
574
558
  this.ionStart.emit();
575
- this.ionPullStart.emit();
576
559
  }
577
560
  // emit "pulling" on every move
578
561
  this.ionPull.emit();
@@ -616,15 +599,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
616
599
  * available right away.
617
600
  */
618
601
  this.restoreOverflowStyle();
619
- /**
620
- * If ionPullStart was emitted, we need to emit ionPullEnd
621
- * even though the gesture was aborted before reaching the
622
- * pulling threshold.
623
- */
624
- if (this.didStart) {
625
- this.didStart = false;
626
- this.ionPullEnd.emit({ reason: 'cancel' });
627
- }
628
602
  }
629
603
  }
630
604
  beginRefresh() {
@@ -671,9 +645,6 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
671
645
  if (this.contentFullscreen && this.backgroundContentEl) {
672
646
  (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
673
647
  }
674
- this.ionPullEnd.emit({
675
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
676
- });
677
648
  }, 600);
678
649
  // reset the styles on the scroll element
679
650
  // set that the refresh is actively cancelling/completing
@@ -727,7 +698,7 @@ const Refresher = /*@__PURE__*/ proxyCustomElement(class Refresher extends HTMLE
727
698
  }
728
699
  render() {
729
700
  const mode = getIonMode(this);
730
- return (h(Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
701
+ return (h(Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
731
702
  [mode]: true,
732
703
  // Used internally for styling
733
704
  [`refresher-${mode}`]: true,
@@ -1471,9 +1471,129 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1471
1471
  };
1472
1472
  };
1473
1473
 
1474
- 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}";
1474
+ /**
1475
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
1476
+ * that trigger the centered dialog layout (non-fullscreen modal).
1477
+ *
1478
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
1479
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
1480
+ * that's the threshold where the modal transitions from fullscreen to centered
1481
+ * dialog — the larger breakpoint only increases the dialog's height.
1482
+ */
1483
+ const MODAL_INSET_MIN_WIDTH = 768;
1484
+ const MODAL_INSET_MIN_HEIGHT = 600;
1485
+ const EDGE_THRESHOLD = 5;
1486
+ /**
1487
+ * Determines if the current viewport meets the CSS media query conditions
1488
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
1489
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
1490
+ */
1491
+ const isCenteredDialogViewport = () => {
1492
+ if (!win)
1493
+ return false;
1494
+ return win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
1495
+ .matches;
1496
+ };
1497
+ /**
1498
+ * Returns the initial safe-area configuration based on modal type.
1499
+ * This is called before animation starts and uses configuration-based prediction.
1500
+ *
1501
+ * @param context - Modal context information
1502
+ * @returns SafeAreaConfig with initial safe-area values
1503
+ */
1504
+ const getInitialSafeAreaConfig = (context) => {
1505
+ const { isSheetModal, isCardModal } = context;
1506
+ // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
1507
+ if (isSheetModal) {
1508
+ return {
1509
+ top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
1510
+ bottom: 'inherit',
1511
+ left: '0px',
1512
+ right: '0px',
1513
+ };
1514
+ }
1515
+ // Card modals need safe-area for height calculation.
1516
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
1517
+ if (isCardModal) {
1518
+ return {
1519
+ top: 'inherit',
1520
+ bottom: 'inherit',
1521
+ left: '0px',
1522
+ right: '0px',
1523
+ };
1524
+ }
1525
+ // On viewports that meet the centered dialog media query breakpoints,
1526
+ // regular modals render as centered dialogs (not fullscreen), so they
1527
+ // don't touch any screen edges and don't need safe-area insets.
1528
+ if (isCenteredDialogViewport()) {
1529
+ return {
1530
+ top: '0px',
1531
+ bottom: '0px',
1532
+ left: '0px',
1533
+ right: '0px',
1534
+ };
1535
+ }
1536
+ // Fullscreen modals on phone - inherit all safe areas
1537
+ return {
1538
+ top: 'inherit',
1539
+ bottom: 'inherit',
1540
+ left: 'inherit',
1541
+ right: 'inherit',
1542
+ };
1543
+ };
1544
+ /**
1545
+ * Returns safe-area configuration based on actual modal position.
1546
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
1547
+ *
1548
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
1549
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
1550
+ * prediction instead. Regular modals use coordinate detection which works reliably
1551
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
1552
+ * once a reliable detection mechanism is available.
1553
+ *
1554
+ * @param wrapperEl - The modal wrapper element to measure
1555
+ * @returns SafeAreaConfig based on position
1556
+ */
1557
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
1558
+ var _a, _b;
1559
+ const rect = wrapperEl.getBoundingClientRect();
1560
+ const vh = (_a = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _a !== void 0 ? _a : 0;
1561
+ const vw = (_b = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _b !== void 0 ? _b : 0;
1562
+ // Only apply safe-area to sides where modal overlaps with screen edge
1563
+ return {
1564
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
1565
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
1566
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
1567
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
1568
+ };
1569
+ };
1570
+ /**
1571
+ * Applies safe-area CSS custom property overrides to the modal host element.
1572
+ *
1573
+ * @param hostEl - The modal host element (ion-modal)
1574
+ * @param config - Safe-area configuration to apply
1575
+ */
1576
+ const applySafeAreaOverrides = (hostEl, config) => {
1577
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
1578
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
1579
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
1580
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
1581
+ };
1582
+ /**
1583
+ * Clears safe-area CSS custom property overrides from the modal host element.
1584
+ *
1585
+ * @param hostEl - The modal host element (ion-modal)
1586
+ */
1587
+ const clearSafeAreaOverrides = (hostEl) => {
1588
+ hostEl.style.removeProperty('--ion-safe-area-top');
1589
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
1590
+ hostEl.style.removeProperty('--ion-safe-area-left');
1591
+ hostEl.style.removeProperty('--ion-safe-area-right');
1592
+ };
1593
+
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}";
1475
1595
 
1476
- 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}";
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}";
1477
1597
 
1478
1598
  const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1479
1599
  constructor(registerHost) {
@@ -1713,6 +1833,9 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1713
1833
  this.triggerController.removeClickListener();
1714
1834
  this.cleanupViewTransitionListener();
1715
1835
  this.cleanupParentRemovalObserver();
1836
+ // Also called in dismiss() — intentional dual cleanup covers both
1837
+ // dismiss-then-remove and direct DOM removal without dismiss.
1838
+ this.cleanupSafeAreaOverrides();
1716
1839
  }
1717
1840
  componentWillLoad() {
1718
1841
  var _a;
@@ -1873,6 +1996,11 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1873
1996
  await waitForMount();
1874
1997
  }
1875
1998
  writeTask(() => this.el.classList.add('show-modal'));
1999
+ // Recalculate isSheetModal before safe-area setup because framework
2000
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
2001
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2002
+ // Set initial safe-area overrides before animation
2003
+ this.setInitialSafeAreaOverrides();
1876
2004
  const hasCardModal = presentingElement !== undefined;
1877
2005
  /**
1878
2006
  * We need to change the status bar at the
@@ -1890,6 +2018,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1890
2018
  backdropBreakpoint: this.backdropBreakpoint,
1891
2019
  expandToScroll: this.expandToScroll,
1892
2020
  });
2021
+ // Update safe-area based on actual position after animation
2022
+ this.updateSafeAreaOverrides();
2023
+ // Apply fullscreen safe-area padding if needed
2024
+ this.applyFullscreenSafeArea();
1893
2025
  /* tslint:disable-next-line */
1894
2026
  if (typeof window !== 'undefined') {
1895
2027
  /**
@@ -1921,13 +2053,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1921
2053
  };
1922
2054
  window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
1923
2055
  }
1924
- /**
1925
- * Recalculate isSheetModal because framework bindings (e.g., Angular)
1926
- * may not have been applied when componentWillLoad ran.
1927
- */
1928
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
1929
- this.isSheetModal = isSheetModal;
1930
- if (isSheetModal) {
2056
+ if (this.isSheetModal) {
1931
2057
  this.initSheetGesture();
1932
2058
  }
1933
2059
  else if (hasCardModal) {
@@ -2004,6 +2130,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2004
2130
  if (this.currentBreakpoint !== breakpoint) {
2005
2131
  this.currentBreakpoint = breakpoint;
2006
2132
  this.ionBreakpointDidChange.emit({ breakpoint });
2133
+ // Update safe-area overrides based on new position
2134
+ this.updateSafeAreaOverrides();
2007
2135
  }
2008
2136
  });
2009
2137
  this.gesture = gesture;
@@ -2171,6 +2299,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2171
2299
  }
2172
2300
  this.cleanupViewTransitionListener();
2173
2301
  this.cleanupParentRemovalObserver();
2302
+ this.cleanupSafeAreaOverrides();
2174
2303
  this.cleanupChildRoutePassthrough();
2175
2304
  }
2176
2305
  this.currentBreakpoint = undefined;
@@ -2291,6 +2420,9 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2291
2420
  this.viewTransitionAnimation = transitionAnimation;
2292
2421
  transitionAnimation.play().then(() => {
2293
2422
  this.viewTransitionAnimation = undefined;
2423
+ // Wait for a layout pass after the transition so getBoundingClientRect()
2424
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
2425
+ raf(() => this.updateSafeAreaOverrides());
2294
2426
  // After orientation transition, recreate the swipe-to-close gesture
2295
2427
  // with updated animation that reflects the new presenting element state
2296
2428
  this.reinitSwipeToClose();
@@ -2417,6 +2549,109 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2417
2549
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2418
2550
  this.parentRemovalObserver = undefined;
2419
2551
  }
2552
+ /**
2553
+ * Creates the context object for safe-area utilities.
2554
+ */
2555
+ getSafeAreaContext() {
2556
+ return {
2557
+ isSheetModal: this.isSheetModal,
2558
+ isCardModal: this.presentingElement !== undefined && getIonMode(this) === 'ios',
2559
+ presentingElement: this.presentingElement,
2560
+ breakpoints: this.breakpoints,
2561
+ currentBreakpoint: this.currentBreakpoint,
2562
+ };
2563
+ }
2564
+ /**
2565
+ * Sets initial safe-area overrides before modal animation.
2566
+ * Called in present() before animation starts.
2567
+ */
2568
+ setInitialSafeAreaOverrides() {
2569
+ const context = this.getSafeAreaContext();
2570
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
2571
+ applySafeAreaOverrides(this.el, safeAreaConfig);
2572
+ }
2573
+ /**
2574
+ * Updates safe-area overrides during dynamic state changes.
2575
+ * Called after animations, during gestures, and on orientation changes.
2576
+ */
2577
+ updateSafeAreaOverrides() {
2578
+ const { wrapperEl, el } = this;
2579
+ 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
+ });
2591
+ return;
2592
+ }
2593
+ // Card modals have fixed safe-area requirements set by initial prediction.
2594
+ if (context.isCardModal)
2595
+ return;
2596
+ // wrapperEl is required for position-based detection below
2597
+ if (!wrapperEl)
2598
+ return;
2599
+ // Regular modals: use position-based detection to correctly handle both
2600
+ // fullscreen modals and centered dialogs with custom dimensions.
2601
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
2602
+ applySafeAreaOverrides(el, safeAreaConfig);
2603
+ }
2604
+ /**
2605
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
2606
+ * content from overlapping system navigation bar.
2607
+ */
2608
+ applyFullscreenSafeArea() {
2609
+ const { wrapperEl, el } = this;
2610
+ if (!wrapperEl)
2611
+ return;
2612
+ const context = this.getSafeAreaContext();
2613
+ if (context.isSheetModal || context.isCardModal)
2614
+ return;
2615
+ // Check for standard Ionic layout children (ion-content, ion-footer),
2616
+ // searching one level deep for wrapped components (e.g.,
2617
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
2618
+ let hasContent = false;
2619
+ let hasFooter = false;
2620
+ for (const child of Array.from(el.children)) {
2621
+ if (child.tagName === 'ION-CONTENT')
2622
+ hasContent = true;
2623
+ if (child.tagName === 'ION-FOOTER')
2624
+ hasFooter = true;
2625
+ for (const grandchild of Array.from(child.children)) {
2626
+ if (grandchild.tagName === 'ION-CONTENT')
2627
+ hasContent = true;
2628
+ if (grandchild.tagName === 'ION-FOOTER')
2629
+ hasFooter = true;
2630
+ }
2631
+ }
2632
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
2633
+ // but no ion-footer). Custom modals with raw HTML are fully
2634
+ // developer-controlled and should not be modified.
2635
+ if (!hasContent || hasFooter)
2636
+ return;
2637
+ // Reduce wrapper height by safe-area and add equivalent padding so the
2638
+ // total visual size stays the same but the flex content area shrinks.
2639
+ // Using height + padding instead of box-sizing: border-box avoids
2640
+ // breaking custom modals that set --border-width (border-box would
2641
+ // include the border inside the height, changing the layout).
2642
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
2643
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2644
+ }
2645
+ /**
2646
+ * Clears all safe-area overrides and padding from wrapper.
2647
+ */
2648
+ cleanupSafeAreaOverrides() {
2649
+ clearSafeAreaOverrides(this.el);
2650
+ if (this.wrapperEl) {
2651
+ this.wrapperEl.style.removeProperty('height');
2652
+ this.wrapperEl.style.removeProperty('padding-bottom');
2653
+ }
2654
+ }
2420
2655
  render() {
2421
2656
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
2422
2657
  const showHandle = handle !== false && isSheetModal;
@@ -2424,20 +2659,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2424
2659
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2425
2660
  const isHandleCycle = handleBehavior === 'cycle';
2426
2661
  const isSheetModalWithHandle = isSheetModal && showHandle;
2427
- return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2662
+ return (h(Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
2428
2663
  // Allow the modal to be navigable when the handle is focusable
2429
2664
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2430
2665
  zIndex: `${20000 + this.overlayIndex}`,
2431
- }, 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: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
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',
2432
2667
  /*
2433
2668
  role and aria-modal must be used on the
2434
2669
  same element. They must also be set inside the
2435
2670
  shadow DOM otherwise ion-button will not be highlighted
2436
2671
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2437
2672
  */
2438
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
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",
2439
2674
  // Prevents the handle from receiving keyboard focus when it does not cycle
2440
- 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: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
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 }))));
2441
2676
  }
2442
2677
  get el() { return this; }
2443
2678
  static get watchers() { return {