@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.
- package/components/content.js +79 -6
- package/components/modal.js +81 -26
- package/dist/cjs/ion-app_8.cjs.entry.js +78 -5
- package/dist/cjs/ion-modal.cjs.entry.js +81 -26
- package/dist/collection/components/content/content.css +10 -0
- package/dist/collection/components/content/content.js +77 -4
- package/dist/collection/components/modal/modal.js +81 -26
- package/dist/docs.json +1 -1
- package/dist/esm/ion-app_8.entry.js +79 -6
- 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 +24 -0
- package/dist/types/components/modal/modal.d.ts +12 -2
- package/hydrate/index.js +159 -32
- package/hydrate/index.mjs +159 -32
- package/package.json +1 -1
- package/dist/ionic/p-732b2fd6.entry.js +0 -4
- package/dist/ionic/p-fedca459.entry.js +0 -4
|
@@ -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 {
|
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
*
|
|
736
|
+
* Uses cached values during gestures to avoid getComputedStyle calls.
|
|
716
737
|
*/
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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.
|
|
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
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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