@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.mjs CHANGED
@@ -10497,7 +10497,7 @@ const isRTL$1 = (hostEl) => {
10497
10497
  return (document === null || document === void 0 ? void 0 : document.dir.toLowerCase()) === 'rtl';
10498
10498
  };
10499
10499
 
10500
- 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)}";
10500
+ 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)}";
10501
10501
 
10502
10502
  /**
10503
10503
  * @slot - Content is placed in the scrollable area if provided without a slot.
@@ -10521,6 +10521,12 @@ class Content {
10521
10521
  this.isMainContent = true;
10522
10522
  this.resizeTimeout = null;
10523
10523
  this.inheritedAttributes = {};
10524
+ /**
10525
+ * Track whether this content has sibling header/footer elements.
10526
+ * When absent, we need to apply safe-area padding directly.
10527
+ */
10528
+ this.hasHeader = false;
10529
+ this.hasFooter = false;
10524
10530
  this.tabsElement = null;
10525
10531
  // Detail is used in a hot loop in the scroll event, by allocating it here
10526
10532
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -10575,7 +10581,13 @@ class Content {
10575
10581
  this.inheritedAttributes = inheritAriaAttributes(this.el);
10576
10582
  }
10577
10583
  connectedCallback() {
10578
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
10584
+ var _a;
10585
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
10586
+ this.isMainContent =
10587
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
10588
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
10589
+ // Detect sibling header/footer for safe-area handling
10590
+ this.detectSiblingElements();
10579
10591
  /**
10580
10592
  * The fullscreen content offsets need to be
10581
10593
  * computed after the tab bar has loaded. Since
@@ -10606,13 +10618,79 @@ class Content {
10606
10618
  * bubbles, we can catch any instances of child tab bars loading by listening
10607
10619
  * on IonTabs.
10608
10620
  */
10609
- this.tabsLoadCallback = () => this.resize();
10621
+ this.tabsLoadCallback = () => {
10622
+ this.resize();
10623
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
10624
+ this.updateSiblingDetection();
10625
+ };
10610
10626
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
10611
10627
  }
10612
10628
  }
10613
10629
  }
