@ionic/core 8.7.17-dev.11767895575.16ea7cef → 8.7.17-dev.11767980394.1d9266a9

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 +111 -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 +122 -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 +109 -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 +111 -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-b55c44c8.entry.js +4 -0
  29. package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
  30. package/dist/types/components/content/content.d.ts +26 -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 +395 -52
  37. package/hydrate/index.mjs +395 -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,101 @@ 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
+ // Watch for dynamic tab bar changes in ion-tabs (common in Angular conditional rendering)
302
+ const tabs = this.el.closest('ion-tabs');
303
+ if (tabs && !this.tabsMutationObserver && index$1.win !== undefined && 'MutationObserver' in index$1.win) {
304
+ this.tabsMutationObserver = new MutationObserver(() => {
305
+ const prevHasFooter = this.hasFooter;
306
+ this.updateSiblingDetection();
307
+ // Only trigger re-render if footer detection actually changed
308
+ if (prevHasFooter !== this.hasFooter) {
309
+ index.forceUpdate(this);
310
+ }
311
+ });
312
+ this.tabsMutationObserver.observe(tabs, { childList: true });
313
+ }
314
+ }
315
+ /**
316
+ * Updates hasHeader/hasFooter based on current DOM state.
317
+ * Checks both direct siblings and elements wrapped in custom components
318
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
319
+ */
320
+ updateSiblingDetection() {
321
+ const parent = this.el.parentElement;
322
+ if (parent) {
323
+ // First check for direct ion-header/ion-footer siblings
324
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
325
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
326
+ // If not found, check if any sibling contains them (wrapped components)
327
+ if (!this.hasHeader) {
328
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
329
+ }
330
+ if (!this.hasFooter) {
331
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
332
+ }
333
+ }
334
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
335
+ if (!this.hasFooter) {
336
+ const tabs = this.el.closest('ion-tabs');
337
+ if (tabs) {
338
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * Checks if any sibling element of ion-content contains the specified element.
344
+ * Only searches one level deep to avoid finding elements in nested pages.
345
+ */
346
+ siblingContainsElement(parent, tagName) {
347
+ for (const sibling of parent.children) {
348
+ // Skip ion-content itself
349
+ if (sibling === this.el)
350
+ continue;
351
+ // Check if this sibling contains the target element as an immediate child
352
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
353
+ return true;
354
+ }
355
+ }
356
+ return false;
357
+ }
264
358
  disconnectedCallback() {
359
+ var _a, _b;
265
360
  this.onScrollEnd();
361
+ // Clean up mutation observers to prevent memory leaks
362
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
363
+ this.parentMutationObserver = undefined;
364
+ (_b = this.tabsMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
365
+ this.tabsMutationObserver = undefined;
266
366
  if (helpers.hasLazyBuild(this.el)) {
267
367
  /**
268
368
  * The event listener and tabs caches need to
@@ -509,26 +609,28 @@ const Content = class {
509
609
  }
510
610
  }
511
611
  render() {
512
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
612
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
513
613
  const rtl = dir.isRTL(el) ? 'rtl' : 'ltr';
514
614
  const mode = ionicGlobal.getIonMode(this);
515
615
  const forceOverscroll = this.shouldForceOverscroll();
516
616
  const transitionShadow = mode === 'ios';
517
617
  this.resize();
518
- return (index.h(index.Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
618
+ return (index.h(index.Host, Object.assign({ key: '8b40855d7b3257fac16b0c945dc54b75a8a36b1f', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
519
619
  [mode]: true,
520
620
  'content-sizing': theme.hostContext('ion-popover', this.el),
521
621
  overscroll: forceOverscroll,
522
622
  [`content-${rtl}`]: true,
623
+ 'safe-area-top': isMainContent && !hasHeader,
624
+ 'safe-area-bottom': isMainContent && !hasFooter,
523
625
  }), style: {
524
626
  '--offset-top': `${this.cTop}px`,
525
627
  '--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: {
628
+ } }, inheritedAttributes), index.h("div", { key: '1ec3de99f33ed7a252a637c14887043c96046536', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: 'dae33d14c489a55fcb6eceabbec3cb3b04726f34', class: {
527
629
  'inner-scroll': true,
528
630
  'scroll-x': scrollX,
529
631
  'scroll-y': scrollY,
530
632
  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));
633
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: 'e99873d5fcc573e5d6ccad0f7c79192e99f4b749' })), 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
634
  }
533
635
  get el() { return index.getElement(this); }
534
636
  };
@@ -646,16 +748,16 @@ const Footer = class {
646
748
  this.destroyCollapsibleFooter();
647
749
  if (hasFade) {
648
750
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
649
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
751
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
650
752
  if (!contentEl) {
651
- index$1.printIonContentErrorMsg(this.el);
753
+ index$2.printIonContentErrorMsg(this.el);
652
754
  return;
653
755
  }
654
756
  this.setupFadeFooter(contentEl);
655
757
  }
656
758
  };
657
759
  this.setupFadeFooter = async (contentEl) => {
658
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
760
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
659
761
  /**
660
762
  * Handle fading of toolbars on scroll
661
763
  */
@@ -968,7 +1070,7 @@ const Header = class {
968
1070
  */
969
1071
  this.translucent = false;
970
1072
  this.setupFadeHeader = async (contentEl, condenseHeader) => {
971
- const scrollEl = (this.scrollEl = await index$1.getScrollElement(contentEl));
1073
+ const scrollEl = (this.scrollEl = await index$2.getScrollElement(contentEl));
972
1074
  /**
973
1075
  * Handle fading of toolbars on scroll
974
1076
  */
@@ -1002,7 +1104,7 @@ const Header = class {
1002
1104
  this.destroyCollapsibleHeader();
1003
1105
  if (hasCondense) {
1004
1106
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1005
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1107
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1006
1108
  // Cloned elements are always needed in iOS transition
1007
1109
  index.writeTask(() => {
1008
1110
  const title = cloneElement('ion-title');
@@ -1013,9 +1115,9 @@ const Header = class {
1013
1115
  }
1014
1116
  else if (hasFade) {
1015
1117
  const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
1016
- const contentEl = pageEl ? index$1.findIonContent(pageEl) : null;
1118
+ const contentEl = pageEl ? index$2.findIonContent(pageEl) : null;
1017
1119
  if (!contentEl) {
1018
- index$1.printIonContentErrorMsg(this.el);
1120
+ index$2.printIonContentErrorMsg(this.el);
1019
1121
  return;
1020
1122
  }
1021
1123
  const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]');
@@ -1038,13 +1140,13 @@ const Header = class {
1038
1140
  }
1039
1141
  async setupCondenseHeader(contentEl, pageEl) {
1040
1142
  if (!contentEl || !pageEl) {
1041
- index$1.printIonContentErrorMsg(this.el);
1143
+ index$2.printIonContentErrorMsg(this.el);
1042
1144
  return;
1043
1145
  }
1044
1146
  if (typeof IntersectionObserver === 'undefined') {
1045
1147
  return;
1046
1148
  }
1047
- this.scrollEl = await index$1.getScrollElement(contentEl);
1149
+ this.scrollEl = await index$2.getScrollElement(contentEl);
1048
1150
  const headers = pageEl.querySelectorAll('ion-header');
1049
1151
  this.collapsibleMainHeader = Array.from(headers).find((header) => header.collapse !== 'condense');
1050
1152
  if (!this.collapsibleMainHeader) {
@@ -1242,7 +1344,7 @@ const RouterOutlet = class {
1242
1344
  const { el, mode } = this;
1243
1345
  const animated = this.animated && index.config.getBoolean('animated', true);
1244
1346
  const animationBuilder = opts.animationBuilder || this.animation || index.config.get('navAnimation');
1245
- await index$2.transition(Object.assign(Object.assign({ mode,
1347
+ await index$3.transition(Object.assign(Object.assign({ mode,
1246
1348
  animated,
1247
1349
  enteringEl,
1248
1350
  leavingEl, baseEl: el,