@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
package/hydrate/index.js CHANGED
@@ -10499,7 +10499,7 @@ const isRTL$1 = (hostEl) => {
10499
10499
  return (document === null || document === void 0 ? void 0 : document.dir.toLowerCase()) === 'rtl';
10500
10500
  };
10501
10501
 
10502
- 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)}";
10502
+ 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)}";
10503
10503
 
10504
10504
  /**
10505
10505
  * @slot - Content is placed in the scrollable area if provided without a slot.
@@ -10523,6 +10523,12 @@ class Content {
10523
10523
  this.isMainContent = true;
10524
10524
  this.resizeTimeout = null;
10525
10525
  this.inheritedAttributes = {};
10526
+ /**
10527
+ * Track whether this content has sibling header/footer elements.
10528
+ * When absent, we need to apply safe-area padding directly.
10529
+ */
10530
+ this.hasHeader = false;
10531
+ this.hasFooter = false;
10526
10532
  this.tabsElement = null;
10527
10533
  // Detail is used in a hot loop in the scroll event, by allocating it here
10528
10534
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -10577,7 +10583,13 @@ class Content {
10577
10583
  this.inheritedAttributes = inheritAriaAttributes(this.el);
10578
10584
  }
10579
10585
  connectedCallback() {
10580
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
10586
+ var _a;
10587
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
10588
+ this.isMainContent =
10589
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
10590
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
10591
+ // Detect sibling header/footer for safe-area handling
10592
+ this.detectSiblingElements();
10581
10593
  /**
10582
10594
  * The fullscreen content offsets need to be
10583
10595
  * computed after the tab bar has loaded. Since
@@ -10608,13 +10620,89 @@ class Content {
10608
10620
  * bubbles, we can catch any instances of child tab bars loading by listening
10609
10621
  * on IonTabs.
10610
10622
  */
10611
- this.tabsLoadCallback = () => this.resize();
10623
+ this.tabsLoadCallback = () => {
10624
+ this.resize();
10625
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
10626
+ this.updateSiblingDetection();
10627
+ };
10612
10628
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
10613
10629
  }
10614
10630
  }
10615
10631
  }
