@ionic/core 8.7.17-dev.11767717752.14fe98a4 → 8.7.17-dev.11767796972.148e4bc4

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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { proxyCustomElement, HTMLElement, createEvent, Build, readTask, forceUpdate, h, Host } from '@stencil/core/internal/client';
4
+ import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, readTask, h, Host } from '@stencil/core/internal/client';
5
5
  import { i as inheritAriaAttributes, k as hasLazyBuild, c as componentOnReady } from './helpers.js';
6
6
  import { b as getIonMode, a as isPlatform } from './ionic-global.js';
7
7
  import { i as isRTL } from './dir.js';
@@ -127,15 +127,39 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
127
127
  }
128
128
  }
129
129
  /**
130
- * Detects sibling ion-header and ion-footer elements.
131
- * When these are absent, content needs to handle safe-area padding directly.
130
+ * Detects sibling ion-header and ion-footer elements and sets up
131
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
132
132
  */
133
133
  detectSiblingElements() {
134
- // Check parent element for sibling header/footer.
134
+ this.updateSiblingDetection();
135
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
136
+ const parent = this.el.parentElement;
137
+ if (parent && !this.parentMutationObserver) {
138
+ this.parentMutationObserver = new MutationObserver(() => {
139
+ this.updateSiblingDetection();
140
+ forceUpdate(this);
141
+ });
142
+ this.parentMutationObserver.observe(parent, { childList: true });
143
+ }
144
+ }
145
+ /**
146
+ * Updates hasHeader/hasFooter based on current DOM state.
147
+ * Checks both direct siblings and elements wrapped in custom components
148
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
149
+ */
150
+ updateSiblingDetection() {
135
151
  const parent = this.el.parentElement;
136
152
  if (parent) {
153
+ // First check for direct ion-header/ion-footer siblings
137
154
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
138
155
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
156
+ // If not found, check if any sibling contains them (wrapped components)
157
+ if (!this.hasHeader) {
158
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
159
+ }
160
+ if (!this.hasFooter) {
161
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
162
+ }
139
163
  }
140
164
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
141
165
  if (!this.hasFooter) {
@@ -145,8 +169,28 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
145
169
  }
146
170
  }
147
171
  }
172
+ /**
173
+ * Checks if any sibling element of ion-content contains the specified element.
174
+ * Only searches one level deep to avoid finding elements in nested pages.
175
+ */
176
+ siblingContainsElement(parent, tagName) {
177
+ for (const sibling of parent.children) {
178
+ // Skip ion-content itself
179
+ if (sibling === this.el)
180
+ continue;
181
+ // Check if this sibling contains the target element as an immediate child
182
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
183
+ return true;
184
+ }
185
+ }
186
+ return false;
187
+ }
148
188
  disconnectedCallback() {
189
+ var _a;
149
190
  this.onScrollEnd();
191
+ // Clean up mutation observer to prevent memory leaks
192
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
193
+ this.parentMutationObserver = undefined;
150
194
  if (hasLazyBuild(this.el)) {
151
195
  /**
152
196
  * The event listener and tabs caches need to
@@ -399,7 +443,7 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
399
443
  const forceOverscroll = this.shouldForceOverscroll();
400
444
  const transitionShadow = mode === 'ios';
401
445
  this.resize();
402
- return (h(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
446
+ return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
403
447
  [mode]: true,
404
448
  'content-sizing': hostContext('ion-popover', this.el),
405
449
  overscroll: forceOverscroll,
@@ -409,12 +453,12 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
409
453
  }), style: {
410
454
  '--offset-top': `${this.cTop}px`,
411
455
  '--offset-bottom': `${this.cBottom}px`,
412
- } }, 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: {
456
+ } }, inheritedAttributes), h("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
413
457
  'inner-scroll': true,
414
458
  'scroll-x': scrollX,
415
459
  'scroll-y': scrollY,
416
460
  overscroll: (scrollX || scrollY) && forceOverscroll,
417
- }, 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));
461
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
418
462
  }
419
463
  get el() { return this; }
420
464
  static get style() { return contentCss; }
@@ -1506,6 +1506,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1506
1506
  this.gestureAnimationDismissing = false;
1507
1507
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1508
1508
  this.skipSafeAreaCoordinateDetection = false;
1509
+ // Track previous safe-area state to avoid redundant DOM writes
1510
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1509
1511
  this.presented = false;
1510
1512
  /** @internal */
1511
1513
  this.hasController = false;
@@ -1696,7 +1698,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1696
1698
  }
1697
1699
  }
