@ionic/core 8.7.17-dev.11767895575.16ea7cef → 8.7.17-dev.11767897190.1ef0f479

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.
Files changed (42) hide show
  1. package/components/content.js +96 -8
  2. package/components/ion-tab-bar.js +3 -23
  3. package/components/modal.js +213 -12
  4. package/components/popover.js +83 -11
  5. package/dist/cjs/ion-app_8.cjs.entry.js +107 -20
  6. package/dist/cjs/ion-modal.cjs.entry.js +213 -12
  7. package/dist/cjs/ion-popover.cjs.entry.js +83 -11
  8. package/dist/cjs/ion-tab-bar_2.cjs.entry.js +3 -23
  9. package/dist/collection/components/content/content.css +10 -0
  10. package/dist/collection/components/content/content.js +94 -6
  11. package/dist/collection/components/modal/gestures/sheet.js +3 -1
  12. package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
  13. package/dist/collection/components/modal/modal.ios.css +0 -4
  14. package/dist/collection/components/modal/modal.js +205 -7
  15. package/dist/collection/components/modal/modal.md.css +0 -4
  16. package/dist/collection/components/popover/animations/ios.enter.js +21 -5
  17. package/dist/collection/components/popover/animations/md.enter.js +30 -5
  18. package/dist/collection/components/popover/utils.js +32 -1
  19. package/dist/collection/components/tab-bar/tab-bar.js +3 -23
  20. package/dist/docs.json +1 -1
  21. package/dist/esm/ion-app_8.entry.js +96 -9
  22. package/dist/esm/ion-modal.entry.js +213 -12
  23. package/dist/esm/ion-popover.entry.js +83 -11
  24. package/dist/esm/ion-tab-bar_2.entry.js +3 -23
  25. package/dist/ionic/ionic.esm.js +1 -1
  26. package/dist/ionic/p-7268efa5.entry.js +4 -0
  27. package/dist/ionic/p-968a55d1.entry.js +4 -0
  28. package/dist/ionic/p-d9fd799f.entry.js +4 -0
  29. package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
  30. package/dist/types/components/content/content.d.ts +24 -0
  31. package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
  32. package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
  33. package/dist/types/components/modal/modal.d.ts +45 -0
  34. package/dist/types/components/popover/utils.d.ts +2 -0
  35. package/dist/types/components/tab-bar/tab-bar.d.ts +0 -1
  36. package/hydrate/index.js +385 -52
  37. package/hydrate/index.mjs +385 -52
  38. package/package.json +1 -1
  39. package/dist/ionic/p-172a579f.entry.js +0 -4
  40. package/dist/ionic/p-732b2fd6.entry.js +0 -4
  41. package/dist/ionic/p-91840a80.entry.js +0 -4
  42. package/dist/ionic/p-f9061316.entry.js +0 -4
@@ -657,6 +657,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
657
657
  let bottom;
658
658
  let originX = contentOriginX;
659
659
  let originY = contentOriginY;
660
+ let checkSafeAreaTop = false;
661
+ let checkSafeAreaBottom = false;
660
662
  let checkSafeAreaLeft = false;
661
663
  let checkSafeAreaRight = false;
662
664
  const triggerTop = triggerCoordinates
@@ -701,10 +703,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
701
703
  * We chose 12 here so that the popover position looks a bit nicer as
702
704
  * it is not right up against the edge of the screen.
703
705
  */
704
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
706
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
705
707
  arrowTop = top + contentHeight;
706
708
  originY = 'bottom';
707
709
  addPopoverBottomClass = true;
710
+ /**
711
+ * If the popover is positioned near the top edge, account for safe area.
712
+ * This ensures the popover doesn't overlap with status bars or notches.
713
+ */
714
+ if (top <= bodyPadding + safeAreaMargin) {
715
+ checkSafeAreaTop = true;
716
+ top = bodyPadding;
717
+ }
708
718
  /**
709
719
  * If not enough room for popover to appear
710
720
  * above trigger, then cut it off.
@@ -712,14 +722,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
712
722
  }
713
723
  else {
714
724
  bottom = bodyPadding;
725
+ /**
726
+ * When the popover is pinned to the bottom, account for safe area.
727
+ * This ensures the popover doesn't overlap with home indicators
728
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
729
+ */
730
+ checkSafeAreaBottom = true;
715
731
  }
716
732
  }