10632
+ /**
10633
+ * Detects sibling ion-header and ion-footer elements and sets up
10634
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
10635
+ */
10636
+ detectSiblingElements() {
10637
+ this.updateSiblingDetection();
10638
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
10639
+ const parent = this.el.parentElement;
10640
+ if (parent && !this.parentMutationObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
10641
+ this.parentMutationObserver = new MutationObserver(() => {
10642
+ this.updateSiblingDetection();
10643
+ });
10644
+ this.parentMutationObserver.observe(parent, { childList: true });
10645
+ }
10646
+ // Watch for dynamic tab bar changes in ion-tabs (common in Angular conditional rendering)
10647
+ const tabs = this.el.closest('ion-tabs');
10648
+ if (tabs && !this.tabsMutationObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
10649
+ this.tabsMutationObserver = new MutationObserver(() => {
10650
+ this.updateSiblingDetection();
10651
+ });
10652
+ this.tabsMutationObserver.observe(tabs, { childList: true });
10653
+ }
10654
+ }
10655
+ /**
10656
+ * Updates hasHeader/hasFooter based on current DOM state.
10657
+ * Checks both direct siblings and elements wrapped in custom components
10658
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
10659
+ */
10660
+ updateSiblingDetection() {
10661
+ const parent = this.el.parentElement;
10662
+ if (parent) {
10663
+ // First check for direct ion-header/ion-footer siblings
10664
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
10665
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
10666
+ // If not found, check if any sibling contains them (wrapped components)
10667
+ if (!this.hasHeader) {
10668
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
10669
+ }
10670
+ if (!this.hasFooter) {
10671
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
10672
+ }
10673
+ }
10674
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
10675
+ if (!this.hasFooter) {
10676
+ const tabs = this.el.closest('ion-tabs');
10677
+ if (tabs) {
10678
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
10679
+ }
10680
+ }
10681
+ }
10682
+ /**
10683
+ * Checks if any sibling element of ion-content contains the specified element.
10684
+ * Only searches one level deep to avoid finding elements in nested pages.
10685
+ */
10686
+ siblingContainsElement(parent, tagName) {
10687
+ for (const sibling of parent.children) {
10688
+ // Skip ion-content itself
10689
+ if (sibling === this.el)
10690
+ continue;
10691
+ // Check if this sibling contains the target element as an immediate child
10692
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
10693
+ return true;
10694
+ }
10695
+ }
10696
+ return false;
10697
+ }
10616
10698
  disconnectedCallback() {
10699
+ var _a, _b;
10617
10700
  this.onScrollEnd();
10701
+ // Clean up mutation observers to prevent memory leaks
10702
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
10703
+ this.parentMutationObserver = undefined;
10704
+ (_b = this.tabsMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
10705
+ this.tabsMutationObserver = undefined;
10618
10706
  if (hasLazyBuild(this.el)) {
10619
10707
  /**
10620
10708
  * The event listener and tabs caches need to
@@ -10843,26 +10931,28 @@ class Content {
10843
10931
  }
10844
10932
  }
10845
10933
  render() {
10846
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10934
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10847
10935
  const rtl = isRTL$1(el) ? 'rtl' : 'ltr';
10848
10936
  const mode = getIonMode$1(this);
10849
10937
  const forceOverscroll = this.shouldForceOverscroll();
10850
10938
  const transitionShadow = mode === 'ios';
10851
10939
  this.resize();
10852
- return (hAsync(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10940
+ return (hAsync(Host, Object.assign({ key: '8b40855d7b3257fac16b0c945dc54b75a8a36b1f', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10853
10941
  [mode]: true,
10854
10942
  'content-sizing': hostContext('ion-popover', this.el),
10855
10943
  overscroll: forceOverscroll,
10856
10944
  [`content-${rtl}`]: true,
10945
+ 'safe-area-top': isMainContent && !hasHeader,
10946
+ 'safe-area-bottom': isMainContent && !hasFooter,
10857
10947
  }), style: {
10858
10948
  '--offset-top': `${this.cTop}px`,
10859
10949
  '--offset-bottom': `${this.cBottom}px`,
10860
- } }, inheritedAttributes), hAsync("div", { key: '95b112d7cae30f22ef778ceffb88edb4d941c170', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: '2fdfcbc39fb66f11b6191911f2941c660f4c12e5', class: {
10950
+ } }, inheritedAttributes), hAsync("div", { key: '1ec3de99f33ed7a252a637c14887043c96046536', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: 'dae33d14c489a55fcb6eceabbec3cb3b04726f34', class: {
10861
10951
  'inner-scroll': true,
10862
10952
  'scroll-x': scrollX,
10863
10953
  'scroll-y': scrollY,
10864
10954
  overscroll: (scrollX || scrollY) && forceOverscroll,
10865
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '6bc77e0054ec8e21635a7f2abfe0ca46e0962e03' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10955
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: 'e99873d5fcc573e5d6ccad0f7c79192e99f4b749' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10866
10956
  }
10867
10957
  get el() { return getElement(this); }
10868
10958
  static get style() { return contentCss; }
@@ -21533,7 +21623,7 @@ const calculateSpringStep = (t) => {
21533
21623
  const SwipeToCloseDefaults = {
21534
21624
  MIN_PRESENTING_SCALE: 0.915,
21535
21625
  };
21536
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
21626
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
21537
21627
  /**
21538
21628
  * The step value at which a card modal
21539
21629
  * is eligible for dismissing via gesture.
@@ -21690,6 +21780,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
21690
21780
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
21691
21781
  const clampedStep = clamp(0.0001, processedStep, maxStep);
21692
21782
  animation.progressStep(clampedStep);
21783
+ // Notify modal of position change for safe-area updates
21784
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
21693
21785
  /**
21694
21786
  * When swiping down half way, the status bar style
21695
21787
  * should be reset to its default value.
@@ -22233,7 +22325,7 @@ const mdLeaveAnimation$2 = (baseEl, opts) => {
22233
22325
  return baseAnimation;
22234
22326
  };
22235
22327
 
22236
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
22328
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
22237
22329
  // Defaults for the sheet swipe animation
22238
22330
  const defaultBackdrop = [
22239
22331
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -22564,6 +22656,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22564
22656
  : step;
22565
22657
  offset = clamp(0.0001, processedStep, maxStep);
22566
22658
  animation.progressStep(offset);
22659
+ // Notify modal of position change for safe-area updates
22660
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
22567
22661
  };
22568
22662
  const onEnd = (detail) => {
22569
22663
  /**
@@ -22758,9 +22852,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22758
22852
  };
22759
22853
  };
22760
22854
 
22761
- const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
22855
+ const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
22762
22856
 
22763
- const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
22857
+ const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
22764
22858
 
22765
22859
  // TODO(FW-2832): types
22766
22860
  /**
@@ -22793,6 +22887,10 @@ class Modal {
22793
22887
  this.inline = false;
22794
22888
  // Whether or not modal is being dismissed via gesture
22795
22889
  this.gestureAnimationDismissing = false;
22890
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22891
+ this.skipSafeAreaCoordinateDetection = false;
22892
+ // Track previous safe-area state to avoid redundant DOM writes
22893
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22796
22894
  this.presented = false;
22797
22895
  /** @internal */
22798
22896
  this.hasController = false;
@@ -22983,7 +23081,10 @@ class Modal {
22983
23081
  }
22984
23082
  }
22985
23083
  onWindowResize() {
22986
- // Only handle resize for iOS card modals when no custom animations are provided
23084
+ // Invalidate safe-area cache on resize (device rotation may change values)
23085
+ this.cachedSafeAreas = undefined;
23086
+ this.updateSafeAreaOverrides();
23087
+ // Only handle view transition for iOS card modals when no custom animations are provided
22987
23088
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
22988
23089
  return;
22989
23090
  }
@@ -23006,6 +23107,8 @@ class Modal {
23006
23107
  this.triggerController.removeClickListener();
23007
23108
  this.cleanupViewTransitionListener();
23008
23109
  this.cleanupParentRemovalObserver();
23110
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
23111
+ this.resetSafeAreaState();
23009
23112
  }
23010
23113
  componentWillLoad() {
23011
23114
  var _a;
@@ -23165,6 +23268,8 @@ class Modal {
23165
23268
  else if (!this.keepContentsMounted) {
23166
23269
  await waitForMount();
23167
23270
  }
23271
+ // Predict safe-area needs based on modal configuration to avoid visual snap
23272
+ this.setInitialSafeAreaOverrides(presentingElement);
23168
23273
  writeTask(() => this.el.classList.add('show-modal'));
23169
23274
  const hasCardModal = presentingElement !== undefined;
23170
23275
  /**
@@ -23226,6 +23331,8 @@ class Modal {
23226
23331
  else if (hasCardModal) {
23227
23332
  this.initSwipeToClose();
23228
23333
  }
23334
+ // Now that animation is complete, update safe-area based on actual position
23335
+ this.updateSafeAreaOverrides();
23229
23336
  // Initialize view transition listener for iOS card modals
23230
23337
  this.initViewTransitionListener();
23231
23338
  // Initialize parent removal observer
@@ -23277,7 +23384,7 @@ class Modal {
23277
23384
  await this.dismiss(undefined, GESTURE);
23278
23385
  this.gestureAnimationDismissing = false;
23279
23386
  });
23280
- });
23387
+ }, () => this.updateSafeAreaOverrides());
23281
23388
  this.gesture.enable(true);
23282
23389
  }
23283
23390
  initSheetGesture() {
@@ -23298,7 +23405,8 @@ class Modal {
23298
23405
  this.currentBreakpoint = breakpoint;
23299
23406
  this.ionBreakpointDidChange.emit({ breakpoint });
23300
23407
  }
23301
- });
23408
+ this.updateSafeAreaOverrides();
23409
+ }, () => this.updateSafeAreaOverrides());
23302
23410
  this.gesture = gesture;
23303
23411
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
23304
23412
  this.gesture.enable(true);
@@ -23376,6 +23484,187 @@ class Modal {
23376
23484
  // Clear the cached reference
23377
23485
  this.cachedPageParent = undefined;
23378
23486
  }
23487
+ /**
23488
+ * Sets initial safe-area overrides based on modal configuration before
23489
+ * the modal becomes visible. This predicts whether the modal will touch
23490
+ * screen edges to avoid a visual snap after animation completes.
23491
+ */
23492
+ setInitialSafeAreaOverrides(presentingElement) {
23493
+ const style = this.el.style;
23494
+ const mode = getIonMode$1(this);
23495
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23496
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
23497
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
23498
+ const isTablet = window.innerWidth >= 768;
23499
+ // Sheet modals always touch bottom edge, never top/left/right
23500
+ if (isSheetModal) {
23501
+ style.setProperty('--ion-safe-area-top', '0px');
23502
+ style.setProperty('--ion-safe-area-left', '0px');
23503
+ style.setProperty('--ion-safe-area-right', '0px');
23504
+ return;
23505
+ }
23506
+ // Card modals have rounded top corners
23507
+ if (isCardModal) {
23508
+ style.setProperty('--ion-safe-area-top', '0px');
23509
+ if (isTablet) {
23510
+ // On tablets, card modals are inset from all edges
23511
+ this.zeroAllSafeAreas();
23512
+ }
23513
+ else {
23514
+ // On phones, card modals still extend to the bottom edge
23515
+ style.setProperty('--ion-safe-area-left', '0px');
23516
+ style.setProperty('--ion-safe-area-right', '0px');
23517
+ this.applyFullscreenSafeArea();
23518
+ }
23519
+ return;
23520
+ }
23521
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
23522
+ if (!isTablet) {
23523
+ this.applyFullscreenSafeArea();
23524
+ return;
23525
+ }
23526
+ // Check if tablet modal is fullscreen via CSS custom properties
23527
+ const computedStyle = getComputedStyle(this.el);
23528
+ const width = computedStyle.getPropertyValue('--width').trim();
23529
+ const height = computedStyle.getPropertyValue('--height').trim();
23530
+ const isFullscreen = width === '100%' && height === '100%';
23531
+ if (isFullscreen) {
23532
+ this.applyFullscreenSafeArea();
23533
+ }
23534
+ else {
23535
+ // Centered dialog doesn't touch edges
23536
+ this.zeroAllSafeAreas();
23537
+ }
23538
+ }
23539
+ /**
23540
+ * Applies safe-area handling for fullscreen modals.
23541
+ * Adds wrapper padding when no footer is present to prevent
23542
+ * content from overlapping system navigation areas.
23543
+ */
23544
+ applyFullscreenSafeArea() {
23545
+ this.skipSafeAreaCoordinateDetection = true;
23546
+ this.updateFooterPadding();
23547
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23548
+ // Use subtree:true to support wrapped footers in framework components
23549
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
23550
+ if (!this.footerObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
23551
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23552
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23553
+ }
23554
+ }
23555
+ /**
23556
+ * Updates wrapper padding based on footer presence.
23557
+ * Called initially and when footer is dynamically added/removed.
23558
+ */
23559
+ updateFooterPadding() {
23560
+ if (!this.wrapperEl)
23561
+ return;
23562
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
23563
+ if (hasFooter) {
23564
+ this.wrapperEl.style.removeProperty('padding-bottom');
23565
+ this.wrapperEl.style.removeProperty('box-sizing');
23566
+ }
23567
+ else {
23568
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23569
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23570
+ }
23571
+ }
23572
+ /**
23573
+ * Sets all safe-area CSS variables to 0px for modals that
23574
+ * don't touch screen edges.
23575
+ */
23576
+ zeroAllSafeAreas() {
23577
+ const style = this.el.style;
23578
+ style.setProperty('--ion-safe-area-top', '0px');
23579
+ style.setProperty('--ion-safe-area-bottom', '0px');
23580
+ style.setProperty('--ion-safe-area-left', '0px');
23581
+ style.setProperty('--ion-safe-area-right', '0px');
23582
+ }
23583
+ /**
23584
+ * Resets all safe-area related state and styles.
23585
+ * Called during dismiss and disconnectedCallback to ensure clean state
23586
+ * for re-presentation of inline modals.
23587
+ */
23588
+ resetSafeAreaState() {
23589
+ var _a;
23590
+ this.skipSafeAreaCoordinateDetection = false;
23591
+ this.cachedSafeAreas = undefined;
23592
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23593
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23594
+ this.footerObserver = undefined;
23595
+ // Clear wrapper styles that may have been set for safe-area handling
23596
+ if (this.wrapperEl) {
23597
+ this.wrapperEl.style.removeProperty('padding-bottom');
23598
+ this.wrapperEl.style.removeProperty('box-sizing');
23599
+ }
23600
+ // Clear safe-area CSS variable overrides
23601
+ const style = this.el.style;
23602
+ style.removeProperty('--ion-safe-area-top');
23603
+ style.removeProperty('--ion-safe-area-bottom');
23604
+ style.removeProperty('--ion-safe-area-left');
23605
+ style.removeProperty('--ion-safe-area-right');
23606
+ }
23607
+ /**
23608
+ * Gets the root safe-area values from the document element.
23609
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23610
+ */
23611
+ getSafeAreaValues() {
23612
+ if (!this.cachedSafeAreas) {
23613
+ const rootStyle = getComputedStyle(document.documentElement);
23614
+ this.cachedSafeAreas = {
23615
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23616
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23617
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23618
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23619
+ };
23620
+ }
23621
+ return this.cachedSafeAreas;
23622
+ }
23623
+ /**
23624
+ * Updates safe-area CSS variable overrides based on whether the modal
23625
+ * extends into each safe-area region. Called after animation
23626
+ * and during gestures to handle dynamic position changes.
23627
+ *
23628
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23629
+ */
23630
+ updateSafeAreaOverrides() {
23631
+ if (this.skipSafeAreaCoordinateDetection) {
23632
+ return;
23633
+ }
23634
+ const wrapper = this.wrapperEl;
23635
+ if (!wrapper) {
23636
+ return;
23637
+ }
23638
+ const rect = wrapper.getBoundingClientRect();
23639
+ const safeAreas = this.getSafeAreaValues();
23640
+ const extendsIntoTop = rect.top < safeAreas.top;
23641
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23642
+ const extendsIntoLeft = rect.left < safeAreas.left;
23643
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23644
+ // Only update DOM when state actually changes
23645
+ const prev = this.prevSafeAreaState;
23646
+ const style = this.el.style;
23647
+ if (extendsIntoTop !== prev.top) {
23648
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23649
+ prev.top = extendsIntoTop;
23650
+ }
23651
+ if (extendsIntoBottom !== prev.bottom) {
23652
+ extendsIntoBottom
23653
+ ? style.removeProperty('--ion-safe-area-bottom')
23654
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23655
+ prev.bottom = extendsIntoBottom;
23656
+ }
23657
+ if (extendsIntoLeft !== prev.left) {
23658
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23659
+ prev.left = extendsIntoLeft;
23660
+ }
23661
+ if (extendsIntoRight !== prev.right) {
23662
+ extendsIntoRight
23663
+ ? style.removeProperty('--ion-safe-area-right')
23664
+ : style.setProperty('--ion-safe-area-right', '0px');
23665
+ prev.right = extendsIntoRight;
23666
+ }
23667
+ }
23379
23668
  sheetOnDismiss() {
23380
23669
  /**
23381
23670
  * While the gesture animation is finishing
@@ -23468,6 +23757,8 @@ class Modal {
23468
23757
  }
23469
23758
  this.currentBreakpoint = undefined;
23470
23759
  this.animation = undefined;
23760
+ // Reset safe-area state for potential re-presentation
23761
+ this.resetSafeAreaState();
23471
23762
  unlock();
23472
23763
  return dismissed;
23473
23764
  }
@@ -23717,20 +24008,20 @@ class Modal {
23717
24008
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23718
24009
  const isHandleCycle = handleBehavior === 'cycle';
23719
24010
  const isSheetModalWithHandle = isSheetModal && showHandle;
23720
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
24011
+ return (hAsync(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
23721
24012
  // Allow the modal to be navigable when the handle is focusable
23722
24013
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23723
24014
  zIndex: `${20000 + this.overlayIndex}`,
23724
- }, 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 }), hAsync("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
24015
+ }, 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 }), hAsync("ion-backdrop", { key: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
23725
24016
  /*
23726
24017
  role and aria-modal must be used on the
23727
24018
  same element. They must also be set inside the
23728
24019
  shadow DOM otherwise ion-button will not be highlighted
23729
24020
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23730
24021
  */
23731
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
24022
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
23732
24023
  // Prevents the handle from receiving keyboard focus when it does not cycle
23733
- 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) })), hAsync("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
24024
+ 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) })), hAsync("slot", { key: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
23734
24025
  }
23735
24026
  get el() { return getElement(this); }
23736
24027
  static get watchers() { return {
@@ -27308,6 +27599,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27308
27599
  let bottom;
27309
27600
  let originX = contentOriginX;
27310
27601
  let originY = contentOriginY;
27602
+ let checkSafeAreaTop = false;
27603
+ let checkSafeAreaBottom = false;
27311
27604
  let checkSafeAreaLeft = false;
27312
27605
  let checkSafeAreaRight = false;
27313
27606
  const triggerTop = triggerCoordinates
@@ -27352,10 +27645,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27352
27645
  * We chose 12 here so that the popover position looks a bit nicer as
27353
27646
  * it is not right up against the edge of the screen.
27354
27647
  */
27355
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27648
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27356
27649
  arrowTop = top + contentHeight;
27357
27650
  originY = 'bottom';
27358
27651
  addPopoverBottomClass = true;
27652
+ /**
27653
+ * If the popover is positioned near the top edge, account for safe area.
27654
+ * This ensures the popover doesn't overlap with status bars or notches.
27655
+ */
27656
+ if (top <= bodyPadding + safeAreaMargin) {
27657
+ checkSafeAreaTop = true;
27658
+ top = bodyPadding;
27659
+ }
27359
27660
  /**
27360
27661
  * If not enough room for popover to appear
27361
27662
  * above trigger, then cut it off.
@@ -27363,14 +27664,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27363
27664
  }
27364
27665
  else {
27365
27666
  bottom = bodyPadding;
27667
+ /**
27668
+ * When the popover is pinned to the bottom, account for safe area.
27669
+ * This ensures the popover doesn't overlap with home indicators
27670
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
27671
+ */
27672
+ checkSafeAreaBottom = true;
27366
27673
  }
27367
27674
  }
27675
+ /**
27676
+ * Final check: If the popover extends into any safe-area region,
27677
+ * ensure the corresponding flag is set regardless of side.
27678
+ * This handles cases where a side-positioned popover (left/right)
27679
+ * still needs bottom safe-area padding because it extends into that region.
27680
+ */
27681
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
27682
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
27683
+ checkSafeAreaBottom = true;
27684
+ }
27685
+ if (top < safeAreaMargin) {
27686
+ checkSafeAreaTop = true;
27687
+ }
27368
27688
  return {
27369
27689
  top,
27370
27690
  left,
27371
27691
  bottom,
27372
27692
  originX,
27373
27693
  originY,
27694
+ checkSafeAreaTop,
27695
+ checkSafeAreaBottom,
27374
27696
  checkSafeAreaLeft,
27375
27697
  checkSafeAreaRight,
27376
27698
  arrowTop,
@@ -27431,7 +27753,7 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27431
27753
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27432
27754
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27433
27755
  const margin = size === 'cover' ? 0 : 25;
27434
- 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);
27756
+ 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);
27435
27757
  const baseAnimation = createAnimation();