10630
+ /**
10631
+ * Detects sibling ion-header and ion-footer elements and sets up
10632
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
10633
+ */
10634
+ detectSiblingElements() {
10635
+ this.updateSiblingDetection();
10636
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
10637
+ const parent = this.el.parentElement;
10638
+ if (parent && !this.parentMutationObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
10639
+ this.parentMutationObserver = new MutationObserver(() => {
10640
+ this.updateSiblingDetection();
10641
+ });
10642
+ this.parentMutationObserver.observe(parent, { childList: true });
10643
+ }
10644
+ }
10645
+ /**
10646
+ * Updates hasHeader/hasFooter based on current DOM state.
10647
+ * Checks both direct siblings and elements wrapped in custom components
10648
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
10649
+ */
10650
+ updateSiblingDetection() {
10651
+ const parent = this.el.parentElement;
10652
+ if (parent) {
10653
+ // First check for direct ion-header/ion-footer siblings
10654
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
10655
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
10656
+ // If not found, check if any sibling contains them (wrapped components)
10657
+ if (!this.hasHeader) {
10658
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
10659
+ }
10660
+ if (!this.hasFooter) {
10661
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
10662
+ }
10663
+ }
10664
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
10665
+ if (!this.hasFooter) {
10666
+ const tabs = this.el.closest('ion-tabs');
10667
+ if (tabs) {
10668
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
10669
+ }
10670
+ }
10671
+ }
10672
+ /**
10673
+ * Checks if any sibling element of ion-content contains the specified element.
10674
+ * Only searches one level deep to avoid finding elements in nested pages.
10675
+ */
10676
+ siblingContainsElement(parent, tagName) {
10677
+ for (const sibling of parent.children) {
10678
+ // Skip ion-content itself
10679
+ if (sibling === this.el)
10680
+ continue;
10681
+ // Check if this sibling contains the target element as an immediate child
10682
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
10683
+ return true;
10684
+ }
10685
+ }
10686
+ return false;
10687
+ }
10614
10688
  disconnectedCallback() {
10689
+ var _a;
10615
10690
  this.onScrollEnd();
10691
+ // Clean up mutation observer to prevent memory leaks
10692
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
10693
+ this.parentMutationObserver = undefined;
10616
10694
  if (hasLazyBuild(this.el)) {
10617
10695
  /**
10618
10696
  * The event listener and tabs caches need to
@@ -10841,26 +10919,28 @@ class Content {
10841
10919
  }
10842
10920
  }
10843
10921
  render() {
10844
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10922
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
10845
10923
  const rtl = isRTL$1(el) ? 'rtl' : 'ltr';
10846
10924
  const mode = getIonMode$1(this);
10847
10925
  const forceOverscroll = this.shouldForceOverscroll();
10848
10926
  const transitionShadow = mode === 'ios';
10849
10927
  this.resize();
10850
- return (hAsync(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10928
+ return (hAsync(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10851
10929
  [mode]: true,
10852
10930
  'content-sizing': hostContext('ion-popover', this.el),
10853
10931
  overscroll: forceOverscroll,
10854
10932
  [`content-${rtl}`]: true,
10933
+ 'safe-area-top': isMainContent && !hasHeader,
10934
+ 'safe-area-bottom': isMainContent && !hasFooter,
10855
10935
  }), style: {
10856
10936
  '--offset-top': `${this.cTop}px`,
10857
10937
  '--offset-bottom': `${this.cBottom}px`,
10858
- } }, 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: {
10938
+ } }, 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: {
10859
10939
  'inner-scroll': true,
10860
10940
  'scroll-x': scrollX,
10861
10941
  'scroll-y': scrollY,
10862
10942
  overscroll: (scrollX || scrollY) && forceOverscroll,
10863
- }, 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));
10943
+ }, 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));
10864
10944
  }
10865
10945
  get el() { return getElement(this); }
10866
10946
  static get style() { return contentCss; }
@@ -21531,7 +21611,7 @@ const calculateSpringStep = (t) => {
21531
21611
  const SwipeToCloseDefaults = {
21532
21612
  MIN_PRESENTING_SCALE: 0.915,
21533
21613
  };
21534
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
21614
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
21535
21615
  /**
21536
21616
  * The step value at which a card modal
21537
21617
  * is eligible for dismissing via gesture.
@@ -21688,6 +21768,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
21688
21768
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
21689
21769
  const clampedStep = clamp(0.0001, processedStep, maxStep);
21690
21770
  animation.progressStep(clampedStep);
21771
+ // Notify modal of position change for safe-area updates
21772
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
21691
21773
  /**
21692
21774
  * When swiping down half way, the status bar style
21693
21775
  * should be reset to its default value.
@@ -22231,7 +22313,7 @@ const mdLeaveAnimation$2 = (baseEl, opts) => {
22231
22313
  return baseAnimation;
22232
22314
  };
22233
22315
 
22234
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
22316
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
22235
22317
  // Defaults for the sheet swipe animation
22236
22318
  const defaultBackdrop = [
22237
22319
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -22562,6 +22644,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22562
22644
  : step;
22563
22645
  offset = clamp(0.0001, processedStep, maxStep);
22564
22646
  animation.progressStep(offset);
22647
+ // Notify modal of position change for safe-area updates
22648
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
22565
22649
  };
22566
22650
  const onEnd = (detail) => {
22567
22651
  /**
@@ -22756,9 +22840,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22756
22840
  };
22757
22841
  };
22758
22842
 
22759
- 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}";
22843
+ 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}";
22760
22844
 
22761
- 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}";
22845
+ 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}";
22762
22846
 
22763
22847
  // TODO(FW-2832): types
22764
22848
  /**
@@ -22791,6 +22875,10 @@ class Modal {
22791
22875
  this.inline = false;
22792
22876
  // Whether or not modal is being dismissed via gesture
22793
22877
  this.gestureAnimationDismissing = false;
22878
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22879
+ this.skipSafeAreaCoordinateDetection = false;
22880
+ // Track previous safe-area state to avoid redundant DOM writes
22881
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22794
22882
  this.presented = false;
22795
22883
  /** @internal */
22796
22884
  this.hasController = false;
@@ -22981,7 +23069,10 @@ class Modal {
22981
23069
  }
22982
23070
  }
22983
23071
  onWindowResize() {
22984
- // Only handle resize for iOS card modals when no custom animations are provided
23072
+ // Invalidate safe-area cache on resize (device rotation may change values)
23073
+ this.cachedSafeAreas = undefined;
23074
+ this.updateSafeAreaOverrides();
23075
+ // Only handle view transition for iOS card modals when no custom animations are provided
22985
23076
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
22986
23077
  return;
22987
23078
  }
@@ -23004,6 +23095,8 @@ class Modal {
23004
23095
  this.triggerController.removeClickListener();
23005
23096
  this.cleanupViewTransitionListener();
23006
23097
  this.cleanupParentRemovalObserver();
23098
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
23099
+ this.resetSafeAreaState();
23007
23100
  }
23008
23101
  componentWillLoad() {
23009
23102
  var _a;
@@ -23163,6 +23256,8 @@ class Modal {
23163
23256
  else if (!this.keepContentsMounted) {
23164
23257
  await waitForMount();
23165
23258
  }
23259
+ // Predict safe-area needs based on modal configuration to avoid visual snap
23260
+ this.setInitialSafeAreaOverrides(presentingElement);
23166
23261
  writeTask(() => this.el.classList.add('show-modal'));
23167
23262
  const hasCardModal = presentingElement !== undefined;
23168
23263
  /**
@@ -23224,6 +23319,8 @@ class Modal {
23224
23319
  else if (hasCardModal) {
23225
23320
  this.initSwipeToClose();
23226
23321
  }
23322
+ // Now that animation is complete, update safe-area based on actual position
23323
+ this.updateSafeAreaOverrides();
23227
23324
  // Initialize view transition listener for iOS card modals
23228
23325
  this.initViewTransitionListener();
23229
23326
  // Initialize parent removal observer
@@ -23275,7 +23372,7 @@ class Modal {
23275
23372
  await this.dismiss(undefined, GESTURE);
23276
23373
  this.gestureAnimationDismissing = false;
23277
23374
  });
23278
- });
23375
+ }, () => this.updateSafeAreaOverrides());
23279
23376
  this.gesture.enable(true);
23280
23377
  }
23281
23378
  initSheetGesture() {
@@ -23296,7 +23393,8 @@ class Modal {
23296
23393
  this.currentBreakpoint = breakpoint;
23297
23394
  this.ionBreakpointDidChange.emit({ breakpoint });
23298
23395
  }
23299
- });
23396
+ this.updateSafeAreaOverrides();
23397
+ }, () => this.updateSafeAreaOverrides());
23300
23398
  this.gesture = gesture;
23301
23399
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
23302
23400
  this.gesture.enable(true);
@@ -23374,6 +23472,187 @@ class Modal {
23374
23472
  // Clear the cached reference
23375
23473
  this.cachedPageParent = undefined;
23376
23474
  }
23475
+ /**
23476
+ * Sets initial safe-area overrides based on modal configuration before
23477
+ * the modal becomes visible. This predicts whether the modal will touch
23478
+ * screen edges to avoid a visual snap after animation completes.
23479
+ */
23480
+ setInitialSafeAreaOverrides(presentingElement) {
23481
+ const style = this.el.style;
23482
+ const mode = getIonMode$1(this);
23483
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23484
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
23485
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
23486
+ const isTablet = window.innerWidth >= 768;
23487
+ // Sheet modals always touch bottom edge, never top/left/right
23488
+ if (isSheetModal) {
23489
+ style.setProperty('--ion-safe-area-top', '0px');
23490
+ style.setProperty('--ion-safe-area-left', '0px');
23491
+ style.setProperty('--ion-safe-area-right', '0px');
23492
+ return;
23493
+ }
23494
+ // Card modals have rounded top corners
23495
+ if (isCardModal) {
23496
+ style.setProperty('--ion-safe-area-top', '0px');
23497
+ if (isTablet) {
23498
+ // On tablets, card modals are inset from all edges
23499
+ this.zeroAllSafeAreas();
23500
+ }
23501
+ else {
23502
+ // On phones, card modals still extend to the bottom edge
23503
+ style.setProperty('--ion-safe-area-left', '0px');
23504
+ style.setProperty('--ion-safe-area-right', '0px');
23505
+ this.applyFullscreenSafeArea();
23506
+ }
23507
+ return;
23508
+ }
23509
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
23510
+ if (!isTablet) {
23511
+ this.applyFullscreenSafeArea();
23512
+ return;
23513
+ }
23514
+ // Check if tablet modal is fullscreen via CSS custom properties
23515
+ const computedStyle = getComputedStyle(this.el);
23516
+ const width = computedStyle.getPropertyValue('--width').trim();
23517
+ const height = computedStyle.getPropertyValue('--height').trim();
23518
+ const isFullscreen = width === '100%' && height === '100%';
23519
+ if (isFullscreen) {
23520
+ this.applyFullscreenSafeArea();
23521
+ }
23522
+ else {
23523
+ // Centered dialog doesn't touch edges
23524
+ this.zeroAllSafeAreas();
23525
+ }
23526
+ }
23527
+ /**
23528
+ * Applies safe-area handling for fullscreen modals.
23529
+ * Adds wrapper padding when no footer is present to prevent
23530
+ * content from overlapping system navigation areas.
23531
+ */
23532
+ applyFullscreenSafeArea() {
23533
+ this.skipSafeAreaCoordinateDetection = true;
23534
+ this.updateFooterPadding();
23535
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23536
+ // Use subtree:true to support wrapped footers in framework components
23537
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
23538
+ if (!this.footerObserver && win$1 !== undefined && 'MutationObserver' in win$1) {
23539
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23540
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23541
+ }
23542
+ }
23543
+ /**
23544
+ * Updates wrapper padding based on footer presence.
23545
+ * Called initially and when footer is dynamically added/removed.
23546
+ */
23547
+ updateFooterPadding() {
23548
+ if (!this.wrapperEl)
23549
+ return;
23550
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
23551
+ if (hasFooter) {
23552
+ this.wrapperEl.style.removeProperty('padding-bottom');
23553
+ this.wrapperEl.style.removeProperty('box-sizing');
23554
+ }
23555
+ else {
23556
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23557
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23558
+ }
23559
+ }
23560
+ /**
23561
+ * Sets all safe-area CSS variables to 0px for modals that
23562
+ * don't touch screen edges.
23563
+ */
23564
+ zeroAllSafeAreas() {
23565
+ const style = this.el.style;
23566
+ style.setProperty('--ion-safe-area-top', '0px');
23567
+ style.setProperty('--ion-safe-area-bottom', '0px');
23568
+ style.setProperty('--ion-safe-area-left', '0px');
23569
+ style.setProperty('--ion-safe-area-right', '0px');
23570
+ }
23571
+ /**
23572
+ * Resets all safe-area related state and styles.
23573
+ * Called during dismiss and disconnectedCallback to ensure clean state
23574
+ * for re-presentation of inline modals.
23575
+ */
23576
+ resetSafeAreaState() {
23577
+ var _a;
23578
+ this.skipSafeAreaCoordinateDetection = false;
23579
+ this.cachedSafeAreas = undefined;
23580
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23581
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23582
+ this.footerObserver = undefined;
23583
+ // Clear wrapper styles that may have been set for safe-area handling
23584
+ if (this.wrapperEl) {
23585
+ this.wrapperEl.style.removeProperty('padding-bottom');
23586
+ this.wrapperEl.style.removeProperty('box-sizing');
23587
+ }
23588
+ // Clear safe-area CSS variable overrides
23589
+ const style = this.el.style;
23590
+ style.removeProperty('--ion-safe-area-top');
23591
+ style.removeProperty('--ion-safe-area-bottom');
23592
+ style.removeProperty('--ion-safe-area-left');
23593
+ style.removeProperty('--ion-safe-area-right');
23594
+ }
23595
+ /**
23596
+ * Gets the root safe-area values from the document element.
23597
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23598
+ */
23599
+ getSafeAreaValues() {
23600
+ if (!this.cachedSafeAreas) {
23601
+ const rootStyle = getComputedStyle(document.documentElement);
23602
+ this.cachedSafeAreas = {
23603
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23604
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23605
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23606
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23607
+ };
23608
+ }
23609
+ return this.cachedSafeAreas;
23610
+ }
23611
+ /**
23612
+ * Updates safe-area CSS variable overrides based on whether the modal
23613
+ * extends into each safe-area region. Called after animation
23614
+ * and during gestures to handle dynamic position changes.
23615
+ *
23616
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23617
+ */
23618
+ updateSafeAreaOverrides() {
23619
+ if (this.skipSafeAreaCoordinateDetection) {
23620
+ return;
23621
+ }
23622
+ const wrapper = this.wrapperEl;
23623
+ if (!wrapper) {
23624
+ return;
23625
+ }
23626
+ const rect = wrapper.getBoundingClientRect();
23627
+ const safeAreas = this.getSafeAreaValues();
23628
+ const extendsIntoTop = rect.top < safeAreas.top;
23629
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23630
+ const extendsIntoLeft = rect.left < safeAreas.left;
23631
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23632
+ // Only update DOM when state actually changes
23633
+ const prev = this.prevSafeAreaState;
23634
+ const style = this.el.style;
23635
+ if (extendsIntoTop !== prev.top) {
23636
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23637
+ prev.top = extendsIntoTop;
23638
+ }
23639
+ if (extendsIntoBottom !== prev.bottom) {
23640
+ extendsIntoBottom
23641
+ ? style.removeProperty('--ion-safe-area-bottom')
23642
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23643
+ prev.bottom = extendsIntoBottom;
23644
+ }
23645
+ if (extendsIntoLeft !== prev.left) {
23646
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23647
+ prev.left = extendsIntoLeft;
23648
+ }
23649
+ if (extendsIntoRight !== prev.right) {
23650
+ extendsIntoRight
23651
+ ? style.removeProperty('--ion-safe-area-right')
23652
+ : style.setProperty('--ion-safe-area-right', '0px');
23653
+ prev.right = extendsIntoRight;
23654
+ }
23655
+ }
23377
23656
  sheetOnDismiss() {
23378
23657
  /**
23379
23658
  * While the gesture animation is finishing
@@ -23466,6 +23745,8 @@ class Modal {
23466
23745
  }
23467
23746
  this.currentBreakpoint = undefined;
23468
23747
  this.animation = undefined;
23748
+ // Reset safe-area state for potential re-presentation
23749
+ this.resetSafeAreaState();
23469
23750
  unlock();
23470
23751
  return dismissed;
23471
23752
  }
@@ -23715,20 +23996,20 @@ class Modal {
23715
23996
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23716
23997
  const isHandleCycle = handleBehavior === 'cycle';
23717
23998
  const isSheetModalWithHandle = isSheetModal && showHandle;
23718
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
23999
+ return (hAsync(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
23719
24000
  // Allow the modal to be navigable when the handle is focusable
23720
24001
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23721
24002
  zIndex: `${20000 + this.overlayIndex}`,
23722
- }, 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',
24003
+ }, 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',
23723
24004
  /*
23724
24005
  role and aria-modal must be used on the
23725
24006
  same element. They must also be set inside the
23726
24007
  shadow DOM otherwise ion-button will not be highlighted
23727
24008
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23728
24009
  */
23729
- 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",
24010
+ 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",
23730
24011
  // Prevents the handle from receiving keyboard focus when it does not cycle
23731
- 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 }))));
24012
+ 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 }))));
23732
24013
  }
