@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.
@@ -1,7 +1,8 @@
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
+ import { w as win } from './index9.js';
5
6
  import { i as inheritAriaAttributes, k as hasLazyBuild, c as componentOnReady } from './helpers.js';
6
7
  import { b as getIonMode, a as isPlatform } from './ionic-global.js';
7
8
  import { i as isRTL } from './dir.js';
@@ -88,7 +89,11 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
88
89
  this.inheritedAttributes = inheritAriaAttributes(this.el);
89
90
  }
90
91
  connectedCallback() {
91
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
92
+ var _a;
93
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
94
+ this.isMainContent =
95
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
96
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
92
97
  // Detect sibling header/footer for safe-area handling
93
98
  this.detectSiblingElements();
94
99
  /**
@@ -121,21 +126,55 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
121
126
  * bubbles, we can catch any instances of child tab bars loading by listening
122
127
  * on IonTabs.
123
128
  */
124
- this.tabsLoadCallback = () => this.resize();
129
+ this.tabsLoadCallback = () => {
130
+ this.resize();
131
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
132
+ this.updateSiblingDetection();
133
+ forceUpdate(this);
134
+ };
125
135
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
126
136
  }
127
137
  }
128
138
  }
129
139
  /**
130
- * Detects sibling ion-header and ion-footer elements.
131
- * When these are absent, content needs to handle safe-area padding directly.
140
+ * Detects sibling ion-header and ion-footer elements and sets up
141
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
132
142
  */
133
143
  detectSiblingElements() {
134
- // Check parent element for sibling header/footer.
144
+ this.updateSiblingDetection();
145
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
146
+ const parent = this.el.parentElement;
147
+ if (parent && !this.parentMutationObserver && win !== undefined && 'MutationObserver' in win) {
148
+ this.parentMutationObserver = new MutationObserver(() => {
149
+ const prevHasHeader = this.hasHeader;
150
+ const prevHasFooter = this.hasFooter;
151
+ this.updateSiblingDetection();
152
+ // Only trigger re-render if header/footer detection actually changed
153
+ if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
154
+ forceUpdate(this);
155
+ }
156
+ });
157
+ this.parentMutationObserver.observe(parent, { childList: true });
158
+ }
159
+ }
160
+ /**
161
+ * Updates hasHeader/hasFooter based on current DOM state.
162
+ * Checks both direct siblings and elements wrapped in custom components
163
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
164
+ */
165
+ updateSiblingDetection() {
135
166
  const parent = this.el.parentElement;
136
167
  if (parent) {
168
+ // First check for direct ion-header/ion-footer siblings
137
169
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
138
170
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
171
+ // If not found, check if any sibling contains them (wrapped components)
172
+ if (!this.hasHeader) {
173
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
174
+ }
175
+ if (!this.hasFooter) {
176
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
177
+ }
139
178
  }
140
179
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
141
180
  if (!this.hasFooter) {
@@ -145,8 +184,28 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
145
184
  }
146
185
  }
147
186
  }