1698
1700
  onWindowResize() {
1699
- // Update safe-area overrides for all modal types on resize
1701
+ // Invalidate safe-area cache on resize (device rotation may change values)
1702
+ this.cachedSafeAreas = undefined;
1700
1703
  this.updateSafeAreaOverrides();
1701
1704
  // Only handle view transition for iOS card modals when no custom animations are provided
1702
1705
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -2155,8 +2158,26 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2155
2158
  */
2156
2159
  applyFullscreenSafeArea() {
2157
2160
  this.skipSafeAreaCoordinateDetection = true;
2161
+ this.updateFooterPadding();
2162
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2163
+ if (!this.footerObserver) {
2164
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2165
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Updates wrapper padding based on footer presence.
2170
+ * Called initially and when footer is dynamically added/removed.
2171
+ */
2172
+ updateFooterPadding() {
2173
+ if (!this.wrapperEl)
2174
+ return;
2158
2175
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2159
- if (!hasFooter && this.wrapperEl) {
2176
+ if (hasFooter) {
2177
+ this.wrapperEl.style.removeProperty('padding-bottom');
2178
+ this.wrapperEl.style.removeProperty('box-sizing');
2179
+ }
2180
+ else {
2160
2181
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2161
2182
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2162
2183
  }
@@ -2174,21 +2195,26 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2174
2195
  }
2175
2196
  /**
2176
2197
  * Gets the root safe-area values from the document element.
2177
- * These represent the actual device safe areas before any overlay overrides.
2198
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2178
2199
  */
2179
- getRootSafeAreaValues() {
2180
- const rootStyle = getComputedStyle(document.documentElement);
2181
- return {
2182
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2183
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2184
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2185
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2186
- };
2200
+ getSafeAreaValues() {
2201
+ if (!this.cachedSafeAreas) {
2202
+ const rootStyle = getComputedStyle(document.documentElement);
2203
+ this.cachedSafeAreas = {
2204
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2205
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2206
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2207
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2208
+ };
2209
+ }
2210
+ return this.cachedSafeAreas;
2187
2211
  }
2188
2212
  /**
2189
2213
  * Updates safe-area CSS variable overrides based on whether the modal
2190
2214
  * extends into each safe-area region. Called after animation
2191
2215
  * and during gestures to handle dynamic position changes.
2216
+ *
2217
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2192
2218
  */
2193
2219
  updateSafeAreaOverrides() {
2194
2220
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2199,20 +2225,34 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2199
2225
  return;
2200
2226
  }
2201
2227
  const rect = wrapper.getBoundingClientRect();
2202
- const safeAreas = this.getRootSafeAreaValues();
2228
+ const safeAreas = this.getSafeAreaValues();
2203
2229
  const extendsIntoTop = rect.top < safeAreas.top;
2204
2230
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2205
2231
  const extendsIntoLeft = rect.left < safeAreas.left;
2206
2232
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2233
+ // Only update DOM when state actually changes
2234
+ const prev = this.prevSafeAreaState;
2207
2235
  const style = this.el.style;
2208
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2209
- extendsIntoBottom
2210
- ? style.removeProperty('--ion-safe-area-bottom')
2211
- : style.setProperty('--ion-safe-area-bottom', '0px');
2212
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2213
- extendsIntoRight
2214
- ? style.removeProperty('--ion-safe-area-right')
2215
- : style.setProperty('--ion-safe-area-right', '0px');
2236
+ if (extendsIntoTop !== prev.top) {
2237
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2238
+ prev.top = extendsIntoTop;
2239
+ }
2240
+ if (extendsIntoBottom !== prev.bottom) {
2241
+ extendsIntoBottom
2242
+ ? style.removeProperty('--ion-safe-area-bottom')
2243
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2244
+ prev.bottom = extendsIntoBottom;
2245
+ }
2246
+ if (extendsIntoLeft !== prev.left) {
2247
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2248
+ prev.left = extendsIntoLeft;
2249
+ }
2250
+ if (extendsIntoRight !== prev.right) {
2251
+ extendsIntoRight
2252
+ ? style.removeProperty('--ion-safe-area-right')
2253
+ : style.setProperty('--ion-safe-area-right', '0px');
2254
+ prev.right = extendsIntoRight;
2255
+ }
2216
2256
  }
2217
2257
  sheetOnDismiss() {
2218
2258
  /**
@@ -2244,7 +2284,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2244
2284
  * For example, `cancel` or `backdrop`.
2245
2285
  */
2246
2286
  async dismiss(data, role) {
2247
- var _a;
2287
+ var _a, _b;
2248
2288
  if (this.gestureAnimationDismissing && role !== GESTURE) {
2249
2289
  return false;
2250
2290
  }
@@ -2306,8 +2346,23 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2306
2346
  }
2307
2347
  this.currentBreakpoint = undefined;
2308
2348
  this.animation = undefined;
2309
- // Reset safe-area detection flag for potential re-presentation
2349
+ // Reset safe-area state for potential re-presentation
2310
2350
  this.skipSafeAreaCoordinateDetection = false;
2351
+ this.cachedSafeAreas = undefined;
2352
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2353
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
2354
+ this.footerObserver = undefined;
2355
+ // Clear styles that may have been set for safe-area handling
2356
+ if (this.wrapperEl) {
2357
+ this.wrapperEl.style.removeProperty('padding-bottom');
2358
+ this.wrapperEl.style.removeProperty('box-sizing');
2359
+ }
2360
+ // Clear safe-area CSS variable overrides
2361
+ const style = this.el.style;
2362
+ style.removeProperty('--ion-safe-area-top');
2363
+ style.removeProperty('--ion-safe-area-bottom');
2364
+ style.removeProperty('--ion-safe-area-left');
2365
+ style.removeProperty('--ion-safe-area-right');
2311
2366
  unlock();
2312
2367
  return dismissed;
2313
2368
  }
@@ -2557,20 +2612,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2557
2612
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2558
2613
  const isHandleCycle = handleBehavior === 'cycle';
2559
2614
  const isSheetModalWithHandle = isSheetModal && showHandle;
2560
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2615
+ return (h(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
2561
2616
  // Allow the modal to be navigable when the handle is focusable
2562
2617
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2563
2618
  zIndex: `${20000 + this.overlayIndex}`,
2564
- }, 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',
2619
+ }, 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: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), h("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
2565
2620
  /*
2566
2621
  role and aria-modal must be used on the
2567
2622
  same element. They must also be set inside the
2568
2623
  shadow DOM otherwise ion-button will not be highlighted
2569
2624
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2570
2625
  */
2571
- 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",
2626
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
2572
2627
  // Prevents the handle from receiving keyboard focus when it does not cycle
2573
- 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 }))));
2628
+ 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: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
2574
2629
  }