23733
24014
  get el() { return getElement(this); }
23734
24015
  static get watchers() { return {
@@ -27306,6 +27587,8 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27306
27587
  let bottom;
27307
27588
  let originX = contentOriginX;
27308
27589
  let originY = contentOriginY;
27590
+ let checkSafeAreaTop = false;
27591
+ let checkSafeAreaBottom = false;
27309
27592
  let checkSafeAreaLeft = false;
27310
27593
  let checkSafeAreaRight = false;
27311
27594
  const triggerTop = triggerCoordinates
@@ -27350,10 +27633,18 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27350
27633
  * We chose 12 here so that the popover position looks a bit nicer as
27351
27634
  * it is not right up against the edge of the screen.
27352
27635
  */
27353
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27636
+ top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27354
27637
  arrowTop = top + contentHeight;
27355
27638
  originY = 'bottom';
27356
27639
  addPopoverBottomClass = true;
27640
+ /**
27641
+ * If the popover is positioned near the top edge, account for safe area.
27642
+ * This ensures the popover doesn't overlap with status bars or notches.
27643
+ */
27644
+ if (top <= bodyPadding + safeAreaMargin) {
27645
+ checkSafeAreaTop = true;
27646
+ top = bodyPadding;
27647
+ }
27357
27648
  /**
27358
27649
  * If not enough room for popover to appear
27359
27650
  * above trigger, then cut it off.
@@ -27361,14 +27652,35 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27361
27652
  }
27362
27653
  else {
27363
27654
  bottom = bodyPadding;
27655
+ /**
27656
+ * When the popover is pinned to the bottom, account for safe area.
27657
+ * This ensures the popover doesn't overlap with home indicators
27658
+ * or navigation bars (e.g., Android API 36+ edge-to-edge).
27659
+ */
27660
+ checkSafeAreaBottom = true;
27364
27661
  }
27365
27662
  }