187
+ /**
188
+ * Checks if any sibling element of ion-content contains the specified element.
189
+ * Only searches one level deep to avoid finding elements in nested pages.
190
+ */
191
+ siblingContainsElement(parent, tagName) {
192
+ for (const sibling of parent.children) {
193
+ // Skip ion-content itself
194
+ if (sibling === this.el)
195
+ continue;
196
+ // Check if this sibling contains the target element as an immediate child
197
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
198
+ return true;
199
+ }
200
+ }
201
+ return false;
202
+ }
148
203
  disconnectedCallback() {
204
+ var _a;
149
205
  this.onScrollEnd();
206
+ // Clean up mutation observer to prevent memory leaks
207
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
208
+ this.parentMutationObserver = undefined;
150
209
  if (hasLazyBuild(this.el)) {
151
210
  /**
152
211
  * The event listener and tabs caches need to
@@ -399,7 +458,7 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
399
458
  const forceOverscroll = this.shouldForceOverscroll();
400
459
  const transitionShadow = mode === 'ios';
401
460
  this.resize();
402
- return (h(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
461
+ return (h(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
403
462
  [mode]: true,
404
463
  'content-sizing': hostContext('ion-popover', this.el),
405
464
  overscroll: forceOverscroll,
@@ -409,12 +468,12 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
409
468
  }), style: {
410
469
  '--offset-top': `${this.cTop}px`,
411
470
  '--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: {
471
+ } }, 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: {
413
472
  'inner-scroll': true,
414
473
  'scroll-x': scrollX,
415
474
  'scroll-y': scrollY,
416
475
  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));
476
+ }, 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));
418
477
  }
419
478
  get el() { return this; }
420
479
  static get style() { return contentCss; }
@@ -2,6 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, writeTask, h, Host } from '@stencil/core/internal/client';
5
+ import { w as win } from './index9.js';
5
6
  import { a as findClosestIonContent, i as isIonContent, d as disableContentScrollY, r as resetContentScrollY, f as findIonContent, p as printIonContentErrorMsg } from './index8.js';
6
7
  import { C as CoreDelegate, a as attachComponent, d as detachComponent } from './framework-delegate.js';
7
8
  import { f as clamp, g as getElementRoot, r as raf, d as inheritAttributes, k as hasLazyBuild } from './helpers.js';
@@ -16,7 +17,6 @@ import { KEYBOARD_DID_OPEN } from './keyboard.js';
16
17
  import { c as createAnimation } from './animation.js';
17
18
  import { g as getTimeGivenProgression } from './cubic-bezier.js';
18
19
  import { createGesture } from './index3.js';
19
- import { w as win } from './index9.js';
20
20
  import { d as defineCustomElement$1 } from './backdrop.js';
21
21
 
22
22
  var Style;
@@ -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) {
@@ -1721,6 +1724,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1721
1724
  this.triggerController.removeClickListener();
1722
1725
  this.cleanupViewTransitionListener();
1723
1726
  this.cleanupParentRemovalObserver();
1727
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
1728
+ this.resetSafeAreaState();
1724
1729
  }
1725
1730
  componentWillLoad() {
1726
1731
  var _a;
@@ -2155,8 +2160,28 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2155
2160
  */
2156
2161
  applyFullscreenSafeArea() {
2157
2162
  this.skipSafeAreaCoordinateDetection = true;
2163
+ this.updateFooterPadding();
2164
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2165
+ // Use subtree:true to support wrapped footers in framework components
2166
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
2167
+ if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
2168
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2169
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2170
+ }
2171
+ }
2172
+ /**
2173
+ * Updates wrapper padding based on footer presence.
2174
+ * Called initially and when footer is dynamically added/removed.
2175
+ */
2176
+ updateFooterPadding() {
2177
+ if (!this.wrapperEl)
2178
+ return;
2158
2179
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2159
- if (!hasFooter && this.wrapperEl) {
2180
+ if (hasFooter) {
2181
+ this.wrapperEl.style.removeProperty('padding-bottom');
2182
+ this.wrapperEl.style.removeProperty('box-sizing');
2183
+ }
2184
+ else {
2160
2185
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2161
2186
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2162
2187
  }
@@ -2172,23 +2197,52 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2172
2197
  style.setProperty('--ion-safe-area-left', '0px');
2173
2198
  style.setProperty('--ion-safe-area-right', '0px');
2174
2199
  }
2200
+ /**
2201
+ * Resets all safe-area related state and styles.
2202
+ * Called during dismiss and disconnectedCallback to ensure clean state
2203
+ * for re-presentation of inline modals.
2204
+ */
2205
+ resetSafeAreaState() {
2206
+ var _a;
2207
+ this.skipSafeAreaCoordinateDetection = false;
2208
+ this.cachedSafeAreas = undefined;
2209
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2210
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2211
+ this.footerObserver = undefined;
2212
+ // Clear wrapper styles that may have been set for safe-area handling
2213
+ if (this.wrapperEl) {
2214
+ this.wrapperEl.style.removeProperty('padding-bottom');
2215
+ this.wrapperEl.style.removeProperty('box-sizing');
2216
+ }
2217
+ // Clear safe-area CSS variable overrides
2218
+ const style = this.el.style;
2219
+ style.removeProperty('--ion-safe-area-top');
2220
+ style.removeProperty('--ion-safe-area-bottom');
2221
+ style.removeProperty('--ion-safe-area-left');
2222
+ style.removeProperty('--ion-safe-area-right');
2223
+ }
2175
2224
  /**
2176
2225
  * Gets the root safe-area values from the document element.
2177
- * These represent the actual device safe areas before any overlay overrides.
2226
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2178
2227
  */
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
- };
2228
+ getSafeAreaValues() {
2229
+ if (!this.cachedSafeAreas) {
2230
+ const rootStyle = getComputedStyle(document.documentElement);
2231
+ this.cachedSafeAreas = {
2232
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2233
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2234
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2235
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2236
+ };
2237
+ }
2238
+ return this.cachedSafeAreas;
2187
2239
  }
2188
2240
  /**
2189
2241
  * Updates safe-area CSS variable overrides based on whether the modal
2190
2242
  * extends into each safe-area region. Called after animation
2191
2243
  * and during gestures to handle dynamic position changes.
2244
+ *
2245
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2192
2246
  */
2193
2247
  updateSafeAreaOverrides() {
2194
2248
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2199,20 +2253,34 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2199
2253
  return;
2200
2254
  }
2201
2255
  const rect = wrapper.getBoundingClientRect();
2202
- const safeAreas = this.getRootSafeAreaValues();
2256
+ const safeAreas = this.getSafeAreaValues();
2203
2257
  const extendsIntoTop = rect.top < safeAreas.top;
2204
2258
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2205
2259
  const extendsIntoLeft = rect.left < safeAreas.left;
2206
2260
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2261
+ // Only update DOM when state actually changes
2262
+ const prev = this.prevSafeAreaState;
2207
2263
  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');
2264
+ if (extendsIntoTop !== prev.top) {
2265
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2266
+ prev.top = extendsIntoTop;
2267
+ }
2268
+ if (extendsIntoBottom !== prev.bottom) {
2269
+ extendsIntoBottom
2270
+ ? style.removeProperty('--ion-safe-area-bottom')
2271
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2272
+ prev.bottom = extendsIntoBottom;
2273
+ }
2274
+ if (extendsIntoLeft !== prev.left) {
2275
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2276
+ prev.left = extendsIntoLeft;
2277
+ }
2278
+ if (extendsIntoRight !== prev.right) {
2279
+ extendsIntoRight
2280
+ ? style.removeProperty('--ion-safe-area-right')
2281
+ : style.setProperty('--ion-safe-area-right', '0px');
2282
+ prev.right = extendsIntoRight;
2283
+ }
2216
2284
  }
