@ionic/core 8.7.18-dev.11771365900.125bf49c → 8.7.18-dev.11772217280.122eed89

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 (90) hide show
  1. package/components/ion-app.js +17 -12
  2. package/components/ion-checkbox.js +1 -1
  3. package/components/ion-content.js +1 -1
  4. package/components/ion-datetime.js +7 -4
  5. package/components/ion-fab-list.js +10 -3
  6. package/components/ion-img.js +11 -2
  7. package/components/ion-label.js +1 -1
  8. package/components/ion-modal.js +1 -1
  9. package/components/ion-popover.js +1 -1
  10. package/components/ion-radio-group.js +1 -1
  11. package/components/ion-searchbar.js +17 -6
  12. package/components/ion-select-modal.js +1 -1
  13. package/components/ion-select-popover.js +1 -1
  14. package/components/ion-select.js +8 -8
  15. package/components/{p-CJrGvZ1Z.js → p-51c2oSfC.js} +3 -3
  16. package/components/{p-BnpDIASH.js → p-BANeLH_G.js} +2 -2
  17. package/components/{p-D3KEyQ8e.js → p-BRLve2EP.js} +3 -3
  18. package/components/{p-DRHwNkVo.js → p-C6dkQWq2.js} +152 -30
  19. package/components/{p-CUfY9DTD.js → p-Cdm3dKWf.js} +8 -3
  20. package/components/{p-DyFBFrV7.js → p-D--b4514.js} +4 -4
  21. package/components/{p-DPm73mCi.js → p-DGOrLIyS.js} +7 -3
  22. package/components/{p-IHgAGzvW.js → p-iyo0Toaz.js} +326 -17
  23. package/dist/cjs/ion-app_8.cjs.entry.js +22 -13
  24. package/dist/cjs/ion-checkbox.cjs.entry.js +3 -3
  25. package/dist/cjs/ion-datetime_3.cjs.entry.js +6 -3
  26. package/dist/cjs/ion-fab_3.cjs.entry.js +10 -3
  27. package/dist/cjs/ion-img.cjs.entry.js +11 -2
  28. package/dist/cjs/ion-item_8.cjs.entry.js +8 -3
  29. package/dist/cjs/ion-modal.cjs.entry.js +326 -17
  30. package/dist/cjs/ion-popover.cjs.entry.js +152 -30
  31. package/dist/cjs/ion-radio_2.cjs.entry.js +2 -2
  32. package/dist/cjs/ion-searchbar.cjs.entry.js +17 -6
  33. package/dist/collection/components/app/app.js +15 -10
  34. package/dist/collection/components/checkbox/checkbox.js +4 -4
  35. package/dist/collection/components/content/content.js +7 -3
  36. package/dist/collection/components/datetime/datetime.js +6 -3
  37. package/dist/collection/components/fab-list/fab-list.js +10 -3
  38. package/dist/collection/components/img/img.js +11 -2
  39. package/dist/collection/components/label/label.js +8 -3
  40. package/dist/collection/components/modal/modal.ios.css +6 -5
  41. package/dist/collection/components/modal/modal.js +164 -15
  42. package/dist/collection/components/modal/modal.md.css +6 -5
  43. package/dist/collection/components/modal/safe-area-utils.js +165 -0
  44. package/dist/collection/components/popover/animations/ios.enter.js +33 -8
  45. package/dist/collection/components/popover/animations/md.enter.js +26 -6
  46. package/dist/collection/components/popover/utils.js +96 -19
  47. package/dist/collection/components/radio-group/radio-group.ios.css +0 -4
  48. package/dist/collection/components/radio-group/radio-group.md.css +0 -4
  49. package/dist/collection/components/searchbar/searchbar.js +17 -6
  50. package/dist/docs.json +1 -1
  51. package/dist/esm/ion-app_8.entry.js +24 -15
  52. package/dist/esm/ion-checkbox.entry.js +4 -4
  53. package/dist/esm/ion-datetime_3.entry.js +6 -3
  54. package/dist/esm/ion-fab_3.entry.js +10 -3
  55. package/dist/esm/ion-img.entry.js +11 -2
  56. package/dist/esm/ion-item_8.entry.js +8 -3
  57. package/dist/esm/ion-modal.entry.js +326 -17
  58. package/dist/esm/ion-popover.entry.js +152 -30
  59. package/dist/esm/ion-radio_2.entry.js +2 -2
  60. package/dist/esm/ion-searchbar.entry.js +17 -6
  61. package/dist/ionic/ionic.esm.js +1 -1
  62. package/dist/ionic/{p-72af946b.entry.js → p-2ac254dc.entry.js} +1 -1
  63. package/dist/ionic/p-7cad388a.entry.js +4 -0
  64. package/dist/ionic/p-7d218555.entry.js +4 -0
  65. package/dist/ionic/p-8303e3fc.entry.js +4 -0
  66. package/dist/ionic/p-915b3e3d.entry.js +4 -0
  67. package/dist/ionic/{p-eaf3f5ff.entry.js → p-b0ebf9f8.entry.js} +1 -1
  68. package/dist/ionic/p-e4c57008.entry.js +4 -0
  69. package/dist/ionic/p-e5d6d0f0.entry.js +4 -0
  70. package/dist/ionic/p-f1dfee25.entry.js +4 -0
  71. package/dist/ionic/{p-6444c606.entry.js → p-f9f7b9db.entry.js} +1 -1
  72. package/dist/types/components/app/app.d.ts +3 -0
  73. package/dist/types/components/datetime/datetime.d.ts +1 -0
  74. package/dist/types/components/fab-list/fab-list.d.ts +2 -0
  75. package/dist/types/components/img/img.d.ts +2 -0
  76. package/dist/types/components/label/label.d.ts +2 -0
  77. package/dist/types/components/modal/modal.d.ts +35 -0
  78. package/dist/types/components/modal/safe-area-utils.d.ts +66 -0
  79. package/dist/types/components/popover/utils.d.ts +23 -1
  80. package/dist/types/components/searchbar/searchbar.d.ts +3 -0
  81. package/hydrate/index.js +556 -73
  82. package/hydrate/index.mjs +556 -73
  83. package/package.json +1 -1
  84. package/dist/ionic/p-01e27965.entry.js +0 -4
  85. package/dist/ionic/p-3ec563c1.entry.js +0 -4
  86. package/dist/ionic/p-42db6404.entry.js +0 -4
  87. package/dist/ionic/p-46d74291.entry.js +0 -4
  88. package/dist/ionic/p-bc293244.entry.js +0 -4
  89. package/dist/ionic/p-d1f54e28.entry.js +0 -4
  90. package/dist/ionic/p-e338f669.entry.js +0 -4
