@ionic/core 8.7.17-dev.11767647939.17c197c2 → 8.7.17-dev.11767796972.148e4bc4

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.
@@ -1505,6 +1505,8 @@ const Modal = class {
1505
1505
  this.gestureAnimationDismissing = false;
1506
1506
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
1507
1507
  this.skipSafeAreaCoordinateDetection = false;
1508
+ // Track previous safe-area state to avoid redundant DOM writes
1509
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
1508
1510
  this.presented = false;
1509
1511
  /** @internal */
1510
1512
  this.hasController = false;
@@ -1695,7 +1697,8 @@ const Modal = class {
1695
1697
  }
1696
1698
  }
1697
1699
  onWindowResize() {
1698
- // Update safe-area overrides for all modal types on resize
1700
+ // Invalidate safe-area cache on resize (device rotation may change values)
1701
+ this.cachedSafeAreas = undefined;
1699
1702
  this.updateSafeAreaOverrides();
1700
1703
  // Only handle view transition for iOS card modals when no custom animations are provided
1701
1704
  if (ionicGlobal.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -2154,8 +2157,26 @@ const Modal = class {
2154
2157
  */
2155
2158
  applyFullscreenSafeArea() {
2156
2159
  this.skipSafeAreaCoordinateDetection = true;
2160
+ this.updateFooterPadding();
2161
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
2162
+ if (!this.footerObserver) {
2163
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
2164
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
2165
+ }
2166
+ }
2167
+ /**
2168
+ * Updates wrapper padding based on footer presence.
2169
+ * Called initially and when footer is dynamically added/removed.
2170
+ */
2171
+ updateFooterPadding() {
2172
+ if (!this.wrapperEl)
2173
+ return;
2157
2174
  const hasFooter = this.el.querySelector('ion-footer') !== null;
2158
- if (!hasFooter && this.wrapperEl) {
2175
+ if (hasFooter) {
2176
+ this.wrapperEl.style.removeProperty('padding-bottom');
2177
+ this.wrapperEl.style.removeProperty('box-sizing');
2178
+ }
2179
+ else {
2159
2180
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2160
2181
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
2161
2182
  }
@@ -2173,21 +2194,26 @@ const Modal = class {
2173
2194
  }
2174
2195
  /**
2175
2196
  * Gets the root safe-area values from the document element.
2176
- * These represent the actual device safe areas before any overlay overrides.
2197
+ * Uses cached values during gestures to avoid getComputedStyle calls.
2177
2198
  */
2178
- getRootSafeAreaValues() {
2179
- const rootStyle = getComputedStyle(document.documentElement);
2180
- return {
2181
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2182
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2183
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2184
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2185
- };
2199
+ getSafeAreaValues() {
2200
+ if (!this.cachedSafeAreas) {
2201
+ const rootStyle = getComputedStyle(document.documentElement);
2202
+ this.cachedSafeAreas = {
2203
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
2204
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
2205
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
2206
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
2207
+ };
2208
+ }
2209
+ return this.cachedSafeAreas;
2186
2210
  }
2187
2211
  /**
2188
2212
  * Updates safe-area CSS variable overrides based on whether the modal
2189
2213
  * extends into each safe-area region. Called after animation
2190
2214
  * and during gestures to handle dynamic position changes.
2215
+ *
2216
+ * Optimized to avoid redundant DOM writes by tracking previous state.
2191
2217
  */
2192
2218
  updateSafeAreaOverrides() {
2193
2219
  if (this.skipSafeAreaCoordinateDetection) {
@@ -2198,20 +2224,34 @@ const Modal = class {
2198
2224
  return;
2199
2225
  }
2200
2226
  const rect = wrapper.getBoundingClientRect();
2201
- const safeAreas = this.getRootSafeAreaValues();
2227
+ const safeAreas = this.getSafeAreaValues();
2202
2228
  const extendsIntoTop = rect.top < safeAreas.top;
2203
2229
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
2204
2230
  const extendsIntoLeft = rect.left < safeAreas.left;
2205
2231
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
2232
+ // Only update DOM when state actually changes
2233
+ const prev = this.prevSafeAreaState;
2206
2234
  const style = this.el.style;
2207
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2208
- extendsIntoBottom
2209
- ? style.removeProperty('--ion-safe-area-bottom')
2210
- : style.setProperty('--ion-safe-area-bottom', '0px');
2211
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2212
- extendsIntoRight
2213
- ? style.removeProperty('--ion-safe-area-right')
2214
- : style.setProperty('--ion-safe-area-right', '0px');
2235
+ if (extendsIntoTop !== prev.top) {
2236
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
2237
+ prev.top = extendsIntoTop;
2238
+ }
2239
+ if (extendsIntoBottom !== prev.bottom) {
2240
+ extendsIntoBottom
2241
+ ? style.removeProperty('--ion-safe-area-bottom')
2242
+ : style.setProperty('--ion-safe-area-bottom', '0px');
2243
+ prev.bottom = extendsIntoBottom;
2244
+ }
2245
+ if (extendsIntoLeft !== prev.left) {
2246
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
2247
+ prev.left = extendsIntoLeft;
2248
+ }
2249
+ if (extendsIntoRight !== prev.right) {
2250
+ extendsIntoRight
2251
+ ? style.removeProperty('--ion-safe-area-right')
2252
+ : style.setProperty('--ion-safe-area-right', '0px');
2253
+ prev.right = extendsIntoRight;
2254
+ }
2215
2255
  }
2216
2256
  sheetOnDismiss() {
2217
2257
  /**
@@ -2243,7 +2283,7 @@ const Modal = class {
2243
2283
  * For example, `cancel` or `backdrop`.
2244
2284
  */
2245
2285
  async dismiss(data, role) {
2246
- var _a;
2286
+ var _a, _b;
2247
2287
  if (this.gestureAnimationDismissing && role !== overlays.GESTURE) {
2248
2288
  return false;
2249
2289
  }
@@ -2305,8 +2345,23 @@ const Modal = class {
2305
2345
  }
2306
2346
  this.currentBreakpoint = undefined;
2307
2347
  this.animation = undefined;
2308
- // Reset safe-area detection flag for potential re-presentation
2348
+ // Reset safe-area state for potential re-presentation
2309
2349
  this.skipSafeAreaCoordinateDetection = false;
2350
+ this.cachedSafeAreas = undefined;
2351
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
2352
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
2353
+ this.footerObserver = undefined;
2354
+ // Clear styles that may have been set for safe-area handling
2355
+ if (this.wrapperEl) {
2356
+ this.wrapperEl.style.removeProperty('padding-bottom');
2357
+ this.wrapperEl.style.removeProperty('box-sizing');
2358
+ }
2359
+ // Clear safe-area CSS variable overrides
2360
+ const style = this.el.style;
2361
+ style.removeProperty('--ion-safe-area-top');
2362
+ style.removeProperty('--ion-safe-area-bottom');
2363
+ style.removeProperty('--ion-safe-area-left');
2364
+ style.removeProperty('--ion-safe-area-right');
2310
2365
  unlock();
2311
2366
  return dismissed;
2312
2367
  }
@@ -2556,20 +2611,20 @@ const Modal = class {
2556
2611
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2557
2612
  const isHandleCycle = handleBehavior === 'cycle';
2558
2613
  const isSheetModalWithHandle = isSheetModal && showHandle;
2559
- return (index$3.h(index$3.Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
2614
+ return (index$3.h(index$3.Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
2560
2615
  // Allow the modal to be navigable when the handle is focusable
2561
2616
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2562
2617
  zIndex: `${20000 + this.overlayIndex}`,
2563
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
2618
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
2564
2619
  /*
2565
2620
  role and aria-modal must be used on the
2566
2621
  same element. They must also be set inside the
2567
2622
  shadow DOM otherwise ion-button will not be highlighted
2568
2623
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2569
2624
  */
2570
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
2625
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
2571
2626
  // Prevents the handle from receiving keyboard focus when it does not cycle
2572
- 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) })), index$3.h("slot", { key: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
2627
+ 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) })), index$3.h("slot", { key: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
2573
2628
  }
2574
2629
  get el() { return index$3.getElement(this); }
2575
2630
  static get watchers() { return {
@@ -263,6 +263,16 @@
263
263
  transform: scaleX(-1);
264
264
  }
265
265
 
266
+ :host(.safe-area-top) #background-content,
267
+ :host(.safe-area-top) .inner-scroll {
268
+ top: var(--ion-safe-area-top, 0px);
269
+ }
270
+
271
+ :host(.safe-area-bottom) #background-content,
272
+ :host(.safe-area-bottom) .inner-scroll {
273
+ bottom: var(--ion-safe-area-bottom, 0px);
274
+ }
275
+
266
276
  ::slotted([slot=fixed]) {
267
277
  position: absolute;
268
278
  /**
@@ -25,6 +25,12 @@ export class Content {
25
25
  this.isMainContent = true;
26
26
  this.resizeTimeout = null;
27
27
  this.inheritedAttributes = {};
28
+ /**
29
+ * Track whether this content has sibling header/footer elements.
30
+ * When absent, we need to apply safe-area padding directly.
31
+ */
32
+ this.hasHeader = false;
33
+ this.hasFooter = false;
28
34
  this.tabsElement = null;
29
35
  // Detail is used in a hot loop in the scroll event, by allocating it here
30
36
  // V8 will be able to inline any read/write to it since it's a monomorphic class.
@@ -80,6 +86,8 @@ export class Content {
80
86
  }
81
87
  connectedCallback() {
82
88
  this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
89
+ // Detect sibling header/footer for safe-area handling
90
+ this.detectSiblingElements();
83
91
  /**
84
92
  * The fullscreen content offsets need to be
85
93
  * computed after the tab bar has loaded. Since
@@ -115,8 +123,71 @@ export class Content {
115
123
  }
116
124
  }
117
125
  }
126
+ /**
127
+ * Detects sibling ion-header and ion-footer elements and sets up
128
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
129
+ */
130
+ detectSiblingElements() {
131
+ this.updateSiblingDetection();
132
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
133
+ const parent = this.el.parentElement;
134
+ if (parent && !this.parentMutationObserver) {
135
+ this.parentMutationObserver = new MutationObserver(() => {
136
+ this.updateSiblingDetection();
137
+ forceUpdate(this);
138
+ });
139
+ this.parentMutationObserver.observe(parent, { childList: true });
140
+ }
141
+ }
142
+ /**
143
+ * Updates hasHeader/hasFooter based on current DOM state.
144
+ * Checks both direct siblings and elements wrapped in custom components
145
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
146
+ */
147
+ updateSiblingDetection() {
148
+ const parent = this.el.parentElement;
149
+ if (parent) {
150
+ // First check for direct ion-header/ion-footer siblings
151
+ this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
152
+ this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
153
+ // If not found, check if any sibling contains them (wrapped components)
154
+ if (!this.hasHeader) {
155
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
156
+ }
157
+ if (!this.hasFooter) {
158
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
159
+ }
160
+ }
161
+ // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
162
+ if (!this.hasFooter) {
163
+ const tabs = this.el.closest('ion-tabs');
164
+ if (tabs) {
165
+ this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
166
+ }
167
+ }
168
+ }
169
+ /**
170
+ * Checks if any sibling element of ion-content contains the specified element.
171
+ * Only searches one level deep to avoid finding elements in nested pages.
172
+ */
173
+ siblingContainsElement(parent, tagName) {
174
+ for (const sibling of parent.children) {
175
+ // Skip ion-content itself
176
+ if (sibling === this.el)
177
+ continue;
178
+ // Check if this sibling contains the target element as an immediate child
179
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
180
+ return true;
181
+ }
182
+ }
183
+ return false;
184
+ }
118
185
  disconnectedCallback() {
186
+ var _a;
119
187
  this.onScrollEnd();
188
+ // Clean up mutation observer to prevent memory leaks
189
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
190
+ this.parentMutationObserver = undefined;
120
191
  if (hasLazyBuild(this.el)) {
121
192
  /**
122
193
  * The event listener and tabs caches need to
@@ -363,26 +434,28 @@ export class Content {
363
434
  }
364
435
  }
365
436
  render() {
366
- const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
437
+ const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
367
438
  const rtl = isRTL(el) ? 'rtl' : 'ltr';
368
439
  const mode = getIonMode(this);
369
440
  const forceOverscroll = this.shouldForceOverscroll();
370
441
  const transitionShadow = mode === 'ios';
371
442
  this.resize();
372
- return (h(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
443
+ return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
373
444
  [mode]: true,
374
445
  'content-sizing': hostContext('ion-popover', this.el),
375
446
  overscroll: forceOverscroll,
376
447
  [`content-${rtl}`]: true,
448
+ 'safe-area-top': isMainContent && !hasHeader,
449
+ 'safe-area-bottom': isMainContent && !hasFooter,
377
450
  }), style: {
378
451
  '--offset-top': `${this.cTop}px`,
379
452
  '--offset-bottom': `${this.cBottom}px`,
380
- } }, 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: {
453
+ } }, inheritedAttributes), h("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
381
454
  'inner-scroll': true,
382
455
  'scroll-x': scrollX,
383
456
  'scroll-y': scrollY,
384
457
  overscroll: (scrollX || scrollY) && forceOverscroll,
385
- }, 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));
458
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
386
459
  }
387
460
  static get is() { return "ion-content"; }
388
461
  static get encapsulation() { return "shadow"; }
@@ -44,6 +44,8 @@ export class Modal {
44
44
  this.gestureAnimationDismissing = false;
45
45
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
46
46
  this.skipSafeAreaCoordinateDetection = false;
47
+ // Track previous safe-area state to avoid redundant DOM writes
48
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
47
49
  this.presented = false;
48
50
  /** @internal */
49
51
  this.hasController = false;
@@ -234,7 +236,8 @@ export class Modal {
234
236
  }
235
237
  }
236
238
  onWindowResize() {
237
- // Update safe-area overrides for all modal types on resize
239
+ // Invalidate safe-area cache on resize (device rotation may change values)
240
+ this.cachedSafeAreas = undefined;
238
241
  this.updateSafeAreaOverrides();
239
242
  // Only handle view transition for iOS card modals when no custom animations are provided
240
243
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -693,8 +696,26 @@ export class Modal {
693
696
  */
694
697
  applyFullscreenSafeArea() {
695
698
  this.skipSafeAreaCoordinateDetection = true;
699
+ this.updateFooterPadding();
700
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
701
+ if (!this.footerObserver) {
702
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
703
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
704
+ }
705
+ }
706
+ /**
707
+ * Updates wrapper padding based on footer presence.
708
+ * Called initially and when footer is dynamically added/removed.
709
+ */
710
+ updateFooterPadding() {
711
+ if (!this.wrapperEl)
712
+ return;
696
713
  const hasFooter = this.el.querySelector('ion-footer') !== null;
697
- if (!hasFooter && this.wrapperEl) {
714
+ if (hasFooter) {
715
+ this.wrapperEl.style.removeProperty('padding-bottom');
716
+ this.wrapperEl.style.removeProperty('box-sizing');
717
+ }
718
+ else {
698
719
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
699
720
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
700
721
  }
@@ -712,21 +733,26 @@ export class Modal {
712
733
  }
713
734
  /**
714
735
  * Gets the root safe-area values from the document element.
715
- * These represent the actual device safe areas before any overlay overrides.
736
+ * Uses cached values during gestures to avoid getComputedStyle calls.
716
737
  */
717
- getRootSafeAreaValues() {
718
- const rootStyle = getComputedStyle(document.documentElement);
719
- return {
720
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
721
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
722
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
723
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
724
- };
738
+ getSafeAreaValues() {
739
+ if (!this.cachedSafeAreas) {
740
+ const rootStyle = getComputedStyle(document.documentElement);
741
+ this.cachedSafeAreas = {
742
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
743
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
744
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
745
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
746
+ };
747
+ }
748
+ return this.cachedSafeAreas;
725
749
  }
726
750
  /**
727
751
  * Updates safe-area CSS variable overrides based on whether the modal
728
752
  * extends into each safe-area region. Called after animation
729
753
  * and during gestures to handle dynamic position changes.
754
+ *
755
+ * Optimized to avoid redundant DOM writes by tracking previous state.
730
756
  */
731
757
  updateSafeAreaOverrides() {
732
758
  if (this.skipSafeAreaCoordinateDetection) {
@@ -737,20 +763,34 @@ export class Modal {
737
763
  return;
738
764
  }
739
765
  const rect = wrapper.getBoundingClientRect();
740
- const safeAreas = this.getRootSafeAreaValues();
766
+ const safeAreas = this.getSafeAreaValues();
741
767
  const extendsIntoTop = rect.top < safeAreas.top;
742
768
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
743
769
  const extendsIntoLeft = rect.left < safeAreas.left;
744
770
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
771
+ // Only update DOM when state actually changes
772
+ const prev = this.prevSafeAreaState;
745
773
  const style = this.el.style;
746
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
747
- extendsIntoBottom
748
- ? style.removeProperty('--ion-safe-area-bottom')
749
- : style.setProperty('--ion-safe-area-bottom', '0px');
750
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
751
- extendsIntoRight
752
- ? style.removeProperty('--ion-safe-area-right')
753
- : style.setProperty('--ion-safe-area-right', '0px');
774
+ if (extendsIntoTop !== prev.top) {
775
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
776
+ prev.top = extendsIntoTop;
777
+ }
778
+ if (extendsIntoBottom !== prev.bottom) {
779
+ extendsIntoBottom
780
+ ? style.removeProperty('--ion-safe-area-bottom')
781
+ : style.setProperty('--ion-safe-area-bottom', '0px');
782
+ prev.bottom = extendsIntoBottom;
783
+ }
784
+ if (extendsIntoLeft !== prev.left) {
785
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
786
+ prev.left = extendsIntoLeft;
787
+ }
788
+ if (extendsIntoRight !== prev.right) {
789
+ extendsIntoRight
790
+ ? style.removeProperty('--ion-safe-area-right')
791
+ : style.setProperty('--ion-safe-area-right', '0px');
792
+ prev.right = extendsIntoRight;
793
+ }
754
794
  }
755
795
  sheetOnDismiss() {
756
796
  /**
@@ -782,7 +822,7 @@ export class Modal {
782
822
  * For example, `cancel` or `backdrop`.
783
823
  */
784
824
  async dismiss(data, role) {
785
- var _a;
825
+ var _a, _b;
786
826
  if (this.gestureAnimationDismissing && role !== GESTURE) {
787
827
  return false;
788
828
  }
@@ -844,8 +884,23 @@ export class Modal {
844
884
  }
845
885
  this.currentBreakpoint = undefined;
846
886
  this.animation = undefined;
847
- // Reset safe-area detection flag for potential re-presentation
887
+ // Reset safe-area state for potential re-presentation
848
888
  this.skipSafeAreaCoordinateDetection = false;
889
+ this.cachedSafeAreas = undefined;
890
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
891
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
892
+ this.footerObserver = undefined;
893
+ // Clear styles that may have been set for safe-area handling
894
+ if (this.wrapperEl) {
895
+ this.wrapperEl.style.removeProperty('padding-bottom');
896
+ this.wrapperEl.style.removeProperty('box-sizing');
897
+ }
898
+ // Clear safe-area CSS variable overrides
899
+ const style = this.el.style;
900
+ style.removeProperty('--ion-safe-area-top');
901
+ style.removeProperty('--ion-safe-area-bottom');
902
+ style.removeProperty('--ion-safe-area-left');
903
+ style.removeProperty('--ion-safe-area-right');
849
904
  unlock();
850
905
  return dismissed;
851
906
  }
@@ -1103,20 +1158,20 @@ export class Modal {
1103
1158
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1104
1159
  const isHandleCycle = handleBehavior === 'cycle';
1105
1160
  const isSheetModalWithHandle = isSheetModal && showHandle;
1106
- return (h(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
1161
+ return (h(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
1107
1162
  // Allow the modal to be navigable when the handle is focusable
1108
1163
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1109
1164
  zIndex: `${20000 + this.overlayIndex}`,
1110
- }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), h("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
1165
+ }, 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: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), h("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
1111
1166
  /*
1112
1167
  role and aria-modal must be used on the
1113
1168
  same element. They must also be set inside the
1114
1169
  shadow DOM otherwise ion-button will not be highlighted
1115
1170
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1116
1171
  */
1117
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
1172
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
1118
1173
  // Prevents the handle from receiving keyboard focus when it does not cycle
1119
- 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
1174
+ 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: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
1120
1175
  }
1121
1176
  static get is() { return "ion-modal"; }
1122
1177
  static get encapsulation() { return "shadow"; }
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-01-05T21:20:51",
2
+ "timestamp": "2026-01-07T14:44:35",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.38.0",