2217
2285
  sheetOnDismiss() {
2218
2286
  /**
@@ -2306,8 +2374,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2306
2374
  }
2307
2375
  this.currentBreakpoint = undefined;
2308
2376
  this.animation = undefined;
2309
- // Reset safe-area detection flag for potential re-presentation
2310
- this.skipSafeAreaCoordinateDetection = false;
2377
+ // Reset safe-area state for potential re-presentation
2378
+ this.resetSafeAreaState();
2311
2379
  unlock();
2312
2380
  return dismissed;
2313
2381
  }
@@ -2557,20 +2625,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2557
2625
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2558
2626
  const isHandleCycle = handleBehavior === 'cycle';
2559
2627
  const isSheetModalWithHandle = isSheetModal && showHandle;
2560
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2628
+ return (h(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
2561
2629
  // Allow the modal to be navigable when the handle is focusable
2562
2630
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2563
2631
  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',
2632
+ }, 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',
2565
2633
  /*
2566
2634
  role and aria-modal must be used on the
2567
2635
  same element. They must also be set inside the
2568
2636
  shadow DOM otherwise ion-button will not be highlighted
2569
2637
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2570
2638
  */
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",
2639
+ 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",
2572
2640
  // 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 }))));
2641
+ 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 }))));
2574
2642
  }
