@ionic/core 8.8.4 → 8.8.5-dev.11776871786.1e73ab78
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/ion-content.js +1 -1
- package/components/ion-modal.js +1 -1
- package/components/ion-select-modal.js +1 -1
- package/components/ion-select.js +1 -1
- package/components/{p-ApmKVjaE.js → p-BGHGpkPX.js} +1 -1
- package/components/p-BZfgPT2N.js +4 -0
- package/components/{p-BTF2nRLo.js → p-MlJRD6E1.js} +1 -1
- package/dist/cjs/ion-app_8.cjs.entry.js +1 -1
- package/dist/cjs/ion-modal.cjs.entry.js +93 -45
- package/dist/collection/components/content/content.css +7 -1
- package/dist/collection/components/modal/modal.js +67 -44
- package/dist/collection/components/modal/safe-area-utils.js +27 -2
- package/dist/docs.json +6 -1
- package/dist/esm/ion-app_8.entry.js +1 -1
- package/dist/esm/ion-modal.entry.js +93 -45
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-4eedd78a.entry.js +4 -0
- package/dist/ionic/p-b5ea8cdd.entry.js +4 -0
- package/dist/types/components/modal/modal.d.ts +35 -3
- package/dist/types/components/modal/safe-area-utils.d.ts +16 -0
- package/hydrate/index.js +94 -46
- package/hydrate/index.mjs +94 -46
- package/package.json +1 -1
- package/components/p-BVnB3eEn.js +0 -4
- package/dist/ionic/p-16b65553.entry.js +0 -4
- package/dist/ionic/p-4819b469.entry.js +0 -4
|
@@ -1647,6 +1647,12 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
|
|
|
1647
1647
|
const MODAL_INSET_MIN_WIDTH = 768;
|
|
1648
1648
|
const MODAL_INSET_MIN_HEIGHT = 600;
|
|
1649
1649
|
const EDGE_THRESHOLD = 5;
|
|
1650
|
+
/**
|
|
1651
|
+
* CSS values for `--width` / `--height` that are treated as fullscreen
|
|
1652
|
+
* (modal touches the corresponding screen edges). Empty string means the
|
|
1653
|
+
* property was not overridden. See `hasCustomModalDimensions()`.
|
|
1654
|
+
*/
|
|
1655
|
+
const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']);
|
|
1650
1656
|
/**
|
|
1651
1657
|
* Cache for resolved root safe-area-top value, invalidated once per frame.
|
|
1652
1658
|
*/
|
|
@@ -1695,6 +1701,22 @@ const getRootSafeAreaTop = () => {
|
|
|
1695
1701
|
}
|
|
1696
1702
|
return value;
|
|
1697
1703
|
};
|
|
1704
|
+
/**
|
|
1705
|
+
* True when the modal host declares BOTH a non-fullscreen `--width` AND a
|
|
1706
|
+
* non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't
|
|
1707
|
+
* touch any screen edge).
|
|
1708
|
+
*
|
|
1709
|
+
* The conservative "both axes" check avoids mis-zeroing safe-area for
|
|
1710
|
+
* partial-custom modals where the modal still touches top/bottom edges
|
|
1711
|
+
* (e.g. only `--width` overridden). Partial cases fall through to the
|
|
1712
|
+
* existing position-based post-animation correction.
|
|
1713
|
+
*/
|
|
1714
|
+
const hasCustomModalDimensions = (hostEl) => {
|
|
1715
|
+
const styles = getComputedStyle(hostEl);
|
|
1716
|
+
const width = styles.getPropertyValue('--width').trim();
|
|
1717
|
+
const height = styles.getPropertyValue('--height').trim();
|
|
1718
|
+
return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height);
|
|
1719
|
+
};
|
|
1698
1720
|
/**
|
|
1699
1721
|
* Returns the initial safe-area configuration based on modal type.
|
|
1700
1722
|
* This is called before animation starts and uses configuration-based prediction.
|
|
@@ -1729,8 +1751,11 @@ const getInitialSafeAreaConfig = (context) => {
|
|
|
1729
1751
|
}
|
|
1730
1752
|
// On viewports that meet the centered dialog media query breakpoints,
|
|
1731
1753
|
// regular modals render as centered dialogs (not fullscreen), so they
|
|
1732
|
-
// don't touch any screen edges and don't need safe-area insets.
|
|
1733
|
-
|
|
1754
|
+
// don't touch any screen edges and don't need safe-area insets. Also
|
|
1755
|
+
// applies to phone viewports when the modal declares custom --width and
|
|
1756
|
+
// --height; these don't touch screen edges either, so the initial
|
|
1757
|
+
// prediction must be zero to avoid a post-animation correction flash.
|
|
1758
|
+
if (isCenteredDialogViewport() || context.hasCustomDimensions) {
|
|
1734
1759
|
return {
|
|
1735
1760
|
top: '0px',
|
|
1736
1761
|
bottom: '0px',
|
|
@@ -2032,12 +2057,10 @@ const Modal = class {
|
|
|
2032
2057
|
// since the viewport may have crossed the centered-dialog breakpoint.
|
|
2033
2058
|
if (!context.isSheetModal && !context.isCardModal) {
|
|
2034
2059
|
this.updateSafeAreaOverrides();
|
|
2035
|
-
// Re-evaluate fullscreen safe-area padding: clear first, then re-apply
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
}
|
|
2040
|
-
this.applyFullscreenSafeArea();
|
|
2060
|
+
// Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
|
|
2061
|
+
const { contentEl, hasFooter } = this.findContentAndFooter();
|
|
2062
|
+
this.clearContentSafeAreaPadding(contentEl);
|
|
2063
|
+
this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
|
|
2041
2064
|
}
|
|
2042
2065
|
}, 50); // Debounce to avoid excessive calls during active resizing
|
|
2043
2066
|
}
|
|
@@ -2784,6 +2807,11 @@ const Modal = class {
|
|
|
2784
2807
|
}
|
|
2785
2808
|
/**
|
|
2786
2809
|
* Creates the context object for safe-area utilities.
|
|
2810
|
+
*
|
|
2811
|
+
* `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
|
|
2812
|
+
* because it is only read by `getInitialSafeAreaConfig()`. Other callers
|
|
2813
|
+
* (resize handler, post-animation update, fullscreen-padding apply) would
|
|
2814
|
+
* pay a `getComputedStyle()` cost for a value they never consult.
|
|
2787
2815
|
*/
|
|
2788
2816
|
getSafeAreaContext() {
|
|
2789
2817
|
return {
|
|
@@ -2805,7 +2833,7 @@ const Modal = class {
|
|
|
2805
2833
|
* sheets to prevent header content from getting double-offset padding).
|
|
2806
2834
|
*/
|
|
2807
2835
|
setInitialSafeAreaOverrides() {
|
|
2808
|
-
const context = this.getSafeAreaContext();
|
|
2836
|
+
const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
|
|
2809
2837
|
const safeAreaConfig = getInitialSafeAreaConfig(context);
|
|
2810
2838
|
applySafeAreaOverrides(this.el, safeAreaConfig);
|
|
2811
2839
|
// Set the internal offset property with the resolved root safe-area-top value
|
|
@@ -2845,59 +2873,79 @@ const Modal = class {
|
|
|
2845
2873
|
applySafeAreaOverrides(el, safeAreaConfig);
|
|
2846
2874
|
}
|
|
2847
2875
|
/**
|
|
2848
|
-
* Applies
|
|
2849
|
-
*
|
|
2876
|
+
* Applies safe-area-bottom scroll padding to ion-content inside
|
|
2877
|
+
* fullscreen modals that have no ion-footer. This prevents content
|
|
2878
|
+
* from being hidden behind the system navigation bar while keeping
|
|
2879
|
+
* the modal background edge-to-edge (no visible gap).
|
|
2850
2880
|
*/
|
|
2851
2881
|
applyFullscreenSafeArea() {
|
|
2852
|
-
const { wrapperEl, el } = this;
|
|
2853
|
-
if (!wrapperEl)
|
|
2854
|
-
return;
|
|
2855
2882
|
const context = this.getSafeAreaContext();
|
|
2856
2883
|
if (context.isSheetModal || context.isCardModal)
|
|
2857
2884
|
return;
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2885
|
+
const { contentEl, hasFooter } = this.findContentAndFooter();
|
|
2886
|
+
this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Sets --ion-content-safe-area-padding-bottom on the given ion-content
|
|
2890
|
+
* when no footer is present, so ion-content's .inner-scroll includes
|
|
2891
|
+
* safe-area-bottom in its scroll padding. This keeps the modal background
|
|
2892
|
+
* edge-to-edge while ensuring content scrolls clear of the system nav bar.
|
|
2893
|
+
*/
|
|
2894
|
+
applyFullscreenSafeAreaTo(contentEl, hasFooter) {
|
|
2895
|
+
// Only apply for standard Ionic layouts (has ion-content but no
|
|
2896
|
+
// ion-footer). When a footer is present it handles its own safe-area
|
|
2897
|
+
// padding. Custom modals with raw HTML are developer-controlled.
|
|
2898
|
+
if (!contentEl || hasFooter)
|
|
2899
|
+
return;
|
|
2900
|
+
contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Removes the internal --ion-content-safe-area-padding-bottom property
|
|
2904
|
+
* from an already-located ion-content. Callers do their own
|
|
2905
|
+
* findContentAndFooter() so they can also read hasFooter if needed.
|
|
2906
|
+
*/
|
|
2907
|
+
clearContentSafeAreaPadding(contentEl) {
|
|
2908
|
+
if (!contentEl)
|
|
2909
|
+
return;
|
|
2910
|
+
contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Finds ion-content and ion-footer among direct children and one level of
|
|
2914
|
+
* grandchildren (for wrapped components like <app-footer><ion-footer>).
|
|
2915
|
+
*
|
|
2916
|
+
* Intentionally does NOT use findIonContent() or querySelector() because
|
|
2917
|
+
* those search the full subtree and would match ion-content inside nested
|
|
2918
|
+
* routes/pages. We only want direct slot children (+ one wrapper level).
|
|
2919
|
+
*
|
|
2920
|
+
* Uses a manual loop instead of querySelector(':scope > ...') because
|
|
2921
|
+
* Stencil's mock-doc (used in spec tests) does not support :scope.
|
|
2922
|
+
*/
|
|
2923
|
+
findContentAndFooter() {
|
|
2924
|
+
let contentEl = null;
|
|
2864
2925
|
let hasFooter = false;
|
|
2865
|
-
for (const child of Array.from(el.children)) {
|
|
2926
|
+
for (const child of Array.from(this.el.children)) {
|
|
2866
2927
|
if (child.tagName === 'ION-CONTENT')
|
|
2867
|
-
|
|
2928
|
+
contentEl = child;
|
|
2868
2929
|
if (child.tagName === 'ION-FOOTER')
|
|
2869
2930
|
hasFooter = true;
|
|
2870
2931
|
for (const grandchild of Array.from(child.children)) {
|
|
2871
|
-
if (grandchild.tagName === 'ION-CONTENT')
|
|
2872
|
-
|
|
2932
|
+
if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
|
|
2933
|
+
contentEl = grandchild;
|
|
2873
2934
|
if (grandchild.tagName === 'ION-FOOTER')
|
|
2874
2935
|
hasFooter = true;
|
|
2875
2936
|
}
|
|
2876
2937
|
}
|
|
2877
|
-
|
|
2878
|
-
// but no ion-footer). Custom modals with raw HTML are fully
|
|
2879
|
-
// developer-controlled and should not be modified.
|
|
2880
|
-
if (!hasContent || hasFooter)
|
|
2881
|
-
return;
|
|
2882
|
-
// Reduce wrapper height by safe-area and add equivalent padding so the
|
|
2883
|
-
// total visual size stays the same but the flex content area shrinks.
|
|
2884
|
-
// Using height + padding instead of box-sizing: border-box avoids
|
|
2885
|
-
// breaking custom modals that set --border-width (border-box would
|
|
2886
|
-
// include the border inside the height, changing the layout).
|
|
2887
|
-
wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
|
|
2888
|
-
wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
2938
|
+
return { contentEl, hasFooter };
|
|
2889
2939
|
}
|
|
2890
2940
|
/**
|
|
2891
|
-
* Clears all safe-area overrides and padding
|
|
2941
|
+
* Clears all safe-area overrides and padding.
|
|
2892
2942
|
*/
|
|
2893
2943
|
cleanupSafeAreaOverrides() {
|
|
2894
2944
|
clearSafeAreaOverrides(this.el);
|
|
2895
2945
|
// Remove internal sheet offset property
|
|
2896
2946
|
this.el.style.removeProperty('--ion-modal-offset-top');
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
2900
|
-
}
|
|
2947
|
+
const { contentEl } = this.findContentAndFooter();
|
|
2948
|
+
this.clearContentSafeAreaPadding(contentEl);
|
|
2901
2949
|
}
|
|
2902
2950
|
render() {
|
|
2903
2951
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
@@ -2906,20 +2954,20 @@ const Modal = class {
|
|
|
2906
2954
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
2907
2955
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
2908
2956
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
2909
|
-
return (index$3.h(index$3.Host, Object.assign({ key: '
|
|
2957
|
+
return (index$3.h(index$3.Host, Object.assign({ key: 'b665328614ae3a0d27ec15ecb8334d14e0d517e7', "no-router": true,
|
|
2910
2958
|
// Allow the modal to be navigable when the handle is focusable
|
|
2911
2959
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
2912
2960
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
2913
|
-
}, 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: '
|
|
2961
|
+
}, 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: '263b41858dc0ad44e5a84cd83cf6eaaf32a804d2', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '65eb2a58f20576941e49f5499de644b4c45ad50e', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '2817d6cc8015ad013204941fea5ee96c331841f5',
|
|
2914
2962
|
/*
|
|
2915
2963
|
role and aria-modal must be used on the
|
|
2916
2964
|
same element. They must also be set inside the
|
|
2917
2965
|
shadow DOM otherwise ion-button will not be highlighted
|
|
2918
2966
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
2919
2967
|
*/
|
|
2920
|
-
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: '
|
|
2968
|
+
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: '318f2c1e903cb41c977fbcce529f509c82378079', class: "modal-handle",
|
|
2921
2969
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
2922
|
-
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: '
|
|
2970
|
+
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: 'de8e6b0d4126820cc6a02be0af229a3e62afa1dd', onSlotchange: this.onSlotChange }))));
|
|
2923
2971
|
}
|
|
2924
2972
|
get el() { return index$3.getElement(this); }
|
|
2925
2973
|
static get watchers() { return {
|
|
@@ -106,6 +106,12 @@
|
|
|
106
106
|
background: var(--background);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* --ion-content-safe-area-padding-bottom is an internal property set by
|
|
111
|
+
* modal.tsx for fullscreen modals without an ion-footer. This decouples
|
|
112
|
+
* safe-area-bottom scroll padding from --padding-bottom (which is a
|
|
113
|
+
* public property consumers may override).
|
|
114
|
+
*/
|
|
109
115
|
.inner-scroll {
|
|
110
116
|
left: 0px;
|
|
111
117
|
right: 0px;
|
|
@@ -116,7 +122,7 @@
|
|
|
116
122
|
-webkit-padding-end: var(--padding-end);
|
|
117
123
|
padding-inline-end: var(--padding-end);
|
|
118
124
|
padding-top: calc(var(--padding-top) + var(--offset-top));
|
|
119
|
-
padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));
|
|
125
|
+
padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px));
|
|
120
126
|
position: absolute;
|
|
121
127
|
color: var(--color);
|
|
122
128
|
box-sizing: border-box;
|
|
@@ -21,7 +21,7 @@ import { mdEnterAnimation } from "./animations/md.enter";
|
|
|
21
21
|
import { mdLeaveAnimation } from "./animations/md.leave";
|
|
22
22
|
import { createSheetGesture } from "./gestures/sheet";
|
|
23
23
|
import { createSwipeToCloseGesture, SwipeToCloseDefaults } from "./gestures/swipe-to-close";
|
|
24
|
-
import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, } from "./safe-area-utils";
|
|
24
|
+
import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, hasCustomModalDimensions, } from "./safe-area-utils";
|
|
25
25
|
import { setCardStatusBarDark, setCardStatusBarDefault } from "./utils";
|
|
26
26
|
// TODO(FW-2832): types
|
|
27
27
|
/**
|
|
@@ -251,12 +251,10 @@ export class Modal {
|
|
|
251
251
|
// since the viewport may have crossed the centered-dialog breakpoint.
|
|
252
252
|
if (!context.isSheetModal && !context.isCardModal) {
|
|
253
253
|
this.updateSafeAreaOverrides();
|
|
254
|
-
// Re-evaluate fullscreen safe-area padding: clear first, then re-apply
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
this.applyFullscreenSafeArea();
|
|
254
|
+
// Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
|
|
255
|
+
const { contentEl, hasFooter } = this.findContentAndFooter();
|
|
256
|
+
this.clearContentSafeAreaPadding(contentEl);
|
|
257
|
+
this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
|
|
260
258
|
}
|
|
261
259
|
}, 50); // Debounce to avoid excessive calls during active resizing
|
|
262
260
|
}
|
|
@@ -1011,6 +1009,11 @@ export class Modal {
|
|
|
1011
1009
|
}
|
|
1012
1010
|
/**
|
|
1013
1011
|
* Creates the context object for safe-area utilities.
|
|
1012
|
+
*
|
|
1013
|
+
* `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
|
|
1014
|
+
* because it is only read by `getInitialSafeAreaConfig()`. Other callers
|
|
1015
|
+
* (resize handler, post-animation update, fullscreen-padding apply) would
|
|
1016
|
+
* pay a `getComputedStyle()` cost for a value they never consult.
|
|
1014
1017
|
*/
|
|
1015
1018
|
getSafeAreaContext() {
|
|
1016
1019
|
return {
|
|
@@ -1032,7 +1035,7 @@ export class Modal {
|
|
|
1032
1035
|
* sheets to prevent header content from getting double-offset padding).
|
|
1033
1036
|
*/
|
|
1034
1037
|
setInitialSafeAreaOverrides() {
|
|
1035
|
-
const context = this.getSafeAreaContext();
|
|
1038
|
+
const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
|
|
1036
1039
|
const safeAreaConfig = getInitialSafeAreaConfig(context);
|
|
1037
1040
|
applySafeAreaOverrides(this.el, safeAreaConfig);
|
|
1038
1041
|
// Set the internal offset property with the resolved root safe-area-top value
|
|
@@ -1072,59 +1075,79 @@ export class Modal {
|
|
|
1072
1075
|
applySafeAreaOverrides(el, safeAreaConfig);
|
|
1073
1076
|
}
|
|
1074
1077
|
/**
|
|
1075
|
-
* Applies
|
|
1076
|
-
*
|
|
1078
|
+
* Applies safe-area-bottom scroll padding to ion-content inside
|
|
1079
|
+
* fullscreen modals that have no ion-footer. This prevents content
|
|
1080
|
+
* from being hidden behind the system navigation bar while keeping
|
|
1081
|
+
* the modal background edge-to-edge (no visible gap).
|
|
1077
1082
|
*/
|
|
1078
1083
|
applyFullscreenSafeArea() {
|
|
1079
|
-
const { wrapperEl, el } = this;
|
|
1080
|
-
if (!wrapperEl)
|
|
1081
|
-
return;
|
|
1082
1084
|
const context = this.getSafeAreaContext();
|
|
1083
1085
|
if (context.isSheetModal || context.isCardModal)
|
|
1084
1086
|
return;
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1087
|
+
const { contentEl, hasFooter } = this.findContentAndFooter();
|
|
1088
|
+
this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Sets --ion-content-safe-area-padding-bottom on the given ion-content
|
|
1092
|
+
* when no footer is present, so ion-content's .inner-scroll includes
|
|
1093
|
+
* safe-area-bottom in its scroll padding. This keeps the modal background
|
|
1094
|
+
* edge-to-edge while ensuring content scrolls clear of the system nav bar.
|
|
1095
|
+
*/
|
|
1096
|
+
applyFullscreenSafeAreaTo(contentEl, hasFooter) {
|
|
1097
|
+
// Only apply for standard Ionic layouts (has ion-content but no
|
|
1098
|
+
// ion-footer). When a footer is present it handles its own safe-area
|
|
1099
|
+
// padding. Custom modals with raw HTML are developer-controlled.
|
|
1100
|
+
if (!contentEl || hasFooter)
|
|
1101
|
+
return;
|
|
1102
|
+
contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Removes the internal --ion-content-safe-area-padding-bottom property
|
|
1106
|
+
* from an already-located ion-content. Callers do their own
|
|
1107
|
+
* findContentAndFooter() so they can also read hasFooter if needed.
|
|
1108
|
+
*/
|
|
1109
|
+
clearContentSafeAreaPadding(contentEl) {
|
|
1110
|
+
if (!contentEl)
|
|
1111
|
+
return;
|
|
1112
|
+
contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Finds ion-content and ion-footer among direct children and one level of
|
|
1116
|
+
* grandchildren (for wrapped components like <app-footer><ion-footer>).
|
|
1117
|
+
*
|
|
1118
|
+
* Intentionally does NOT use findIonContent() or querySelector() because
|
|
1119
|
+
* those search the full subtree and would match ion-content inside nested
|
|
1120
|
+
* routes/pages. We only want direct slot children (+ one wrapper level).
|
|
1121
|
+
*
|
|
1122
|
+
* Uses a manual loop instead of querySelector(':scope > ...') because
|
|
1123
|
+
* Stencil's mock-doc (used in spec tests) does not support :scope.
|
|
1124
|
+
*/
|
|
1125
|
+
findContentAndFooter() {
|
|
1126
|
+
let contentEl = null;
|
|
1091
1127
|
let hasFooter = false;
|
|
1092
|
-
for (const child of Array.from(el.children)) {
|
|
1128
|
+
for (const child of Array.from(this.el.children)) {
|
|
1093
1129
|
if (child.tagName === 'ION-CONTENT')
|
|
1094
|
-
|
|
1130
|
+
contentEl = child;
|
|
1095
1131
|
if (child.tagName === 'ION-FOOTER')
|
|
1096
1132
|
hasFooter = true;
|
|
1097
1133
|
for (const grandchild of Array.from(child.children)) {
|
|
1098
|
-
if (grandchild.tagName === 'ION-CONTENT')
|
|
1099
|
-
|
|
1134
|
+
if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
|
|
1135
|
+
contentEl = grandchild;
|
|
1100
1136
|
if (grandchild.tagName === 'ION-FOOTER')
|
|
1101
1137
|
hasFooter = true;
|
|
1102
1138
|
}
|
|
1103
1139
|
}
|
|
1104
|
-
|
|
1105
|
-
// but no ion-footer). Custom modals with raw HTML are fully
|
|
1106
|
-
// developer-controlled and should not be modified.
|
|
1107
|
-
if (!hasContent || hasFooter)
|
|
1108
|
-
return;
|
|
1109
|
-
// Reduce wrapper height by safe-area and add equivalent padding so the
|
|
1110
|
-
// total visual size stays the same but the flex content area shrinks.
|
|
1111
|
-
// Using height + padding instead of box-sizing: border-box avoids
|
|
1112
|
-
// breaking custom modals that set --border-width (border-box would
|
|
1113
|
-
// include the border inside the height, changing the layout).
|
|
1114
|
-
wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
|
|
1115
|
-
wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
1140
|
+
return { contentEl, hasFooter };
|
|
1116
1141
|
}
|
|
1117
1142
|
/**
|
|
1118
|
-
* Clears all safe-area overrides and padding
|
|
1143
|
+
* Clears all safe-area overrides and padding.
|
|
1119
1144
|
*/
|
|
1120
1145
|
cleanupSafeAreaOverrides() {
|
|
1121
1146
|
clearSafeAreaOverrides(this.el);
|
|
1122
1147
|
// Remove internal sheet offset property
|
|
1123
1148
|
this.el.style.removeProperty('--ion-modal-offset-top');
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
1127
|
-
}
|
|
1149
|
+
const { contentEl } = this.findContentAndFooter();
|
|
1150
|
+
this.clearContentSafeAreaPadding(contentEl);
|
|
1128
1151
|
}
|
|
1129
1152
|
render() {
|
|
1130
1153
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
@@ -1133,20 +1156,20 @@ export class Modal {
|
|
|
1133
1156
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
1134
1157
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
1135
1158
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
1136
|
-
return (h(Host, Object.assign({ key: '
|
|
1159
|
+
return (h(Host, Object.assign({ key: 'b665328614ae3a0d27ec15ecb8334d14e0d517e7', "no-router": true,
|
|
1137
1160
|
// Allow the modal to be navigable when the handle is focusable
|
|
1138
1161
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
1139
1162
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
1140
|
-
}, 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: '
|
|
1163
|
+
}, 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: '263b41858dc0ad44e5a84cd83cf6eaaf32a804d2', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '65eb2a58f20576941e49f5499de644b4c45ad50e', class: "modal-shadow" }), h("div", Object.assign({ key: '2817d6cc8015ad013204941fea5ee96c331841f5',
|
|
1141
1164
|
/*
|
|
1142
1165
|
role and aria-modal must be used on the
|
|
1143
1166
|
same element. They must also be set inside the
|
|
1144
1167
|
shadow DOM otherwise ion-button will not be highlighted
|
|
1145
1168
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
1146
1169
|
*/
|
|
1147
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
1170
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '318f2c1e903cb41c977fbcce529f509c82378079', class: "modal-handle",
|
|
1148
1171
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
1149
|
-
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: '
|
|
1172
|
+
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: 'de8e6b0d4126820cc6a02be0af229a3e62afa1dd', onSlotchange: this.onSlotChange }))));
|
|
1150
1173
|
}
|
|
1151
1174
|
static get is() { return "ion-modal"; }
|
|
1152
1175
|
static get encapsulation() { return "shadow"; }
|
|
@@ -15,6 +15,12 @@ import { raf } from "../../utils/helpers";
|
|
|
15
15
|
const MODAL_INSET_MIN_WIDTH = 768;
|
|
16
16
|
const MODAL_INSET_MIN_HEIGHT = 600;
|
|
17
17
|
const EDGE_THRESHOLD = 5;
|
|
18
|
+
/**
|
|
19
|
+
* CSS values for `--width` / `--height` that are treated as fullscreen
|
|
20
|
+
* (modal touches the corresponding screen edges). Empty string means the
|
|
21
|
+
* property was not overridden. See `hasCustomModalDimensions()`.
|
|
22
|
+
*/
|
|
23
|
+
const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']);
|
|
18
24
|
/**
|
|
19
25
|
* Cache for resolved root safe-area-top value, invalidated once per frame.
|
|
20
26
|
*/
|
|
@@ -63,6 +69,22 @@ export const getRootSafeAreaTop = () => {
|
|
|
63
69
|
}
|
|
64
70
|
return value;
|
|
65
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* True when the modal host declares BOTH a non-fullscreen `--width` AND a
|
|
74
|
+
* non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't
|
|
75
|
+
* touch any screen edge).
|
|
76
|
+
*
|
|
77
|
+
* The conservative "both axes" check avoids mis-zeroing safe-area for
|
|
78
|
+
* partial-custom modals where the modal still touches top/bottom edges
|
|
79
|
+
* (e.g. only `--width` overridden). Partial cases fall through to the
|
|
80
|
+
* existing position-based post-animation correction.
|
|
81
|
+
*/
|
|
82
|
+
export const hasCustomModalDimensions = (hostEl) => {
|
|
83
|
+
const styles = getComputedStyle(hostEl);
|
|
84
|
+
const width = styles.getPropertyValue('--width').trim();
|
|
85
|
+
const height = styles.getPropertyValue('--height').trim();
|
|
86
|
+
return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height);
|
|
87
|
+
};
|
|
66
88
|
/**
|
|
67
89
|
* Returns the initial safe-area configuration based on modal type.
|
|
68
90
|
* This is called before animation starts and uses configuration-based prediction.
|
|
@@ -97,8 +119,11 @@ export const getInitialSafeAreaConfig = (context) => {
|
|
|
97
119
|
}
|
|
98
120
|
// On viewports that meet the centered dialog media query breakpoints,
|
|
99
121
|
// regular modals render as centered dialogs (not fullscreen), so they
|
|
100
|
-
// don't touch any screen edges and don't need safe-area insets.
|
|
101
|
-
|
|
122
|
+
// don't touch any screen edges and don't need safe-area insets. Also
|
|
123
|
+
// applies to phone viewports when the modal declares custom --width and
|
|
124
|
+
// --height; these don't touch screen edges either, so the initial
|
|
125
|
+
// prediction must be zero to avoid a post-animation correction flash.
|
|
126
|
+
if (isCenteredDialogViewport() || context.hasCustomDimensions) {
|
|
102
127
|
return {
|
|
103
128
|
top: '0px',
|
|
104
129
|
bottom: '0px',
|
package/dist/docs.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"timestamp": "2026-04-
|
|
2
|
+
"timestamp": "2026-04-22T15:31:51",
|
|
3
3
|
"compiler": {
|
|
4
4
|
"name": "@stencil/core",
|
|
5
5
|
"version": "4.43.0",
|
|
@@ -8379,6 +8379,11 @@
|
|
|
8379
8379
|
"annotation": "prop",
|
|
8380
8380
|
"docs": "Color of the content"
|
|
8381
8381
|
},
|
|
8382
|
+
{
|
|
8383
|
+
"name": "--ion-content-safe-area-padding-bottom is an internal property set by modal.tsx for fullscreen modals without an ion-footer. This decouples safe-area-bottom scroll padding from --padding-bottom (which is a public property consumers may override).",
|
|
8384
|
+
"annotation": "prop",
|
|
8385
|
+
"docs": ""
|
|
8386
|
+
},
|
|
8382
8387
|
{
|
|
8383
8388
|
"name": "--keyboard-offset",
|
|
8384
8389
|
"annotation": "prop",
|
|
@@ -157,7 +157,7 @@ Buttons.style = {
|
|
|
157
157
|
md: buttonsMdCss()
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
const contentCss = () => `:host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:""}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}`;
|
|
160
|
+
const contentCss = () => `:host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:""}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}`;
|
|
161
161
|
|
|
162
162
|
const Content = class {
|
|
163
163
|
constructor(hostRef) {
|