27436
27758
  const backdropAnimation = createAnimation();
27437
27759
  const contentAnimation = createAnimation();
@@ -27461,19 +27783,35 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27461
27783
  if (addPopoverBottomClass) {
27462
27784
  baseEl.classList.add('popover-bottom');
27463
27785
  }
27464
- if (bottom !== undefined) {
27465
- contentEl.style.setProperty('bottom', `${bottom}px`);
27466
- }
27786
+ /**
27787
+ * Safe area CSS variable adjustments.
27788
+ * When the popover is positioned near an edge, we add the corresponding
27789
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27790
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27791
+ */
27792
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27793
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27467
27794
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27468
27795
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27796
+ let topValue = `${top}px`;
27797
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27469
27798
  let leftValue = `${left}px`;
27799
+ if (checkSafeAreaTop) {
27800
+ topValue = `${top}px${safeAreaTop}`;
27801
+ }
27802
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27803
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27804
+ }
27470
27805
  if (checkSafeAreaLeft) {
27471
27806
  leftValue = `${left}px${safeAreaLeft}`;
27472
27807
  }
27473
27808
  if (checkSafeAreaRight) {
27474
27809
  leftValue = `${left}px${safeAreaRight}`;
27475
27810
  }
27476
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27811
+ if (bottomValue !== undefined) {
27812
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27813
+ }
27814
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27477
27815
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27478
27816
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27479
27817
  if (arrowEl !== null) {
@@ -27549,7 +27887,32 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27549
27887
  };