2575
2643
  get el() { return this; }
2576
2644
  static get watchers() { return {
@@ -945,7 +945,7 @@ const mdEnterAnimation = (baseEl, opts) => {
945
945
  };
946
946
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
947
947
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
948
- 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);
948
+ 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);
949
949
  /**
950
950
  * Safe area CSS variable adjustments.
951
951
  * When the popover is positioned near an edge, we add the corresponding
@@ -954,14 +954,23 @@ const mdEnterAnimation = (baseEl, opts) => {
954
954
  */
955
955
  const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
956
956
  const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
957
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
958
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
957
959
  let topValue = `${top}px`;
958
960
  let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
961
+ let leftValue = `${left}px`;
959
962
  if (checkSafeAreaTop) {
960
963
  topValue = `${top}px${safeAreaTop}`;
961
964
  }
962
965
  if (checkSafeAreaBottom && bottomValue !== undefined) {
963
966
  bottomValue = `${bottom}px${safeAreaBottom}`;
964
967
  }
968
+ if (checkSafeAreaLeft) {
969
+ leftValue = `${left}px${safeAreaLeft}`;
970
+ }
971
+ if (checkSafeAreaRight) {
972
+ leftValue = `${left}px${safeAreaRight}`;
973
+ }
965
974
  const baseAnimation = createAnimation();
966
975
  const backdropAnimation = createAnimation();
967
976
  const wrapperAnimation = createAnimation();
