@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.
@@ -124,15 +124,39 @@ export class Content {
124
124
  }
125
125
  }
126
126
  /**
127
- * Detects sibling ion-header and ion-footer elements.
128
- * When these are absent, content needs to handle safe-area padding directly.
127
+ * Detects sibling ion-header and ion-footer elements and sets up
128
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
129
129
  */
130
130
  detectSiblingElements() {
131
- // Check parent element for sibling header/footer.
131
+ this.updateSiblingDetection();
132
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
133
+ const parent = this.el.parentElement;
134
+ if (parent && !this.parentMutationObserver) {
135
+ this.parentMutationObserver = new MutationObserver(() => {
136
+ this.updateSiblingDetection();
137
+ forceUpdate(this);
138
+ });
139
+ this.parentMutationObserver.observe(parent, { childList: true });
140
+ }
141
+ }
142
+ /**
143
+ * Updates hasHeader/hasFooter based on current DOM state.
144
+ * Checks both direct siblings and elements wrapped in custom components
145
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
146
+ */
147
+ updateSiblingDetection() {
132
148
  const parent = this.el.parentElement;
133
149
  if (parent) {
150
+ // First check for direct ion-header/ion-footer siblings
134
151
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
135
152
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
153
+ // If not found, check if any sibling contains them (wrapped components)
154
+ if (!this.hasHeader) {
155
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
156
+ }
157
+ if (!this.hasFooter) {
158
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
159
+ }
136
160
  }
137
161
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
138
162
  if (!this.hasFooter) {
@@ -142,8 +166,28 @@ export class Content {
142
166
  }
143
167
  }
144
168
  }
