@ionic/core 8.7.17-dev.11767647939.17c197c2 → 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,13 +1,13 @@
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';
8
8
  import { c as createColorClasses, h as hostContext } from './theme.js';
9
9
 
10
- const contentCss = ":host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:\"\"}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}";
10
+ const contentCss = ":host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:\"\"}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}:host(.safe-area-top) #background-content,:host(.safe-area-top) .inner-scroll{top:var(--ion-safe-area-top, 0px)}:host(.safe-area-bottom) #background-content,:host(.safe-area-bottom) .inner-scroll{bottom:var(--ion-safe-area-bottom, 0px)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}";
11
11
 
12
12
  const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLElement {
13
13
  constructor(registerHost) {
@@ -28,6 +28,12 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
28
28
  this.isMainContent = true;
29
29
  this.resizeTimeout = null;
30
30
  this.inheritedAttributes = {};
31
+ /**
32
+ * Track whether this content has sibling header/footer elements.
33
+ * When absent, we need to apply safe-area padding directly.
34
+ */
35
+ this.hasHeader = false;
36
+ this.hasFooter = false;
31
37
  this.tabsElement = null;
32
38
  // Detail is used in a hot loop in the scroll event, by allocating it here
33
39
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -83,6 +89,8 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
83
89
  }
84
90
  connectedCallback() {
85
91
  this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
92
+ // Detect sibling header/footer for safe-area handling
93
+ this.detectSiblingElements();
86
94
  /**
87
95
  * The fullscreen content offsets need to be
88
96
  * computed after the tab bar has loaded. Since
@@ -118,8 +126,71 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
118
126
  }
119
127
  }
120
128
  }
129
+ /**
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
+ */
133
+ detectSiblingElements() {
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() {
151
+ const parent = this.el.parentElement;
152
+ if (parent) {
153
+ // First check for direct ion-header/ion-footer siblings
154
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
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
+ }
163
+ }
164
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
165
+ if (!this.hasFooter) {
166
+ const tabs = this.el.closest('ion-tabs');
167
+ if (tabs) {
168
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
169
+ }
170
+ }
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
+ }
121
188
  disconnectedCallback() {
189
+ var _a;
122
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;
123
194
  if (hasLazyBuild(this.el)) {
124
195
  /**
125
196
  * The event listener and tabs caches need to
@@ -366,26 +437,28 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
366
437
  }
367
438
  }
368
439
  render() {
369
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
440
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
370
441
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
371
442
  const mode = getIonMode(this);
372
443
  const forceOverscroll = this.shouldForceOverscroll();
373
444
  const transitionShadow = mode === 'ios';
374
445
  this.resize();
375
- return (h(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
446
+ return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
376
447
  [mode]: true,
377
448
  'content-sizing': hostContext('ion-popover', this.el),
378
449
  overscroll: forceOverscroll,
379
450
  [`content-${rtl}`]: true,
451
+ 'safe-area-top': isMainContent && !hasHeader,
452
+ 'safe-area-bottom': isMainContent && !hasFooter,
380
453
  }), style: {
381
454
  '--offset-top': `${this.cTop}px`,
382
455
  '--offset-bottom': `${this.cBottom}px`,
383
- } }, inheritedAttributes), h("div", { key: '95b112d7cae30f22ef778ceffb88edb4d941c170', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: '2fdfcbc39fb66f11b6191911f2941c660f4c12e5', 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: {
384
457
  'inner-scroll': true,
385
458
  'scroll-x': scrollX,
386
459
  'scroll-y': scrollY,
387
460
  overscroll: (scrollX || scrollY) && forceOverscroll,
388
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '6bc77e0054ec8e21635a7f2abfe0ca46e0962e03' })), 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));
389
462
  }
390
463
  get el() { return this; }
391
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 {
@@ -154,7 +154,7 @@ Buttons.style = {
154
154
  md: buttonsMdCss
155
155
  };
156
156
 
157
- const contentCss = ":host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:\"\"}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}";
157
+ const contentCss = ":host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:\"\"}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}:host(.safe-area-top) #background-content,:host(.safe-area-top) .inner-scroll{top:var(--ion-safe-area-top, 0px)}:host(.safe-area-bottom) #background-content,:host(.safe-area-bottom) .inner-scroll{bottom:var(--ion-safe-area-bottom, 0px)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}";
158
158
 
159
159
  const Content = class {
160
160
  constructor(hostRef) {
@@ -171,6 +171,12 @@ const Content = class {
171
171
  this.isMainContent = true;
172
172
  this.resizeTimeout = null;
173
173
  this.inheritedAttributes = {};
174
+ /**
175
+ * Track whether this content has sibling header/footer elements.
176
+ * When absent, we need to apply safe-area padding directly.
177
+ */
178
+ this.hasHeader = false;
179
+ this.hasFooter = false;
174
180
  this.tabsElement = null;
175
181
  // Detail is used in a hot loop in the scroll event, by allocating it here
176
182
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -226,6 +232,8 @@ const Content = class {
226
232
  }
227
233
  connectedCallback() {
228
234
  this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
235
+ // Detect sibling header/footer for safe-area handling
236
+ this.detectSiblingElements();
229
237
  /**
230
238
  * The fullscreen content offsets need to be
231
239
  * computed after the tab bar has loaded. Since
@@ -261,8 +269,71 @@ const Content = class {
261
269
  }
262
270
  }
263
271
  }
272
+ /**
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
+ */
276
+ detectSiblingElements() {
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() {
294
+ const parent = this.el.parentElement;
295
+ if (parent) {
296
+ // First check for direct ion-header/ion-footer siblings
297
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
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
+ }
306
+ }
307
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
308
+ if (!this.hasFooter) {
309
+ const tabs = this.el.closest('ion-tabs');
310
+ if (tabs) {
311
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
312
+ }
313
+ }
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
+ }
264
331
  disconnectedCallback() {
332
+ var _a;
265
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;
266
337
  if (helpers.hasLazyBuild(this.el)) {
267
338
  /**
268
339
  * The event listener and tabs caches need to
@@ -509,26 +580,28 @@ const Content = class {
509
580
  }
510
581
  }
511
582
  render() {
512
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
583
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
513
584
  const rtl = dir.isRTL(el) ? 'rtl' : 'ltr';
514
585
  const mode = ionicGlobal.getIonMode(this);
515
586
  const forceOverscroll = this.shouldForceOverscroll();
516
587
  const transitionShadow = mode === 'ios';
517
588
  this.resize();
518
- return (index.h(index.Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', 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, {
519
590
  [mode]: true,
520
591
  'content-sizing': theme.hostContext('ion-popover', this.el),
521
592
  overscroll: forceOverscroll,
522
593
  [`content-${rtl}`]: true,
594
+ 'safe-area-top': isMainContent && !hasHeader,
595
+ 'safe-area-bottom': isMainContent && !hasFooter,
523
596
  }), style: {
524
597
  '--offset-top': `${this.cTop}px`,
525
598
  '--offset-bottom': `${this.cBottom}px`,
526
- } }, inheritedAttributes), index.h("div", { key: '95b112d7cae30f22ef778ceffb88edb4d941c170', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: '2fdfcbc39fb66f11b6191911f2941c660f4c12e5', 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: {
527
600
  'inner-scroll': true,
528
601
  'scroll-x': scrollX,
529
602
  'scroll-y': scrollY,
530
603
  overscroll: (scrollX || scrollY) && forceOverscroll,
531
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '6bc77e0054ec8e21635a7f2abfe0ca46e0962e03' })), 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));
532
605
  }
533
606
  get el() { return index.getElement(this); }
534
607
  };