@ionic/core 8.7.17-dev.11767717752.14fe98a4 → 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.
- package/components/content.js +51 -7
- package/components/modal.js +81 -26
- package/dist/cjs/ion-app_8.cjs.entry.js +50 -6
- package/dist/cjs/ion-modal.cjs.entry.js +81 -26
- package/dist/collection/components/content/content.js +50 -6
- package/dist/collection/components/modal/modal.js +81 -26
- package/dist/docs.json +1 -1
- package/dist/esm/ion-app_8.entry.js +51 -7
- package/dist/esm/ion-modal.entry.js +81 -26
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-3830217a.entry.js +4 -0
- package/dist/ionic/p-cecd141b.entry.js +4 -0
- package/dist/types/components/content/content.d.ts +15 -2
- package/dist/types/components/modal/modal.d.ts +12 -2
- package/hydrate/index.js +131 -33
- package/hydrate/index.mjs +131 -33
- package/package.json +1 -1
- package/dist/ionic/p-34cdcd15.entry.js +0 -4
- package/dist/ionic/p-fedca459.entry.js +0 -4
package/components/content.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
|
-
import { proxyCustomElement, HTMLElement, createEvent, Build, readTask,
|
|
4
|
+
import { proxyCustomElement, HTMLElement, createEvent, forceUpdate, Build, readTask, h, Host } from '@stencil/core/internal/client';
|
|
5
5
|
import { i as inheritAriaAttributes, k as hasLazyBuild, c as componentOnReady } from './helpers.js';
|
|
6
6
|
import { b as getIonMode, a as isPlatform } from './ionic-global.js';
|
|
7
7
|
import { i as isRTL } from './dir.js';
|
|
@@ -127,15 +127,39 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
-
* Detects sibling ion-header and ion-footer elements
|
|
131
|
-
*
|
|
130
|
+
* Detects sibling ion-header and ion-footer elements and sets up
|
|
131
|
+
* a mutation observer to handle dynamic changes (e.g., conditional rendering).
|
|
132
132
|
*/
|
|
133
133
|
detectSiblingElements() {
|
|
134
|
-
|
|
134
|
+
this.updateSiblingDetection();
|
|
135
|
+
// Watch for dynamic header/footer changes (common in React conditional rendering)
|
|
136
|
+
const parent = this.el.parentElement;
|
|
137
|
+
if (parent && !this.parentMutationObserver) {
|
|
138
|
+
this.parentMutationObserver = new MutationObserver(() => {
|
|
139
|
+
this.updateSiblingDetection();
|
|
140
|
+
forceUpdate(this);
|
|
141
|
+
});
|
|
142
|
+
this.parentMutationObserver.observe(parent, { childList: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Updates hasHeader/hasFooter based on current DOM state.
|
|
147
|
+
* Checks both direct siblings and elements wrapped in custom components
|
|
148
|
+
* (e.g., <my-header><ion-header>...</ion-header></my-header>).
|
|
149
|
+
*/
|
|
150
|
+
updateSiblingDetection() {
|
|
135
151
|
const parent = this.el.parentElement;
|
|
136
152
|
if (parent) {
|
|
153
|
+
// First check for direct ion-header/ion-footer siblings
|
|
137
154
|
this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
|
|
138
155
|
this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
|
|
156
|
+
// If not found, check if any sibling contains them (wrapped components)
|
|
157
|
+
if (!this.hasHeader) {
|
|
158
|
+
this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
|
|
159
|
+
}
|
|
160
|
+
if (!this.hasFooter) {
|
|
161
|
+
this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
|
|
162
|
+
}
|
|
139
163
|
}
|
|
140
164
|
// If no footer found, check if we're inside ion-tabs which has ion-tab-bar
|
|
141
165
|
if (!this.hasFooter) {
|
|
@@ -145,8 +169,28 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
|
|
|
145
169
|
}
|
|
146
170
|
}
|
|
147
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Checks if any sibling element of ion-content contains the specified element.
|
|
174
|
+
* Only searches one level deep to avoid finding elements in nested pages.
|
|
175
|
+
*/
|
|
176
|
+
siblingContainsElement(parent, tagName) {
|
|
177
|
+
for (const sibling of parent.children) {
|
|
178
|
+
// Skip ion-content itself
|
|
179
|
+
if (sibling === this.el)
|
|
180
|
+
continue;
|
|
181
|
+
// Check if this sibling contains the target element as an immediate child
|
|
182
|
+
if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
148
188
|
disconnectedCallback() {
|
|
189
|
+
var _a;
|
|
149
190
|
this.onScrollEnd();
|
|
191
|
+
// Clean up mutation observer to prevent memory leaks
|
|
192
|
+
(_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
193
|
+
this.parentMutationObserver = undefined;
|
|
150
194
|
if (hasLazyBuild(this.el)) {
|
|
151
195
|
/**
|
|
152
196
|
* The event listener and tabs caches need to
|
|
@@ -399,7 +443,7 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
|
|
|
399
443
|
const forceOverscroll = this.shouldForceOverscroll();
|
|
400
444
|
const transitionShadow = mode === 'ios';
|
|
401
445
|
this.resize();
|
|
402
|
-
return (h(Host, Object.assign({ key: '
|
|
446
|
+
return (h(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
|
|
403
447
|
[mode]: true,
|
|
404
448
|
'content-sizing': hostContext('ion-popover', this.el),
|
|
405
449
|
overscroll: forceOverscroll,
|
|
@@ -409,12 +453,12 @@ const Content = /*@__PURE__*/ proxyCustomElement(class Content extends HTMLEleme
|
|
|
409
453
|
}), style: {
|
|
410
454
|
'--offset-top': `${this.cTop}px`,
|
|
411
455
|
'--offset-bottom': `${this.cBottom}px`,
|
|
412
|
-
} }, inheritedAttributes), h("div", { key: '
|
|
456
|
+
} }, 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: {
|
|
413
457
|
'inner-scroll': true,
|
|
414
458
|
'scroll-x': scrollX,
|
|
415
459
|
'scroll-y': scrollY,
|
|
416
460
|
overscroll: (scrollX || scrollY) && forceOverscroll,
|
|
417
|
-
}, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '
|
|
461
|
+
}, 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));
|
|
418
462
|
}
|
|
419
463
|
get el() { return this; }
|
|
420
464
|
static get style() { return contentCss; }
|
package/components/modal.js
CHANGED
|
@@ -1506,6 +1506,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
1506
1506
|
this.gestureAnimationDismissing = false;
|
|
1507
1507
|
// Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
|
|
1508
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 };
|
|
1509
1511
|
this.presented = false;
|
|
1510
1512
|
/** @internal */
|
|
1511
1513
|
this.hasController = false;
|
|
@@ -1696,7 +1698,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
1696
1698
|
}
|
|
1697
1699
|
}
|
|
1698
1700
|
onWindowResize() {
|
|
1699
|
-
//
|
|
1701
|
+
// Invalidate safe-area cache on resize (device rotation may change values)
|
|
1702
|
+
this.cachedSafeAreas = undefined;
|
|
1700
1703
|
this.updateSafeAreaOverrides();
|
|
1701
1704
|
// Only handle view transition for iOS card modals when no custom animations are provided
|
|
1702
1705
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
@@ -2155,8 +2158,26 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2155
2158
|
*/
|
|
2156
2159
|
applyFullscreenSafeArea() {
|
|
2157
2160
|
this.skipSafeAreaCoordinateDetection = true;
|
|
2161
|
+
this.updateFooterPadding();
|
|
2162
|
+
// Watch for dynamic footer additions/removals (e.g., async data loading)
|
|
2163
|
+
if (!this.footerObserver) {
|
|
2164
|
+
this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
|
|
2165
|
+
this.footerObserver.observe(this.el, { childList: true, subtree: true });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Updates wrapper padding based on footer presence.
|
|
2170
|
+
* Called initially and when footer is dynamically added/removed.
|
|
2171
|
+
*/
|
|
2172
|
+
updateFooterPadding() {
|
|
2173
|
+
if (!this.wrapperEl)
|
|
2174
|
+
return;
|
|
2158
2175
|
const hasFooter = this.el.querySelector('ion-footer') !== null;
|
|
2159
|
-
if (
|
|
2176
|
+
if (hasFooter) {
|
|
2177
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
2178
|
+
this.wrapperEl.style.removeProperty('box-sizing');
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2160
2181
|
this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
2161
2182
|
this.wrapperEl.style.setProperty('box-sizing', 'border-box');
|
|
2162
2183
|
}
|
|
@@ -2174,21 +2195,26 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2174
2195
|
}
|
|
2175
2196
|
/**
|
|
2176
2197
|
* Gets the root safe-area values from the document element.
|
|
2177
|
-
*
|
|
2198
|
+
* Uses cached values during gestures to avoid getComputedStyle calls.
|
|
2178
2199
|
*/
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2200
|
+
getSafeAreaValues() {
|
|
2201
|
+
if (!this.cachedSafeAreas) {
|
|
2202
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
2203
|
+
this.cachedSafeAreas = {
|
|
2204
|
+
top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
|
|
2205
|
+
bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
|
|
2206
|
+
left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
|
|
2207
|
+
right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
return this.cachedSafeAreas;
|
|
2187
2211
|
}
|
|
2188
2212
|
/**
|
|
2189
2213
|
* Updates safe-area CSS variable overrides based on whether the modal
|
|
2190
2214
|
* extends into each safe-area region. Called after animation
|
|
2191
2215
|
* and during gestures to handle dynamic position changes.
|
|
2216
|
+
*
|
|
2217
|
+
* Optimized to avoid redundant DOM writes by tracking previous state.
|
|
2192
2218
|
*/
|
|
2193
2219
|
updateSafeAreaOverrides() {
|
|
2194
2220
|
if (this.skipSafeAreaCoordinateDetection) {
|
|
@@ -2199,20 +2225,34 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2199
2225
|
return;
|
|
2200
2226
|
}
|
|
2201
2227
|
const rect = wrapper.getBoundingClientRect();
|
|
2202
|
-
const safeAreas = this.
|
|
2228
|
+
const safeAreas = this.getSafeAreaValues();
|
|
2203
2229
|
const extendsIntoTop = rect.top < safeAreas.top;
|
|
2204
2230
|
const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
|
|
2205
2231
|
const extendsIntoLeft = rect.left < safeAreas.left;
|
|
2206
2232
|
const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
|
|
2233
|
+
// Only update DOM when state actually changes
|
|
2234
|
+
const prev = this.prevSafeAreaState;
|
|
2207
2235
|
const style = this.el.style;
|
|
2208
|
-
extendsIntoTop
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2236
|
+
if (extendsIntoTop !== prev.top) {
|
|
2237
|
+
extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
|
|
2238
|
+
prev.top = extendsIntoTop;
|
|
2239
|
+
}
|
|
2240
|
+
if (extendsIntoBottom !== prev.bottom) {
|
|
2241
|
+
extendsIntoBottom
|
|
2242
|
+
? style.removeProperty('--ion-safe-area-bottom')
|
|
2243
|
+
: style.setProperty('--ion-safe-area-bottom', '0px');
|
|
2244
|
+
prev.bottom = extendsIntoBottom;
|
|
2245
|
+
}
|
|
2246
|
+
if (extendsIntoLeft !== prev.left) {
|
|
2247
|
+
extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
|
|
2248
|
+
prev.left = extendsIntoLeft;
|
|
2249
|
+
}
|
|
2250
|
+
if (extendsIntoRight !== prev.right) {
|
|
2251
|
+
extendsIntoRight
|
|
2252
|
+
? style.removeProperty('--ion-safe-area-right')
|
|
2253
|
+
: style.setProperty('--ion-safe-area-right', '0px');
|
|
2254
|
+
prev.right = extendsIntoRight;
|
|
2255
|
+
}
|
|
2216
2256
|
}
|
|
2217
2257
|
sheetOnDismiss() {
|
|
2218
2258
|
/**
|
|
@@ -2244,7 +2284,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2244
2284
|
* For example, `cancel` or `backdrop`.
|
|
2245
2285
|
*/
|
|
2246
2286
|
async dismiss(data, role) {
|
|
2247
|
-
var _a;
|
|
2287
|
+
var _a, _b;
|
|
2248
2288
|
if (this.gestureAnimationDismissing && role !== GESTURE) {
|
|
2249
2289
|
return false;
|
|
2250
2290
|
}
|
|
@@ -2306,8 +2346,23 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2306
2346
|
}
|
|
2307
2347
|
this.currentBreakpoint = undefined;
|
|
2308
2348
|
this.animation = undefined;
|
|
2309
|
-
// Reset safe-area
|
|
2349
|
+
// Reset safe-area state for potential re-presentation
|
|
2310
2350
|
this.skipSafeAreaCoordinateDetection = false;
|
|
2351
|
+
this.cachedSafeAreas = undefined;
|
|
2352
|
+
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
2353
|
+
(_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
2354
|
+
this.footerObserver = undefined;
|
|
2355
|
+
// Clear styles that may have been set for safe-area handling
|
|
2356
|
+
if (this.wrapperEl) {
|
|
2357
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
2358
|
+
this.wrapperEl.style.removeProperty('box-sizing');
|
|
2359
|
+
}
|
|
2360
|
+
// Clear safe-area CSS variable overrides
|
|
2361
|
+
const style = this.el.style;
|
|
2362
|
+
style.removeProperty('--ion-safe-area-top');
|
|
2363
|
+
style.removeProperty('--ion-safe-area-bottom');
|
|
2364
|
+
style.removeProperty('--ion-safe-area-left');
|
|
2365
|
+
style.removeProperty('--ion-safe-area-right');
|
|
2311
2366
|
unlock();
|
|
2312
2367
|
return dismissed;
|
|
2313
2368
|
}
|
|
@@ -2557,20 +2612,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
|
|
|
2557
2612
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
2558
2613
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
2559
2614
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
2560
|
-
return (h(Host, Object.assign({ key: '
|
|
2615
|
+
return (h(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
|
|
2561
2616
|
// Allow the modal to be navigable when the handle is focusable
|
|
2562
2617
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
2563
2618
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
2564
|
-
}, 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: '
|
|
2619
|
+
}, 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',
|
|
2565
2620
|
/*
|
|
2566
2621
|
role and aria-modal must be used on the
|
|
2567
2622
|
same element. They must also be set inside the
|
|
2568
2623
|
shadow DOM otherwise ion-button will not be highlighted
|
|
2569
2624
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
2570
2625
|
*/
|
|
2571
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
2626
|
+
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",
|
|
2572
2627
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
2573
|
-
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: '
|
|
2628
|
+
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 }))));
|
|
2574
2629
|
}
|
|
2575
2630
|
get el() { return this; }
|
|
2576
2631
|
static get watchers() { return {
|
|
@@ -270,15 +270,39 @@ const Content = class {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
/**
|
|
273
|
-
* Detects sibling ion-header and ion-footer elements
|
|
274
|
-
*
|
|
273
|
+
* Detects sibling ion-header and ion-footer elements and sets up
|
|
274
|
+
* a mutation observer to handle dynamic changes (e.g., conditional rendering).
|
|
275
275
|
*/
|
|
276
276
|
detectSiblingElements() {
|
|
277
|
-
|
|
277
|
+
this.updateSiblingDetection();
|
|
278
|
+
// Watch for dynamic header/footer changes (common in React conditional rendering)
|
|
279
|
+
const parent = this.el.parentElement;
|
|
280
|
+
if (parent && !this.parentMutationObserver) {
|
|
281
|
+
this.parentMutationObserver = new MutationObserver(() => {
|
|
282
|
+
this.updateSiblingDetection();
|
|
283
|
+
index.forceUpdate(this);
|
|
284
|
+
});
|
|
285
|
+
this.parentMutationObserver.observe(parent, { childList: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Updates hasHeader/hasFooter based on current DOM state.
|
|
290
|
+
* Checks both direct siblings and elements wrapped in custom components
|
|
291
|
+
* (e.g., <my-header><ion-header>...</ion-header></my-header>).
|
|
292
|
+
*/
|
|
293
|
+
updateSiblingDetection() {
|
|
278
294
|
const parent = this.el.parentElement;
|
|
279
295
|
if (parent) {
|
|
296
|
+
// First check for direct ion-header/ion-footer siblings
|
|
280
297
|
this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
|
|
281
298
|
this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
|
|
299
|
+
// If not found, check if any sibling contains them (wrapped components)
|
|
300
|
+
if (!this.hasHeader) {
|
|
301
|
+
this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
|
|
302
|
+
}
|
|
303
|
+
if (!this.hasFooter) {
|
|
304
|
+
this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
|
|
305
|
+
}
|
|
282
306
|
}
|
|
283
307
|
// If no footer found, check if we're inside ion-tabs which has ion-tab-bar
|
|
284
308
|
if (!this.hasFooter) {
|
|
@@ -288,8 +312,28 @@ const Content = class {
|
|
|
288
312
|
}
|
|
289
313
|
}
|
|
290
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Checks if any sibling element of ion-content contains the specified element.
|
|
317
|
+
* Only searches one level deep to avoid finding elements in nested pages.
|
|
318
|
+
*/
|
|
319
|
+
siblingContainsElement(parent, tagName) {
|
|
320
|
+
for (const sibling of parent.children) {
|
|
321
|
+
// Skip ion-content itself
|
|
322
|
+
if (sibling === this.el)
|
|
323
|
+
continue;
|
|
324
|
+
// Check if this sibling contains the target element as an immediate child
|
|
325
|
+
if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
291
331
|
disconnectedCallback() {
|
|
332
|
+
var _a;
|
|
292
333
|
this.onScrollEnd();
|
|
334
|
+
// Clean up mutation observer to prevent memory leaks
|
|
335
|
+
(_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
336
|
+
this.parentMutationObserver = undefined;
|
|
293
337
|
if (helpers.hasLazyBuild(this.el)) {
|
|
294
338
|
/**
|
|
295
339
|
* The event listener and tabs caches need to
|
|
@@ -542,7 +586,7 @@ const Content = class {
|
|
|
542
586
|
const forceOverscroll = this.shouldForceOverscroll();
|
|
543
587
|
const transitionShadow = mode === 'ios';
|
|
544
588
|
this.resize();
|
|
545
|
-
return (index.h(index.Host, Object.assign({ key: '
|
|
589
|
+
return (index.h(index.Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: theme.createColorClasses(this.color, {
|
|
546
590
|
[mode]: true,
|
|
547
591
|
'content-sizing': theme.hostContext('ion-popover', this.el),
|
|
548
592
|
overscroll: forceOverscroll,
|
|
@@ -552,12 +596,12 @@ const Content = class {
|
|
|
552
596
|
}), style: {
|
|
553
597
|
'--offset-top': `${this.cTop}px`,
|
|
554
598
|
'--offset-bottom': `${this.cBottom}px`,
|
|
555
|
-
} }, inheritedAttributes), index.h("div", { key: '
|
|
599
|
+
} }, inheritedAttributes), index.h("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? index.h("slot", { name: "fixed" }) : null, index.h("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
|
|
556
600
|
'inner-scroll': true,
|
|
557
601
|
'scroll-x': scrollX,
|
|
558
602
|
'scroll-y': scrollY,
|
|
559
603
|
overscroll: (scrollX || scrollY) && forceOverscroll,
|
|
560
|
-
}, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '
|
|
604
|
+
}, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, index.h("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (index.h("div", { class: "transition-effect" }, index.h("div", { class: "transition-cover" }), index.h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? index.h("slot", { name: "fixed" }) : null));
|
|
561
605
|
}
|
|
562
606
|
get el() { return index.getElement(this); }
|
|
563
607
|
};
|
|
@@ -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
|
-
//
|
|
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 (
|
|
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
|
-
*
|
|
2197
|
+
* Uses cached values during gestures to avoid getComputedStyle calls.
|
|
2177
2198
|
*/
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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.
|
|
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
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 {
|