@ionic/core 8.7.17-dev.11767895575.16ea7cef → 8.7.17-dev.11767897190.1ef0f479
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/content.js +96 -8
- package/components/ion-tab-bar.js +3 -23
- package/components/modal.js +213 -12
- package/components/popover.js +83 -11
- package/dist/cjs/ion-app_8.cjs.entry.js +107 -20
- package/dist/cjs/ion-modal.cjs.entry.js +213 -12
- package/dist/cjs/ion-popover.cjs.entry.js +83 -11
- package/dist/cjs/ion-tab-bar_2.cjs.entry.js +3 -23
- package/dist/collection/components/content/content.css +10 -0
- package/dist/collection/components/content/content.js +94 -6
- package/dist/collection/components/modal/gestures/sheet.js +3 -1
- package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
- package/dist/collection/components/modal/modal.ios.css +0 -4
- package/dist/collection/components/modal/modal.js +205 -7
- package/dist/collection/components/modal/modal.md.css +0 -4
- package/dist/collection/components/popover/animations/ios.enter.js +21 -5
- package/dist/collection/components/popover/animations/md.enter.js +30 -5
- package/dist/collection/components/popover/utils.js +32 -1
- package/dist/collection/components/tab-bar/tab-bar.js +3 -23
- package/dist/docs.json +1 -1
- package/dist/esm/ion-app_8.entry.js +96 -9
- package/dist/esm/ion-modal.entry.js +213 -12
- package/dist/esm/ion-popover.entry.js +83 -11
- package/dist/esm/ion-tab-bar_2.entry.js +3 -23
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-7268efa5.entry.js +4 -0
- package/dist/ionic/p-968a55d1.entry.js +4 -0
- package/dist/ionic/p-d9fd799f.entry.js +4 -0
- package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
- package/dist/types/components/content/content.d.ts +24 -0
- package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
- package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
- package/dist/types/components/modal/modal.d.ts +45 -0
- package/dist/types/components/popover/utils.d.ts +2 -0
- package/dist/types/components/tab-bar/tab-bar.d.ts +0 -1
- package/hydrate/index.js +385 -52
- package/hydrate/index.mjs +385 -52
- package/package.json +1 -1
- package/dist/ionic/p-172a579f.entry.js +0 -4
- package/dist/ionic/p-732b2fd6.entry.js +0 -4
- package/dist/ionic/p-91840a80.entry.js +0 -4
- package/dist/ionic/p-f9061316.entry.js +0 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Build, Host, forceUpdate, h, readTask } from "@stencil/core";
|
|
5
|
+
import { win } from "../../utils/browser/index";
|
|
5
6
|
import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from "../../utils/helpers";
|
|
6
7
|
import { isPlatform } from "../../utils/platform";
|
|
7
8
|
import { isRTL } from "../../utils/rtl/index";
|
|
@@ -25,6 +26,12 @@ export class Content {
|
|
|
25
26
|
this.isMainContent = true;
|
|
26
27
|
this.resizeTimeout = null;
|
|
27
28
|
this.inheritedAttributes = {};
|
|
29
|
+
/**
|
|
30
|
+
* Track whether this content has sibling header/footer elements.
|
|
31
|
+
* When absent, we need to apply safe-area padding directly.
|
|
32
|
+
*/
|
|
33
|
+
this.hasHeader = false;
|
|
34
|
+
this.hasFooter = false;
|
|
28
35
|
this.tabsElement = null;
|
|
29
36
|
// Detail is used in a hot loop in the scroll event, by allocating it here
|
|
30
37
|
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
|
@@ -79,7 +86,13 @@ export class Content {
|
|
|
79
86
|
this.inheritedAttributes = inheritAriaAttributes(this.el);
|
|
80
87
|
}
|
|
81
88
|
connectedCallback() {
|
|
82
|
-
|
|
89
|
+
var _a;
|
|
90
|
+
// Content is "main" if not inside menu/popover/modal and not nested in another ion-content
|
|
91
|
+
this.isMainContent =
|
|
92
|
+
this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
|
|
93
|
+
((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest('ion-content')) === null;
|
|
94
|
+
// Detect sibling header/footer for safe-area handling
|
|
95
|
+
this.detectSiblingElements();
|
|
83
96
|
/**
|
|
84
97
|
* The fullscreen content offsets need to be
|
|
85
98
|
* computed after the tab bar has loaded. Since
|
|
@@ -110,13 +123,86 @@ export class Content {
|
|
|
110
123
|
* bubbles, we can catch any instances of child tab bars loading by listening
|
|
111
124
|
* on IonTabs.
|
|
112
125
|
*/
|
|
113
|
-
this.tabsLoadCallback = () =>
|
|
126
|
+
this.tabsLoadCallback = () => {
|
|
127
|
+
this.resize();
|
|
128
|
+
// Re-detect footer when tab bar loads (it may not exist during initial detection)
|
|
129
|
+
this.updateSiblingDetection();
|
|
130
|
+
forceUpdate(this);
|
|
131
|
+
};
|
|
114
132
|
closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
|
|
115
133
|
}
|
|
116
134
|
}
|
|
117
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Detects sibling ion-header and ion-footer elements and sets up
|
|
138
|
+
* a mutation observer to handle dynamic changes (e.g., conditional rendering).
|
|
139
|
+
*/
|
|
140
|
+
detectSiblingElements() {
|
|
141
|
+
this.updateSiblingDetection();
|
|
142
|
+
// Watch for dynamic header/footer changes (common in React conditional rendering)
|
|
143
|
+
const parent = this.el.parentElement;
|
|
144
|
+
if (parent && !this.parentMutationObserver && win !== undefined && 'MutationObserver' in win) {
|
|
145
|
+
this.parentMutationObserver = new MutationObserver(() => {
|
|
146
|
+
const prevHasHeader = this.hasHeader;
|
|
147
|
+
const prevHasFooter = this.hasFooter;
|
|
148
|
+
this.updateSiblingDetection();
|
|
149
|
+
// Only trigger re-render if header/footer detection actually changed
|
|
150
|
+
if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
|
|
151
|
+
forceUpdate(this);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
this.parentMutationObserver.observe(parent, { childList: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Updates hasHeader/hasFooter based on current DOM state.
|
|
159
|
+
* Checks both direct siblings and elements wrapped in custom components
|
|
160
|
+
* (e.g., <my-header><ion-header>...</ion-header></my-header>).
|
|
161
|
+
*/
|
|
162
|
+
updateSiblingDetection() {
|
|
163
|
+
const parent = this.el.parentElement;
|
|
164
|
+
if (parent) {
|
|
165
|
+
// First check for direct ion-header/ion-footer siblings
|
|
166
|
+
this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
|
|
167
|
+
this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
|
|
168
|
+
// If not found, check if any sibling contains them (wrapped components)
|
|
169
|
+
if (!this.hasHeader) {
|
|
170
|
+
this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
|
|
171
|
+
}
|
|
172
|
+
if (!this.hasFooter) {
|
|
173
|
+
this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// If no footer found, check if we're inside ion-tabs which has ion-tab-bar
|
|
177
|
+
if (!this.hasFooter) {
|
|
178
|
+
const tabs = this.el.closest('ion-tabs');
|
|
179
|
+
if (tabs) {
|
|
180
|
+
this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Checks if any sibling element of ion-content contains the specified element.
|
|
186
|
+
* Only searches one level deep to avoid finding elements in nested pages.
|
|
187
|
+
*/
|
|
188
|
+
siblingContainsElement(parent, tagName) {
|
|
189
|
+
for (const sibling of parent.children) {
|
|
190
|
+
// Skip ion-content itself
|
|
191
|
+
if (sibling === this.el)
|
|
192
|
+
continue;
|
|
193
|
+
// Check if this sibling contains the target element as an immediate child
|
|
194
|
+
if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
118
200
|
disconnectedCallback() {
|
|
201
|
+
var _a;
|
|
119
202
|
this.onScrollEnd();
|
|
203
|
+
// Clean up mutation observer to prevent memory leaks
|
|
204
|
+
(_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
205
|
+
this.parentMutationObserver = undefined;
|
|
120
206
|
if (hasLazyBuild(this.el)) {
|
|
121
207
|
/**
|
|
122
208
|
* The event listener and tabs caches need to
|
|
@@ -363,26 +449,28 @@ export class Content {
|
|
|
363
449
|
}
|
|
364
450
|
}
|
|
365
451
|
render() {
|
|
366
|
-
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
|
|
452
|
+
const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
|
|
367
453
|
const rtl = isRTL(el) ? 'rtl' : 'ltr';
|
|
368
454
|
const mode = getIonMode(this);
|
|
369
455
|
const forceOverscroll = this.shouldForceOverscroll();
|
|
370
456
|
const transitionShadow = mode === 'ios';
|
|
371
457
|
this.resize();
|
|
372
|
-
return (h(Host, Object.assign({ key: '
|
|
458
|
+
return (h(Host, Object.assign({ key: 'f7218f733e4022a30875441bd949747537d28aa1', role: isMainContent ? 'main' : undefined, class: createColorClasses(this.color, {
|
|
373
459
|
[mode]: true,
|
|
374
460
|
'content-sizing': hostContext('ion-popover', this.el),
|
|
375
461
|
overscroll: forceOverscroll,
|
|
376
462
|
[`content-${rtl}`]: true,
|
|
463
|
+
'safe-area-top': isMainContent && !hasHeader,
|
|
464
|
+
'safe-area-bottom': isMainContent && !hasFooter,
|
|
377
465
|
}), style: {
|
|
378
466
|
'--offset-top': `${this.cTop}px`,
|
|
379
467
|
'--offset-bottom': `${this.cBottom}px`,
|
|
380
|
-
} }, inheritedAttributes), h("div", { key: '
|
|
468
|
+
} }, inheritedAttributes), h("div", { key: 'b735ec68c18c0b99c3595bb194029830e6542cde', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? h("slot", { name: "fixed" }) : null, h("div", { key: 'e76c00d030342d44ade6648c3f9e32ca990787ba', class: {
|
|
381
469
|
'inner-scroll': true,
|
|
382
470
|
'scroll-x': scrollX,
|
|
383
471
|
'scroll-y': scrollY,
|
|
384
472
|
overscroll: (scrollX || scrollY) && forceOverscroll,
|
|
385
|
-
}, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '
|
|
473
|
+
}, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, h("slot", { key: '9049be4cea9b5da5ec1e1012248b05286fddeb7a' })), transitionShadow ? (h("div", { class: "transition-effect" }, h("div", { class: "transition-cover" }), h("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? h("slot", { name: "fixed" }) : null));
|
|
386
474
|
}
|
|
387
475
|
static get is() { return "ion-content"; }
|
|
388
476
|
static get encapsulation() { return "shadow"; }
|
|
@@ -7,7 +7,7 @@ import { clamp, getElementRoot, raf } from "../../../utils/helpers";
|
|
|
7
7
|
import { FOCUS_TRAP_DISABLE_CLASS } from "../../../utils/overlays";
|
|
8
8
|
import { getBackdropValueForSheet } from "../utils";
|
|
9
9
|
import { calculateSpringStep, handleCanDismiss } from "./utils";
|
|
10
|
-
export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
|
|
10
|
+
export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
|
|
11
11
|
// Defaults for the sheet swipe animation
|
|
12
12
|
const defaultBackdrop = [
|
|
13
13
|
{ offset: 0, opacity: 'var(--backdrop-opacity)' },
|
|
@@ -338,6 +338,8 @@ export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpo
|
|
|
338
338
|
: step;
|
|
339
339
|
offset = clamp(0.0001, processedStep, maxStep);
|
|
340
340
|
animation.progressStep(offset);
|
|
341
|
+
// Notify modal of position change for safe-area updates
|
|
342
|
+
onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
|
|
341
343
|
};
|
|
342
344
|
const onEnd = (detail) => {
|
|
343
345
|
/**
|
|
@@ -12,7 +12,7 @@ import { calculateSpringStep, handleCanDismiss } from "./utils";
|
|
|
12
12
|
export const SwipeToCloseDefaults = {
|
|
13
13
|
MIN_PRESENTING_SCALE: 0.915,
|
|
14
14
|
};
|
|
15
|
-
export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
|
|
15
|
+
export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
|
|
16
16
|
/**
|
|
17
17
|
* The step value at which a card modal
|
|
18
18
|
* is eligible for dismissing via gesture.
|
|
@@ -169,6 +169,8 @@ export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismi
|
|
|
169
169
|
const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
|
|
170
170
|
const clampedStep = clamp(0.0001, processedStep, maxStep);
|
|
171
171
|
animation.progressStep(clampedStep);
|
|
172
|
+
// Notify modal of position change for safe-area updates
|
|
173
|
+
onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
|
|
172
174
|
/**
|
|
173
175
|
* When swiping down half way, the status bar style
|
|
174
176
|
* should be reset to its default value.
|
|
@@ -135,10 +135,6 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
-
--ion-safe-area-top: 0px;
|
|
139
|
-
--ion-safe-area-bottom: 0px;
|
|
140
|
-
--ion-safe-area-right: 0px;
|
|
141
|
-
--ion-safe-area-left: 0px;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Host, h, writeTask } from "@stencil/core";
|
|
5
|
+
import { win } from "../../utils/browser/index";
|
|
5
6
|
import { findIonContent, printIonContentErrorMsg } from "../../utils/content/index";
|
|
6
7
|
import { CoreDelegate, attachComponent, detachComponent } from "../../utils/framework-delegate";
|
|
7
8
|
import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from "../../utils/helpers";
|
|
@@ -42,6 +43,10 @@ export class Modal {
|
|
|
42
43
|
this.inline = false;
|
|
43
44
|
// Whether or not modal is being dismissed via gesture
|
|
44
45
|
this.gestureAnimationDismissing = false;
|
|
46
|
+
// Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
|
|
47
|
+
this.skipSafeAreaCoordinateDetection = false;
|
|
48
|
+
// Track previous safe-area state to avoid redundant DOM writes
|
|
49
|
+
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
45
50
|
this.presented = false;
|
|
46
51
|
/** @internal */
|
|
47
52
|
this.hasController = false;
|
|
@@ -232,7 +237,10 @@ export class Modal {
|
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
onWindowResize() {
|
|
235
|
-
//
|
|
240
|
+
// Invalidate safe-area cache on resize (device rotation may change values)
|
|
241
|
+
this.cachedSafeAreas = undefined;
|
|
242
|
+
this.updateSafeAreaOverrides();
|
|
243
|
+
// Only handle view transition for iOS card modals when no custom animations are provided
|
|
236
244
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
237
245
|
return;
|
|
238
246
|
}
|
|
@@ -255,6 +263,8 @@ export class Modal {
|
|
|
255
263
|
this.triggerController.removeClickListener();
|
|
256
264
|
this.cleanupViewTransitionListener();
|
|
257
265
|
this.cleanupParentRemovalObserver();
|
|
266
|
+
// Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
|
|
267
|
+
this.resetSafeAreaState();
|
|
258
268
|
}
|
|
259
269
|
componentWillLoad() {
|
|
260
270
|
var _a;
|
|
@@ -414,6 +424,8 @@ export class Modal {
|
|
|
414
424
|
else if (!this.keepContentsMounted) {
|
|
415
425
|
await waitForMount();
|
|
416
426
|
}
|
|
427
|
+
// Predict safe-area needs based on modal configuration to avoid visual snap
|
|
428
|
+
this.setInitialSafeAreaOverrides(presentingElement);
|
|
417
429
|
writeTask(() => this.el.classList.add('show-modal'));
|
|
418
430
|
const hasCardModal = presentingElement !== undefined;
|
|
419
431
|
/**
|
|
@@ -475,6 +487,8 @@ export class Modal {
|
|
|
475
487
|
else if (hasCardModal) {
|
|
476
488
|
this.initSwipeToClose();
|
|
477
489
|
}
|
|
490
|
+
// Now that animation is complete, update safe-area based on actual position
|
|
491
|
+
this.updateSafeAreaOverrides();
|
|
478
492
|
// Initialize view transition listener for iOS card modals
|
|
479
493
|
this.initViewTransitionListener();
|
|
480
494
|
// Initialize parent removal observer
|
|
@@ -526,7 +540,7 @@ export class Modal {
|
|
|
526
540
|
await this.dismiss(undefined, GESTURE);
|
|
527
541
|
this.gestureAnimationDismissing = false;
|
|
528
542
|
});
|
|
529
|
-
});
|
|
543
|
+
}, () => this.updateSafeAreaOverrides());
|
|
530
544
|
this.gesture.enable(true);
|
|
531
545
|
}
|
|
532
546
|
initSheetGesture() {
|
|
@@ -547,7 +561,8 @@ export class Modal {
|
|
|
547
561
|
this.currentBreakpoint = breakpoint;
|
|
548
562
|
this.ionBreakpointDidChange.emit({ breakpoint });
|
|
549
563
|
}
|
|
550
|
-
|
|
564
|
+
this.updateSafeAreaOverrides();
|
|
565
|
+
}, () => this.updateSafeAreaOverrides());
|
|
551
566
|
this.gesture = gesture;
|
|
552
567
|
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
|
|
553
568
|
this.gesture.enable(true);
|
|
@@ -625,6 +640,187 @@ export class Modal {
|
|
|
625
640
|
// Clear the cached reference
|
|
626
641
|
this.cachedPageParent = undefined;
|
|
627
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Sets initial safe-area overrides based on modal configuration before
|
|
645
|
+
* the modal becomes visible. This predicts whether the modal will touch
|
|
646
|
+
* screen edges to avoid a visual snap after animation completes.
|
|
647
|
+
*/
|
|
648
|
+
setInitialSafeAreaOverrides(presentingElement) {
|
|
649
|
+
const style = this.el.style;
|
|
650
|
+
const mode = getIonMode(this);
|
|
651
|
+
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
652
|
+
// Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
|
|
653
|
+
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
654
|
+
const isTablet = window.innerWidth >= 768;
|
|
655
|
+
// Sheet modals always touch bottom edge, never top/left/right
|
|
656
|
+
if (isSheetModal) {
|
|
657
|
+
style.setProperty('--ion-safe-area-top', '0px');
|
|
658
|
+
style.setProperty('--ion-safe-area-left', '0px');
|
|
659
|
+
style.setProperty('--ion-safe-area-right', '0px');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Card modals have rounded top corners
|
|
663
|
+
if (isCardModal) {
|
|
664
|
+
style.setProperty('--ion-safe-area-top', '0px');
|
|
665
|
+
if (isTablet) {
|
|
666
|
+
// On tablets, card modals are inset from all edges
|
|
667
|
+
this.zeroAllSafeAreas();
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// On phones, card modals still extend to the bottom edge
|
|
671
|
+
style.setProperty('--ion-safe-area-left', '0px');
|
|
672
|
+
style.setProperty('--ion-safe-area-right', '0px');
|
|
673
|
+
this.applyFullscreenSafeArea();
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// Phone-sized fullscreen modals inherit safe areas and use wrapper padding
|
|
678
|
+
if (!isTablet) {
|
|
679
|
+
this.applyFullscreenSafeArea();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Check if tablet modal is fullscreen via CSS custom properties
|
|
683
|
+
const computedStyle = getComputedStyle(this.el);
|
|
684
|
+
const width = computedStyle.getPropertyValue('--width').trim();
|
|
685
|
+
const height = computedStyle.getPropertyValue('--height').trim();
|
|
686
|
+
const isFullscreen = width === '100%' && height === '100%';
|
|
687
|
+
if (isFullscreen) {
|
|
688
|
+
this.applyFullscreenSafeArea();
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
// Centered dialog doesn't touch edges
|
|
692
|
+
this.zeroAllSafeAreas();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Applies safe-area handling for fullscreen modals.
|
|
697
|
+
* Adds wrapper padding when no footer is present to prevent
|
|
698
|
+
* content from overlapping system navigation areas.
|
|
699
|
+
*/
|
|
700
|
+
applyFullscreenSafeArea() {
|
|
701
|
+
this.skipSafeAreaCoordinateDetection = true;
|
|
702
|
+
this.updateFooterPadding();
|
|
703
|
+
// Watch for dynamic footer additions/removals (e.g., async data loading)
|
|
704
|
+
// Use subtree:true to support wrapped footers in framework components
|
|
705
|
+
// (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
|
|
706
|
+
if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
|
|
707
|
+
this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
|
|
708
|
+
this.footerObserver.observe(this.el, { childList: true, subtree: true });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Updates wrapper padding based on footer presence.
|
|
713
|
+
* Called initially and when footer is dynamically added/removed.
|
|
714
|
+
*/
|
|
715
|
+
updateFooterPadding() {
|
|
716
|
+
if (!this.wrapperEl)
|
|
717
|
+
return;
|
|
718
|
+
const hasFooter = this.el.querySelector('ion-footer') !== null;
|
|
719
|
+
if (hasFooter) {
|
|
720
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
721
|
+
this.wrapperEl.style.removeProperty('box-sizing');
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
725
|
+
this.wrapperEl.style.setProperty('box-sizing', 'border-box');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Sets all safe-area CSS variables to 0px for modals that
|
|
730
|
+
* don't touch screen edges.
|
|
731
|
+
*/
|
|
732
|
+
zeroAllSafeAreas() {
|
|
733
|
+
const style = this.el.style;
|
|
734
|
+
style.setProperty('--ion-safe-area-top', '0px');
|
|
735
|
+
style.setProperty('--ion-safe-area-bottom', '0px');
|
|
736
|
+
style.setProperty('--ion-safe-area-left', '0px');
|
|
737
|
+
style.setProperty('--ion-safe-area-right', '0px');
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Resets all safe-area related state and styles.
|
|
741
|
+
* Called during dismiss and disconnectedCallback to ensure clean state
|
|
742
|
+
* for re-presentation of inline modals.
|
|
743
|
+
*/
|
|
744
|
+
resetSafeAreaState() {
|
|
745
|
+
var _a;
|
|
746
|
+
this.skipSafeAreaCoordinateDetection = false;
|
|
747
|
+
this.cachedSafeAreas = undefined;
|
|
748
|
+
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
749
|
+
(_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
750
|
+
this.footerObserver = undefined;
|
|
751
|
+
// Clear wrapper styles that may have been set for safe-area handling
|
|
752
|
+
if (this.wrapperEl) {
|
|
753
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
754
|
+
this.wrapperEl.style.removeProperty('box-sizing');
|
|
755
|
+
}
|
|
756
|
+
// Clear safe-area CSS variable overrides
|
|
757
|
+
const style = this.el.style;
|
|
758
|
+
style.removeProperty('--ion-safe-area-top');
|
|
759
|
+
style.removeProperty('--ion-safe-area-bottom');
|
|
760
|
+
style.removeProperty('--ion-safe-area-left');
|
|
761
|
+
style.removeProperty('--ion-safe-area-right');
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Gets the root safe-area values from the document element.
|
|
765
|
+
* Uses cached values during gestures to avoid getComputedStyle calls.
|
|
766
|
+
*/
|
|
767
|
+
getSafeAreaValues() {
|
|
768
|
+
if (!this.cachedSafeAreas) {
|
|
769
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
770
|
+
this.cachedSafeAreas = {
|
|
771
|
+
top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
|
|
772
|
+
bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
|
|
773
|
+
left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
|
|
774
|
+
right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
return this.cachedSafeAreas;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Updates safe-area CSS variable overrides based on whether the modal
|
|
781
|
+
* extends into each safe-area region. Called after animation
|
|
782
|
+
* and during gestures to handle dynamic position changes.
|
|
783
|
+
*
|
|
784
|
+
* Optimized to avoid redundant DOM writes by tracking previous state.
|
|
785
|
+
*/
|
|
786
|
+
updateSafeAreaOverrides() {
|
|
787
|
+
if (this.skipSafeAreaCoordinateDetection) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const wrapper = this.wrapperEl;
|
|
791
|
+
if (!wrapper) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const rect = wrapper.getBoundingClientRect();
|
|
795
|
+
const safeAreas = this.getSafeAreaValues();
|
|
796
|
+
const extendsIntoTop = rect.top < safeAreas.top;
|
|
797
|
+
const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
|
|
798
|
+
const extendsIntoLeft = rect.left < safeAreas.left;
|
|
799
|
+
const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
|
|
800
|
+
// Only update DOM when state actually changes
|
|
801
|
+
const prev = this.prevSafeAreaState;
|
|
802
|
+
const style = this.el.style;
|
|
803
|
+
if (extendsIntoTop !== prev.top) {
|
|
804
|
+
extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
|
|
805
|
+
prev.top = extendsIntoTop;
|
|
806
|
+
}
|
|
807
|
+
if (extendsIntoBottom !== prev.bottom) {
|
|
808
|
+
extendsIntoBottom
|
|
809
|
+
? style.removeProperty('--ion-safe-area-bottom')
|
|
810
|
+
: style.setProperty('--ion-safe-area-bottom', '0px');
|
|
811
|
+
prev.bottom = extendsIntoBottom;
|
|
812
|
+
}
|
|
813
|
+
if (extendsIntoLeft !== prev.left) {
|
|
814
|
+
extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
|
|
815
|
+
prev.left = extendsIntoLeft;
|
|
816
|
+
}
|
|
817
|
+
if (extendsIntoRight !== prev.right) {
|
|
818
|
+
extendsIntoRight
|
|
819
|
+
? style.removeProperty('--ion-safe-area-right')
|
|
820
|
+
: style.setProperty('--ion-safe-area-right', '0px');
|
|
821
|
+
prev.right = extendsIntoRight;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
628
824
|
sheetOnDismiss() {
|
|
629
825
|
/**
|
|
630
826
|
* While the gesture animation is finishing
|
|
@@ -717,6 +913,8 @@ export class Modal {
|
|
|
717
913
|
}
|
|
718
914
|
this.currentBreakpoint = undefined;
|
|
719
915
|
this.animation = undefined;
|
|
916
|
+
// Reset safe-area state for potential re-presentation
|
|
917
|
+
this.resetSafeAreaState();
|
|
720
918
|
unlock();
|
|
721
919
|
return dismissed;
|
|
722
920
|
}
|
|
@@ -974,20 +1172,20 @@ export class Modal {
|
|
|
974
1172
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
975
1173
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
976
1174
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
977
|
-
return (h(Host, Object.assign({ key: '
|
|
1175
|
+
return (h(Host, Object.assign({ key: '44022099fcaf047b97d1c2cb45b9b51c930e707c', "no-router": true,
|
|
978
1176
|
// Allow the modal to be navigable when the handle is focusable
|
|
979
1177
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
980
1178
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
981
|
-
}, 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: '
|
|
1179
|
+
}, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: 'ddd7e4f6eef51ac1f62ac70e0af10fb01e707f07', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '58620980e3e4ec273c6787bde026e1c010b904b7', class: "modal-shadow" }), h("div", Object.assign({ key: '3fb7f6218644ba898fc504467775593eb89426a0',
|
|
982
1180
|
/*
|
|
983
1181
|
role and aria-modal must be used on the
|
|
984
1182
|
same element. They must also be set inside the
|
|
985
1183
|
shadow DOM otherwise ion-button will not be highlighted
|
|
986
1184
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
987
1185
|
*/
|
|
988
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
1186
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '9745cd590fdaa9d023a14b487ec2c87ddbafd7f7', class: "modal-handle",
|
|
989
1187
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
990
|
-
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: '
|
|
1188
|
+
tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: 'b9a8b5d2d3d3c9b06f99179f496c9f08907d0bad', onSlotchange: this.onSlotChange }))));
|
|
991
1189
|
}
|
|
992
1190
|
static get is() { return "ion-modal"; }
|
|
993
1191
|
static get encapsulation() { return "shadow"; }
|
|
@@ -135,10 +135,6 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
-
--ion-safe-area-top: 0px;
|
|
139
|
-
--ion-safe-area-bottom: 0px;
|
|
140
|
-
--ion-safe-area-right: 0px;
|
|
141
|
-
--ion-safe-area-left: 0px;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|
|
@@ -31,7 +31,7 @@ export const iosEnterAnimation = (baseEl, opts) => {
|
|
|
31
31
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
32
32
|
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
33
33
|
const margin = size === 'cover' ? 0 : 25;
|
|
34
|
-
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
34
|
+
const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
35
35
|
const baseAnimation = createAnimation();
|
|
36
36
|
const backdropAnimation = createAnimation();
|
|
37
37
|
const contentAnimation = createAnimation();
|
|
@@ -61,19 +61,35 @@ export const iosEnterAnimation = (baseEl, opts) => {
|
|
|
61
61
|
if (addPopoverBottomClass) {
|
|
62
62
|
baseEl.classList.add('popover-bottom');
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Safe area CSS variable adjustments.
|
|
66
|
+
* When the popover is positioned near an edge, we add the corresponding
|
|
67
|
+
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
68
|
+
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
69
|
+
*/
|
|
70
|
+
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
71
|
+
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
67
72
|
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
68
73
|
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
74
|
+
let topValue = `${top}px`;
|
|
75
|
+
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
69
76
|
let leftValue = `${left}px`;
|
|
77
|
+
if (checkSafeAreaTop) {
|
|
78
|
+
topValue = `${top}px${safeAreaTop}`;
|
|
79
|
+
}
|
|
80
|
+
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
81
|
+
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
82
|
+
}
|
|
70
83
|
if (checkSafeAreaLeft) {
|
|
71
84
|
leftValue = `${left}px${safeAreaLeft}`;
|
|
72
85
|
}
|
|
73
86
|
if (checkSafeAreaRight) {
|
|
74
87
|
leftValue = `${left}px${safeAreaRight}`;
|
|
75
88
|
}
|
|
76
|
-
|
|
89
|
+
if (bottomValue !== undefined) {
|
|
90
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
91
|
+
}
|
|
92
|
+
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
|
77
93
|
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
78
94
|
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
79
95
|
if (arrowEl !== null) {
|
|
@@ -28,7 +28,32 @@ export const mdEnterAnimation = (baseEl, opts) => {
|
|
|
28
28
|
};
|
|
29
29
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
|
|
30
30
|
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
|
31
|
-
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
|
|
31
|
+
const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
|
|
32
|
+
/**
|
|
33
|
+
* Safe area CSS variable adjustments.
|
|
34
|
+
* When the popover is positioned near an edge, we add the corresponding
|
|
35
|
+
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
36
|
+
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
37
|
+
*/
|
|
38
|
+
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
39
|
+
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
40
|
+
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
41
|
+
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
42
|
+
let topValue = `${top}px`;
|
|
43
|
+
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
44
|
+
let leftValue = `${left}px`;
|
|
45
|
+
if (checkSafeAreaTop) {
|
|
46
|
+
topValue = `${top}px${safeAreaTop}`;
|
|
47
|
+
}
|
|
48
|
+
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
49
|
+
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
50
|
+
}
|
|
51
|
+
if (checkSafeAreaLeft) {
|
|
52
|
+
leftValue = `${left}px${safeAreaLeft}`;
|
|
53
|
+
}
|
|
54
|
+
if (checkSafeAreaRight) {
|
|
55
|
+
leftValue = `${left}px${safeAreaRight}`;
|
|
56
|
+
}
|
|
32
57
|
const baseAnimation = createAnimation();
|
|
33
58
|
const backdropAnimation = createAnimation();
|
|
34
59
|
const wrapperAnimation = createAnimation();
|
|
@@ -45,13 +70,13 @@ export const mdEnterAnimation = (baseEl, opts) => {
|
|
|
45
70
|
contentAnimation
|
|
46
71
|
.addElement(contentEl)
|
|
47
72
|
.beforeStyles({
|
|
48
|
-
top: `calc(${
|
|
49
|
-
left: `calc(${
|
|
73
|
+
top: `calc(${topValue} + var(--offset-y, 0px))`,
|
|
74
|
+
left: `calc(${leftValue} + var(--offset-x, 0px))`,
|
|
50
75
|
'transform-origin': `${originY} ${originX}`,
|
|
51
76
|
})
|
|
52
77
|
.beforeAddWrite(() => {
|
|
53
|
-
if (
|
|
54
|
-
contentEl.style.setProperty('bottom',
|
|
78
|
+
if (bottomValue !== undefined) {
|
|
79
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
55
80
|
}
|
|
56
81
|
})
|
|
57
82
|
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|