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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/components/content.js +96 -8
  2. package/components/ion-tab-bar.js +3 -23
  3. package/components/modal.js +213 -12
  4. package/components/popover.js +83 -11
  5. package/dist/cjs/ion-app_8.cjs.entry.js +107 -20
  6. package/dist/cjs/ion-modal.cjs.entry.js +213 -12
  7. package/dist/cjs/ion-popover.cjs.entry.js +83 -11
  8. package/dist/cjs/ion-tab-bar_2.cjs.entry.js +3 -23
  9. package/dist/collection/components/content/content.css +10 -0
  10. package/dist/collection/components/content/content.js +94 -6
  11. package/dist/collection/components/modal/gestures/sheet.js +3 -1
  12. package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
  13. package/dist/collection/components/modal/modal.ios.css +0 -4
  14. package/dist/collection/components/modal/modal.js +205 -7
  15. package/dist/collection/components/modal/modal.md.css +0 -4
  16. package/dist/collection/components/popover/animations/ios.enter.js +21 -5
  17. package/dist/collection/components/popover/animations/md.enter.js +30 -5
  18. package/dist/collection/components/popover/utils.js +32 -1
  19. package/dist/collection/components/tab-bar/tab-bar.js +3 -23
  20. package/dist/docs.json +1 -1
  21. package/dist/esm/ion-app_8.entry.js +96 -9
  22. package/dist/esm/ion-modal.entry.js +213 -12
  23. package/dist/esm/ion-popover.entry.js +83 -11
  24. package/dist/esm/ion-tab-bar_2.entry.js +3 -23
  25. package/dist/ionic/ionic.esm.js +1 -1
  26. package/dist/ionic/p-7268efa5.entry.js +4 -0
  27. package/dist/ionic/p-968a55d1.entry.js +4 -0
  28. package/dist/ionic/p-d9fd799f.entry.js +4 -0
  29. package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
  30. package/dist/types/components/content/content.d.ts +24 -0
  31. package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
  32. package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
  33. package/dist/types/components/modal/modal.d.ts +45 -0
  34. package/dist/types/components/popover/utils.d.ts +2 -0
  35. package/dist/types/components/tab-bar/tab-bar.d.ts +0 -1
  36. package/hydrate/index.js +385 -52
  37. package/hydrate/index.mjs +385 -52
  38. package/package.json +1 -1
  39. package/dist/ionic/p-172a579f.entry.js +0 -4
  40. package/dist/ionic/p-732b2fd6.entry.js +0 -4
  41. package/dist/ionic/p-91840a80.entry.js +0 -4
  42. package/dist/ionic/p-f9061316.entry.js +0 -4
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,79 @@ 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
+ }
10647
+ /**
10648
+ * Updates hasHeader/hasFooter based on current DOM state.
10649
+ * Checks both direct siblings and elements wrapped in custom components
10650
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
10651
+ */
10652
+ updateSiblingDetection() {
10653
+ const parent = this.el.parentElement;
10654
+ if (parent) {
10655
+ // First check for direct ion-header/ion-footer siblings
10656
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
10657
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
10658
+ // If not found, check if any sibling contains them (wrapped components)
10659
+ if (!this.hasHeader) {
10660
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
10661
+ }
10662
+ if (!this.hasFooter) {
10663
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
10664
+ }
10665
+ }
10666
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
10667
+ if (!this.hasFooter) {
10668
+ const tabs = this.el.closest('ion-tabs');
10669
+ if (tabs) {
10670
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
10671
+ }
10672
+ }
10673
+ }
10674
+ /**
10675
+ * Checks if any sibling element of ion-content contains the specified element.
10676
+ * Only searches one level deep to avoid finding elements in nested pages.
10677
+ */
10678
+ siblingContainsElement(parent, tagName) {
10679
+ for (const sibling of parent.children) {
10680
+ // Skip ion-content itself
10681
+ if (sibling === this.el)
10682
+ continue;
10683
+ // Check if this sibling contains the target element as an immediate child
10684
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
10685
+ return true;
10686
+ }
10687
+ }
10688
+ return false;
10689
+ }
10616
10690
  disconnectedCallback() {
10691
+ var _a;
10617
10692
  this.onScrollEnd();
10693
+ // Clean up mutation observer to prevent memory leaks
10694
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
10695
+ this.parentMutationObserver = undefined;
10618
10696
  if (hasLazyBuild(this.el)) {
10619
10697
  /**
10620
10698
  * The event listener and tabs caches need to
@@ -10843,26 +10921,28 @@ class Content {
10843
10921
  }
10844
10922
  }
10845
10923
  render() {
10846
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10924
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10847
10925
  const rtl = isRTL$1(el) ? 'rtl' : 'ltr';
10848
10926
  const mode = getIonMode$1(this);
10849
10927
  const forceOverscroll = this.shouldForceOverscroll();
10850
10928
  const transitionShadow = mode === 'ios';
10851
10929
  this.resize();
10852
- return (hAsync(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10930
+ return (hAsync(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10853
10931
  [mode]: true,
10854
10932
  'content-sizing': hostContext('ion-popover', this.el),
10855
10933
  overscroll: forceOverscroll,
10856
10934
  [`content-${rtl}`]: true,
10935
+ 'safe-area-top': isMainContent && !hasHeader,
10936
+ 'safe-area-bottom': isMainContent && !hasFooter,
10857
10937
  }), style: {
10858
10938
  '--offset-top': `${this.cTop}px`,
10859
10939
  '--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: {
10940
+ } }, inheritedAttributes), hAsync("div", { key: 'b735ec68c18c0b99c3595bb194029830e6542cde', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: 'e76c00d030342d44ade6648c3f9e32ca990787ba', class: {
10861
10941
  'inner-scroll': true,
10862
10942
  'scroll-x': scrollX,
10863
10943
  'scroll-y': scrollY,
10864
10944
  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));
10945
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '9049be4cea9b5da5ec1e1012248b05286fddeb7a' })), 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
10946
  }
10867
10947
  get el() { return getElement(this); }
10868
10948
  static get style() { return contentCss; }
@@ -21533,7 +21613,7 @@ const calculateSpringStep = (t) => {
21533
21613
  const SwipeToCloseDefaults = {
21534
21614
  MIN_PRESENTING_SCALE: 0.915,
21535
21615
  };
21536
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
21616
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
21537
21617
  /**
21538
21618
  * The step value at which a card modal
21539
21619
  * is eligible for dismissing via gesture.
@@ -21690,6 +21770,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
21690
21770
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
21691
21771
  const clampedStep = clamp(0.0001, processedStep, maxStep);
21692
21772
  animation.progressStep(clampedStep);
21773
+ // Notify modal of position change for safe-area updates
21774
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
21693
21775
  /**
21694
21776
  * When swiping down half way, the status bar style
21695
21777
  * should be reset to its default value.
@@ -22233,7 +22315,7 @@ const mdLeaveAnimation$2 = (baseEl, opts) => {
22233
22315
  return baseAnimation;
22234
22316
  };
22235
22317
 
22236
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
22318
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
22237
22319
  // Defaults for the sheet swipe animation
22238
22320
  const defaultBackdrop = [
22239
22321
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -22564,6 +22646,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22564
22646
  : step;
22565
22647
  offset = clamp(0.0001, processedStep, maxStep);
22566
22648
  animation.progressStep(offset);
22649
+ // Notify modal of position change for safe-area updates
22650
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
22567
22651
  };
22568
22652
  const onEnd = (detail) => {
22569
22653
  /**
@@ -22758,9 +22842,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22758
22842
  };
22759
22843
  };
22760
22844
 
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}";
22845
+ 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
22846
 
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}";
22847
+ 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
22848
 
22765
22849
  // TODO(FW-2832): types
22766
22850
  /**
@@ -22793,6 +22877,10 @@ class Modal {
22793
22877
  this.inline = false;
22794
22878
  // Whether or not modal is being dismissed via gesture
22795
22879
  this.gestureAnimationDismissing = false;
22880
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22881
+ this.skipSafeAreaCoordinateDetection = false;
22882
+ // Track previous safe-area state to avoid redundant DOM writes
22883
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22796
22884
  this.presented = false;
22797
22885
  /** @internal */
22798
22886
  this.hasController = false;
@@ -22983,7 +23071,10 @@ class Modal {
22983
23071
  }
22984
23072
  }
22985
23073
  onWindowResize() {
22986
- // Only handle resize for iOS card modals when no custom animations are provided
23074
+ // Invalidate safe-area cache on resize (device rotation may change values)
23075
+ this.cachedSafeAreas = undefined;
23076
+ this.updateSafeAreaOverrides();
23077
+ // Only handle view transition for iOS card modals when no custom animations are provided
22987
23078
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
22988
23079
  return;
22989
23080
  }
@@ -23006,6 +23097,8 @@ class Modal {
23006
23097
  this.triggerController.removeClickListener();
23007
23098
  this.cleanupViewTransitionListener();
23008
23099
  this.cleanupParentRemovalObserver();
23100
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
23101
+ this.resetSafeAreaState();
23009
23102
  }
23010
23103
  componentWillLoad() {
23011
23104
  var _a;
@@ -23165,6 +23258,8 @@ class Modal {
23165
23258
  else if (!this.keepContentsMounted) {
23166
23259
  await waitForMount();
23167
23260
  }
23261
+ // Predict safe-area needs based on modal configuration to avoid visual snap
23262
+ this.setInitialSafeAreaOverrides(presentingElement);
23168
23263
  writeTask(() => this.el.classList.add('show-modal'));
23169
23264
  const hasCardModal = presentingElement !== undefined;
23170
23265
  /**
@@ -23226,6 +23321,8 @@ class Modal {
23226
23321
  else if (hasCardModal) {
23227
23322
  this.initSwipeToClose();
23228
23323
  }
23324
+ // Now that animation is complete, update safe-area based on actual position
23325
+ this.updateSafeAreaOverrides();
23229
23326
  // Initialize view transition listener for iOS card modals
23230
23327
  this.initViewTransitionListener();
23231
23328
  // Initialize parent removal observer
@@ -23277,7 +23374,7 @@ class Modal {
23277
23374
  await this.dismiss(undefined, GESTURE);
23278
23375
  this.gestureAnimationDismissing = false;
23279
23376
  });
23280
- });
23377
+ }, () => this.updateSafeAreaOverrides());
23281
23378
  this.gesture.enable(true);
23282
23379
  }
23283
23380
  initSheetGesture() {
@@ -23298,7 +23395,8 @@ class Modal {
23298
23395
  this.currentBreakpoint = breakpoint;
23299
23396
  this.ionBreakpointDidChange.emit({ breakpoint });
23300
23397
  }
23301
- });
23398
+ this.updateSafeAreaOverrides();
23399
+ }, () => this.updateSafeAreaOverrides());
23302
23400
  this.gesture = gesture;
23303
23401
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
23304
23402
  this.gesture.enable(true);
@@ -23376,6 +23474,187 @@ class Modal {
23376
23474
  // Clear the cached reference
23377
23475
  this.cachedPageParent = undefined;
23378
23476
  }
23477
+ /**
23478
+ * Sets initial safe-area overrides based on modal configuration before
23479
+ * the modal becomes visible. This predicts whether the modal will touch
23480
+ * screen edges to avoid a visual snap after animation completes.
23481
+ */
23482
+ setInitialSafeAreaOverrides(presentingElement) {
23483
+ const style = this.el.style;
23484
+ const mode = getIonMode$1(this);
23485
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23486
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
23487
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
23488
+ const isTablet = window.innerWidth >= 768;
23489
+ // Sheet modals always touch bottom edge, never top/left/right
23490
+ if (isSheetModal) {
23491
+ style.setProperty('--ion-safe-area-top', '0px');
23492
+ style.setProperty('--ion-safe-area-left', '0px');
23493
+ style.setProperty('--ion-safe-area-right', '0px');
23494
+ return;
23495
+ }
23496
+ // Card modals have rounded top corners
23497
+ if (isCardModal) {
23498
+ style.setProperty('--ion-safe-area-top', '0px');
23499
+ if (isTablet) {
23500
+ // On tablets, card modals are inset from all edges
23501
+ this.zeroAllSafeAreas();
23502
+ }
23503
+ else {
23504
+ // On phones, card modals still extend to the bottom edge
23505
+ style.setProperty('--ion-safe-area-left', '0px');
23506
+ style.setProperty('--ion-safe-area-right', '0px');
23507
+ this.applyFullscreenSafeArea();
23508
+ }
23509
+ return;
23510
+ }
23511
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
23512
+ if (!isTablet) {
23513
+ this.applyFullscreenSafeArea();
23514
+ return;
23515
+ }
23516
+ // Check if tablet modal is fullscreen via CSS custom properties
23517
+ const computedStyle = getComputedStyle(this.el);
23518
+ const width = computedStyle.getPropertyValue('--width').trim();
23519
+ const height = computedStyle.getPropertyValue('--height').trim();
23520
+ const isFullscreen = width === '100%' && height === '100%';
23521
+ if (isFullscreen) {
23522
+ this.applyFullscreenSafeArea();
23523
+ }
23524
+ else {
23525
+ // Centered dialog doesn't touch edges
23526
+ this.zeroAllSafeAreas();
23527
+ }
23528
+ }
23529
+ /**
23530
+ * Applies safe-area handling for fullscreen modals.
23531
+ * Adds wrapper padding when no footer is present to prevent
23532
+ * content from overlapping system navigation areas.
23533
+ */
23534
+ applyFullscreenSafeArea() {
23535
+ this.skipSafeAreaCoordinateDetection = true;
23536
+ this.updateFooterPadding();
23537
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23538
+ // Use subtree:true to support wrapped footers in framework components
23539
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
23540
+ if (!this.footerObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
23541
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23542
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23543
+ }
23544
+ }
23545
+ /**
23546
+ * Updates wrapper padding based on footer presence.
23547
+ * Called initially and when footer is dynamically added/removed.
23548
+ */
23549
+ updateFooterPadding() {
23550
+ if (!this.wrapperEl)
23551
+ return;
23552
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
23553
+ if (hasFooter) {
23554
+ this.wrapperEl.style.removeProperty('padding-bottom');
23555
+ this.wrapperEl.style.removeProperty('box-sizing');
23556
+ }
23557
+ else {
23558
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23559
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23560
+ }
23561
+ }
23562
+ /**
23563
+ * Sets all safe-area CSS variables to 0px for modals that
23564
+ * don't touch screen edges.
23565
+ */
23566
+ zeroAllSafeAreas() {
23567
+ const style = this.el.style;
23568
+ style.setProperty('--ion-safe-area-top', '0px');
23569
+ style.setProperty('--ion-safe-area-bottom', '0px');
23570
+ style.setProperty('--ion-safe-area-left', '0px');
23571
+ style.setProperty('--ion-safe-area-right', '0px');
23572
+ }
23573
+ /**
23574
+ * Resets all safe-area related state and styles.
23575
+ * Called during dismiss and disconnectedCallback to ensure clean state
23576
+ * for re-presentation of inline modals.
23577
+ */
23578
+ resetSafeAreaState() {
23579
+ var _a;
23580
+ this.skipSafeAreaCoordinateDetection = false;
23581
+ this.cachedSafeAreas = undefined;
23582
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23583
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23584
+ this.footerObserver = undefined;
23585
+ // Clear wrapper styles that may have been set for safe-area handling
23586
+ if (this.wrapperEl) {
23587
+ this.wrapperEl.style.removeProperty('padding-bottom');
23588
+ this.wrapperEl.style.removeProperty('box-sizing');
23589
+ }
23590
+ // Clear safe-area CSS variable overrides
23591
+ const style = this.el.style;
23592
+ style.removeProperty('--ion-safe-area-top');
23593
+ style.removeProperty('--ion-safe-area-bottom');
23594
+ style.removeProperty('--ion-safe-area-left');
23595
+ style.removeProperty('--ion-safe-area-right');
23596
+ }
23597
+ /**
23598
+ * Gets the root safe-area values from the document element.
23599
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23600
+ */
23601
+ getSafeAreaValues() {
23602
+ if (!this.cachedSafeAreas) {
23603
+ const rootStyle = getComputedStyle(document.documentElement);
23604
+ this.cachedSafeAreas = {
23605
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23606
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23607
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23608
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23609
+ };
23610
+ }
23611
+ return this.cachedSafeAreas;
23612
+ }
23613
+ /**
23614
+ * Updates safe-area CSS variable overrides based on whether the modal
23615
+ * extends into each safe-area region. Called after animation
23616
+ * and during gestures to handle dynamic position changes.
23617
+ *
23618
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23619
+ */
23620
+ updateSafeAreaOverrides() {
23621
+ if (this.skipSafeAreaCoordinateDetection) {
23622
+ return;
23623
+ }
23624
+ const wrapper = this.wrapperEl;
23625
+ if (!wrapper) {
23626
+ return;
23627
+ }
23628
+ const rect = wrapper.getBoundingClientRect();
23629
+ const safeAreas = this.getSafeAreaValues();
23630
+ const extendsIntoTop = rect.top < safeAreas.top;
23631
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23632
+ const extendsIntoLeft = rect.left < safeAreas.left;
23633
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23634
+ // Only update DOM when state actually changes
23635
+ const prev = this.prevSafeAreaState;
23636
+ const style = this.el.style;
23637
+ if (extendsIntoTop !== prev.top) {
23638
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23639
+ prev.top = extendsIntoTop;
23640
+ }
23641
+ if (extendsIntoBottom !== prev.bottom) {
23642
+ extendsIntoBottom
23643
+ ? style.removeProperty('--ion-safe-area-bottom')
23644
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23645
+ prev.bottom = extendsIntoBottom;
23646
+ }
23647
+ if (extendsIntoLeft !== prev.left) {
23648
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23649
+ prev.left = extendsIntoLeft;
23650
+ }
23651
+ if (extendsIntoRight !== prev.right) {
23652
+ extendsIntoRight
23653
+ ? style.removeProperty('--ion-safe-area-right')
23654
+ : style.setProperty('--ion-safe-area-right', '0px');
23655
+ prev.right = extendsIntoRight;
23656
+ }
23657
+ }
23379
23658
  sheetOnDismiss() {
23380
23659
  /**
23381
23660
  * While the gesture animation is finishing
@@ -23468,6 +23747,8 @@ class Modal {
23468
23747
  }
23469
23748
  this.currentBreakpoint = undefined;
23470
23749
  this.animation = undefined;
23750
+ // Reset safe-area state for potential re-presentation
23751
+ this.resetSafeAreaState();
23471
23752
  unlock();
23472
23753
  return dismissed;
23473
23754
  }
@@ -23717,20 +23998,20 @@ class Modal {
23717
23998
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23718
23999
  const isHandleCycle = handleBehavior === 'cycle';
23719
24000
  const isSheetModalWithHandle = isSheetModal && showHandle;
23720
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
24001
+ return (hAsync(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
23721
24002
  // Allow the modal to be navigable when the handle is focusable
23722
24003
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23723
24004
  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',
24005
+ }, 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
24006
  /*
23726
24007
  role and aria-modal must be used on the
23727
24008
  same element. They must also be set inside the
23728
24009
  shadow DOM otherwise ion-button will not be highlighted
23729
24010
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23730
24011
  */
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",
24012
+ 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
24013
  // 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 }))));
24014
+ 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
24015
  }
23735
24016
  get el() { return getElement(this); }
23736
24017
  static get watchers() { return {
@@ -27308,6 +27589,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27308
27589
  let bottom;
27309
27590
  let originX = contentOriginX;
27310
27591
  let originY = contentOriginY;
27592
+ let checkSafeAreaTop = false;
27593
+ let checkSafeAreaBottom = false;
27311
27594
  let checkSafeAreaLeft = false;
27312
27595
  let checkSafeAreaRight = false;
27313
27596
  const triggerTop = triggerCoordinates
@@ -27352,10 +27635,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27352
27635
  * We chose 12 here so that the popover position looks a bit nicer as
27353
27636
  * it is not right up against the edge of the screen.
27354
27637
  */
27355
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27638
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27356
27639
  arrowTop = top + contentHeight;
27357
27640
  originY = 'bottom';
27358
27641
  addPopoverBottomClass = true;
27642
+ /**
27643
+ * If the popover is positioned near the top edge, account for safe area.
27644
+ * This ensures the popover doesn't overlap with status bars or notches.
27645
+ */
27646
+ if (top <= bodyPadding + safeAreaMargin) {
27647
+ checkSafeAreaTop = true;
27648
+ top = bodyPadding;
27649
+ }
27359
27650
  /**
27360
27651
  * If not enough room for popover to appear
27361
27652
  * above trigger, then cut it off.
@@ -27363,14 +27654,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27363
27654
  }
27364
27655
  else {
27365
27656
  bottom = bodyPadding;
27657
+ /**
27658
+ * When the popover is pinned to the bottom, account for safe area.
27659
+ * This ensures the popover doesn't overlap with home indicators
27660
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
27661
+ */
27662
+ checkSafeAreaBottom = true;
27366
27663
  }
27367
27664
  }
27665
+ /**
27666
+ * Final check: If the popover extends into any safe-area region,
27667
+ * ensure the corresponding flag is set regardless of side.
27668
+ * This handles cases where a side-positioned popover (left/right)
27669
+ * still needs bottom safe-area padding because it extends into that region.
27670
+ */
27671
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
27672
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
27673
+ checkSafeAreaBottom = true;
27674
+ }
27675
+ if (top < safeAreaMargin) {
27676
+ checkSafeAreaTop = true;
27677
+ }
27368
27678
  return {
27369
27679
  top,
27370
27680
  left,
27371
27681
  bottom,
27372
27682
  originX,
27373
27683
  originY,
27684
+ checkSafeAreaTop,
27685
+ checkSafeAreaBottom,
27374
27686
  checkSafeAreaLeft,
27375
27687
  checkSafeAreaRight,
27376
27688
  arrowTop,
@@ -27431,7 +27743,7 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27431
27743
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27432
27744
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27433
27745
  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);
27746
+ 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
27747
  const baseAnimation = createAnimation();
27436
27748
  const backdropAnimation = createAnimation();
27437
27749
  const contentAnimation = createAnimation();
@@ -27461,19 +27773,35 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27461
27773
  if (addPopoverBottomClass) {
27462
27774
  baseEl.classList.add('popover-bottom');
27463
27775
  }
27464
- if (bottom !== undefined) {
27465
- contentEl.style.setProperty('bottom', `${bottom}px`);
27466
- }
27776
+ /**
27777
+ * Safe area CSS variable adjustments.
27778
+ * When the popover is positioned near an edge, we add the corresponding
27779
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27780
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27781
+ */
27782
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27783
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27467
27784
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27468
27785
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27786
+ let topValue = `${top}px`;
27787
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27469
27788
  let leftValue = `${left}px`;
27789
+ if (checkSafeAreaTop) {
27790
+ topValue = `${top}px${safeAreaTop}`;
27791
+ }
27792
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27793
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27794
+ }
27470
27795
  if (checkSafeAreaLeft) {
27471
27796
  leftValue = `${left}px${safeAreaLeft}`;
27472
27797
  }
27473
27798
  if (checkSafeAreaRight) {
27474
27799
  leftValue = `${left}px${safeAreaRight}`;
27475
27800
  }
27476
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27801
+ if (bottomValue !== undefined) {
27802
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27803
+ }
27804
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27477
27805
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27478
27806
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27479
27807
  if (arrowEl !== null) {
@@ -27549,7 +27877,32 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27549
27877
  };
27550
27878
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27551
27879
  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);
