@ionic/core 8.7.17-dev.11767717752.14fe98a4 → 8.7.17-dev.11767891829.1a63afa3

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.
@@ -4,6 +4,7 @@
4
4
  'use strict';
5
5
 
6
6
  var index$3 = require('./index-D6Wc6v08.js');
7
+ var index = require('./index-DkNv4J_i.js');
7
8
  var index$2 = require('./index-CO6eryBo.js');
8
9
  var frameworkDelegate = require('./framework-delegate-DMJRBuDi.js');
9
10
  var helpers = require('./helpers-DrTqNghc.js');
@@ -17,7 +18,6 @@ var keyboard = require('./keyboard-hHzlEQpk.js');
17
18
  var animation = require('./animation-Bt3H9L1C.js');
18
19
  var cubicBezier = require('./cubic-bezier-DAjy1V-e.js');
19
20
  var index$1 = require('./index-CAvQ7Tka.js');
20
- var index = require('./index-DkNv4J_i.js');
21
21
  require('./hardware-back-button-VCK4V3mG.js');
22
22
  require('./gesture-controller-dtqlP_q4.js');
23
23
  require('./keyboard-UuAS4D_9.js');
@@ -1505,6 +1505,8 @@ const Modal = class {
1505
1505
  this.gestureAnimationDismissing = false;
1506
1506
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1507
1507
  this.skipSafeAreaCoordinateDetection = false;
1508
+ // Track previous safe-area state to avoid redundant DOM writes
1509
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1508
1510
  this.presented = false;
1509
1511
  /** @internal */
1510
1512
  this.hasController = false;
@@ -1695,7 +1697,8 @@ const Modal = class {
1695
1697
  }
1696
1698
  }