@@ -20,11 +20,16 @@ export class Label {
20
20
  }
21
21
  componentDidLoad() {
22
22
  if (this.noAnimate) {
23
- setTimeout(() => {
23
+ this.loadTimeout = setTimeout(() => {
24
24
  this.noAnimate = false;
25
25
  }, 1000);
26
26
  }
27
27
  }
28
+ disconnectedCallback() {
29
+ if (this.loadTimeout) {
30
+ clearTimeout(this.loadTimeout);
31
+ }
32
+ }
28
33
  colorChanged() {
29
34
  this.emitColor();
30
35
  }
@@ -53,13 +58,13 @@ export class Label {
53
58
  render() {
54
59
  const position = this.position;
55
60
  const mode = getIonMode(this);
56
- return (h(Host, { key: '684ddfa719e34d743489ff05a0df067829257c73', class: createColorClasses(this.color, {
61
+ return (h(Host, { key: 'd603670c442213c28737b0c052c094705b8c84ef', class: createColorClasses(this.color, {
57
62
  [mode]: true,
58
63
  'in-item-color': hostContext('ion-item.ion-color', this.el),
59
64
  [`label-${position}`]: position !== undefined,
60
65
  [`label-no-animate`]: this.noAnimate,
61
66
  'label-rtl': document.dir === 'rtl',
62
- }) }, h("slot", { key: '4aace3e2b36250e832c44f63bea21116d77220fb' })));
67
+ }) }, h("slot", { key: 'a8c3aed240a20ee5856c423662878481a1e6d4cf' })));
63
68
  }
64
69
  static get is() { return "ion-label"; }
65
70
  static get encapsulation() { return "scoped"; }
@@ -135,10 +135,6 @@ ion-backdrop {
135
135
  :host {
136
136
  --width: 600px;
137
137
  --height: 500px;
138
- --ion-safe-area-top: 0px;
139
- --ion-safe-area-bottom: 0px;
140
- --ion-safe-area-right: 0px;
141
- --ion-safe-area-left: 0px;
142
138
  }
143
139
  }
144
140
  @media only screen and (min-width: 768px) and (min-height: 768px) {
@@ -190,9 +186,14 @@ ion-backdrop {
190
186
  /**
191
187
  * Ensure that the sheet modal does not
192
188
  * completely cover the content.
189
+ *
190
+ * --ion-modal-offset-top is an internal property set by modal.tsx
191
+ * with the resolved root safe-area-top pixel value. This decouples
192
+ * the height calculation from --ion-safe-area-top (which is zeroed
193
+ * for sheet modals to prevent header double-padding).
193
194
  */
194
195
  :host(.modal-sheet) {
195
- --height: calc(100% - (var(--ion-safe-area-top) + 10px));
196
+ --height: calc(100% - (var(--ion-modal-offset-top, 0px) + 10px));
196
197
  }
197
198
 
198
199
  :host(.modal-sheet) .modal-wrapper,
@@ -21,6 +21,7 @@ import { mdEnterAnimation } from "./animations/md.enter";
21
21
  import { mdLeaveAnimation } from "./animations/md.leave";
22
22
  import { createSheetGesture } from "./gestures/sheet";
23
23
  import { createSwipeToCloseGesture, SwipeToCloseDefaults } from "./gestures/swipe-to-close";
24
+ import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, } from "./safe-area-utils";
24
25
  import { setCardStatusBarDark, setCardStatusBarDefault } from "./utils";
25
26
  // TODO(FW-2832): types
26
27
  /**
@@ -232,13 +233,31 @@ export class Modal {
232
233
  }
233
234
  }
234
235
  onWindowResize() {
235
- // Only handle resize for iOS card modals when no custom animations are provided
236
- if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
236
+ if (!this.presented)
237
237
  return;
238
- }
239
238
  clearTimeout(this.resizeTimeout);
240
239
  this.resizeTimeout = setTimeout(() => {
241
- this.handleViewTransition();
240
+ const context = this.getSafeAreaContext();
241
+ // iOS card modals: handle portrait/landscape view transitions
242
+ if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
243
+ this.handleViewTransition();
244
+ }
245
+ // Sheet modals: re-compute the internal offset property since safe-area
246
+ // values may change on device rotation (e.g., portrait notch vs landscape).
247
+ if (context.isSheetModal) {
248
+ this.updateSheetOffsetTop();
249
+ }
250
+ // Regular (non-sheet, non-card) modals: update safe-area overrides
251
+ // since the viewport may have crossed the centered-dialog breakpoint.
252
+ if (!context.isSheetModal && !context.isCardModal) {
253
+ this.updateSafeAreaOverrides();
254
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
255
+ if (this.wrapperEl) {
256
+ this.wrapperEl.style.removeProperty('height');
257
+ this.wrapperEl.style.removeProperty('padding-bottom');
258
+ }
259
+ this.applyFullscreenSafeArea();
260
+ }
242
261
  }, 50); // Debounce to avoid excessive calls during active resizing
243
262
  }
244
263
  breakpointsChanged(breakpoints) {
@@ -255,6 +274,9 @@ export class Modal {
255
274
  this.triggerController.removeClickListener();
256
275
  this.cleanupViewTransitionListener();
257
276
  this.cleanupParentRemovalObserver();
277
+ // Also called in dismiss() — intentional dual cleanup covers both
278
+ // dismiss-then-remove and direct DOM removal without dismiss.
279
+ this.cleanupSafeAreaOverrides();
258
280
  }
259
281
  componentWillLoad() {
260
282
  var _a;
@@ -415,6 +437,11 @@ export class Modal {
415
437
  await waitForMount();
416
438
  }
417
439
  writeTask(() => this.el.classList.add('show-modal'));
440
+ // Recalculate isSheetModal before safe-area setup because framework
441
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
442
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
443
+ // Set initial safe-area overrides before animation
444
+ this.setInitialSafeAreaOverrides();
418
445
  const hasCardModal = presentingElement !== undefined;
419
446
  /**
420
447
  * We need to change the status bar at the
@@ -432,6 +459,10 @@ export class Modal {
432
459
  backdropBreakpoint: this.backdropBreakpoint,
433
460
  expandToScroll: this.expandToScroll,
434
461
  });
462
+ // Update safe-area based on actual position after animation
463
+ this.updateSafeAreaOverrides();
464
+ // Apply fullscreen safe-area padding if needed
465
+ this.applyFullscreenSafeArea();
435
466
  /* tslint:disable-next-line */
436
467
  if (typeof window !== 'undefined') {
437
468
  /**
@@ -463,13 +494,7 @@ export class Modal {
463
494
  };
464
495
  window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
465
496
  }
466
- /**
467
- * Recalculate isSheetModal because framework bindings (e.g., Angular)
468
- * may not have been applied when componentWillLoad ran.
469
- */
470
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
471
- this.isSheetModal = isSheetModal;
472
- if (isSheetModal) {
497
+ if (this.isSheetModal) {
473
498
  this.initSheetGesture();
474
499
  }
475
500
  else if (hasCardModal) {
@@ -659,6 +684,9 @@ export class Modal {
659
684
  if (this.gestureAnimationDismissing && role !== GESTURE) {
660
685
  return false;
661
686
  }
687
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
688
+ clearTimeout(this.resizeTimeout);
689
+ this.resizeTimeout = undefined;
662
690
  /**
663
691
  * Because the canDismiss check below is async,
664
692
  * we need to claim a lock before the check happens,
@@ -713,6 +741,7 @@ export class Modal {
713
741
  }
714
742
  this.cleanupViewTransitionListener();
715
743
  this.cleanupParentRemovalObserver();
744
+ this.cleanupSafeAreaOverrides();
716
745
  this.cleanupChildRoutePassthrough();
717
746
  }
718
747
  this.currentBreakpoint = undefined;
@@ -841,6 +870,9 @@ export class Modal {
841
870
  this.viewTransitionAnimation = transitionAnimation;
842
871
  transitionAnimation.play().then(() => {
843
872
  this.viewTransitionAnimation = undefined;
873
+ // Wait for a layout pass after the transition so getBoundingClientRect()
874
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
875
+ raf(() => this.updateSafeAreaOverrides());
844
876
  // After orientation transition, recreate the swipe-to-close gesture
845
877
  // with updated animation that reflects the new presenting element state
846
878
  this.reinitSwipeToClose();
@@ -967,6 +999,123 @@ export class Modal {
967
999
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
968
1000
  this.parentRemovalObserver = undefined;
969
1001
  }
1002
+ /**
1003
+ * Creates the context object for safe-area utilities.
1004
+ */
1005
+ getSafeAreaContext() {
1006
+ return {
1007
+ isSheetModal: this.isSheetModal,
1008
+ isCardModal: this.presentingElement !== undefined && getIonMode(this) === 'ios',
1009
+ presentingElement: this.presentingElement,
1010
+ breakpoints: this.breakpoints,
1011
+ currentBreakpoint: this.currentBreakpoint,
1012
+ };
1013
+ }
1014
+ /**
1015
+ * Sets initial safe-area overrides before modal animation.
1016
+ * Called in present() before animation starts.
1017
+ *
1018
+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
1019
+ * (an internal property) instead of --ion-safe-area-top. We resolve the
1020
+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
1021
+ * the height calculation from --ion-safe-area-top (which is zeroed for
1022
+ * sheets to prevent header content from getting double-offset padding).
1023
+ */
1024
+ setInitialSafeAreaOverrides() {
1025
+ const context = this.getSafeAreaContext();
1026
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
1027
+ applySafeAreaOverrides(this.el, safeAreaConfig);
1028
+ // Set the internal offset property with the resolved root safe-area-top value
1029
+ if (context.isSheetModal) {
1030
+ this.updateSheetOffsetTop();
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Resolves the current root --ion-safe-area-top value and sets the
1035
+ * internal --ion-modal-offset-top property on the host element.
1036
+ * Called on present and on resize (e.g., device rotation changes safe-area).
1037
+ */
1038
+ updateSheetOffsetTop() {
1039
+ const safeAreaTop = getRootSafeAreaTop();
1040
+ this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
1041
+ }
1042
+ /**
1043
+ * Updates safe-area overrides during dynamic state changes.
1044
+ * Called after animations, during gestures, and on orientation changes.
1045
+ */
1046
+ updateSafeAreaOverrides() {
1047
+ const { wrapperEl, el } = this;
1048
+ const context = this.getSafeAreaContext();
1049
+ // Sheet modals: safe-area is fully determined at presentation time
1050
+ // (top is always 0px, height is frozen). Nothing to update.
1051
+ if (context.isSheetModal)
1052
+ return;
1053
+ // Card modals have fixed safe-area requirements set by initial prediction.
1054
+ if (context.isCardModal)
1055
+ return;
1056
+ // wrapperEl is required for position-based detection below
1057
+ if (!wrapperEl)
1058
+ return;
1059
+ // Regular modals: use position-based detection to correctly handle both
1060
+ // fullscreen modals and centered dialogs with custom dimensions.
1061
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
1062
+ applySafeAreaOverrides(el, safeAreaConfig);
1063
+ }
1064
+ /**
1065
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
1066
+ * content from overlapping system navigation bar.
1067
+ */
1068
+ applyFullscreenSafeArea() {
1069
+ const { wrapperEl, el } = this;
1070
+ if (!wrapperEl)
1071
+ return;
1072
+ const context = this.getSafeAreaContext();
1073
+ if (context.isSheetModal || context.isCardModal)
1074
+ return;
1075
+ // Check for standard Ionic layout children (ion-content, ion-footer),
1076
+ // searching one level deep for wrapped components (e.g.,
1077
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
1078
+ // Note: uses a manual loop instead of querySelector(':scope > ...') because
1079
+ // Stencil's mock-doc (used in spec tests) does not support :scope.
1080
+ let hasContent = false;
1081
+ let hasFooter = false;
1082
+ for (const child of Array.from(el.children)) {
1083
+ if (child.tagName === 'ION-CONTENT')
1084
+ hasContent = true;
1085
+ if (child.tagName === 'ION-FOOTER')
1086
+ hasFooter = true;
1087
+ for (const grandchild of Array.from(child.children)) {
1088
+ if (grandchild.tagName === 'ION-CONTENT')
1089
+ hasContent = true;
1090
+ if (grandchild.tagName === 'ION-FOOTER')
1091
+ hasFooter = true;
1092
+ }
1093
+ }
1094
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
1095
+ // but no ion-footer). Custom modals with raw HTML are fully
1096
+ // developer-controlled and should not be modified.
1097
+ if (!hasContent || hasFooter)
1098
+ return;
1099
+ // Reduce wrapper height by safe-area and add equivalent padding so the
1100
+ // total visual size stays the same but the flex content area shrinks.
1101
+ // Using height + padding instead of box-sizing: border-box avoids
1102
+ // breaking custom modals that set --border-width (border-box would
1103
+ // include the border inside the height, changing the layout).
1104
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
1105
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
1106
+ }
1107
+ /**
1108
+ * Clears all safe-area overrides and padding from wrapper.
1109
+ */
1110
+ cleanupSafeAreaOverrides() {
1111
+ clearSafeAreaOverrides(this.el);
1112
+ // Remove internal sheet offset property
1113
+ this.el.style.removeProperty('--ion-modal-offset-top');
1114
+ if (this.wrapperEl) {
1115
+ this.wrapperEl.style.removeProperty('height');
1116
+ this.wrapperEl.style.removeProperty('padding-bottom');
1117
+ }
1118
+ }
970
1119
  render() {
971
1120
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
972
1121
  const showHandle = handle !== false && isSheetModal;
@@ -974,20 +1123,20 @@ export class Modal {
974
1123
  const isCardModal = presentingElement !== undefined && mode === 'ios';
975
1124
  const isHandleCycle = handleBehavior === 'cycle';
976
1125
  const isSheetModalWithHandle = isSheetModal && showHandle;
977
- return (h(Host, Object.assign({ key: '978ba9cc81464b2d9b4caaf36285d5f7bf7e568d', "no-router": true,
1126
+ return (h(Host, Object.assign({ key: '952cbd52d1fc737332a23d523b02d5fa60abf30a', "no-router": true,
978
1127
  // Allow the modal to be navigable when the handle is focusable
979
1128
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
980
1129
  zIndex: `${20000 + this.overlayIndex}`,
981
- }, 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: '9f95566d8e0c06bb9607f514484848c250f35f1f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '91ff26d4f3ab4367f3a2940f51c6a5a3d4c972c3', class: "modal-shadow" }), h("div", Object.assign({ key: '064b2dba13854505a4eb1f5e54296ac79cb7a2bd',
1130
+ }, 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: '66e3658cfcc2961d99ecc1174f26b1794ceb65e9', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '3d1b9b26daf87374e2029d0e0a7fc09b9257fd77', class: "modal-shadow" }), h("div", Object.assign({ key: 'e652c2b3eb66afa8367d94c3b14ba0e825877a82',
982
1131
  /*
983
1132
  role and aria-modal must be used on the
984
1133
  same element. They must also be set inside the
985
1134
  shadow DOM otherwise ion-button will not be highlighted
986
1135
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
987
1136
  */
988
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b579b08b114022032d078a527111456ceefc7b35', class: "modal-handle",
1137
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '41662183450e8ef2f7d96dac2bea5952bad8b7a5', class: "modal-handle",
989
1138
  // Prevents the handle from receiving keyboard focus when it does not cycle
990
- 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: 'dacea58475b72d9c0209934ff31f3be6b9ac8c85', onSlotchange: this.onSlotChange }))));
1139
+ 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: '4aac4e2928929b9e5e59b33c83395182105da529', onSlotchange: this.onSlotChange }))));
991
1140
  }
992
1141
  static get is() { return "ion-modal"; }
993
1142
  static get encapsulation() { return "shadow"; }
@@ -135,10 +135,6 @@ ion-backdrop {
135
135
  :host {
136
136
  --width: 600px;
137
137
  --height: 500px;
138
- --ion-safe-area-top: 0px;
139
- --ion-safe-area-bottom: 0px;
140
- --ion-safe-area-right: 0px;
141
- --ion-safe-area-left: 0px;
142
138
  }
143
139
  }
144
140
  @media only screen and (min-width: 768px) and (min-height: 768px) {
@@ -190,9 +186,14 @@ ion-backdrop {
190
186
  /**
191
187
  * Ensure that the sheet modal does not
192
188
  * completely cover the content.
189
+ *
190
+ * --ion-modal-offset-top is an internal property set by modal.tsx
191
+ * with the resolved root safe-area-top pixel value. This decouples
192
+ * the height calculation from --ion-safe-area-top (which is zeroed
193
+ * for sheet modals to prevent header double-padding).
193
194
  */
194
195
  :host(.modal-sheet) {
195
- --height: calc(100% - (var(--ion-safe-area-top) + 10px));
196
+ --height: calc(100% - (var(--ion-modal-offset-top, 0px) + 10px));
196
197
  }
197
198
 
198
199
  :host(.modal-sheet) .modal-wrapper,
@@ -0,0 +1,165 @@
1
+ /*!
2
+ * (C) Ionic http://ionicframework.com - MIT License
3
+ */
4
+ import { win } from "../../utils/browser/index";
5
+ import { raf } from "../../utils/helpers";
6
+ /**
7
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
8
+ * that trigger the centered dialog layout (non-fullscreen modal).
9
+ *
10
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
11
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
12
+ * that's the threshold where the modal transitions from fullscreen to centered
13
+ * dialog — the larger breakpoint only increases the dialog's height.
14
+ */
15
+ const MODAL_INSET_MIN_WIDTH = 768;
16
+ const MODAL_INSET_MIN_HEIGHT = 600;
17
+ const EDGE_THRESHOLD = 5;
18
+ /**
19
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
20
+ */
21
+ let cachedRootSafeAreaTop = null;
22
+ let cacheInvalidationScheduled = false;
23
+ /**
24
+ * Determines if the current viewport meets the CSS media query conditions
25
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
26
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
27
+ */
28
+ const isCenteredDialogViewport = () => {
29
+ if (!win)
30
+ return false;
31
+ return win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
32
+ .matches;
33
+ };
34
+ /**
35
+ * Resolves the current root --ion-safe-area-top value to pixels.
36
+ * Uses a temporary element because getComputedStyle on :root returns
37
+ * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
38
+ * rather than a resolved number.
39
+ *
40
+ * Results are cached for the current frame to avoid repeated reflows.
41
+ */
42
+ export const getRootSafeAreaTop = () => {
43
+ if (cachedRootSafeAreaTop !== null) {
44
+ return cachedRootSafeAreaTop;
45
+ }
46
+ const doc = win === null || win === void 0 ? void 0 : win.document;
47
+ if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
48
+ return 0;
49
+ }
50
+ const el = doc.createElement('div');
51
+ el.style.cssText =
52
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
53
+ doc.body.appendChild(el);
54
+ const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
55
+ el.remove();
56
+ cachedRootSafeAreaTop = value;
57
+ if (!cacheInvalidationScheduled) {
58
+ cacheInvalidationScheduled = true;
59
+ raf(() => {
60
+ cachedRootSafeAreaTop = null;
61
+ cacheInvalidationScheduled = false;
62
+ });
63
+ }
64
+ return value;
65
+ };
66
+ /**
67
+ * Returns the initial safe-area configuration based on modal type.
68
+ * This is called before animation starts and uses configuration-based prediction.
69
+ *
70
+ * @param context - Modal context information
71
+ * @returns SafeAreaConfig with initial safe-area values
72
+ */
73
+ export const getInitialSafeAreaConfig = (context) => {
74
+ const { isSheetModal, isCardModal } = context;
75
+ // Sheet modals always zero top safe-area. The sheet height offset from the
76
+ // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
77
+ // the resolved root value, so --ion-safe-area-top is never needed for
78
+ // height calculation. Keeping it at 0px prevents header content from
79
+ // getting double-offset padding.
80
+ if (isSheetModal) {
81
+ return {
82
+ top: '0px',
83
+ bottom: 'inherit',
84
+ left: '0px',
85
+ right: '0px',
86
+ };
87
+ }
88
+ // Card modals need safe-area for height calculation.
89
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
90
+ if (isCardModal) {
91
+ return {
92
+ top: 'inherit',
93
+ bottom: 'inherit',
94
+ left: '0px',
95
+ right: '0px',
96
+ };
97
+ }
98
+ // On viewports that meet the centered dialog media query breakpoints,
99
+ // regular modals render as centered dialogs (not fullscreen), so they
100
+ // don't touch any screen edges and don't need safe-area insets.
101
+ if (isCenteredDialogViewport()) {
102
+ return {
103
+ top: '0px',
104
+ bottom: '0px',
105
+ left: '0px',
106
+ right: '0px',
107
+ };
108
+ }
109
+ // Fullscreen modals on phone - inherit all safe areas
110
+ return {
111
+ top: 'inherit',
112
+ bottom: 'inherit',
113
+ left: 'inherit',
114
+ right: 'inherit',
115
+ };
116
+ };
117
+ /**
118
+ * Returns safe-area configuration based on actual modal position.
119
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
120
+ *
121
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
122
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
123
+ * prediction instead. Regular modals use coordinate detection which works reliably
124
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
125
+ * once a reliable detection mechanism is available.
126
+ *
127
+ * @param wrapperEl - The modal wrapper element to measure
128
+ * @returns SafeAreaConfig based on position
129
+ */
130
+ export const getPositionBasedSafeAreaConfig = (wrapperEl) => {
131
+ var _a, _b;
132
+ const rect = wrapperEl.getBoundingClientRect();
133
+ const vh = (_a = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _a !== void 0 ? _a : 0;
134
+ const vw = (_b = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _b !== void 0 ? _b : 0;
135
+ // Only apply safe-area to sides where modal overlaps with screen edge
136
+ return {
137
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
138
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
139
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
140
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
141
+ };
142
+ };
143
+ /**
144
+ * Applies safe-area CSS custom property overrides to the modal host element.
145
+ *
146
+ * @param hostEl - The modal host element (ion-modal)
147
+ * @param config - Safe-area configuration to apply
148
+ */
149
+ export const applySafeAreaOverrides = (hostEl, config) => {
150
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
151
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
152
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
153
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
154
+ };
155
+ /**
156
+ * Clears safe-area CSS custom property overrides from the modal host element.
157
+ *
158
+ * @param hostEl - The modal host element (ion-modal)
159
+ */
160
+ export const clearSafeAreaOverrides = (hostEl) => {
161
+ hostEl.style.removeProperty('--ion-safe-area-top');
162
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
163
+ hostEl.style.removeProperty('--ion-safe-area-left');
164
+ hostEl.style.removeProperty('--ion-safe-area-right');
165
+ };
@@ -3,8 +3,17 @@
3
3
  */
4
4
  import { createAnimation } from "../../../utils/animation/animation";
5
5
  import { getElementRoot } from "../../../utils/helpers";
6
- import { calculateWindowAdjustment, getArrowDimensions, getPopoverDimensions, getPopoverPosition, shouldShowArrow, } from "../utils";
6
+ import { calculateWindowAdjustment, getArrowDimensions, getPopoverDimensions, getPopoverPosition, getSafeAreaInsets, shouldShowArrow, } from "../utils";
7
7
  const POPOVER_IOS_BODY_PADDING = 5;
8
+ /**
9
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
10
+ * edges on devices without safe areas (e.g., older iPhones without notches).
11
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
12
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
13
+ * insets are read dynamically, this floor preserves the visual spacing when
14
+ * safe-area values are 0.
15
+ */
16
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
8
17
  /**
9
18
  * iOS Popover Enter Animation
10
19
  */
@@ -30,8 +39,16 @@ export const iosEnterAnimation = (baseEl, opts) => {
30
39
  };
31
40
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
32
41
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
33
- const margin = size === 'cover' ? 0 : 25;
34
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
42
+ const rawSafeArea = getSafeAreaInsets(doc);
43
+ const safeArea = size === 'cover'
44
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
45
+ : {
46
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
47
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
48
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
49
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
50
+ };
51
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, arrowTop, arrowLeft, addPopoverBottomClass, hideArrow, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
35
52
  const baseAnimation = createAnimation();
36
53
  const backdropAnimation = createAnimation();
37
54
  const contentAnimation = createAnimation();
@@ -62,10 +79,14 @@ export const iosEnterAnimation = (baseEl, opts) => {
62
79
  baseEl.classList.add('popover-bottom');
63
80
  }
64
81
  if (bottom !== undefined) {
65
- contentEl.style.setProperty('bottom', `${bottom}px`);
82
+ let bottomValue = `${bottom}px`;
83
+ if (checkSafeAreaBottom) {
84
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
85
+ }
86
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
66
87
  }
67
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
68
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
88
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
89
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
69
90
  let leftValue = `${left}px`;
70
91
  if (checkSafeAreaLeft) {
71
92
  leftValue = `${left}px${safeAreaLeft}`;
@@ -73,12 +94,16 @@ export const iosEnterAnimation = (baseEl, opts) => {
73
94
  if (checkSafeAreaRight) {
74
95
  leftValue = `${left}px${safeAreaRight}`;
75
96
  }
76
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
97
+ let topValue = `${top}px`;
98
+ if (checkSafeAreaTop) {
99
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
100
+ }
101
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
77
102
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
78
103
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
79
104
  if (arrowEl !== null) {
80
105
  const didAdjustBounds = results.top !== top || results.left !== left;
81
- const showArrow = shouldShowArrow(side, didAdjustBounds, ev, trigger);
106
+ const showArrow = !hideArrow && shouldShowArrow(side, didAdjustBounds, ev, trigger);
82
107
  if (showArrow) {
83
108
  arrowEl.style.setProperty('top', `calc(${arrowTop}px + var(--offset-y, 0))`);
84
109
  arrowEl.style.setProperty('left', `calc(${arrowLeft}px + var(--offset-x, 0))`);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { createAnimation } from "../../../utils/animation/animation";
5
5
  import { getElementRoot } from "../../../utils/helpers";
6
- import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition } from "../utils";
6
+ import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition, getSafeAreaInsets } from "../utils";
7
7
  const POPOVER_MD_BODY_PADDING = 12;
8
8
  /**
9
9
  * Md Popover Enter Animation
@@ -28,7 +28,23 @@ export const mdEnterAnimation = (baseEl, opts) => {
28
28
  };
29
29
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
30
30
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
31
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
31
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
32
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
33
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
34
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates);
35
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
36
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
37
+ let leftValue = `${left}px`;
38
+ if (checkSafeAreaLeft) {
39
+ leftValue = `${left}px${safeAreaLeftCalc}`;
40
+ }
41
+ if (checkSafeAreaRight) {
42
+ leftValue = `${left}px${safeAreaRightCalc}`;
43
+ }
44
+ let topValue = `${top}px`;
45
+ if (checkSafeAreaTop) {
46
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
47
+ }
32
48
  const baseAnimation = createAnimation();
33
49
  const backdropAnimation = createAnimation();
34
50
  const wrapperAnimation = createAnimation();
@@ -45,13 +61,17 @@ export const mdEnterAnimation = (baseEl, opts) => {
45
61
  contentAnimation
46
62
  .addElement(contentEl)
47
63
  .beforeStyles({
48
- top: `calc(${top}px + var(--offset-y, 0px))`,
49
- left: `calc(${left}px + var(--offset-x, 0px))`,
64
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
65
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
50
66
  'transform-origin': `${originY} ${originX}`,
51
67
  })
52
68
  .beforeAddWrite(() => {
53
69
  if (bottom !== undefined) {
54
- contentEl.style.setProperty('bottom', `${bottom}px`);
70
+ let bottomValue = `${bottom}px`;
71
+ if (checkSafeAreaBottom) {
72
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
73
+ }
74
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
55
75
  }
56
76
  })
57
77
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -63,7 +83,7 @@ export const mdEnterAnimation = (baseEl, opts) => {
63
83
  if (size === 'cover') {
64
84
  baseEl.style.setProperty('--width', `${contentWidth}px`);
65
85
  }
66
- if (originY === 'bottom') {
86
+ if (addPopoverBottomClass) {
67
87
  baseEl.classList.add('popover-bottom');
68
88
  }
69
89
  })