27550
27888
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27551
27889
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27552
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
27890
+ 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);
27891
+ /**
27892
+ * Safe area CSS variable adjustments.
27893
+ * When the popover is positioned near an edge, we add the corresponding
27894
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27895
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27896
+ */
27897
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27898
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27899
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27900
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27901
+ let topValue = `${top}px`;
27902
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27903
+ let leftValue = `${left}px`;
27904
+ if (checkSafeAreaTop) {
27905
+ topValue = `${top}px${safeAreaTop}`;
27906
+ }
27907
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27908
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27909
+ }
27910
+ if (checkSafeAreaLeft) {
27911
+ leftValue = `${left}px${safeAreaLeft}`;
27912
+ }
27913
+ if (checkSafeAreaRight) {
27914
+ leftValue = `${left}px${safeAreaRight}`;
27915
+ }
27553
27916
  const baseAnimation = createAnimation();
27554
27917
  const backdropAnimation = createAnimation();
27555
27918
  const wrapperAnimation = createAnimation();
@@ -27566,13 +27929,13 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27566
27929
  contentAnimation
27567
27930
  .addElement(contentEl)
27568
27931
  .beforeStyles({
27569
- top: `calc(${top}px + var(--offset-y, 0px))`,
27570
- left: `calc(${left}px + var(--offset-x, 0px))`,
27932
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27933
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27571
27934
  'transform-origin': `${originY} ${originX}`,
27572
27935
  })