169
+ /**
170
+ * Checks if any sibling element of ion-content contains the specified element.
171
+ * Only searches one level deep to avoid finding elements in nested pages.
172
+ */
173
+ siblingContainsElement(parent, tagName) {
174
+ for (const sibling of parent.children) {
175
+ // Skip ion-content itself
176
+ if (sibling === this.el)
177
+ continue;
178
+ // Check if this sibling contains the target element as an immediate child
179
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
180
+ return true;
181
+ }
182
+ }
183
+ return false;
184
+ }
145
185
  disconnectedCallback() {
186
+ var _a;
146
187
  this.onScrollEnd();
188
+ // Clean up mutation observer to prevent memory leaks
189
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
190
+ this.parentMutationObserver = undefined;
147
191
  if (hasLazyBuild(this.el)) {
148
192
  /**
149
193
  * The event listener and tabs caches need to
@@ -396,7 +440,7 @@ export class Content {
396
440
  const forceOverscroll = this.shouldForceOverscroll();
397
441
  const transitionShadow = mode === 'ios';
398
442
  this.resize();
399
- return (h(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
443
+ return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
400
444
  [mode]: true,
401
445
  'content-sizing': hostContext('ion-popover', this.el),
402
446
  overscroll: forceOverscroll,
@@ -406,12 +450,12 @@ export class Content {
406
450
  }), style: {
407
451
  '--offset-top': `${this.cTop}px`,
408
452
  '--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: {
453
+ } }, 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: {
410
454
  'inner-scroll': true,
411
455
  'scroll-x': scrollX,
412
456
  'scroll-y': scrollY,
413
457
  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));
458
+ }, 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));
415
459
  }
416
460
  static get is() { return "ion-content"; }
417
461
  static get encapsulation() { return "shadow"; }
@@ -44,6 +44,8 @@ export class Modal {
44
44
  this.gestureAnimationDismissing = false;
45
45
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
46
46
  this.skipSafeAreaCoordinateDetection = false;
47
+ // Track previous safe-area state to avoid redundant DOM writes
48
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
47
49
  this.presented = false;
48
50
  /** @internal */
49
51
  this.hasController = false;
@@ -234,7 +236,8 @@ export class Modal {
234
236
  }
235
237
  }
236
238
  onWindowResize() {
237
- // Update safe-area overrides for all modal types on resize
239
+ // Invalidate safe-area cache on resize (device rotation may change values)
240
+ this.cachedSafeAreas = undefined;
238
241
  this.updateSafeAreaOverrides();
239
242
  // Only handle view transition for iOS card modals when no custom animations are provided
240
243
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -693,8 +696,26 @@ export class Modal {
693
696
  */
694
697
  applyFullscreenSafeArea() {
695
698
  this.skipSafeAreaCoordinateDetection = true;
699
+ this.updateFooterPadding();
700
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
701
+ if (!this.footerObserver) {
702
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
703
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
704
+ }
705
+ }
706
+ /**
707
+ * Updates wrapper padding based on footer presence.
708
+ * Called initially and when footer is dynamically added/removed.
709
+ */
710
+ updateFooterPadding() {
711
+ if (!this.wrapperEl)
712
+ return;
696
713
  const hasFooter = this.el.querySelector('ion-footer') !== null;
697
- if (!hasFooter && this.wrapperEl) {
714
+ if (hasFooter) {
715
+ this.wrapperEl.style.removeProperty('padding-bottom');
716
+ this.wrapperEl.style.removeProperty('box-sizing');
717
+ }
718
+ else {
698
719
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
699
720
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
700
721
  }
@@ -712,21 +733,26 @@ export class Modal {
712
733
  }
713
734
  /**
714
735
  * Gets the root safe-area values from the document element.
715
- * These represent the actual device safe areas before any overlay overrides.
736
+ * Uses cached values during gestures to avoid getComputedStyle calls.
716
737
  */
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
- };
738
+ getSafeAreaValues() {
739
+ if (!this.cachedSafeAreas) {
740
+ const rootStyle = getComputedStyle(document.documentElement);
741
+ this.cachedSafeAreas = {
742
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
743
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
744
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
745
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
746
+ };
747
+ }
748
+ return this.cachedSafeAreas;
725
749
  }
726
750
  /**
727
751
  * Updates safe-area CSS variable overrides based on whether the modal
728
752
  * extends into each safe-area region. Called after animation
729
753
  * and during gestures to handle dynamic position changes.
754
+ *
755
+ * Optimized to avoid redundant DOM writes by tracking previous state.
730
756
  */
731
757
  updateSafeAreaOverrides() {
732
758
  if (this.skipSafeAreaCoordinateDetection) {
@@ -737,20 +763,34 @@ export class Modal {
737
763
  return;
738
764
  }
739
765
  const rect = wrapper.getBoundingClientRect();
740
- const safeAreas = this.getRootSafeAreaValues();
766
+ const safeAreas = this.getSafeAreaValues();
741
767
  const extendsIntoTop = rect.top < safeAreas.top;
742
768
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
743
769
  const extendsIntoLeft = rect.left < safeAreas.left;
744
770
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
771
+ // Only update DOM when state actually changes
772
+ const prev = this.prevSafeAreaState;
745
773
  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');
774
+ if (extendsIntoTop !== prev.top) {
775
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
776
+ prev.top = extendsIntoTop;
777
+ }
778
+ if (extendsIntoBottom !== prev.bottom) {
779
+ extendsIntoBottom
780
+ ? style.removeProperty('--ion-safe-area-bottom')
781
+ : style.setProperty('--ion-safe-area-bottom', '0px');
782
+ prev.bottom = extendsIntoBottom;
783
+ }
784
+ if (extendsIntoLeft !== prev.left) {
785
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
786
+ prev.left = extendsIntoLeft;
787
+ }
788
+ if (extendsIntoRight !== prev.right) {
789
+ extendsIntoRight
790
+ ? style.removeProperty('--ion-safe-area-right')
791
+ : style.setProperty('--ion-safe-area-right', '0px');
792
+ prev.right = extendsIntoRight;
793
+ }
754
794
  }
755
795
  sheetOnDismiss() {
756
796
  /**
@@ -782,7 +822,7 @@ export class Modal {
782
822
  * For example, `cancel` or `backdrop`.
783
823
  */
784
824
  async dismiss(data, role) {
785
- var _a;
825
+ var _a, _b;
786
826
  if (this.gestureAnimationDismissing && role !== GESTURE) {
787
827
  return false;
788
828
  }
@@ -844,8 +884,23 @@ export class Modal {
844
884
  }
845
885
  this.currentBreakpoint = undefined;
846
886
  this.animation = undefined;
847
- // Reset safe-area detection flag for potential re-presentation
887
+ // Reset safe-area state for potential re-presentation
848
888
  this.skipSafeAreaCoordinateDetection = false;
889
+ this.cachedSafeAreas = undefined;
890
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
891
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
892
+ this.footerObserver = undefined;
893
+ // Clear styles that may have been set for safe-area handling
894
+ if (this.wrapperEl) {
895
+ this.wrapperEl.style.removeProperty('padding-bottom');
896
+ this.wrapperEl.style.removeProperty('box-sizing');
897
+ }
898
+ // Clear safe-area CSS variable overrides
899
+ const style = this.el.style;
900
+ style.removeProperty('--ion-safe-area-top');
901
+ style.removeProperty('--ion-safe-area-bottom');
902
+ style.removeProperty('--ion-safe-area-left');
903
+ style.removeProperty('--ion-safe-area-right');
849
904
  unlock();
850
905
  return dismissed;
851
906
  }
@@ -1103,20 +1158,20 @@ export class Modal {
1103
1158
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1104
1159
  const isHandleCycle = handleBehavior === 'cycle';
1105
1160
  const isSheetModalWithHandle = isSheetModal && showHandle;
1106
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
1161
+ return (h(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
1107
1162
  // Allow the modal to be navigable when the handle is focusable
1108
1163
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1109
1164
  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',
1165
+ }, 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',
1111
1166
  /*
1112
1167
  role and aria-modal must be used on the
1113
1168
  same element. They must also be set inside the
1114
1169
  shadow DOM otherwise ion-button will not be highlighted
1115
1170
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1116
1171
  */
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",
1172
+ 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",
1118
1173
  // 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 }))));
1174
+ 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 }))));
1120
1175
  }
1121
1176
  static get is() { return "ion-modal"; }
1122
1177
  static get encapsulation() { return "shadow"; }
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-01-06T16:44:21",
2
+ "timestamp": "2026-01-07T14:44:35",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.38.0",
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { r as registerInstance, h, e as config, d as Host, g as getElement, f as printIonWarning, c as createEvent, a as readTask, i as forceUpdate, w as writeTask, j as printIonError } from './index-C8IsBmNU.js';
4
+ import { r as registerInstance, h, e as config, d as Host, g as getElement, f as printIonWarning, c as createEvent, i as forceUpdate, a as readTask, w as writeTask, j as printIonError } from './index-C8IsBmNU.js';
5
5
  import { shouldUseCloseWatcher } from './hardware-back-button-CPLxO-Ev.js';
6
6
  import { b as getIonMode, a as isPlatform } from './ionic-global-CDrldh-5.js';
7
7
  import { i as inheritAriaAttributes, h as hasLazyBuild, c as componentOnReady, e as clamp, s as shallowEqualStringMap } from './helpers-DEn3pfjm.js';
@@ -268,15 +268,39 @@ const Content = class {
268
268
  }
269
269
  }
270
270
  /**
271
- * Detects sibling ion-header and ion-footer elements.
272
- * When these are absent, content needs to handle safe-area padding directly.
271
+ * Detects sibling ion-header and ion-footer elements and sets up
272
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
273
273
  */
274
274
  detectSiblingElements() {
275
- // Check parent element for sibling header/footer.
275
+ this.updateSiblingDetection();
276
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
277
+ const parent = this.el.parentElement;
278
+ if (parent && !this.parentMutationObserver) {
279
+ this.parentMutationObserver = new MutationObserver(() => {
280
+ this.updateSiblingDetection();
281
+ forceUpdate(this);
282
+ });
283
+ this.parentMutationObserver.observe(parent, { childList: true });
284
+ }
285
+ }
286
+ /**
287
+ * Updates hasHeader/hasFooter based on current DOM state.
288
+ * Checks both direct siblings and elements wrapped in custom components
289
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
290
+ */
291
+ updateSiblingDetection() {
276
292
  const parent = this.el.parentElement;
277
293
  if (parent) {
294
+ // First check for direct ion-header/ion-footer siblings
278
295
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
279
296
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
297
+ // If not found, check if any sibling contains them (wrapped components)
298
+ if (!this.hasHeader) {
299
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
300
+ }
301
+ if (!this.hasFooter) {
302
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
303
+ }
280
304
  }
281
305
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
282
306
  if (!this.hasFooter) {
@@ -286,8 +310,28 @@ const Content = class {
286
310
  }
287
311
  }
288
312
  }
313
+ /**
314
+ * Checks if any sibling element of ion-content contains the specified element.
315
+ * Only searches one level deep to avoid finding elements in nested pages.
316
+ */
317
+ siblingContainsElement(parent, tagName) {
318
+ for (const sibling of parent.children) {
319
+ // Skip ion-content itself
320
+ if (sibling === this.el)
321
+ continue;
322
+ // Check if this sibling contains the target element as an immediate child
323
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
324
+ return true;
325
+ }
326
+ }
327
+ return false;
328
+ }
289
329
  disconnectedCallback() {
330
+ var _a;
290
331
  this.onScrollEnd();
332
+ // Clean up mutation observer to prevent memory leaks
333
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
334
+ this.parentMutationObserver = undefined;
291
335
  if (hasLazyBuild(this.el)) {
292
336
  /**
293
337
  * The event listener and tabs caches need to
@@ -540,7 +584,7 @@ const Content = class {
540
584
  const forceOverscroll = this.shouldForceOverscroll();
541
585
  const transitionShadow = mode === 'ios';
542
586
  this.resize();
543
- return (h(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
587
+ return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
544
588
  [mode]: true,
545
589
  'content-sizing': hostContext('ion-popover', this.el),
546
590
  overscroll: forceOverscroll,
@@ -550,12 +594,12 @@ const Content = class {
550
594
  }), style: {
551
595
  '--offset-top': `${this.cTop}px`,
552
596
  '--offset-bottom': `${this.cBottom}px`,
553
- } }, 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: {
597
+ } }, 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: {
554
598
  'inner-scroll': true,
555
599
  'scroll-x': scrollX,
556
600
  'scroll-y': scrollY,
557
601
  overscroll: (scrollX || scrollY) && forceOverscroll,
558
- }, 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));
602
+ }, 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));
559
603
  }
560
604
  get el() { return getElement(this); }
561
605
  };
@@ -1503,6 +1503,8 @@ const Modal = class {
1503
1503
  this.gestureAnimationDismissing = false;
1504
1504
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1505
1505
  this.skipSafeAreaCoordinateDetection = false;
1506
+ // Track previous safe-area state to avoid redundant DOM writes
1507
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1506
1508
  this.presented = false;
1507
1509
  /** @internal */
1508
1510
  this.hasController = false;
@@ -1693,7 +1695,8 @@ const Modal = class {
1693
1695
  }
1694
1696
  }
1695
1697
  onWindowResize() {
1696
- // Update safe-area overrides for all modal types on resize
1698
+ // Invalidate safe-area cache on resize (device rotation may change values)
1699
+ this.cachedSafeAreas = undefined;
1697
1700
  this.updateSafeAreaOverrides();
1698
1701
  // Only handle view transition for iOS card modals when no custom animations are provided
1699
1702
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -2152,8 +2155,26 @@ const Modal = class {
2152
2155
  */
2153
2156
  applyFullscreenSafeArea() {
2154
2157
  this.skipSafeAreaCoordinateDetection = true;
2158
+ this.updateFooterPadding();
2159
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2160
+ if (!this.footerObserver) {
2161
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2162
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2163
+ }
2164
+ }
2165
+ /**
2166
+ * Updates wrapper padding based on footer presence.
2167
+ * Called initially and when footer is dynamically added/removed.
2168
+ */
2169
+ updateFooterPadding() {
2170
+ if (!this.wrapperEl)
2171
+ return;
2155
2172
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2156
- if (!hasFooter && this.wrapperEl) {
2173
+ if (hasFooter) {
2174
+ this.wrapperEl.style.removeProperty('padding-bottom');
2175
+ this.wrapperEl.style.removeProperty('box-sizing');
2176
+ }
2177
+ else {
2157
2178
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2158
2179
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2159
2180
  }
@@ -2171,21 +2192,26 @@ const Modal = class {
2171
2192
  }
2172
2193
  /**
2173
2194
  * Gets the root safe-area values from the document element.
2174
- * These represent the actual device safe areas before any overlay overrides.
2195
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2175
2196
  */
2176
- getRootSafeAreaValues() {
2177
- const rootStyle = getComputedStyle(document.documentElement);
2178
- return {
2179
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2180
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2181
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2182
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2183
- };
2197
+ getSafeAreaValues() {
2198
+ if (!this.cachedSafeAreas) {
2199
+ const rootStyle = getComputedStyle(document.documentElement);
2200
+ this.cachedSafeAreas = {
2201
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2202
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2203
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2204
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2205
+ };
2206
+ }
2207
+ return this.cachedSafeAreas;
2184
2208
  }
2185
2209
  /**
2186
2210
  * Updates safe-area CSS variable overrides based on whether the modal
2187
2211
  * extends into each safe-area region. Called after animation
2188
2212
  * and during gestures to handle dynamic position changes.
2213
+ *
2214
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2189
2215
  */
2190
2216
  updateSafeAreaOverrides() {
2191
2217
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2196,20 +2222,34 @@ const Modal = class {
2196
2222
  return;
2197
2223
  }
2198
2224
  const rect = wrapper.getBoundingClientRect();
2199
- const safeAreas = this.getRootSafeAreaValues();
2225
+ const safeAreas = this.getSafeAreaValues();
2200
2226
  const extendsIntoTop = rect.top < safeAreas.top;
2201
2227
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2202
2228
  const extendsIntoLeft = rect.left < safeAreas.left;
2203
2229
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2230
+ // Only update DOM when state actually changes
2231
+ const prev = this.prevSafeAreaState;
2204
2232
  const style = this.el.style;
2205
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2206
- extendsIntoBottom
2207
- ? style.removeProperty('--ion-safe-area-bottom')
2208
- : style.setProperty('--ion-safe-area-bottom', '0px');
2209
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2210
- extendsIntoRight
2211
- ? style.removeProperty('--ion-safe-area-right')
2212
- : style.setProperty('--ion-safe-area-right', '0px');
2233
+ if (extendsIntoTop !== prev.top) {
2234
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2235
+ prev.top = extendsIntoTop;
2236
+ }
2237
+ if (extendsIntoBottom !== prev.bottom) {
2238
+ extendsIntoBottom
2239
+ ? style.removeProperty('--ion-safe-area-bottom')
2240
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2241
+ prev.bottom = extendsIntoBottom;
2242
+ }
2243
+ if (extendsIntoLeft !== prev.left) {
2244
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2245
+ prev.left = extendsIntoLeft;
2246
+ }
2247
+ if (extendsIntoRight !== prev.right) {
2248
+ extendsIntoRight
2249
+ ? style.removeProperty('--ion-safe-area-right')
2250
+ : style.setProperty('--ion-safe-area-right', '0px');
2251
+ prev.right = extendsIntoRight;
2252
+ }
2213
2253
  }
2214
2254
  sheetOnDismiss() {
2215
2255
  /**
@@ -2241,7 +2281,7 @@ const Modal = class {
2241
2281
  * For example, `cancel` or `backdrop`.
2242
2282
  */
2243
2283
  async dismiss(data, role) {
2244
- var _a;
2284
+ var _a, _b;
2245
2285
  if (this.gestureAnimationDismissing && role !== GESTURE) {
2246
2286
  return false;
2247
2287
  }
@@ -2303,8 +2343,23 @@ const Modal = class {
2303
2343
  }
2304
2344
  this.currentBreakpoint = undefined;
2305
2345
  this.animation = undefined;
2306
- // Reset safe-area detection flag for potential re-presentation
2346
+ // Reset safe-area state for potential re-presentation
2307
2347
  this.skipSafeAreaCoordinateDetection = false;
2348
+ this.cachedSafeAreas = undefined;
2349
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2350
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
2351
+ this.footerObserver = undefined;
2352
+ // Clear styles that may have been set for safe-area handling
2353
+ if (this.wrapperEl) {
2354
+ this.wrapperEl.style.removeProperty('padding-bottom');
2355
+ this.wrapperEl.style.removeProperty('box-sizing');
2356
+ }
2357
+ // Clear safe-area CSS variable overrides
2358
+ const style = this.el.style;
2359
+ style.removeProperty('--ion-safe-area-top');
2360
+ style.removeProperty('--ion-safe-area-bottom');
2361
+ style.removeProperty('--ion-safe-area-left');
2362
+ style.removeProperty('--ion-safe-area-right');
2308
2363
  unlock();
2309
2364
  return dismissed;
2310
2365
  }
@@ -2554,20 +2609,20 @@ const Modal = class {
2554
2609
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2555
2610
  const isHandleCycle = handleBehavior === 'cycle';
2556
2611
  const isSheetModalWithHandle = isSheetModal && showHandle;
2557
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2612
+ return (h(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
2558
2613
  // Allow the modal to be navigable when the handle is focusable
2559
2614
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2560
2615
  zIndex: `${20000 + this.overlayIndex}`,
2561
- }, 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',
2616
+ }, 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',
2562
2617
  /*
2563
2618
  role and aria-modal must be used on the
2564
2619
  same element. They must also be set inside the
2565
2620
  shadow DOM otherwise ion-button will not be highlighted
2566
2621
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2567
2622
  */
2568
- 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",
2623
+ 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",
2569
2624
  // Prevents the handle from receiving keyboard focus when it does not cycle
2570
- 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 }))));
2625
+ 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 }))));
2571
2626
  }
2572
2627
  get el() { return getElement(this); }
2573
2628
  static get watchers() { return {