27880
+ 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);
27881
+ /**
27882
+ * Safe area CSS variable adjustments.
27883
+ * When the popover is positioned near an edge, we add the corresponding
27884
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27885
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27886
+ */
27887
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27888
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27889
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27890
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27891
+ let topValue = `${top}px`;
27892
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27893
+ let leftValue = `${left}px`;
27894
+ if (checkSafeAreaTop) {
27895
+ topValue = `${top}px${safeAreaTop}`;
27896
+ }
27897
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27898
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27899
+ }
27900
+ if (checkSafeAreaLeft) {
27901
+ leftValue = `${left}px${safeAreaLeft}`;
27902
+ }
27903
+ if (checkSafeAreaRight) {
27904
+ leftValue = `${left}px${safeAreaRight}`;
27905
+ }
27553
27906
  const baseAnimation = createAnimation();
27554
27907
  const backdropAnimation = createAnimation();
27555
27908
  const wrapperAnimation = createAnimation();
@@ -27566,13 +27919,13 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27566
27919
  contentAnimation
27567
27920
  .addElement(contentEl)
27568
27921
  .beforeStyles({
27569
- top: `calc(${top}px + var(--offset-y, 0px))`,
27570
- left: `calc(${left}px + var(--offset-x, 0px))`,
27922
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27923
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27571
27924
  'transform-origin': `${originY} ${originX}`,
27572
27925
  })