27663
+ /**
27664
+ * Final check: If the popover extends into any safe-area region,
27665
+ * ensure the corresponding flag is set regardless of side.
27666
+ * This handles cases where a side-positioned popover (left/right)
27667
+ * still needs bottom safe-area padding because it extends into that region.
27668
+ */
27669
+ const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
27670
+ if (popoverBottom + safeAreaMargin > bodyHeight) {
27671
+ checkSafeAreaBottom = true;
27672
+ }
27673
+ if (top < safeAreaMargin) {
27674
+ checkSafeAreaTop = true;
27675
+ }
27366
27676
  return {
27367
27677
  top,
27368
27678
  left,
27369
27679
  bottom,
27370
27680
  originX,
27371
27681
  originY,
27682
+ checkSafeAreaTop,
27683
+ checkSafeAreaBottom,
27372
27684
  checkSafeAreaLeft,
27373
27685
  checkSafeAreaRight,
27374
27686
  arrowTop,
@@ -27429,7 +27741,7 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27429
27741
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27430
27742
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27431
27743
  const margin = size === 'cover' ? 0 : 25;
27432
- 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);
27744
+ 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);
27433
27745
  const baseAnimation = createAnimation();
27434
27746
  const backdropAnimation = createAnimation();
27435
27747
  const contentAnimation = createAnimation();