@@ -979,7 +988,7 @@ const mdEnterAnimation = (baseEl, opts) => {
979
988
  .addElement(contentEl)
980
989
  .beforeStyles({
981
990
  top: `calc(${topValue} + var(--offset-y, 0px))`,
982
- left: `calc(${left}px + var(--offset-x, 0px))`,
991
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
983
992
  'transform-origin': `${originY} ${originX}`,
984
993
  })
985
994
  .beforeAddWrite(() => {
@@ -6,16 +6,16 @@
6
6
  var index = require('./index-D6Wc6v08.js');
7
7
  var hardwareBackButton = require('./hardware-back-button-VCK4V3mG.js');
8
8
  var ionicGlobal = require('./ionic-global-HMVqOFGO.js');
9
+ var index$1 = require('./index-DkNv4J_i.js');
9
10
  var helpers = require('./helpers-DrTqNghc.js');
10
11
  var dir = require('./dir-Cn0z1rJH.js');
11
12
  var theme = require('./theme-CeDs6Hcv.js');
12
- var index$1 = require('./index-CO6eryBo.js');
13
+ var index$2 = require('./index-CO6eryBo.js');
13
14
  var keyboardController = require('./keyboard-controller-GXBiBRKS.js');
14
15
  var cubicBezier = require('./cubic-bezier-DAjy1V-e.js');
15
16
  var frameworkDelegate = require('./framework-delegate-DMJRBuDi.js');
16
17
  var lockController = require('./lock-controller-aDB9wrEf.js');
17
- var index$2 = require('./index-094mMFB-.js');
18
- require('./index-DkNv4J_i.js');
18
+ var index$3 = require('./index-094mMFB-.js');
19
19
  require('./keyboard-UuAS4D_9.js');
20
20
  require('./capacitor-DmA66EwP.js');
21
21
 
@@ -231,7 +231,11 @@ const Content = class {
231
231
  this.inheritedAttributes = helpers.inheritAriaAttributes(this.el);
232
232
  }
233
233
  connectedCallback() {
234
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
234
+ var _a;
235
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
236
+ this.isMainContent =
237
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
238
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
235
239
  // Detect sibling header/footer for safe-area handling
236
240
  this.detectSiblingElements();
237
241
  /**
@@ -264,21 +268,55 @@ const Content = class {
264
268
  * bubbles, we can catch any instances of child tab bars loading by listening
265
269
  * on IonTabs.
266
270
  */
267
- this.tabsLoadCallback = () => this.resize();
271
+ this.tabsLoadCallback = () => {
272
+ this.resize();
273
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
274
+ this.updateSiblingDetection();
275
+ index.forceUpdate(this);
276
+ };
268
277
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
269
278
  }
270
279
  }
271
280
  }
272
281
  /**
273
- * Detects sibling ion-header and ion-footer elements.
274
- * When these are absent, content needs to handle safe-area padding directly.
282
+ * Detects sibling ion-header and ion-footer elements and sets up
283
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
275
284
  */
276
285
  detectSiblingElements() {
277
- // Check parent element for sibling header/footer.
286
+ this.updateSiblingDetection();
287
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
288
+ const parent = this.el.parentElement;
289
+ if (parent && !this.parentMutationObserver && index$1.win !== undefined && 'MutationObserver' in index$1.win) {
290
+ this.parentMutationObserver = new MutationObserver(() => {
291
+ const prevHasHeader = this.hasHeader;
292
+ const prevHasFooter = this.hasFooter;
293
+ this.updateSiblingDetection();
294
+ // Only trigger re-render if header/footer detection actually changed
295
+ if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
296
+ index.forceUpdate(this);
297
+ }
298
+ });
299
+ this.parentMutationObserver.observe(parent, { childList: true });
300
+ }
301
+ }
302
+ /**
303
+ * Updates hasHeader/hasFooter based on current DOM state.
304
+ * Checks both direct siblings and elements wrapped in custom components
305
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
306
+ */
307
+ updateSiblingDetection() {
278
308
  const parent = this.el.parentElement;
279
309
  if (parent) {
310
+ // First check for direct ion-header/ion-footer siblings
280
311
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
281
312
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
313
+ // If not found, check if any sibling contains them (wrapped components)
314
+ if (!this.hasHeader) {
315
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
316
+ }
317
+ if (!this.hasFooter) {
318
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
319
+ }
282
320
  }
283
321
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
284
322
  if (!this.hasFooter) {
@@ -288,8 +326,28 @@ const Content = class {
288
326
  }
289
327
  }
290
328
  }