27573
27936
  .beforeAddWrite(() => {
27574
- if (bottom !== undefined) {
27575
- contentEl.style.setProperty('bottom', `${bottom}px`);
27937
+ if (bottomValue !== undefined) {
27938
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27576
27939
  }
27577
27940
  })
27578
27941
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -34990,7 +35353,6 @@ class TabBar {
34990
35353
  this.ionTabBarChanged = createEvent(this, "ionTabBarChanged", 7);
34991
35354
  this.ionTabBarLoaded = createEvent(this, "ionTabBarLoaded", 7);
34992
35355
  this.keyboardCtrl = null;
34993
- this.keyboardCtrlPromise = null;
34994
35356
  this.didLoad = false;
34995
35357
  this.keyboardVisible = false;
34996
35358
  /**
@@ -35026,7 +35388,7 @@ class TabBar {
35026
35388
  }
35027
35389
  }
35028
35390
  async connectedCallback() {
35029
- const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
35391
+ this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
35030
35392
  /**
35031
35393
  * If the keyboard is hiding, then we need to wait
35032
35394
  * for the webview to resize. Otherwise, the tab bar
@@ -35037,40 +35399,21 @@ class TabBar {
35037
35399
  }
35038
35400
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
35039
35401
  });
35040
- this.keyboardCtrlPromise = promise;
35041
- const keyboardCtrl = await promise;
35042
- /**
35043
- * Only assign if this is still the current promise.
35044
- * Otherwise, a new connectedCallback has started or
35045
- * disconnectedCallback was called, so destroy this instance.
35046
- */
35047
- if (this.keyboardCtrlPromise === promise) {
35048
- this.keyboardCtrl = keyboardCtrl;
35049
- this.keyboardCtrlPromise = null;
35050
- }
35051
- else {
35052
- keyboardCtrl.destroy();
35053
- }
35054
35402
  }
35055
35403
  disconnectedCallback() {
35056
- if (this.keyboardCtrlPromise) {
35057
- this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
35058
- this.keyboardCtrlPromise = null;
35059
- }
35060
35404
  if (this.keyboardCtrl) {
35061
35405
  this.keyboardCtrl.destroy();
35062
- this.keyboardCtrl = null;
35063
35406
  }
35064
35407
  }
35065
35408
  render() {
35066
35409
  const { color, translucent, keyboardVisible } = this;
35067
35410
  const mode = getIonMode$1(this);
35068
35411
  const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
35069
- return (hAsync(Host, { key: '9daf4e2acaff6e3ce3878cf9dd5109fb1afbbebe', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses$1(color, {
35412
+ return (hAsync(Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses$1(color, {
35070
35413
  [mode]: true,
35071
35414
  'tab-bar-translucent': translucent,
35072
35415
  'tab-bar-hidden': shouldHide,
35073
- }) }, hAsync("slot", { key: '1d15aa2da8501e8e7eff11ad4a491478be845c43' })));
35416
+ }) }, hAsync("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
35074
35417
  }
35075
35418
  get el() { return getElement(this); }
35076
35419
  static get watchers() { return {