733
+ /**
734
+ * Final check: If the popover extends into any safe-area region,
735
+ * ensure the corresponding flag is set regardless of side.
736
+ * This handles cases where a side-positioned popover (left/right)
737
+ * still needs bottom safe-area padding because it extends into that region.
738
+ */
739
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
740
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
741
+ checkSafeAreaBottom = true;
742
+ }
743
+ if (top < safeAreaMargin) {
744
+ checkSafeAreaTop = true;
745
+ }
717
746
  return {
718
747
  top,
719
748
  left,
720
749
  bottom,
721
750
  originX,
722
751
  originY,
752
+ checkSafeAreaTop,
753
+ checkSafeAreaBottom,
723
754
  checkSafeAreaLeft,
724
755
  checkSafeAreaRight,
725
756
  arrowTop,
@@ -780,7 +811,7 @@ const iosEnterAnimation = (baseEl, opts) => {
780
811
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
781
812
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
782
813
  const margin = size === 'cover' ? 0 : 25;
783
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
814
+ const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
784
815
  const baseAnimation = createAnimation();
785
816
  const backdropAnimation = createAnimation();
786
817
  const contentAnimation = createAnimation();
@@ -810,19 +841,35 @@ const iosEnterAnimation = (baseEl, opts) => {
810
841
  if (addPopoverBottomClass) {
811
842
  baseEl.classList.add('popover-bottom');
812
843
  }
813
- if (bottom !== undefined) {
814
- contentEl.style.setProperty('bottom', `${bottom}px`);
815
- }
844
+ /**
845
+ * Safe area CSS variable adjustments.
846
+ * When the popover is positioned near an edge, we add the corresponding
847
+ * safe-area inset to ensure the popover doesn't overlap with system UI
848
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
849
+ */
850
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
851
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
816
852
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
817
853
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
854
+ let topValue = `${top}px`;
855
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
818
856
  let leftValue = `${left}px`;
857
+ if (checkSafeAreaTop) {
858
+ topValue = `${top}px${safeAreaTop}`;
859
+ }
860
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
861
+ bottomValue = `${bottom}px${safeAreaBottom}`;
862
+ }
819
863
  if (checkSafeAreaLeft) {
820
864
  leftValue = `${left}px${safeAreaLeft}`;
821
865
  }
822
866
  if (checkSafeAreaRight) {
823
867
  leftValue = `${left}px${safeAreaRight}`;
824
868
  }
825
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
869
+ if (bottomValue !== undefined) {
870
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
871
+ }
872
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
826
873
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
827
874
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
828
875
  if (arrowEl !== null) {
@@ -898,7 +945,32 @@ const mdEnterAnimation = (baseEl, opts) => {
898
945
  };
899
946
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
900
947
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
901
- const { originX, originY, top, left, bottom } = 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
+ /**
950
+ * Safe area CSS variable adjustments.
951
+ * When the popover is positioned near an edge, we add the corresponding
952
+ * safe-area inset to ensure the popover doesn't overlap with system UI
953
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
954
+ */
955
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
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)';
959
+ let topValue = `${top}px`;
960
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
961
+ let leftValue = `${left}px`;
962
+ if (checkSafeAreaTop) {
963
+ topValue = `${top}px${safeAreaTop}`;
964
+ }
965
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
966
+ bottomValue = `${bottom}px${safeAreaBottom}`;
967
+ }
968
+ if (checkSafeAreaLeft) {
969
+ leftValue = `${left}px${safeAreaLeft}`;
970
+ }
971
+ if (checkSafeAreaRight) {
972
+ leftValue = `${left}px${safeAreaRight}`;
973
+ }
902
974
  const baseAnimation = createAnimation();
903
975
  const backdropAnimation = createAnimation();
904
976
  const wrapperAnimation = createAnimation();
@@ -915,13 +987,13 @@ const mdEnterAnimation = (baseEl, opts) => {
915
987
  contentAnimation
916
988
  .addElement(contentEl)
917
989
  .beforeStyles({
918
- top: `calc(${top}px + var(--offset-y, 0px))`,
919
- left: `calc(${left}px + var(--offset-x, 0px))`,
990
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
991
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
920
992
  'transform-origin': `${originY} ${originX}`,
921
993
  })
922
994
  .beforeAddWrite(() => {
923
- if (bottom !== undefined) {
924
- contentEl.style.setProperty('bottom', `${bottom}px`);
995
+ if (bottomValue !== undefined) {
996
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
925
997
  }
926
998
  })
927
999
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -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
 
@@ -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.
@@ -225,7 +231,13 @@ const Content = class {
225
231
  this.inheritedAttributes = helpers.inheritAriaAttributes(this.el);
226
232
  }
227
233
  connectedCallback() {
228
- 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;
239
+ // Detect sibling header/footer for safe-area handling
240
+ this.detectSiblingElements();
229
241
  /**
230
242
  * The fullscreen content offsets need to be
231
243
  * computed after the tab bar has loaded. Since
@@ -256,13 +268,86 @@ const Content = class {
256
268
  * bubbles, we can catch any instances of child tab bars loading by listening
257
269
  * on IonTabs.
258
270
  */
259
- 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
+ };
260
277
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
261
278
  }
262
279
  }
263
280
  }