@@ -27459,19 +27771,35 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27459
27771
  if (addPopoverBottomClass) {
27460
27772
  baseEl.classList.add('popover-bottom');
27461
27773
  }
27462
- if (bottom !== undefined) {
27463
- contentEl.style.setProperty('bottom', `${bottom}px`);
27464
- }
27774
+ /**
27775
+ * Safe area CSS variable adjustments.
27776
+ * When the popover is positioned near an edge, we add the corresponding
27777
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27778
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27779
+ */
27780
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27781
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27465
27782
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27466
27783
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27784
+ let topValue = `${top}px`;
27785
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27467
27786
  let leftValue = `${left}px`;
27787
+ if (checkSafeAreaTop) {
27788
+ topValue = `${top}px${safeAreaTop}`;
27789
+ }
27790
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27791
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27792
+ }
27468
27793
  if (checkSafeAreaLeft) {
27469
27794
  leftValue = `${left}px${safeAreaLeft}`;
27470
27795
  }
27471
27796
  if (checkSafeAreaRight) {
27472
27797
  leftValue = `${left}px${safeAreaRight}`;
27473
27798
  }
27474
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27799
+ if (bottomValue !== undefined) {
27800
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27801
+ }
27802
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27475
27803
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27476
27804
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27477
27805
  if (arrowEl !== null) {
@@ -27547,7 +27875,32 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27547
27875
  };