2575
2630
  get el() { return this; }
2576
2631
  static get watchers() { return {
@@ -270,15 +270,39 @@ const Content = class {
270
270
  }
271
271
  }
272
272
  /**
273
- * Detects sibling ion-header and ion-footer elements.
274
- * When these are absent, content needs to handle safe-area padding directly.
273
+ * Detects sibling ion-header and ion-footer elements and sets up
274
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
275
275
  */
276
276
  detectSiblingElements() {
277
- // Check parent element for sibling header/footer.
277
+ this.updateSiblingDetection();
278
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
279
+ const parent = this.el.parentElement;
280
+ if (parent && !this.parentMutationObserver) {
281
+ this.parentMutationObserver = new MutationObserver(() => {
282
+ this.updateSiblingDetection();
283
+ index.forceUpdate(this);
284
+ });
285
+ this.parentMutationObserver.observe(parent, { childList: true });
286
+ }
287
+ }
288
+ /**
289
+ * Updates hasHeader/hasFooter based on current DOM state.
290
+ * Checks both direct siblings and elements wrapped in custom components
291
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
292
+ */
293
+ updateSiblingDetection() {
278
294
  const parent = this.el.parentElement;
279
295
  if (parent) {
296
+ // First check for direct ion-header/ion-footer siblings
280
297
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
281
298
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
299
+ // If not found, check if any sibling contains them (wrapped components)
300
+ if (!this.hasHeader) {
301
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
302
+ }
303
+ if (!this.hasFooter) {
304
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
305
+ }
282
306
  }
283
307
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
284
308
  if (!this.hasFooter) {
@@ -288,8 +312,28 @@ const Content = class {
288
312
  }
289
313
  }
290
314
  }
315
+ /**
316
+ * Checks if any sibling element of ion-content contains the specified element.
317
+ * Only searches one level deep to avoid finding elements in nested pages.
318
+ */
319
+ siblingContainsElement(parent, tagName) {
320
+ for (const sibling of parent.children) {
321
+ // Skip ion-content itself
322
+ if (sibling === this.el)
323
+ continue;
324
+ // Check if this sibling contains the target element as an immediate child
325
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
326
+ return true;
327
+ }
328
+ }
329
+ return false;
330
+ }
291
331
  disconnectedCallback() {
332
+ var _a;
292
333
  this.onScrollEnd();
334
+ // Clean up mutation observer to prevent memory leaks
335
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
336
+ this.parentMutationObserver = undefined;
293
337
  if (helpers.hasLazyBuild(this.el)) {
294
338
  /**
295
339
  * The event listener and tabs caches need to
@@ -542,7 +586,7 @@ const Content = class {
542
586
  const forceOverscroll = this.shouldForceOverscroll();
543
587
  const transitionShadow = mode === 'ios';
544
588
  this.resize();
545
- return (index.h(index.Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
589
+ return (index.h(index.Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
546
590
  [mode]: true,
547
591
  'content-sizing': theme.hostContext('ion-popover', this.el),
548
592
  overscroll: forceOverscroll,
@@ -552,12 +596,12 @@ const Content = class {
552
596
  }), style: {
553
597
  '--offset-top': `${this.cTop}px`,
554
598
  '--offset-bottom': `${this.cBottom}px`,
555
- } }, inheritedAttributes), index.h("div", { key: '75d7cf9315bc8dfb150c3b5bcc356a1f9c793b5a', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: 'f68bc5843c93ed6f32e998b80c5edc553c77a2d7', class: {
599
+ } }, inheritedAttributes), index.h("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
556
600
  'inner-scroll': true,
557
601
  'scroll-x': scrollX,
558
602
  'scroll-y': scrollY,
559
603
  overscroll: (scrollX || scrollY) && forceOverscroll,
560
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '8d4b2be00f036f6a24bfa65e6324ca715dd93b60' })), transitionShadow ? (index.h("div", { class: "transition-effect" }, index.h("div", { class: "transition-cover" }), index.h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? index.h("slot", { name: "fixed" }) : null));
604
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (index.h("div", { class: "transition-effect" }, index.h("div", { class: "transition-cover" }), index.h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? index.h("slot", { name: "fixed" }) : null));
561
605
  }
562
606
  get el() { return index.getElement(this); }
563
607
  };
@@ -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) {
@@ -2154,8 +2157,26 @@ const Modal = class {
2154
2157
  */
2155
2158
  applyFullscreenSafeArea() {
2156
2159
  this.skipSafeAreaCoordinateDetection = true;
2160
+ this.updateFooterPadding();
2161
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2162
+ if (!this.footerObserver) {
2163
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2164
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2165
+ }
2166
+ }
2167
+ /**
2168
+ * Updates wrapper padding based on footer presence.
2169
+ * Called initially and when footer is dynamically added/removed.
2170
+ */
2171
+ updateFooterPadding() {
2172
+ if (!this.wrapperEl)
2173
+ return;
2157
2174
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2158
- if (!hasFooter && this.wrapperEl) {
2175
+ if (hasFooter) {
2176
+ this.wrapperEl.style.removeProperty('padding-bottom');
2177
+ this.wrapperEl.style.removeProperty('box-sizing');
2178
+ }
2179
+ else {
2159
2180
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2160
2181
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2161
2182
  }
@@ -2173,21 +2194,26 @@ const Modal = class {
2173
2194
  }
2174
2195
  /**
2175
2196
  * Gets the root safe-area values from the document element.
2176
- * These represent the actual device safe areas before any overlay overrides.
2197
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2177
2198
  */
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
- };
2199
+ getSafeAreaValues() {
2200
+ if (!this.cachedSafeAreas) {
2201
+ const rootStyle = getComputedStyle(document.documentElement);
2202
+ this.cachedSafeAreas = {
2203
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2204
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2205
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2206
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2207
+ };
2208
+ }
2209
+ return this.cachedSafeAreas;
2186
2210
  }
2187
2211
  /**
2188
2212
  * Updates safe-area CSS variable overrides based on whether the modal
2189
2213
  * extends into each safe-area region. Called after animation
2190
2214
  * and during gestures to handle dynamic position changes.
2215
+ *
2216
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2191
2217
  */
2192
2218
  updateSafeAreaOverrides() {
2193
2219
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2198,20 +2224,34 @@ const Modal = class {
2198
2224
  return;
2199
2225
  }
2200
2226
  const rect = wrapper.getBoundingClientRect();
2201
- const safeAreas = this.getRootSafeAreaValues();
2227
+ const safeAreas = this.getSafeAreaValues();
2202
2228
  const extendsIntoTop = rect.top < safeAreas.top;
2203
2229
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2204
2230
  const extendsIntoLeft = rect.left < safeAreas.left;
2205
2231
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2232
+ // Only update DOM when state actually changes
2233
+ const prev = this.prevSafeAreaState;
2206
2234
  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');
2235
+ if (extendsIntoTop !== prev.top) {
2236
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2237
+ prev.top = extendsIntoTop;
2238
+ }
2239
+ if (extendsIntoBottom !== prev.bottom) {
2240
+ extendsIntoBottom
2241
+ ? style.removeProperty('--ion-safe-area-bottom')
2242
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2243
+ prev.bottom = extendsIntoBottom;
2244
+ }
2245
+ if (extendsIntoLeft !== prev.left) {
2246
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2247
+ prev.left = extendsIntoLeft;
2248
+ }
2249
+ if (extendsIntoRight !== prev.right) {
2250
+ extendsIntoRight
2251
+ ? style.removeProperty('--ion-safe-area-right')
2252
+ : style.setProperty('--ion-safe-area-right', '0px');
2253
+ prev.right = extendsIntoRight;
2254
+ }
2215
2255
  }
2216
2256
  sheetOnDismiss() {
2217
2257
  /**
@@ -2243,7 +2283,7 @@ const Modal = class {
2243
2283
  * For example, `cancel` or `backdrop`.
2244
2284
  */
2245
2285
  async dismiss(data, role) {
2246
- var _a;
2286
+ var _a, _b;
2247
2287
  if (this.gestureAnimationDismissing && role !== overlays.GESTURE) {
2248
2288
  return false;
2249
2289
  }
@@ -2305,8 +2345,23 @@ const Modal = class {
2305
2345
  }
2306
2346
  this.currentBreakpoint = undefined;
2307
2347
  this.animation = undefined;
2308
- // Reset safe-area detection flag for potential re-presentation
2348
+ // Reset safe-area state for potential re-presentation
2309
2349
  this.skipSafeAreaCoordinateDetection = false;
2350
+ this.cachedSafeAreas = undefined;
2351
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2352
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
2353
+ this.footerObserver = undefined;
2354
+ // Clear styles that may have been set for safe-area handling
2355
+ if (this.wrapperEl) {
2356
+ this.wrapperEl.style.removeProperty('padding-bottom');
2357
+ this.wrapperEl.style.removeProperty('box-sizing');
2358
+ }
2359
+ // Clear safe-area CSS variable overrides
2360
+ const style = this.el.style;
2361
+ style.removeProperty('--ion-safe-area-top');
2362
+ style.removeProperty('--ion-safe-area-bottom');
2363
+ style.removeProperty('--ion-safe-area-left');
2364
+ style.removeProperty('--ion-safe-area-right');
2310
2365
  unlock();
2311
2366
  return dismissed;
2312
2367
  }
@@ -2556,20 +2611,20 @@ const Modal = class {
2556
2611
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2557
2612
  const isHandleCycle = handleBehavior === 'cycle';
2558
2613
  const isSheetModalWithHandle = isSheetModal && showHandle;
2559
- return (index$3.h(index$3.Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2614
+ return (index$3.h(index$3.Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
2560
2615
  // Allow the modal to be navigable when the handle is focusable
2561
2616
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2562
2617
  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',
2618
+ }, 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: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
2564
2619
  /*
2565
2620
  role and aria-modal must be used on the
2566
2621
  same element. They must also be set inside the
2567
2622
  shadow DOM otherwise ion-button will not be highlighted
2568
2623
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2569
2624
  */
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",
2625
+ 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: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
2571
2626
  // 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 }))));
2627
+ 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: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
2573
2628
  }
2574
2629
  get el() { return index$3.getElement(this); }
2575
2630
  static get watchers() { return {