1697
1699
  onWindowResize() {
1698
- // Update safe-area overrides for all modal types on resize
1700
+ // Invalidate safe-area cache on resize (device rotation may change values)
1701
+ this.cachedSafeAreas = undefined;
1699
1702
  this.updateSafeAreaOverrides();
1700
1703
  // Only handle view transition for iOS card modals when no custom animations are provided
1701
1704
  if (ionicGlobal.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -1720,6 +1723,8 @@ const Modal = class {
1720
1723
  this.triggerController.removeClickListener();
1721
1724
  this.cleanupViewTransitionListener();
1722
1725
  this.cleanupParentRemovalObserver();
1726
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
1727
+ this.resetSafeAreaState();
1723
1728
  }
1724
1729
  componentWillLoad() {
1725
1730
  var _a;
@@ -2154,8 +2159,28 @@ const Modal = class {
2154
2159
  */
2155
2160
  applyFullscreenSafeArea() {
2156
2161
  this.skipSafeAreaCoordinateDetection = true;
2162
+ this.updateFooterPadding();
2163
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2164
+ // Use subtree:true to support wrapped footers in framework components
2165
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
2166
+ if (!this.footerObserver && index.win !== undefined && 'MutationObserver' in index.win) {
2167
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2168
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2169
+ }
2170
+ }
2171
+ /**
2172
+ * Updates wrapper padding based on footer presence.
2173
+ * Called initially and when footer is dynamically added/removed.
2174
+ */
2175
+ updateFooterPadding() {
2176
+ if (!this.wrapperEl)
2177
+ return;
2157
2178
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2158
- if (!hasFooter && this.wrapperEl) {
2179
+ if (hasFooter) {
2180
+ this.wrapperEl.style.removeProperty('padding-bottom');
2181
+ this.wrapperEl.style.removeProperty('box-sizing');
2182
+ }
2183
+ else {
2159
2184
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2160
2185
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2161
2186
  }
@@ -2171,23 +2196,52 @@ const Modal = class {
2171
2196
  style.setProperty('--ion-safe-area-left', '0px');
2172
2197
  style.setProperty('--ion-safe-area-right', '0px');
2173
2198
  }
2199
+ /**
2200
+ * Resets all safe-area related state and styles.
2201
+ * Called during dismiss and disconnectedCallback to ensure clean state
2202
+ * for re-presentation of inline modals.
2203
+ */
2204
+ resetSafeAreaState() {
2205
+ var _a;
2206
+ this.skipSafeAreaCoordinateDetection = false;
2207
+ this.cachedSafeAreas = undefined;
2208
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2209
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2210
+ this.footerObserver = undefined;
2211
+ // Clear wrapper styles that may have been set for safe-area handling
2212
+ if (this.wrapperEl) {
2213
+ this.wrapperEl.style.removeProperty('padding-bottom');
2214
+ this.wrapperEl.style.removeProperty('box-sizing');
2215
+ }
2216
+ // Clear safe-area CSS variable overrides
2217
+ const style = this.el.style;
2218
+ style.removeProperty('--ion-safe-area-top');
2219
+ style.removeProperty('--ion-safe-area-bottom');
2220
+ style.removeProperty('--ion-safe-area-left');
2221
+ style.removeProperty('--ion-safe-area-right');
2222
+ }
2174
2223
  /**
2175
2224
  * Gets the root safe-area values from the document element.
2176
- * These represent the actual device safe areas before any overlay overrides.
2225
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2177
2226
  */
2178
- getRootSafeAreaValues() {
2179
- const rootStyle = getComputedStyle(document.documentElement);
2180
- return {
2181
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2182
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2183
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2184
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2185
- };
2227
+ getSafeAreaValues() {
2228
+ if (!this.cachedSafeAreas) {
2229
+ const rootStyle = getComputedStyle(document.documentElement);
2230
+ this.cachedSafeAreas = {
2231
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2232
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2233
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2234
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2235
+ };
2236
+ }
2237
+ return this.cachedSafeAreas;
2186
2238
  }
2187
2239
  /**
2188
2240
  * Updates safe-area CSS variable overrides based on whether the modal
2189
2241
  * extends into each safe-area region. Called after animation
2190
2242
  * and during gestures to handle dynamic position changes.
2243
+ *
2244
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2191
2245
  */
2192
2246
  updateSafeAreaOverrides() {
2193
2247
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2198,20 +2252,34 @@ const Modal = class {
2198
2252
  return;
2199
2253
  }
2200
2254
  const rect = wrapper.getBoundingClientRect();
2201
- const safeAreas = this.getRootSafeAreaValues();
2255
+ const safeAreas = this.getSafeAreaValues();
2202
2256
  const extendsIntoTop = rect.top < safeAreas.top;
2203
2257
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2204
2258
  const extendsIntoLeft = rect.left < safeAreas.left;
2205
2259
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2260
+ // Only update DOM when state actually changes
2261
+ const prev = this.prevSafeAreaState;
2206
2262
  const style = this.el.style;
2207
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2208
- extendsIntoBottom
2209
- ? style.removeProperty('--ion-safe-area-bottom')
2210
- : style.setProperty('--ion-safe-area-bottom', '0px');
2211
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2212
- extendsIntoRight
2213
- ? style.removeProperty('--ion-safe-area-right')
2214
- : style.setProperty('--ion-safe-area-right', '0px');
2263
+ if (extendsIntoTop !== prev.top) {
2264
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2265
+ prev.top = extendsIntoTop;
2266
+ }
2267
+ if (extendsIntoBottom !== prev.bottom) {
2268
+ extendsIntoBottom
2269
+ ? style.removeProperty('--ion-safe-area-bottom')
2270
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2271
+ prev.bottom = extendsIntoBottom;
2272
+ }
2273
+ if (extendsIntoLeft !== prev.left) {
2274
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2275
+ prev.left = extendsIntoLeft;
2276
+ }
2277
+ if (extendsIntoRight !== prev.right) {
2278
+ extendsIntoRight
2279
+ ? style.removeProperty('--ion-safe-area-right')
2280
+ : style.setProperty('--ion-safe-area-right', '0px');
2281
+ prev.right = extendsIntoRight;
2282
+ }
2215
2283
  }
2216
2284
  sheetOnDismiss() {
2217
2285
  /**
@@ -2305,8 +2373,8 @@ const Modal = class {
2305
2373
  }
2306
2374
  this.currentBreakpoint = undefined;
2307
2375
  this.animation = undefined;
2308
- // Reset safe-area detection flag for potential re-presentation
2309
- this.skipSafeAreaCoordinateDetection = false;
2376
+ // Reset safe-area state for potential re-presentation
2377
+ this.resetSafeAreaState();
2310
2378
  unlock();
2311
2379
  return dismissed;
2312
2380
  }
@@ -2556,20 +2624,20 @@ const Modal = class {
2556
2624
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2557
2625
  const isHandleCycle = handleBehavior === 'cycle';
2558
2626
  const isSheetModalWithHandle = isSheetModal && showHandle;
2559
- return (index$3.h(index$3.Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2627
+ return (index$3.h(index$3.Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
2560
2628
  // Allow the modal to be navigable when the handle is focusable
2561
2629
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2562
2630
  zIndex: `${20000 + this.overlayIndex}`,
2563
- }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
2631
+ }, 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: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
2564
2632
  /*
2565
2633
  role and aria-modal must be used on the
2566
2634
  same element. They must also be set inside the
2567
2635
  shadow DOM otherwise ion-button will not be highlighted
2568
2636
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2569
2637
  */
2570
- 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: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
2638
+ 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: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
2571
2639
  // Prevents the handle from receiving keyboard focus when it does not cycle
2572
- 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
2640
+ 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: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
2573
2641
  }
2574
2642
  get el() { return index$3.getElement(this); }
2575
2643
  static get watchers() { return {
@@ -948,7 +948,7 @@ const mdEnterAnimation = (baseEl, opts) => {
948
948
  };
949
949
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
950
950
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
951
- const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
951
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
952
952
  /**
953
953
  * Safe area CSS variable adjustments.
954
954
  * When the popover is positioned near an edge, we add the corresponding
@@ -957,14 +957,23 @@ const mdEnterAnimation = (baseEl, opts) => {
957
957
  */
958
958
  const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
959
959
  const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
960
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
961
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
960
962
  let topValue = `${top}px`;
961
963
  let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
964
+ let leftValue = `${left}px`;
962
965
  if (checkSafeAreaTop) {
963
966
  topValue = `${top}px${safeAreaTop}`;
964
967
  }
965
968
  if (checkSafeAreaBottom && bottomValue !== undefined) {
966
969
  bottomValue = `${bottom}px${safeAreaBottom}`;
967
970
  }
971
+ if (checkSafeAreaLeft) {
972
+ leftValue = `${left}px${safeAreaLeft}`;
973
+ }
974
+ if (checkSafeAreaRight) {
975
+ leftValue = `${left}px${safeAreaRight}`;
976
+ }
968
977
  const baseAnimation = animation.createAnimation();
969
978
  const backdropAnimation = animation.createAnimation();
970
979
  const wrapperAnimation = animation.createAnimation();
@@ -982,7 +991,7 @@ const mdEnterAnimation = (baseEl, opts) => {
982
991
  .addElement(contentEl)
983
992
  .beforeStyles({
984
993
  top: `calc(${topValue} + var(--offset-y, 0px))`,
985
- left: `calc(${left}px + var(--offset-x, 0px))`,
994
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
986
995
  'transform-origin': `${originY} ${originX}`,
987
996
  })
988
997
  .beforeAddWrite(() => {
@@ -2,6 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { Build, Host, forceUpdate, h, readTask } from "@stencil/core";
5
+ import { win } from "../../utils/browser/index";
5
6
  import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from "../../utils/helpers";
6
7
  import { isPlatform } from "../../utils/platform";
7
8
  import { isRTL } from "../../utils/rtl/index";
@@ -85,7 +86,11 @@ export class Content {
85
86
  this.inheritedAttributes = inheritAriaAttributes(this.el);
86
87
  }
87
88
  connectedCallback() {
88
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
89
+ var _a;
90
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
91
+ this.isMainContent =
92
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
93
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
89
94
  // Detect sibling header/footer for safe-area handling
90
95
  this.detectSiblingElements();
91
96
  /**
@@ -118,21 +123,55 @@ export class Content {
118
123
  * bubbles, we can catch any instances of child tab bars loading by listening
119
124
  * on IonTabs.
120
125
  */
121
- this.tabsLoadCallback = () => this.resize();
126
+ this.tabsLoadCallback = () => {
127
+ this.resize();
128
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
129
+ this.updateSiblingDetection();
130
+ forceUpdate(this);
131
+ };
122
132
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
123
133
  }
124
134
  }
125
135
  }
126
136
  /**
127
- * Detects sibling ion-header and ion-footer elements.
128
- * When these are absent, content needs to handle safe-area padding directly.
137
+ * Detects sibling ion-header and ion-footer elements and sets up
138
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
129
139
  */
130
140
  detectSiblingElements() {
131
- // Check parent element for sibling header/footer.
141
+ this.updateSiblingDetection();
142
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
143
+ const parent = this.el.parentElement;
144
+ if (parent && !this.parentMutationObserver && win !== undefined && 'MutationObserver' in win) {
145
+ this.parentMutationObserver = new MutationObserver(() => {
146
+ const prevHasHeader = this.hasHeader;
147
+ const prevHasFooter = this.hasFooter;
148
+ this.updateSiblingDetection();
149
+ // Only trigger re-render if header/footer detection actually changed
150
+ if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
151
+ forceUpdate(this);
152
+ }
153
+ });
154
+ this.parentMutationObserver.observe(parent, { childList: true });
155
+ }
156
+ }
157
+ /**
158
+ * Updates hasHeader/hasFooter based on current DOM state.
159
+ * Checks both direct siblings and elements wrapped in custom components
160
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
161
+ */
162
+ updateSiblingDetection() {
132
163
  const parent = this.el.parentElement;
133
164
  if (parent) {
165
+ // First check for direct ion-header/ion-footer siblings
134
166
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
135
167
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
168
+ // If not found, check if any sibling contains them (wrapped components)
169
+ if (!this.hasHeader) {
170
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
171
+ }
172
+ if (!this.hasFooter) {
173
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
174
+ }
136
175
  }
137
176
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
138
177
  if (!this.hasFooter) {
@@ -142,8 +181,28 @@ export class Content {
142
181
  }
143
182
  }
144
183
  }
184
+ /**
185
+ * Checks if any sibling element of ion-content contains the specified element.
186
+ * Only searches one level deep to avoid finding elements in nested pages.
187
+ */
188
+ siblingContainsElement(parent, tagName) {
189
+ for (const sibling of parent.children) {
190
+ // Skip ion-content itself
191
+ if (sibling === this.el)
192
+ continue;
193
+ // Check if this sibling contains the target element as an immediate child
194
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
195
+ return true;
196
+ }
197
+ }
198
+ return false;
199
+ }
145
200
  disconnectedCallback() {
201
+ var _a;
146
202
  this.onScrollEnd();
203
+ // Clean up mutation observer to prevent memory leaks
204
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
205
+ this.parentMutationObserver = undefined;
147
206
  if (hasLazyBuild(this.el)) {
148
207
  /**
149
208
  * The event listener and tabs caches need to
@@ -396,7 +455,7 @@ export class Content {
396
455
  const forceOverscroll = this.shouldForceOverscroll();
397
456
  const transitionShadow = mode === 'ios';
398
457
  this.resize();
399
- return (h(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
458
+ return (h(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
400
459
  [mode]: true,
401
460
  'content-sizing': hostContext('ion-popover', this.el),
402
461
  overscroll: forceOverscroll,
@@ -406,12 +465,12 @@ export class Content {
406
465
  }), style: {
407
466
  '--offset-top': `${this.cTop}px`,
408
467
  '--offset-bottom': `${this.cBottom}px`,
409
- } }, inheritedAttributes), h("div", { key: '75d7cf9315bc8dfb150c3b5bcc356a1f9c793b5a', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: 'f68bc5843c93ed6f32e998b80c5edc553c77a2d7', class: {
468
+ } }, inheritedAttributes), h("div", { key: 'b735ec68c18c0b99c3595bb194029830e6542cde', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: 'e76c00d030342d44ade6648c3f9e32ca990787ba', class: {
410
469
  'inner-scroll': true,
411
470
  'scroll-x': scrollX,
412
471
  'scroll-y': scrollY,
413
472
  overscroll: (scrollX || scrollY) && forceOverscroll,
414
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '8d4b2be00f036f6a24bfa65e6324ca715dd93b60' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
473
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '9049be4cea9b5da5ec1e1012248b05286fddeb7a' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
415
474
  }
416
475
  static get is() { return "ion-content"; }
417
476
  static get encapsulation() { return "shadow"; }
@@ -2,6 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { Host, h, writeTask } from "@stencil/core";
5
+ import { win } from "../../utils/browser/index";
5
6
  import { findIonContent, printIonContentErrorMsg } from "../../utils/content/index";
6
7
  import { CoreDelegate, attachComponent, detachComponent } from "../../utils/framework-delegate";
7
8
  import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from "../../utils/helpers";
@@ -44,6 +45,8 @@ export class Modal {
44
45
  this.gestureAnimationDismissing = false;
45
46
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
46
47
  this.skipSafeAreaCoordinateDetection = false;
48
+ // Track previous safe-area state to avoid redundant DOM writes
49
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
47
50
  this.presented = false;
48
51
  /** @internal */
49
52
  this.hasController = false;
@@ -234,7 +237,8 @@ export class Modal {
234
237
  }
235
238
  }
236
239
  onWindowResize() {
237
- // Update safe-area overrides for all modal types on resize
240
+ // Invalidate safe-area cache on resize (device rotation may change values)
241
+ this.cachedSafeAreas = undefined;
238
242
  this.updateSafeAreaOverrides();
239
243
  // Only handle view transition for iOS card modals when no custom animations are provided
240
244
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -259,6 +263,8 @@ export class Modal {
259
263
  this.triggerController.removeClickListener();
260
264
  this.cleanupViewTransitionListener();
261
265
  this.cleanupParentRemovalObserver();
266
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
267
+ this.resetSafeAreaState();
262
268
  }
263
269
  componentWillLoad() {
264
270
  var _a;
@@ -693,8 +699,28 @@ export class Modal {
693
699
  */
694
700
  applyFullscreenSafeArea() {
695
701
  this.skipSafeAreaCoordinateDetection = true;
702
+ this.updateFooterPadding();
703
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
704
+ // Use subtree:true to support wrapped footers in framework components
705
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
706
+ if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
707
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
708
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
709
+ }
710
+ }
711
+ /**
712
+ * Updates wrapper padding based on footer presence.
713
+ * Called initially and when footer is dynamically added/removed.
714
+ */
715
+ updateFooterPadding() {
716
+ if (!this.wrapperEl)
717
+ return;
696
718
  const hasFooter = this.el.querySelector('ion-footer') !== null;
697
- if (!hasFooter && this.wrapperEl) {
719
+ if (hasFooter) {
720
+ this.wrapperEl.style.removeProperty('padding-bottom');
721
+ this.wrapperEl.style.removeProperty('box-sizing');
722
+ }
723
+ else {
698
724
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
699
725
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
700
726
  }
@@ -710,23 +736,52 @@ export class Modal {
710
736
  style.setProperty('--ion-safe-area-left', '0px');
711
737
  style.setProperty('--ion-safe-area-right', '0px');
712
738
  }
739
+ /**
740
+ * Resets all safe-area related state and styles.
741
+ * Called during dismiss and disconnectedCallback to ensure clean state
742
+ * for re-presentation of inline modals.
743
+ */
744
+ resetSafeAreaState() {
745
+ var _a;
746
+ this.skipSafeAreaCoordinateDetection = false;
747
+ this.cachedSafeAreas = undefined;
748
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
749
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
750
+ this.footerObserver = undefined;
751
+ // Clear wrapper styles that may have been set for safe-area handling
752
+ if (this.wrapperEl) {
753
+ this.wrapperEl.style.removeProperty('padding-bottom');
754
+ this.wrapperEl.style.removeProperty('box-sizing');
755
+ }
756
+ // Clear safe-area CSS variable overrides
757
+ const style = this.el.style;
758
+ style.removeProperty('--ion-safe-area-top');
759
+ style.removeProperty('--ion-safe-area-bottom');
760
+ style.removeProperty('--ion-safe-area-left');
761
+ style.removeProperty('--ion-safe-area-right');
762
+ }
713
763
  /**
714
764
  * Gets the root safe-area values from the document element.
715
- * These represent the actual device safe areas before any overlay overrides.
765
+ * Uses cached values during gestures to avoid getComputedStyle calls.
716
766
  */
717
- getRootSafeAreaValues() {
718
- const rootStyle = getComputedStyle(document.documentElement);
719
- return {
720
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
721
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
722
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
723
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
724
- };
767
+ getSafeAreaValues() {
768
+ if (!this.cachedSafeAreas) {
769
+ const rootStyle = getComputedStyle(document.documentElement);
770
+ this.cachedSafeAreas = {
771
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
772
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
773
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
774
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
775
+ };
776
+ }
777
+ return this.cachedSafeAreas;
725
778
  }
726
779
  /**
727
780
  * Updates safe-area CSS variable overrides based on whether the modal
728
781
  * extends into each safe-area region. Called after animation
729
782
  * and during gestures to handle dynamic position changes.
783
+ *
784
+ * Optimized to avoid redundant DOM writes by tracking previous state.
730
785
  */
731
786
  updateSafeAreaOverrides() {
732
787
  if (this.skipSafeAreaCoordinateDetection) {
@@ -737,20 +792,34 @@ export class Modal {
737
792
  return;
738
793
  }
739
794
  const rect = wrapper.getBoundingClientRect();
740
- const safeAreas = this.getRootSafeAreaValues();
795
+ const safeAreas = this.getSafeAreaValues();
741
796
  const extendsIntoTop = rect.top < safeAreas.top;
742
797
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
743
798
  const extendsIntoLeft = rect.left < safeAreas.left;
744
799
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
800
+ // Only update DOM when state actually changes
801
+ const prev = this.prevSafeAreaState;
745
802
  const style = this.el.style;
746
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
747
- extendsIntoBottom
748
- ? style.removeProperty('--ion-safe-area-bottom')
749
- : style.setProperty('--ion-safe-area-bottom', '0px');
750
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
751
- extendsIntoRight
752
- ? style.removeProperty('--ion-safe-area-right')
753
- : style.setProperty('--ion-safe-area-right', '0px');
803
+ if (extendsIntoTop !== prev.top) {
804
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
805
+ prev.top = extendsIntoTop;
806
+ }
807
+ if (extendsIntoBottom !== prev.bottom) {
808
+ extendsIntoBottom
809
+ ? style.removeProperty('--ion-safe-area-bottom')
810
+ : style.setProperty('--ion-safe-area-bottom', '0px');
811
+ prev.bottom = extendsIntoBottom;
812
+ }
813
+ if (extendsIntoLeft !== prev.left) {
814
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
815
+ prev.left = extendsIntoLeft;
816
+ }
817
+ if (extendsIntoRight !== prev.right) {
818
+ extendsIntoRight
819
+ ? style.removeProperty('--ion-safe-area-right')
820
+ : style.setProperty('--ion-safe-area-right', '0px');
821
+ prev.right = extendsIntoRight;
822
+ }
754
823
  }
755
824
  sheetOnDismiss() {
756
825
  /**
@@ -844,8 +913,8 @@ export class Modal {
844
913
  }
845
914
  this.currentBreakpoint = undefined;
846
915
  this.animation = undefined;
847
- // Reset safe-area detection flag for potential re-presentation
848
- this.skipSafeAreaCoordinateDetection = false;
916
+ // Reset safe-area state for potential re-presentation
917
+ this.resetSafeAreaState();
849
918
  unlock();
850
919
  return dismissed;
851
920
  }
@@ -1103,20 +1172,20 @@ export class Modal {
1103
1172
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1104
1173
  const isHandleCycle = handleBehavior === 'cycle';
1105
1174
  const isSheetModalWithHandle = isSheetModal && showHandle;
1106
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
1175
+ return (h(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
1107
1176
  // Allow the modal to be navigable when the handle is focusable
1108
1177
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1109
1178
  zIndex: `${20000 + this.overlayIndex}`,
1110
- }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), h("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
1179
+ }, 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: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), h("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
1111
1180
  /*
1112
1181
  role and aria-modal must be used on the
1113
1182
  same element. They must also be set inside the
1114
1183
  shadow DOM otherwise ion-button will not be highlighted
1115
1184
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1116
1185
  */
1117
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
1186
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
1118
1187
  // Prevents the handle from receiving keyboard focus when it does not cycle
1119
- 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
1188
+ 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: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
1120
1189
  }
1121
1190
  static get is() { return "ion-modal"; }
1122
1191
  static get encapsulation() { return "shadow"; }
@@ -28,7 +28,7 @@ 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, checkSafeAreaTop, checkSafeAreaBottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
31
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
32
32
  /**
33
33
  * Safe area CSS variable adjustments.
34
34
  * When the popover is positioned near an edge, we add the corresponding
@@ -37,14 +37,23 @@ export const mdEnterAnimation = (baseEl, opts) => {
37
37
  */
38
38
  const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
39
39
  const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
40
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
41
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
40
42
  let topValue = `${top}px`;
41
43
  let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
44
+ let leftValue = `${left}px`;
42
45
  if (checkSafeAreaTop) {
43
46
  topValue = `${top}px${safeAreaTop}`;
44
47
  }
45
48
  if (checkSafeAreaBottom && bottomValue !== undefined) {
46
49
  bottomValue = `${bottom}px${safeAreaBottom}`;
47
50
  }
51
+ if (checkSafeAreaLeft) {
52
+ leftValue = `${left}px${safeAreaLeft}`;
53
+ }
54
+ if (checkSafeAreaRight) {
55
+ leftValue = `${left}px${safeAreaRight}`;
56
+ }
48
57
  const baseAnimation = createAnimation();
49
58
  const backdropAnimation = createAnimation();
50
59
  const wrapperAnimation = createAnimation();
@@ -62,7 +71,7 @@ export const mdEnterAnimation = (baseEl, opts) => {
62
71
  .addElement(contentEl)
63
72
  .beforeStyles({
64
73
  top: `calc(${topValue} + var(--offset-y, 0px))`,
65
- left: `calc(${left}px + var(--offset-x, 0px))`,
74
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
66
75
  'transform-origin': `${originY} ${originX}`,
67
76
  })
68
77
  .beforeAddWrite(() => {