27548
27876
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27549
27877
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27550
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
27878
+ 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);
27879
+ /**
27880
+ * Safe area CSS variable adjustments.
27881
+ * When the popover is positioned near an edge, we add the corresponding
27882
+ * safe-area inset to ensure the popover doesn't overlap with system UI
27883
+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
27884
+ */
27885
+ const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
27886
+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
27887
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27888
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27889
+ let topValue = `${top}px`;
27890
+ let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
27891
+ let leftValue = `${left}px`;
27892
+ if (checkSafeAreaTop) {
27893
+ topValue = `${top}px${safeAreaTop}`;
27894
+ }
27895
+ if (checkSafeAreaBottom && bottomValue !== undefined) {
27896
+ bottomValue = `${bottom}px${safeAreaBottom}`;
27897
+ }
27898
+ if (checkSafeAreaLeft) {
27899
+ leftValue = `${left}px${safeAreaLeft}`;
27900
+ }
27901
+ if (checkSafeAreaRight) {
27902
+ leftValue = `${left}px${safeAreaRight}`;
27903
+ }
27551
27904
  const baseAnimation = createAnimation();
27552
27905
  const backdropAnimation = createAnimation();
27553
27906
  const wrapperAnimation = createAnimation();
@@ -27564,13 +27917,13 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27564
27917
  contentAnimation
