@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
@@ -1,13 +1,14 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { proxyCustomElement, HTMLElement, createEvent, Build, readTask, forceUpdate, h, Host } from '@stencil/core/internal/client';
4
+ import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, readTask, h, Host } from '@stencil/core/internal/client';
5
+ import { w as win } from './index9.js';
5
6
  import { i as inheritAriaAttributes, k as hasLazyBuild, c as componentOnReady } from './helpers.js';
6
7
  import { b as getIonMode, a as isPlatform } from './ionic-global.js';
7
8
  import { i as isRTL } from './dir.js';
8
9
  import { c as createColorClasses, h as hostContext } from './theme.js';
9
10
 
10
- 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)}";
11
+ 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)}";
11
12
 
12
13
  const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLElement {
13
14
  constructor(registerHost) {
@@ -28,6 +29,12 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
28
29
  this.isMainContent = true;
29
30
  this.resizeTimeout = null;
30
31
  this.inheritedAttributes = {};
32
+ /**
33
+ * Track whether this content has sibling header/footer elements.
34
+ * When absent, we need to apply safe-area padding directly.
35
+ */
36
+ this.hasHeader = false;
37
+ this.hasFooter = false;
31
38
  this.tabsElement = null;
32
39
  // Detail is used in a hot loop in the scroll event, by allocating it here
33
40
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -82,7 +89,13 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
82
89
  this.inheritedAttributes = inheritAriaAttributes(this.el);
83
90
  }
84
91
  connectedCallback() {
85
- this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
92
+ var _a;
93
+ // Content is "main" if not inside menu/popover/modal and not nested in another ion-content
94
+ this.isMainContent =
95
+ this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
96
+ ((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
97
+ // Detect sibling header/footer for safe-area handling
98
+ this.detectSiblingElements();
86
99
  /**
87
100
  * The fullscreen content offsets need to be
88
101
  * computed after the tab bar has loaded. Since
@@ -113,13 +126,86 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
113
126
  * bubbles, we can catch any instances of child tab bars loading by listening
114
127
  * on IonTabs.
115
128
  */
116
- this.tabsLoadCallback = () => this.resize();
129
+ this.tabsLoadCallback = () => {
130
+ this.resize();
131
+ // Re-detect footer when tab bar loads (it may not exist during initial detection)
132
+ this.updateSiblingDetection();
133
+ forceUpdate(this);
134
+ };
117
135
  closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
118
136
  }
119
137
  }
120
138
  }
139
+ /**
140
+ * Detects sibling ion-header and ion-footer elements and sets up
141
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
142
+ */
143
+ detectSiblingElements() {
144
+ this.updateSiblingDetection();
145
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
146
+ const parent = this.el.parentElement;
147
+ if (parent && !this.parentMutationObserver && win !== undefined && 'MutationObserver' in win) {
148
+ this.parentMutationObserver = new MutationObserver(() => {
149
+ const prevHasHeader = this.hasHeader;
150
+ const prevHasFooter = this.hasFooter;
151
+ this.updateSiblingDetection();
152
+ // Only trigger re-render if header/footer detection actually changed
153
+ if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
154
+ forceUpdate(this);
155
+ }
156
+ });
157
+ this.parentMutationObserver.observe(parent, { childList: true });
158
+ }
159
+ }
160
+ /**
161
+ * Updates hasHeader/hasFooter based on current DOM state.
162
+ * Checks both direct siblings and elements wrapped in custom components
163
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
164
+ */
165
+ updateSiblingDetection() {
166
+ const parent = this.el.parentElement;
167
+ if (parent) {
168
+ // First check for direct ion-header/ion-footer siblings
169
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
170
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
171
+ // If not found, check if any sibling contains them (wrapped components)
172
+ if (!this.hasHeader) {
173
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
174
+ }
175
+ if (!this.hasFooter) {
176
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
177
+ }
178
+ }
179
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
180
+ if (!this.hasFooter) {
181
+ const tabs = this.el.closest('ion-tabs');
182
+ if (tabs) {
183
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * Checks if any sibling element of ion-content contains the specified element.
189
+ * Only searches one level deep to avoid finding elements in nested pages.
190
+ */
191
+ siblingContainsElement(parent, tagName) {
192
+ for (const sibling of parent.children) {
193
+ // Skip ion-content itself
194
+ if (sibling === this.el)
195
+ continue;
196
+ // Check if this sibling contains the target element as an immediate child
197
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
198
+ return true;
199
+ }
200
+ }
201
+ return false;
202
+ }
121
203
  disconnectedCallback() {
204
+ var _a;
122
205
  this.onScrollEnd();
206
+ // Clean up mutation observer to prevent memory leaks
207
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
208
+ this.parentMutationObserver = undefined;
123
209
  if (hasLazyBuild(this.el)) {
124
210
  /**
125
211
  * The event listener and tabs caches need to
@@ -366,26 +452,28 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
366
452
  }
367
453
  }
368
454
  render() {
369
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
455
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
370
456
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
371
457
  const mode = getIonMode(this);
372
458
  const forceOverscroll = this.shouldForceOverscroll();
373
459
  const transitionShadow = mode === 'ios';
374
460
  this.resize();
375
- return (h(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
461
+ return (h(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
376
462
  [mode]: true,
377
463
  'content-sizing': hostContext('ion-popover', this.el),
378
464
  overscroll: forceOverscroll,
379
465
  [`content-${rtl}`]: true,
466
+ 'safe-area-top': isMainContent && !hasHeader,
467
+ 'safe-area-bottom': isMainContent && !hasFooter,
380
468
  }), style: {
381
469
  '--offset-top': `${this.cTop}px`,
382
470
  '--offset-bottom': `${this.cBottom}px`,
383
- } }, inheritedAttributes), h("div", { key: '95b112d7cae30f22ef778ceffb88edb4d941c170', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: '2fdfcbc39fb66f11b6191911f2941c660f4c12e5', class: {
471
+ } }, inheritedAttributes), h("div", { key: 'b735ec68c18c0b99c3595bb194029830e6542cde', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: 'e76c00d030342d44ade6648c3f9e32ca990787ba', class: {
384
472
  'inner-scroll': true,
385
473
  'scroll-x': scrollX,
386
474
  'scroll-y': scrollY,
387
475
  overscroll: (scrollX || scrollY) && forceOverscroll,
388
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '6bc77e0054ec8e21635a7f2abfe0ca46e0962e03' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
476
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '9049be4cea9b5da5ec1e1012248b05286fddeb7a' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
389
477
  }
390
478
  get el() { return this; }
391
479
  static get style() { return contentCss; }
@@ -20,7 +20,6 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
20
20
  this.ionTabBarChanged = createEvent(this, "ionTabBarChanged", 7);
21
21
  this.ionTabBarLoaded = createEvent(this, "ionTabBarLoaded", 7);
22
22
  this.keyboardCtrl = null;
23
- this.keyboardCtrlPromise = null;
24
23
  this.didLoad = false;
25
24
  this.keyboardVisible = false;
26
25
  /**
@@ -56,7 +55,7 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
56
55
  }
57
56
  }
58
57
  async connectedCallback() {
59
- const promise = createKeyboardController(async (keyboardOpen, waitForResize) => {
58
+ this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
60
59
  /**
61
60
  * If the keyboard is hiding, then we need to wait
62
61
  * for the webview to resize. Otherwise, the tab bar
@@ -67,40 +66,21 @@ const TabBar = /*@__PURE__*/ proxyCustomElement(class TabBar extends HTMLElement
67
66
  }
68
67
  this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
69
68
  });
70
- this.keyboardCtrlPromise = promise;
71
- const keyboardCtrl = await promise;
72
- /**
73
- * Only assign if this is still the current promise.
74
- * Otherwise, a new connectedCallback has started or
75
- * disconnectedCallback was called, so destroy this instance.
76
- */
77
- if (this.keyboardCtrlPromise === promise) {
78
- this.keyboardCtrl = keyboardCtrl;
79
- this.keyboardCtrlPromise = null;
80
- }
81
- else {
82
- keyboardCtrl.destroy();
83
- }
84
69
  }
85
70
  disconnectedCallback() {
86
- if (this.keyboardCtrlPromise) {
87
- this.keyboardCtrlPromise.then((ctrl) => ctrl.destroy());
88
- this.keyboardCtrlPromise = null;
89
- }
90
71
  if (this.keyboardCtrl) {
91
72
  this.keyboardCtrl.destroy();
92
- this.keyboardCtrl = null;
93
73
  }
94
74
  }
95
75
  render() {
96
76
  const { color, translucent, keyboardVisible } = this;
97
77
  const mode = getIonMode(this);
98
78
  const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
99
- return (h(Host, { key: '9daf4e2acaff6e3ce3878cf9dd5109fb1afbbebe', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses(color, {
79
+ return (h(Host, { key: '388ec37ce308035bab78d6c9a016bb616e9517a9', role: "tablist", "aria-hidden": shouldHide ? 'true' : null, class: createColorClasses(color, {
100
80
  [mode]: true,
101
81
  'tab-bar-translucent': translucent,
102
82
  'tab-bar-hidden': shouldHide,
103
- }) }, h("slot", { key: '1d15aa2da8501e8e7eff11ad4a491478be845c43' })));
83
+ }) }, h("slot", { key: 'ce10ade2b86725e24f3254516483eeedd8ecb16a' })));
104
84
  }
105
85
  get el() { return this; }
106
86
  static get watchers() { return {
@@ -2,6 +2,7 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { proxyCustomElement, HTMLElement, createEvent, writeTask, h, Host } from '@stencil/core/internal/client';
5
+ import { w as win } from './index9.js';
5
6
  import { a as findClosestIonContent, i as isIonContent, d as disableContentScrollY, r as resetContentScrollY, f as findIonContent, p as printIonContentErrorMsg } from './index8.js';
6
7
  import { C as CoreDelegate, a as attachComponent, d as detachComponent } from './framework-delegate.js';
7
8
  import { f as clamp, g as getElementRoot, r as raf, d as inheritAttributes, k as hasLazyBuild } from './helpers.js';
@@ -16,7 +17,6 @@ import { KEYBOARD_DID_OPEN } from './keyboard.js';
16
17
  import { c as createAnimation } from './animation.js';
17
18
  import { g as getTimeGivenProgression } from './cubic-bezier.js';
18
19
  import { createGesture } from './index3.js';
19
- import { w as win } from './index9.js';
20
20
  import { d as defineCustomElement$1 } from './backdrop.js';
21
21
 
22
22
  var Style;
@@ -246,7 +246,7 @@ const calculateSpringStep = (t) => {
246
246
  const SwipeToCloseDefaults = {
247
247
  MIN_PRESENTING_SCALE: 0.915,
248
248
  };
249
- const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
249
+ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
250
250
  /**
251
251
  * The step value at which a card modal
252
252
  * is eligible for dismissing via gesture.
@@ -403,6 +403,8 @@ const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) =>
403
403
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
404
404
  const clampedStep = clamp(0.0001, processedStep, maxStep);
405
405
  animation.progressStep(clampedStep);
406
+ // Notify modal of position change for safe-area updates
407
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
406
408
  /**
407
409
  * When swiping down half way, the status bar style
408
410
  * should be reset to its default value.
@@ -946,7 +948,7 @@ const mdLeaveAnimation = (baseEl, opts) => {
946
948
  return baseAnimation;
947
949
  };
948
950
 
949
- const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
951
+ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
950
952
  // Defaults for the sheet swipe animation
951
953
  const defaultBackdrop = [
952
954
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -1277,6 +1279,8 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1277
1279
  : step;
1278
1280
  offset = clamp(0.0001, processedStep, maxStep);
1279
1281
  animation.progressStep(offset);
1282
+ // Notify modal of position change for safe-area updates
1283
+ onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
1280
1284
  };
1281
1285
  const onEnd = (detail) => {
1282
1286
  /**
@@ -1471,9 +1475,9 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1471
1475
  };
1472
1476
  };
1473
1477
 
1474
- 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}";
1478
+ 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}";
1475
1479
 
1476
- 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}";
1480
+ 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}";
1477
1481
 
1478
1482
  const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1479
1483
  constructor(registerHost) {
@@ -1500,6 +1504,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1500
1504
  this.inline = false;
1501
1505
  // Whether or not modal is being dismissed via gesture
1502
1506
  this.gestureAnimationDismissing = false;
1507
+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1508
+ this.skipSafeAreaCoordinateDetection = false;
1509
+ // Track previous safe-area state to avoid redundant DOM writes
1510
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1503
1511
  this.presented = false;
1504
1512
  /** @internal */
1505
1513
  this.hasController = false;
@@ -1690,7 +1698,10 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1690
1698
  }
1691
1699
  }
1692
1700
  onWindowResize() {
1693
- // Only handle resize for iOS card modals when no custom animations are provided
1701
+ // Invalidate safe-area cache on resize (device rotation may change values)
1702
+ this.cachedSafeAreas = undefined;
1703
+ this.updateSafeAreaOverrides();
1704
+ // Only handle view transition for iOS card modals when no custom animations are provided
1694
1705
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1695
1706
  return;
1696
1707
  }
@@ -1713,6 +1724,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1713
1724
  this.triggerController.removeClickListener();
1714
1725
  this.cleanupViewTransitionListener();
1715
1726
  this.cleanupParentRemovalObserver();
1727
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
1728
+ this.resetSafeAreaState();
1716
1729
  }
1717
1730
  componentWillLoad() {
1718
1731
  var _a;
@@ -1872,6 +1885,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1872
1885
  else if (!this.keepContentsMounted) {
1873
1886
  await waitForMount();
1874
1887
  }
1888
+ // Predict safe-area needs based on modal configuration to avoid visual snap
1889
+ this.setInitialSafeAreaOverrides(presentingElement);
1875
1890
  writeTask(() => this.el.classList.add('show-modal'));
1876
1891
  const hasCardModal = presentingElement !== undefined;
1877
1892
  /**
@@ -1933,6 +1948,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1933
1948
  else if (hasCardModal) {
1934
1949
  this.initSwipeToClose();
1935
1950
  }
1951
+ // Now that animation is complete, update safe-area based on actual position
1952
+ this.updateSafeAreaOverrides();
1936
1953
  // Initialize view transition listener for iOS card modals
1937
1954
  this.initViewTransitionListener();
1938
1955
  // Initialize parent removal observer
@@ -1984,7 +2001,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1984
2001
  await this.dismiss(undefined, GESTURE);
1985
2002
  this.gestureAnimationDismissing = false;
1986
2003
  });
1987
- });
2004
+ }, () => this.updateSafeAreaOverrides());
1988
2005
  this.gesture.enable(true);
1989
2006
  }
1990
2007
  initSheetGesture() {
@@ -2005,7 +2022,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2005
2022
  this.currentBreakpoint = breakpoint;
2006
2023
  this.ionBreakpointDidChange.emit({ breakpoint });
2007
2024
  }
2008
- });
2025
+ this.updateSafeAreaOverrides();
2026
+ }, () => this.updateSafeAreaOverrides());
2009
2027
  this.gesture = gesture;
2010
2028
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
2011
2029
  this.gesture.enable(true);
@@ -2083,6 +2101,187 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2083
2101
  // Clear the cached reference
2084
2102
  this.cachedPageParent = undefined;
2085
2103
  }
2104
+ /**
2105
+ * Sets initial safe-area overrides based on modal configuration before
2106
+ * the modal becomes visible. This predicts whether the modal will touch
2107
+ * screen edges to avoid a visual snap after animation completes.
2108
+ */
2109
+ setInitialSafeAreaOverrides(presentingElement) {
2110
+ const style = this.el.style;
2111
+ const mode = getIonMode(this);
2112
+ const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
2113
+ // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
2114
+ const isCardModal = presentingElement !== undefined && mode === 'ios';
2115
+ const isTablet = window.innerWidth >= 768;
2116
+ // Sheet modals always touch bottom edge, never top/left/right
2117
+ if (isSheetModal) {
2118
+ style.setProperty('--ion-safe-area-top', '0px');
2119
+ style.setProperty('--ion-safe-area-left', '0px');
2120
+ style.setProperty('--ion-safe-area-right', '0px');
2121
+ return;
2122
+ }
2123
+ // Card modals have rounded top corners
2124
+ if (isCardModal) {
2125
+ style.setProperty('--ion-safe-area-top', '0px');
2126
+ if (isTablet) {
2127
+ // On tablets, card modals are inset from all edges
2128
+ this.zeroAllSafeAreas();
2129
+ }
2130
+ else {
2131
+ // On phones, card modals still extend to the bottom edge
2132
+ style.setProperty('--ion-safe-area-left', '0px');
2133
+ style.setProperty('--ion-safe-area-right', '0px');
2134
+ this.applyFullscreenSafeArea();
2135
+ }
2136
+ return;
2137
+ }
2138
+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
2139
+ if (!isTablet) {
2140
+ this.applyFullscreenSafeArea();
2141
+ return;
2142
+ }
2143
+ // Check if tablet modal is fullscreen via CSS custom properties
2144
+ const computedStyle = getComputedStyle(this.el);
2145
+ const width = computedStyle.getPropertyValue('--width').trim();
2146
+ const height = computedStyle.getPropertyValue('--height').trim();
2147
+ const isFullscreen = width === '100%' && height === '100%';
2148
+ if (isFullscreen) {
2149
+ this.applyFullscreenSafeArea();
2150
+ }
2151
+ else {
2152
+ // Centered dialog doesn't touch edges
2153
+ this.zeroAllSafeAreas();
2154
+ }
2155
+ }
2156
+ /**
2157
+ * Applies safe-area handling for fullscreen modals.
2158
+ * Adds wrapper padding when no footer is present to prevent
2159
+ * content from overlapping system navigation areas.
2160
+ */
2161
+ applyFullscreenSafeArea() {
2162
+ this.skipSafeAreaCoordinateDetection = true;
2163
+ this.updateFooterPadding();
2164
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2165
+ // Use subtree:true to support wrapped footers in framework components
2166
+ // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
2167
+ if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
2168
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2169
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2170
+ }
2171
+ }
2172
+ /**
2173
+ * Updates wrapper padding based on footer presence.
2174
+ * Called initially and when footer is dynamically added/removed.
2175
+ */
2176
+ updateFooterPadding() {
2177
+ if (!this.wrapperEl)
2178
+ return;
2179
+ const hasFooter = this.el.querySelector('ion-footer') !== null;
2180
+ if (hasFooter) {
2181
+ this.wrapperEl.style.removeProperty('padding-bottom');
2182
+ this.wrapperEl.style.removeProperty('box-sizing');
2183
+ }
2184
+ else {
2185
+ this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2186
+ this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2187
+ }
2188
+ }
2189
+ /**
2190
+ * Sets all safe-area CSS variables to 0px for modals that
2191
+ * don't touch screen edges.
2192
+ */
2193
+ zeroAllSafeAreas() {
2194
+ const style = this.el.style;
2195
+ style.setProperty('--ion-safe-area-top', '0px');
2196
+ style.setProperty('--ion-safe-area-bottom', '0px');
2197
+ style.setProperty('--ion-safe-area-left', '0px');
2198
+ style.setProperty('--ion-safe-area-right', '0px');
2199
+ }
2200
+ /**
2201
+ * Resets all safe-area related state and styles.
2202
+ * Called during dismiss and disconnectedCallback to ensure clean state
2203
+ * for re-presentation of inline modals.
2204
+ */
2205
+ resetSafeAreaState() {
2206
+ var _a;
2207
+ this.skipSafeAreaCoordinateDetection = false;
2208
+ this.cachedSafeAreas = undefined;
2209
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2210
+ (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
2211
+ this.footerObserver = undefined;
2212
+ // Clear wrapper styles that may have been set for safe-area handling
2213
+ if (this.wrapperEl) {
2214
+ this.wrapperEl.style.removeProperty('padding-bottom');
2215
+ this.wrapperEl.style.removeProperty('box-sizing');
2216
+ }
2217
+ // Clear safe-area CSS variable overrides
2218
+ const style = this.el.style;
2219
+ style.removeProperty('--ion-safe-area-top');
2220
+ style.removeProperty('--ion-safe-area-bottom');
2221
+ style.removeProperty('--ion-safe-area-left');
2222
+ style.removeProperty('--ion-safe-area-right');
2223
+ }
2224
+ /**
2225
+ * Gets the root safe-area values from the document element.
2226
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2227
+ */
2228
+ getSafeAreaValues() {
2229
+ if (!this.cachedSafeAreas) {
2230
+ const rootStyle = getComputedStyle(document.documentElement);
2231
+ this.cachedSafeAreas = {
2232
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2233
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2234
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2235
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2236
+ };
2237
+ }
2238
+ return this.cachedSafeAreas;
2239
+ }
2240
+ /**
2241
+ * Updates safe-area CSS variable overrides based on whether the modal
2242
+ * extends into each safe-area region. Called after animation
2243
+ * and during gestures to handle dynamic position changes.
2244
+ *
2245
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2246
+ */
2247
+ updateSafeAreaOverrides() {
2248
+ if (this.skipSafeAreaCoordinateDetection) {
2249
+ return;
2250
+ }
2251
+ const wrapper = this.wrapperEl;
2252
+ if (!wrapper) {
2253
+ return;
2254
+ }
2255
+ const rect = wrapper.getBoundingClientRect();
2256
+ const safeAreas = this.getSafeAreaValues();
2257
+ const extendsIntoTop = rect.top < safeAreas.top;
2258
+ const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2259
+ const extendsIntoLeft = rect.left < safeAreas.left;
2260
+ const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2261
+ // Only update DOM when state actually changes
2262
+ const prev = this.prevSafeAreaState;
2263
+ const style = this.el.style;
2264
+ if (extendsIntoTop !== prev.top) {
2265
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2266
+ prev.top = extendsIntoTop;
2267
+ }
2268
+ if (extendsIntoBottom !== prev.bottom) {
2269
+ extendsIntoBottom
2270
+ ? style.removeProperty('--ion-safe-area-bottom')
2271
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2272
+ prev.bottom = extendsIntoBottom;
2273
+ }
2274
+ if (extendsIntoLeft !== prev.left) {
2275
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2276
+ prev.left = extendsIntoLeft;
2277
+ }
2278
+ if (extendsIntoRight !== prev.right) {
2279
+ extendsIntoRight
2280
+ ? style.removeProperty('--ion-safe-area-right')
2281
+ : style.setProperty('--ion-safe-area-right', '0px');
2282
+ prev.right = extendsIntoRight;
2283
+ }
2284
+ }
2086
2285
  sheetOnDismiss() {
2087
2286
  /**
2088
2287
  * While the gesture animation is finishing
@@ -2175,6 +2374,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2175
2374
  }
2176
2375
  this.currentBreakpoint = undefined;
2177
2376
  this.animation = undefined;
2377
+ // Reset safe-area state for potential re-presentation
2378
+ this.resetSafeAreaState();
2178
2379
  unlock();
2179
2380
  return dismissed;
2180
2381
  }
@@ -2424,20 +2625,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2424
2625
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2425
2626
  const isHandleCycle = handleBehavior === 'cycle';
2426
2627
  const isSheetModalWithHandle = isSheetModal && showHandle;
2427
- return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
2628
+ return (h(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
2428
2629
  // Allow the modal to be navigable when the handle is focusable
2429
2630
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2430
2631
  zIndex: `${20000 + this.overlayIndex}`,
2431
- }, 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 }), h("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
2632
+ }, 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 }), h("ion-backdrop", { key: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), h("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
2432
2633
  /*
2433
2634
  role and aria-modal must be used on the
2434
2635
  same element. They must also be set inside the
2435
2636
  shadow DOM otherwise ion-button will not be highlighted
2436
2637
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2437
2638
  */
2438
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
2639
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
2439
2640
  // Prevents the handle from receiving keyboard focus when it does not cycle
2440
- 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) })), h("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
2641
+ 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) })), h("slot", { key: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
2441
2642
  }
2442
2643
  get el() { return this; }
2443
2644
  static get watchers() { return {