281
+ /**
282
+ * Detects sibling ion-header and ion-footer elements and sets up
283
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
284
+ */
285
+ detectSiblingElements() {
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() {
308
+ const parent = this.el.parentElement;
309
+ if (parent) {
310
+ // First check for direct ion-header/ion-footer siblings
311
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
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
+ }
320
+ }
321
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
322
+ if (!this.hasFooter) {
323
+ const tabs = this.el.closest('ion-tabs');
324
+ if (tabs) {
325
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
326
+ }
327
+ }
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
+ }
264
345
  disconnectedCallback() {
346
+ var _a;
265
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;
266
351
  if (helpers.hasLazyBuild(this.el)) {
267
352
  /**
268
353
  * The event listener and tabs caches need to
@@ -509,26 +594,28 @@ const Content = class {
509
594
  }
510
595
  }
511
596
  render() {
512
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
597
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
513
598
  const rtl = dir.isRTL(el) ? 'rtl' : 'ltr';
514
599
  const mode = ionicGlobal.getIonMode(this);
515
600
  const forceOverscroll = this.shouldForceOverscroll();
516
601
  const transitionShadow = mode === 'ios';
517
602
  this.resize();
518
- return (index.h(index.Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', 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, {
519
604
  [mode]: true,
520
605
  'content-sizing': theme.hostContext('ion-popover', this.el),
521
606
  overscroll: forceOverscroll,
522
607
  [`content-${rtl}`]: true,
608
+ 'safe-area-top': isMainContent && !hasHeader,
609
+ 'safe-area-bottom': isMainContent && !hasFooter,
523
610
  }), style: {
524
611
  '--offset-top': `${this.cTop}px`,
525
612
  '--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: {
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: {
527
614
  'inner-scroll': true,
528
615
  'scroll-x': scrollX,
529
616
  'scroll-y': scrollY,
530
617
  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));
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));
532
619
  }
533
620
  get el() { return index.getElement(this); }
534
621
  };
@@ -646,16 +733,16 @@ const Footer = class {
646
733
  this.destroyCollapsibleFooter();
647
734
  if (hasFade) {
648
735
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
649
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
736
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
650
737
  if (!contentEl) {
651
- index$1.printIonContentErrorMsg(this.el);
738
+ index$2.printIonContentErrorMsg(this.el);
652
739
  return;
653
740
  }
654
741
  this.setupFadeFooter(contentEl);
655
742
  }
656
743
  };
657
744
  this.setupFadeFooter = async (contentEl) => {
658
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
745
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
659
746
  /**
660
747
  * Handle fading of toolbars on scroll
661
748
  */
@@ -968,7 +1055,7 @@ const Header = class {
968
1055
  */
969
1056
  this.translucent = false;
970
1057
  this.setupFadeHeader = async (contentEl, condenseHeader) => {
971
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
1058
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
972
1059
  /**
973
1060
  * Handle fading of toolbars on scroll
974
1061
  */
@@ -1002,7 +1089,7 @@ const Header = class {
1002
1089
  this.destroyCollapsibleHeader();
1003
1090
  if (hasCondense) {
1004
1091
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1005
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1092
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1006
1093
  // Cloned elements are always needed in iOS transition
1007
1094
  index.writeTask(() => {
1008
1095
  const title = cloneElement('ion-title');
@@ -1013,9 +1100,9 @@ const Header = class {
1013
1100
  }
1014
1101
  else if (hasFade) {
1015
1102
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1016
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1103
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1017
1104
  if (!contentEl) {
1018
- index$1.printIonContentErrorMsg(this.el);
1105
+ index$2.printIonContentErrorMsg(this.el);
1019
1106
  return;
1020
1107
  }
1021
1108
  const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]');
@@ -1038,13 +1125,13 @@ const Header = class {
1038
1125
  }
1039
1126
  async setupCondenseHeader(contentEl, pageEl) {
1040
1127
  if (!contentEl || !pageEl) {
1041
- index$1.printIonContentErrorMsg(this.el);
1128
+ index$2.printIonContentErrorMsg(this.el);
1042
1129
  return;
1043
1130
  }
1044
1131
  if (typeof IntersectionObserver === 'undefined') {
1045
1132
  return;
1046
1133
  }
1047
- this.scrollEl = await index$1.getScrollElement(contentEl);
1134
+ this.scrollEl = await index$2.getScrollElement(contentEl);
1048
1135
  const headers = pageEl.querySelectorAll('ion-header');
1049
1136
  this.collapsibleMainHeader = Array.from(headers).find((header) => header.collapse !== 'condense');
1050
1137
  if (!this.collapsibleMainHeader) {
@@ -1242,7 +1329,7 @@ const RouterOutlet = class {
1242
1329
  const { el, mode } = this;
1243
1330
  const animated = this.animated && index.config.getBoolean('animated', true);
1244
1331
  const animationBuilder = opts.animationBuilder || this.animation || index.config.get('navAnimation');
1245
- await index$2.transition(Object.assign(Object.assign({ mode,
1332
+ await index$3.transition(Object.assign(Object.assign({ mode,
1246
1333
  animated,
1247
1334
  enteringEl,
1248
1335
  leavingEl, baseEl: el,