27565
27918
  .addElement(contentEl)
27566
27919
  .beforeStyles({
27567
- top: `calc(${top}px + var(--offset-y, 0px))`,
27568
- left: `calc(${left}px + var(--offset-x, 0px))`,
27920
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27921
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27569
27922
  'transform-origin': `${originY} ${originX}`,
27570
27923
  })
27571
27924
  .beforeAddWrite(() => {
27572
- if (bottom !== undefined) {
27573
- contentEl.style.setProperty('bottom', `${bottom}px`);
27925
+ if (bottomValue !== undefined) {
27926
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27574
27927
  }
27575
27928
  })
27576
27929
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -34988,7 +35341,6 @@ class TabBar {
34988
35341
  this.ionTabBarChanged = createEvent(this, "ionTabBarChanged", 7);
34989
35342
  this.ionTabBarLoaded = createEvent(this, "ionTabBarLoaded", 7);
34990
35343
  this.keyboardCtrl = null;
34991
- this.keyboardCtrlPromise = null;
34992
35344
  this.didLoad = false;
34993
35345
  this.keyboardVisible = false;
34994
35346
  /**
@@ -35024,7 +35376,7 @@ class TabBar {
35024
35376
  }
35025
35377
  }
35026
35378
  async connectedCallback() {
35027
- const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
35379
+ this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
35028
35380
  /**
35029
35381
  * If the keyboard is hiding, then we need to wait
35030
35382
  * for the webview to resize. Otherwise, the tab bar
@@ -35035,40 +35387,21 @@ class TabBar {
35035
35387
  }
35036
35388
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
35037
35389
  });
35038
- this.keyboardCtrlPromise = promise;
35039
- const keyboardCtrl = await promise;
35040
- /**
35041
- * Only assign if this is still the current promise.
35042
- * Otherwise, a new connectedCallback has started or
35043
- * disconnectedCallback was called, so destroy this instance.
35044
- */
35045
- if (this.keyboardCtrlPromise === promise) {
35046
- this.keyboardCtrl = keyboardCtrl;
35047
- this.keyboardCtrlPromise = null;
35048
- }
35049
- else {
35050
- keyboardCtrl.destroy();
35051
- }
35052
35390
  }
35053
35391
  disconnectedCallback() {
35054
- if (this.keyboardCtrlPromise) {
35055
- this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
35056
- this.keyboardCtrlPromise = null;
35057
- }
35058
35392
  if (this.keyboardCtrl) {
35059
35393
  this.keyboardCtrl.destroy();
35060
- this.keyboardCtrl = null;
35061
35394
  }
35062
35395
  }
35063
35396
  render() {
35064
35397
  const { color, translucent, keyboardVisible } = this;
35065
35398
  const mode = getIonMode$1(this);
35066
35399
  const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
35067
- return (hAsync(Host, { key: '9daf4e2acaff6e3ce3878cf9dd5109fb1afbbebe', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses$1(color, {
35400
+ return (hAsync(Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses$1(color, {
35068
35401
  [mode]: true,
35069
35402
  'tab-bar-translucent': translucent,
35070
35403
  'tab-bar-hidden': shouldHide,
35071
- }) }, hAsync("slot", { key: '1d15aa2da8501e8e7eff11ad4a491478be845c43' })));
35404
+ }) }, hAsync("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
35072
35405
  }
35073
35406
  get el() { return getElement(this); }
35074
35407
  static get watchers() { return {