27573
27926
  .beforeAddWrite(() => {
27574
- if (bottom !== undefined) {
27575
- contentEl.style.setProperty('bottom', `${bottom}px`);
27927
+ if (bottomValue !== undefined) {
27928
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27576
27929
  }
27577
27930
  })
27578
27931
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -34990,7 +35343,6 @@ class TabBar {
34990
35343
  this.ionTabBarChanged = createEvent(this, "ionTabBarChanged", 7);
34991
35344
  this.ionTabBarLoaded = createEvent(this, "ionTabBarLoaded", 7);
34992
35345
  this.keyboardCtrl = null;
34993
- this.keyboardCtrlPromise = null;
34994
35346
  this.didLoad = false;
34995
35347
  this.keyboardVisible = false;
34996
35348
  /**
@@ -35026,7 +35378,7 @@ class TabBar {
35026
35378
  }
35027
35379
  }
35028
35380
  async connectedCallback() {
35029
- const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
35381
+ this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
35030
35382
  /**
35031
35383
  * If the keyboard is hiding, then we need to wait
35032
35384
  * for the webview to resize. Otherwise, the tab bar
@@ -35037,40 +35389,21 @@ class TabBar {
35037
35389
  }
35038
35390
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
35039
35391
  });
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
35392
  }
35055
35393
  disconnectedCallback() {
35056
- if (this.keyboardCtrlPromise) {
35057
- this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
35058
- this.keyboardCtrlPromise = null;
35059
- }
35060
35394
  if (this.keyboardCtrl) {
35061
35395
  this.keyboardCtrl.destroy();
35062
- this.keyboardCtrl = null;
35063
35396
  }
35064
35397
  }
35065
35398
  render() {
35066
35399
  const { color, translucent, keyboardVisible } = this;
35067
35400
  const mode = getIonMode$1(this);
35068
35401
  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, {
35402
+ return (hAsync(Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses$1(color, {
35070
35403
  [mode]: true,
35071
35404
  'tab-bar-translucent': translucent,
35072
35405
  'tab-bar-hidden': shouldHide,
35073
- }) }, hAsync("slot", { key: '1d15aa2da8501e8e7eff11ad4a491478be845c43' })));
35406
+ }) }, hAsync("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
35074
35407
  }
35075
35408
  get el() { return getElement(this); }
35076
35409
  static get watchers() { return {