329
+ /**
330
+ * Checks if any sibling element of ion-content contains the specified element.
331
+ * Only searches one level deep to avoid finding elements in nested pages.
332
+ */
333
+ siblingContainsElement(parent, tagName) {
334
+ for (const sibling of parent.children) {
335
+ // Skip ion-content itself
336
+ if (sibling === this.el)
337
+ continue;
338
+ // Check if this sibling contains the target element as an immediate child
339
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
340
+ return true;
341
+ }
342
+ }
343
+ return false;
344
+ }
291
345
  disconnectedCallback() {
346
+ var _a;
292
347
  this.onScrollEnd();
348
+ // Clean up mutation observer to prevent memory leaks
349
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
350
+ this.parentMutationObserver = undefined;
293
351
  if (helpers.hasLazyBuild(this.el)) {
294
352
  /**
295
353
  * The event listener and tabs caches need to
@@ -542,7 +600,7 @@ const Content = class {
542
600
  const forceOverscroll = this.shouldForceOverscroll();
543
601
  const transitionShadow = mode === 'ios';
544
602
  this.resize();
545
- return (index.h(index.Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
603
+ return (index.h(index.Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
546
604
  [mode]: true,
547
605
  'content-sizing': theme.hostContext('ion-popover', this.el),
548
606
  overscroll: forceOverscroll,
@@ -552,12 +610,12 @@ const Content = class {
552
610
  }), style: {
553
611
  '--offset-top': `${this.cTop}px`,
554
612
  '--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: {
613
+ } }, inheritedAttributes), index.h("div", { key: 'b735ec68c18c0b99c3595bb194029830e6542cde', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: 'e76c00d030342d44ade6648c3f9e32ca990787ba', class: {
556
614
  'inner-scroll': true,
557
615
  'scroll-x': scrollX,
558
616
  'scroll-y': scrollY,
559
617
  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));
618
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '9049be4cea9b5da5ec1e1012248b05286fddeb7a' })), 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
619
  }
562
620
  get el() { return index.getElement(this); }
563
621
  };
@@ -675,16 +733,16 @@ const Footer = class {
675
733
  this.destroyCollapsibleFooter();
676
734
  if (hasFade) {
677
735
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
678
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
736
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
679
737
  if (!contentEl) {
680
- index$1.printIonContentErrorMsg(this.el);
738
+ index$2.printIonContentErrorMsg(this.el);
681
739
  return;
682
740
  }
683
741
  this.setupFadeFooter(contentEl);
684
742
  }
685
743
  };
686
744
  this.setupFadeFooter = async (contentEl) => {
687
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
745
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
688
746
  /**
689
747
  * Handle fading of toolbars on scroll
690
748
  */
@@ -997,7 +1055,7 @@ const Header = class {
997
1055
  */
998
1056
  this.translucent = false;
999
1057
  this.setupFadeHeader = async (contentEl, condenseHeader) => {
1000
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
1058
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
1001
1059
  /**
1002
1060
  * Handle fading of toolbars on scroll
1003
1061
  */
@@ -1031,7 +1089,7 @@ const Header = class {
1031
1089
  this.destroyCollapsibleHeader();
1032
1090
  if (hasCondense) {
1033
1091
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1034
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1092
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1035
1093
  // Cloned elements are always needed in iOS transition
1036
1094
  index.writeTask(() => {
1037
1095
  const title = cloneElement('ion-title');
@@ -1042,9 +1100,9 @@ const Header = class {
1042
1100
  }
1043
1101
  else if (hasFade) {
1044
1102
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1045
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1103
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1046
1104
  if (!contentEl) {
1047
- index$1.printIonContentErrorMsg(this.el);
1105
+ index$2.printIonContentErrorMsg(this.el);
1048
1106
  return;
1049
1107
  }
1050
1108
  const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]');
@@ -1067,13 +1125,13 @@ const Header = class {
1067
1125
  }
1068
1126
  async setupCondenseHeader(contentEl, pageEl) {
1069
1127
  if (!contentEl || !pageEl) {
1070
- index$1.printIonContentErrorMsg(this.el);
1128
+ index$2.printIonContentErrorMsg(this.el);
1071
1129
  return;
1072
1130
  }
1073
1131
  if (typeof IntersectionObserver === 'undefined') {
1074
1132
  return;
1075
1133
  }
1076
- this.scrollEl = await index$1.getScrollElement(contentEl);
1134
+ this.scrollEl = await index$2.getScrollElement(contentEl);
1077
1135
  const headers = pageEl.querySelectorAll('ion-header');
1078
1136
  this.collapsibleMainHeader = Array.from(headers).find((header) => header.collapse !== 'condense');
1079
1137
  if (!this.collapsibleMainHeader) {
@@ -1271,7 +1329,7 @@ const RouterOutlet = class {
1271
1329
  const { el, mode } = this;
1272
1330
  const animated = this.animated && index.config.getBoolean('animated', true);
1273
1331
  const animationBuilder = opts.animationBuilder || this.animation || index.config.get('navAnimation');
1274
- await index$2.transition(Object.assign(Object.assign({ mode,
1332
+ await index$3.transition(Object.assign(Object.assign({ mode,
1275
1333
  animated,
1276
1334
  enteringEl,
1277
1335
  leavingEl, baseEl: el,