@ionic/core 8.7.18-dev.11770318664.15f47a5e → 8.7.18-dev.11770916502.174efd7f
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-refresher.js +3 -32
- package/components/modal.js +326 -17
- package/components/popover.js +152 -30
- package/dist/cjs/ion-modal.cjs.entry.js +326 -17
- package/dist/cjs/ion-popover.cjs.entry.js +152 -30
- package/dist/cjs/ion-refresher_2.cjs.entry.js +3 -32
- package/dist/collection/components/modal/modal.ios.css +6 -5
- package/dist/collection/components/modal/modal.js +164 -15
- package/dist/collection/components/modal/modal.md.css +6 -5
- package/dist/collection/components/modal/safe-area-utils.js +165 -0
- package/dist/collection/components/popover/animations/ios.enter.js +33 -8
- package/dist/collection/components/popover/animations/md.enter.js +26 -6
- package/dist/collection/components/popover/utils.js +96 -19
- package/dist/collection/components/refresher/refresher.js +4 -70
- package/dist/docs.json +4 -49
- package/dist/esm/ion-modal.entry.js +326 -17
- package/dist/esm/ion-popover.entry.js +152 -30
- package/dist/esm/ion-refresher_2.entry.js +3 -32
- package/dist/html.html-data.json +1 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-3e69999d.entry.js +4 -0
- package/dist/ionic/p-639dd543.entry.js +4 -0
- package/dist/ionic/p-cc3446d5.entry.js +4 -0
- package/dist/types/components/modal/modal.d.ts +35 -0
- package/dist/types/components/modal/safe-area-utils.d.ts +66 -0
- package/dist/types/components/popover/utils.d.ts +23 -1
- package/dist/types/components/refresher/refresher-interface.d.ts +0 -7
- package/dist/types/components/refresher/refresher.d.ts +3 -16
- package/dist/types/components.d.ts +5 -16
- package/dist/types/interface.d.ts +1 -1
- package/hydrate/index.js +481 -79
- package/hydrate/index.mjs +481 -79
- package/package.json +1 -1
- package/dist/ionic/p-90f2d3ff.entry.js +0 -4
- package/dist/ionic/p-91840a80.entry.js +0 -4
- package/dist/ionic/p-f9061316.entry.js +0 -4
package/hydrate/index.mjs
CHANGED
|
@@ -22804,9 +22804,170 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
|
|
|
22804
22804
|
};
|
|
22805
22805
|
};
|
|
22806
22806
|
|
|
22807
|
-
|
|
22807
|
+
/**
|
|
22808
|
+
* These thresholds match the SCSS media query breakpoints in modal.vars.scss
|
|
22809
|
+
* that trigger the centered dialog layout (non-fullscreen modal).
|
|
22810
|
+
*
|
|
22811
|
+
* SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
|
|
22812
|
+
* and $modal-inset-min-height-large (768px). We use the smaller one because
|
|
22813
|
+
* that's the threshold where the modal transitions from fullscreen to centered
|
|
22814
|
+
* dialog — the larger breakpoint only increases the dialog's height.
|
|
22815
|
+
*/
|
|
22816
|
+
const MODAL_INSET_MIN_WIDTH = 768;
|
|
22817
|
+
const MODAL_INSET_MIN_HEIGHT = 600;
|
|
22818
|
+
const EDGE_THRESHOLD = 5;
|
|
22819
|
+
/**
|
|
22820
|
+
* Cache for resolved root safe-area-top value, invalidated once per frame.
|
|
22821
|
+
*/
|
|
22822
|
+
let cachedRootSafeAreaTop = null;
|
|
22823
|
+
let cacheInvalidationScheduled$1 = false;
|
|
22824
|
+
/**
|
|
22825
|
+
* Determines if the current viewport meets the CSS media query conditions
|
|
22826
|
+
* that cause regular modals to render as centered dialogs instead of fullscreen.
|
|
22827
|
+
* Matches: @media (min-width: 768px) and (min-height: 600px)
|
|
22828
|
+
*/
|
|
22829
|
+
const isCenteredDialogViewport = () => {
|
|
22830
|
+
if (!win$1)
|
|
22831
|
+
return false;
|
|
22832
|
+
return win$1.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
|
|
22833
|
+
.matches;
|
|
22834
|
+
};
|
|
22835
|
+
/**
|
|
22836
|
+
* Resolves the current root --ion-safe-area-top value to pixels.
|
|
22837
|
+
* Uses a temporary element because getComputedStyle on :root returns
|
|
22838
|
+
* the declared value of custom properties (e.g. "env(safe-area-inset-top)")
|
|
22839
|
+
* rather than a resolved number.
|
|
22840
|
+
*
|
|
22841
|
+
* Results are cached for the current frame to avoid repeated reflows.
|
|
22842
|
+
*/
|
|
22843
|
+
const getRootSafeAreaTop = () => {
|
|
22844
|
+
if (cachedRootSafeAreaTop !== null) {
|
|
22845
|
+
return cachedRootSafeAreaTop;
|
|
22846
|
+
}
|
|
22847
|
+
const doc = win$1 === null || win$1 === void 0 ? void 0 : win$1.document;
|
|
22848
|
+
if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
|
|
22849
|
+
return 0;
|
|
22850
|
+
}
|
|
22851
|
+
const el = doc.createElement('div');
|
|
22852
|
+
el.style.cssText =
|
|
22853
|
+
'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
|
|
22854
|
+
doc.body.appendChild(el);
|
|
22855
|
+
const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
|
|
22856
|
+
el.remove();
|
|
22857
|
+
cachedRootSafeAreaTop = value;
|
|
22858
|
+
if (!cacheInvalidationScheduled$1) {
|
|
22859
|
+
cacheInvalidationScheduled$1 = true;
|
|
22860
|
+
raf(() => {
|
|
22861
|
+
cachedRootSafeAreaTop = null;
|
|
22862
|
+
cacheInvalidationScheduled$1 = false;
|
|
22863
|
+
});
|
|
22864
|
+
}
|
|
22865
|
+
return value;
|
|
22866
|
+
};
|
|
22867
|
+
/**
|
|
22868
|
+
* Returns the initial safe-area configuration based on modal type.
|
|
22869
|
+
* This is called before animation starts and uses configuration-based prediction.
|
|
22870
|
+
*
|
|
22871
|
+
* @param context - Modal context information
|
|
22872
|
+
* @returns SafeAreaConfig with initial safe-area values
|
|
22873
|
+
*/
|
|
22874
|
+
const getInitialSafeAreaConfig = (context) => {
|
|
22875
|
+
const { isSheetModal, isCardModal } = context;
|
|
22876
|
+
// Sheet modals always zero top safe-area. The sheet height offset from the
|
|
22877
|
+
// top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
|
|
22878
|
+
// the resolved root value, so --ion-safe-area-top is never needed for
|
|
22879
|
+
// height calculation. Keeping it at 0px prevents header content from
|
|
22880
|
+
// getting double-offset padding.
|
|
22881
|
+
if (isSheetModal) {
|
|
22882
|
+
return {
|
|
22883
|
+
top: '0px',
|
|
22884
|
+
bottom: 'inherit',
|
|
22885
|
+
left: '0px',
|
|
22886
|
+
right: '0px',
|
|
22887
|
+
};
|
|
22888
|
+
}
|
|
22889
|
+
// Card modals need safe-area for height calculation.
|
|
22890
|
+
// Note: isCardModal is already gated on mode === 'ios' by the caller.
|
|
22891
|
+
if (isCardModal) {
|
|
22892
|
+
return {
|
|
22893
|
+
top: 'inherit',
|
|
22894
|
+
bottom: 'inherit',
|
|
22895
|
+
left: '0px',
|
|
22896
|
+
right: '0px',
|
|
22897
|
+
};
|
|
22898
|
+
}
|
|
22899
|
+
// On viewports that meet the centered dialog media query breakpoints,
|
|
22900
|
+
// regular modals render as centered dialogs (not fullscreen), so they
|
|
22901
|
+
// don't touch any screen edges and don't need safe-area insets.
|
|
22902
|
+
if (isCenteredDialogViewport()) {
|
|
22903
|
+
return {
|
|
22904
|
+
top: '0px',
|
|
22905
|
+
bottom: '0px',
|
|
22906
|
+
left: '0px',
|
|
22907
|
+
right: '0px',
|
|
22908
|
+
};
|
|
22909
|
+
}
|
|
22910
|
+
// Fullscreen modals on phone - inherit all safe areas
|
|
22911
|
+
return {
|
|
22912
|
+
top: 'inherit',
|
|
22913
|
+
bottom: 'inherit',
|
|
22914
|
+
left: 'inherit',
|
|
22915
|
+
right: 'inherit',
|
|
22916
|
+
};
|
|
22917
|
+
};
|
|
22918
|
+
/**
|
|
22919
|
+
* Returns safe-area configuration based on actual modal position.
|
|
22920
|
+
* Detects which edges the modal overlaps with and only applies safe-area to those edges.
|
|
22921
|
+
*
|
|
22922
|
+
* Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
|
|
22923
|
+
* inconsistent values. Sheet and card modals avoid this by using configuration-based
|
|
22924
|
+
* prediction instead. Regular modals use coordinate detection which works reliably
|
|
22925
|
+
* on web and iOS; Android edge-to-edge may need a configuration-based fallback
|
|
22926
|
+
* once a reliable detection mechanism is available.
|
|
22927
|
+
*
|
|
22928
|
+
* @param wrapperEl - The modal wrapper element to measure
|
|
22929
|
+
* @returns SafeAreaConfig based on position
|
|
22930
|
+
*/
|
|
22931
|
+
const getPositionBasedSafeAreaConfig = (wrapperEl) => {
|
|
22932
|
+
var _a, _b;
|
|
22933
|
+
const rect = wrapperEl.getBoundingClientRect();
|
|
22934
|
+
const vh = (_a = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerHeight) !== null && _a !== void 0 ? _a : 0;
|
|
22935
|
+
const vw = (_b = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerWidth) !== null && _b !== void 0 ? _b : 0;
|
|
22936
|
+
// Only apply safe-area to sides where modal overlaps with screen edge
|
|
22937
|
+
return {
|
|
22938
|
+
top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
|
|
22939
|
+
bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
|
|
22940
|
+
left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
|
|
22941
|
+
right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
|
|
22942
|
+
};
|
|
22943
|
+
};
|
|
22944
|
+
/**
|
|
22945
|
+
* Applies safe-area CSS custom property overrides to the modal host element.
|
|
22946
|
+
*
|
|
22947
|
+
* @param hostEl - The modal host element (ion-modal)
|
|
22948
|
+
* @param config - Safe-area configuration to apply
|
|
22949
|
+
*/
|
|
22950
|
+
const applySafeAreaOverrides = (hostEl, config) => {
|
|
22951
|
+
hostEl.style.setProperty('--ion-safe-area-top', config.top);
|
|
22952
|
+
hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
|
|
22953
|
+
hostEl.style.setProperty('--ion-safe-area-left', config.left);
|
|
22954
|
+
hostEl.style.setProperty('--ion-safe-area-right', config.right);
|
|
22955
|
+
};
|
|
22956
|
+
/**
|
|
22957
|
+
* Clears safe-area CSS custom property overrides from the modal host element.
|
|
22958
|
+
*
|
|
22959
|
+
* @param hostEl - The modal host element (ion-modal)
|
|
22960
|
+
*/
|
|
22961
|
+
const clearSafeAreaOverrides = (hostEl) => {
|
|
22962
|
+
hostEl.style.removeProperty('--ion-safe-area-top');
|
|
22963
|
+
hostEl.style.removeProperty('--ion-safe-area-bottom');
|
|
22964
|
+
hostEl.style.removeProperty('--ion-safe-area-left');
|
|
22965
|
+
hostEl.style.removeProperty('--ion-safe-area-right');
|
|
22966
|
+
};
|
|
22967
|
+
|
|
22968
|
+
const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-modal-offset-top, 0px) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
|
|
22808
22969
|
|
|
22809
|
-
const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px
|
|
22970
|
+
const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-modal-offset-top, 0px) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
|
|
22810
22971
|
|
|
22811
22972
|
// TODO(FW-2832): types
|
|
22812
22973
|
/**
|
|
@@ -23029,13 +23190,31 @@ class Modal {
|
|
|
23029
23190
|
}
|
|
23030
23191
|
}
|
|
23031
23192
|
onWindowResize() {
|
|
23032
|
-
|
|
23033
|
-
if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
23193
|
+
if (!this.presented)
|
|
23034
23194
|
return;
|
|
23035
|
-
}
|
|
23036
23195
|
clearTimeout(this.resizeTimeout);
|
|
23037
23196
|
this.resizeTimeout = setTimeout(() => {
|
|
23038
|
-
this.
|
|
23197
|
+
const context = this.getSafeAreaContext();
|
|
23198
|
+
// iOS card modals: handle portrait/landscape view transitions
|
|
23199
|
+
if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
|
|
23200
|
+
this.handleViewTransition();
|
|
23201
|
+
}
|
|
23202
|
+
// Sheet modals: re-compute the internal offset property since safe-area
|
|
23203
|
+
// values may change on device rotation (e.g., portrait notch vs landscape).
|
|
23204
|
+
if (context.isSheetModal) {
|
|
23205
|
+
this.updateSheetOffsetTop();
|
|
23206
|
+
}
|
|
23207
|
+
// Regular (non-sheet, non-card) modals: update safe-area overrides
|
|
23208
|
+
// since the viewport may have crossed the centered-dialog breakpoint.
|
|
23209
|
+
if (!context.isSheetModal && !context.isCardModal) {
|
|
23210
|
+
this.updateSafeAreaOverrides();
|
|
23211
|
+
// Re-evaluate fullscreen safe-area padding: clear first, then re-apply
|
|
23212
|
+
if (this.wrapperEl) {
|
|
23213
|
+
this.wrapperEl.style.removeProperty('height');
|
|
23214
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
23215
|
+
}
|
|
23216
|
+
this.applyFullscreenSafeArea();
|
|
23217
|
+
}
|
|
23039
23218
|
}, 50); // Debounce to avoid excessive calls during active resizing
|
|
23040
23219
|
}
|
|
23041
23220
|
breakpointsChanged(breakpoints) {
|
|
@@ -23052,6 +23231,9 @@ class Modal {
|
|
|
23052
23231
|
this.triggerController.removeClickListener();
|
|
23053
23232
|
this.cleanupViewTransitionListener();
|
|
23054
23233
|
this.cleanupParentRemovalObserver();
|
|
23234
|
+
// Also called in dismiss() — intentional dual cleanup covers both
|
|
23235
|
+
// dismiss-then-remove and direct DOM removal without dismiss.
|
|
23236
|
+
this.cleanupSafeAreaOverrides();
|
|
23055
23237
|
}
|
|
23056
23238
|
componentWillLoad() {
|
|
23057
23239
|
var _a;
|
|
@@ -23212,6 +23394,11 @@ class Modal {
|
|
|
23212
23394
|
await waitForMount();
|
|
23213
23395
|
}
|
|
23214
23396
|
writeTask(() => this.el.classList.add('show-modal'));
|
|
23397
|
+
// Recalculate isSheetModal before safe-area setup because framework
|
|
23398
|
+
// bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
|
|
23399
|
+
this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
23400
|
+
// Set initial safe-area overrides before animation
|
|
23401
|
+
this.setInitialSafeAreaOverrides();
|
|
23215
23402
|
const hasCardModal = presentingElement !== undefined;
|
|
23216
23403
|
/**
|
|
23217
23404
|
* We need to change the status bar at the
|
|
@@ -23229,6 +23416,10 @@ class Modal {
|
|
|
23229
23416
|
backdropBreakpoint: this.backdropBreakpoint,
|
|
23230
23417
|
expandToScroll: this.expandToScroll,
|
|
23231
23418
|
});
|
|
23419
|
+
// Update safe-area based on actual position after animation
|
|
23420
|
+
this.updateSafeAreaOverrides();
|
|
23421
|
+
// Apply fullscreen safe-area padding if needed
|
|
23422
|
+
this.applyFullscreenSafeArea();
|
|
23232
23423
|
/* tslint:disable-next-line */
|
|
23233
23424
|
if (typeof window !== 'undefined') {
|
|
23234
23425
|
/**
|
|
@@ -23260,13 +23451,7 @@ class Modal {
|
|
|
23260
23451
|
};
|
|
23261
23452
|
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
|
23262
23453
|
}
|
|
23263
|
-
|
|
23264
|
-
* Recalculate isSheetModal because framework bindings (e.g., Angular)
|
|
23265
|
-
* may not have been applied when componentWillLoad ran.
|
|
23266
|
-
*/
|
|
23267
|
-
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
23268
|
-
this.isSheetModal = isSheetModal;
|
|
23269
|
-
if (isSheetModal) {
|
|
23454
|
+
if (this.isSheetModal) {
|
|
23270
23455
|
this.initSheetGesture();
|
|
23271
23456
|
}
|
|
23272
23457
|
else if (hasCardModal) {
|
|
@@ -23456,6 +23641,9 @@ class Modal {
|
|
|
23456
23641
|
if (this.gestureAnimationDismissing && role !== GESTURE) {
|
|
23457
23642
|
return false;
|
|
23458
23643
|
}
|
|
23644
|
+
// Cancel any pending resize timeout to prevent stale updates during dismiss
|
|
23645
|
+
clearTimeout(this.resizeTimeout);
|
|
23646
|
+
this.resizeTimeout = undefined;
|
|
23459
23647
|
/**
|
|
23460
23648
|
* Because the canDismiss check below is async,
|
|
23461
23649
|
* we need to claim a lock before the check happens,
|
|
@@ -23510,6 +23698,7 @@ class Modal {
|
|
|
23510
23698
|
}
|
|
23511
23699
|
this.cleanupViewTransitionListener();
|
|
23512
23700
|
this.cleanupParentRemovalObserver();
|
|
23701
|
+
this.cleanupSafeAreaOverrides();
|
|
23513
23702
|
this.cleanupChildRoutePassthrough();
|
|
23514
23703
|
}
|
|
23515
23704
|
this.currentBreakpoint = undefined;
|
|
@@ -23630,6 +23819,9 @@ class Modal {
|
|
|
23630
23819
|
this.viewTransitionAnimation = transitionAnimation;
|
|
23631
23820
|
transitionAnimation.play().then(() => {
|
|
23632
23821
|
this.viewTransitionAnimation = undefined;
|
|
23822
|
+
// Wait for a layout pass after the transition so getBoundingClientRect()
|
|
23823
|
+
// in getPositionBasedSafeAreaConfig() reflects the new dimensions.
|
|
23824
|
+
raf(() => this.updateSafeAreaOverrides());
|
|
23633
23825
|
// After orientation transition, recreate the swipe-to-close gesture
|
|
23634
23826
|
// with updated animation that reflects the new presenting element state
|
|
23635
23827
|
this.reinitSwipeToClose();
|
|
@@ -23756,6 +23948,123 @@ class Modal {
|
|
|
23756
23948
|
(_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
23757
23949
|
this.parentRemovalObserver = undefined;
|
|
23758
23950
|
}
|
|
23951
|
+
/**
|
|
23952
|
+
* Creates the context object for safe-area utilities.
|
|
23953
|
+
*/
|
|
23954
|
+
getSafeAreaContext() {
|
|
23955
|
+
return {
|
|
23956
|
+
isSheetModal: this.isSheetModal,
|
|
23957
|
+
isCardModal: this.presentingElement !== undefined && getIonMode$1(this) === 'ios',
|
|
23958
|
+
presentingElement: this.presentingElement,
|
|
23959
|
+
breakpoints: this.breakpoints,
|
|
23960
|
+
currentBreakpoint: this.currentBreakpoint,
|
|
23961
|
+
};
|
|
23962
|
+
}
|
|
23963
|
+
/**
|
|
23964
|
+
* Sets initial safe-area overrides before modal animation.
|
|
23965
|
+
* Called in present() before animation starts.
|
|
23966
|
+
*
|
|
23967
|
+
* For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
|
|
23968
|
+
* (an internal property) instead of --ion-safe-area-top. We resolve the
|
|
23969
|
+
* root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
|
|
23970
|
+
* the height calculation from --ion-safe-area-top (which is zeroed for
|
|
23971
|
+
* sheets to prevent header content from getting double-offset padding).
|
|
23972
|
+
*/
|
|
23973
|
+
setInitialSafeAreaOverrides() {
|
|
23974
|
+
const context = this.getSafeAreaContext();
|
|
23975
|
+
const safeAreaConfig = getInitialSafeAreaConfig(context);
|
|
23976
|
+
applySafeAreaOverrides(this.el, safeAreaConfig);
|
|
23977
|
+
// Set the internal offset property with the resolved root safe-area-top value
|
|
23978
|
+
if (context.isSheetModal) {
|
|
23979
|
+
this.updateSheetOffsetTop();
|
|
23980
|
+
}
|
|
23981
|
+
}
|
|
23982
|
+
/**
|
|
23983
|
+
* Resolves the current root --ion-safe-area-top value and sets the
|
|
23984
|
+
* internal --ion-modal-offset-top property on the host element.
|
|
23985
|
+
* Called on present and on resize (e.g., device rotation changes safe-area).
|
|
23986
|
+
*/
|
|
23987
|
+
updateSheetOffsetTop() {
|
|
23988
|
+
const safeAreaTop = getRootSafeAreaTop();
|
|
23989
|
+
this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
|
|
23990
|
+
}
|
|
23991
|
+
/**
|
|
23992
|
+
* Updates safe-area overrides during dynamic state changes.
|
|
23993
|
+
* Called after animations, during gestures, and on orientation changes.
|
|
23994
|
+
*/
|
|
23995
|
+
updateSafeAreaOverrides() {
|
|
23996
|
+
const { wrapperEl, el } = this;
|
|
23997
|
+
const context = this.getSafeAreaContext();
|
|
23998
|
+
// Sheet modals: safe-area is fully determined at presentation time
|
|
23999
|
+
// (top is always 0px, height is frozen). Nothing to update.
|
|
24000
|
+
if (context.isSheetModal)
|
|
24001
|
+
return;
|
|
24002
|
+
// Card modals have fixed safe-area requirements set by initial prediction.
|
|
24003
|
+
if (context.isCardModal)
|
|
24004
|
+
return;
|
|
24005
|
+
// wrapperEl is required for position-based detection below
|
|
24006
|
+
if (!wrapperEl)
|
|
24007
|
+
return;
|
|
24008
|
+
// Regular modals: use position-based detection to correctly handle both
|
|
24009
|
+
// fullscreen modals and centered dialogs with custom dimensions.
|
|
24010
|
+
const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
|
|
24011
|
+
applySafeAreaOverrides(el, safeAreaConfig);
|
|
24012
|
+
}
|
|
24013
|
+
/**
|
|
24014
|
+
* Applies padding-bottom to fullscreen modal wrapper to prevent
|
|
24015
|
+
* content from overlapping system navigation bar.
|
|
24016
|
+
*/
|
|
24017
|
+
applyFullscreenSafeArea() {
|
|
24018
|
+
const { wrapperEl, el } = this;
|
|
24019
|
+
if (!wrapperEl)
|
|
24020
|
+
return;
|
|
24021
|
+
const context = this.getSafeAreaContext();
|
|
24022
|
+
if (context.isSheetModal || context.isCardModal)
|
|
24023
|
+
return;
|
|
24024
|
+
// Check for standard Ionic layout children (ion-content, ion-footer),
|
|
24025
|
+
// searching one level deep for wrapped components (e.g.,
|
|
24026
|
+
// <app-footer><ion-footer>...</ion-footer></app-footer>).
|
|
24027
|
+
// Note: uses a manual loop instead of querySelector(':scope > ...') because
|
|
24028
|
+
// Stencil's mock-doc (used in spec tests) does not support :scope.
|
|
24029
|
+
let hasContent = false;
|
|
24030
|
+
let hasFooter = false;
|
|
24031
|
+
for (const child of Array.from(el.children)) {
|
|
24032
|
+
if (child.tagName === 'ION-CONTENT')
|
|
24033
|
+
hasContent = true;
|
|
24034
|
+
if (child.tagName === 'ION-FOOTER')
|
|
24035
|
+
hasFooter = true;
|
|
24036
|
+
for (const grandchild of Array.from(child.children)) {
|
|
24037
|
+
if (grandchild.tagName === 'ION-CONTENT')
|
|
24038
|
+
hasContent = true;
|
|
24039
|
+
if (grandchild.tagName === 'ION-FOOTER')
|
|
24040
|
+
hasFooter = true;
|
|
24041
|
+
}
|
|
24042
|
+
}
|
|
24043
|
+
// Only apply wrapper padding for standard Ionic layouts (has ion-content
|
|
24044
|
+
// but no ion-footer). Custom modals with raw HTML are fully
|
|
24045
|
+
// developer-controlled and should not be modified.
|
|
24046
|
+
if (!hasContent || hasFooter)
|
|
24047
|
+
return;
|
|
24048
|
+
// Reduce wrapper height by safe-area and add equivalent padding so the
|
|
24049
|
+
// total visual size stays the same but the flex content area shrinks.
|
|
24050
|
+
// Using height + padding instead of box-sizing: border-box avoids
|
|
24051
|
+
// breaking custom modals that set --border-width (border-box would
|
|
24052
|
+
// include the border inside the height, changing the layout).
|
|
24053
|
+
wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
|
|
24054
|
+
wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
24055
|
+
}
|
|
24056
|
+
/**
|
|
24057
|
+
* Clears all safe-area overrides and padding from wrapper.
|
|
24058
|
+
*/
|
|
24059
|
+
cleanupSafeAreaOverrides() {
|
|
24060
|
+
clearSafeAreaOverrides(this.el);
|
|
24061
|
+
// Remove internal sheet offset property
|
|
24062
|
+
this.el.style.removeProperty('--ion-modal-offset-top');
|
|
24063
|
+
if (this.wrapperEl) {
|
|
24064
|
+
this.wrapperEl.style.removeProperty('height');
|
|
24065
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
24066
|
+
}
|
|
24067
|
+
}
|
|
23759
24068
|
render() {
|
|
23760
24069
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
23761
24070
|
const showHandle = handle !== false && isSheetModal;
|
|
@@ -23763,20 +24072,20 @@ class Modal {
|
|
|
23763
24072
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
23764
24073
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
23765
24074
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
23766
|
-
return (hAsync(Host, Object.assign({ key: '
|
|
24075
|
+
return (hAsync(Host, Object.assign({ key: '038ae26c2cb040043c7d0c217a4de6a7ae909235', "no-router": true,
|
|
23767
24076
|
// Allow the modal to be navigable when the handle is focusable
|
|
23768
24077
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
23769
24078
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
23770
|
-
}, 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 }), hAsync("ion-backdrop", { key: '
|
|
24079
|
+
}, 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 }), hAsync("ion-backdrop", { key: '6bab796467b370e79b24ce17c5a3d51adb07ee8c', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'b9a19e86a2c7f079c88ef7a914c7c890aa601887', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'f4315c34aa78be4e3540361755964be2ddf43265',
|
|
23771
24080
|
/*
|
|
23772
24081
|
role and aria-modal must be used on the
|
|
23773
24082
|
same element. They must also be set inside the
|
|
23774
24083
|
shadow DOM otherwise ion-button will not be highlighted
|
|
23775
24084
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
23776
24085
|
*/
|
|
23777
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '
|
|
24086
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '3f7792bd65e0f8634e62bf27f9edcb636a03030a', class: "modal-handle",
|
|
23778
24087
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
23779
|
-
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) })), hAsync("slot", { key: '
|
|
24088
|
+
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) })), hAsync("slot", { key: '001e5b01831e240bd98f75ff62446b90b64d8394', onSlotchange: this.onSlotChange }))));
|
|
23780
24089
|
}
|
|
23781
24090
|
get el() { return getElement(this); }
|
|
23782
24091
|
static get watchers() { return {
|
|
@@ -26710,6 +27019,58 @@ class PickerColumnOption {
|
|
|
26710
27019
|
}; }
|
|
26711
27020
|
}
|
|
26712
27021
|
|
|
27022
|
+
/**
|
|
27023
|
+
* Shared per-frame cache for safe-area insets. Avoids creating a temporary
|
|
27024
|
+
* DOM element and forcing a synchronous reflow on every call within the same
|
|
27025
|
+
* frame (e.g., multiple popovers presenting simultaneously). Invalidated
|
|
27026
|
+
* after the next animation frame so values stay fresh across orientation
|
|
27027
|
+
* changes and viewport resizes.
|
|
27028
|
+
*/
|
|
27029
|
+
let cachedInsets = null;
|
|
27030
|
+
let cacheInvalidationScheduled = false;
|
|
27031
|
+
/**
|
|
27032
|
+
* Resolves the current --ion-safe-area-* CSS custom property values
|
|
27033
|
+
* to actual pixel numbers. A temporary element is needed because
|
|
27034
|
+
* getComputedStyle on :root returns the declared value of custom
|
|
27035
|
+
* properties (e.g. "env(safe-area-inset-top)") rather than a
|
|
27036
|
+
* resolved number. By assigning them to standard padding properties,
|
|
27037
|
+
* the browser resolves the value.
|
|
27038
|
+
*
|
|
27039
|
+
* Results are cached for the current frame to avoid repeated reflows.
|
|
27040
|
+
*/
|
|
27041
|
+
const getSafeAreaInsets = (doc) => {
|
|
27042
|
+
if (cachedInsets !== null) {
|
|
27043
|
+
return cachedInsets;
|
|
27044
|
+
}
|
|
27045
|
+
if (doc.body === null) {
|
|
27046
|
+
return { top: 0, bottom: 0, left: 0, right: 0 };
|
|
27047
|
+
}
|
|
27048
|
+
const el = doc.createElement('div');
|
|
27049
|
+
el.style.cssText =
|
|
27050
|
+
'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
|
|
27051
|
+
'padding-top:var(--ion-safe-area-top,0px);' +
|
|
27052
|
+
'padding-bottom:var(--ion-safe-area-bottom,0px);' +
|
|
27053
|
+
'padding-left:var(--ion-safe-area-left,0px);' +
|
|
27054
|
+
'padding-right:var(--ion-safe-area-right,0px);';
|
|
27055
|
+
doc.body.appendChild(el);
|
|
27056
|
+
const style = getComputedStyle(el);
|
|
27057
|
+
const insets = {
|
|
27058
|
+
top: parseFloat(style.paddingTop) || 0,
|
|
27059
|
+
bottom: parseFloat(style.paddingBottom) || 0,
|
|
27060
|
+
left: parseFloat(style.paddingLeft) || 0,
|
|
27061
|
+
right: parseFloat(style.paddingRight) || 0,
|
|
27062
|
+
};
|
|
27063
|
+
el.remove();
|
|
27064
|
+
cachedInsets = insets;
|
|
27065
|
+
if (!cacheInvalidationScheduled) {
|
|
27066
|
+
cacheInvalidationScheduled = true;
|
|
27067
|
+
raf(() => {
|
|
27068
|
+
cachedInsets = null;
|
|
27069
|
+
cacheInvalidationScheduled = false;
|
|
27070
|
+
});
|
|
27071
|
+
}
|
|
27072
|
+
return insets;
|
|
27073
|
+
};
|
|
26713
27074
|
/**
|
|
26714
27075
|
* Returns the dimensions of the popover
|
|
26715
27076
|
* arrow on `ios` mode. If arrow is disabled
|
|
@@ -27345,8 +27706,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
|
|
|
27345
27706
|
* Adjusts popover positioning coordinates
|
|
27346
27707
|
* such that popover does not appear offscreen
|
|
27347
27708
|
* or overlapping safe area bounds.
|
|
27709
|
+
*
|
|
27710
|
+
* @internal - This is not part of the public API.
|
|
27348
27711
|
*/
|
|
27349
|
-
const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight,
|
|
27712
|
+
const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
|
|
27350
27713
|
let arrowTop = coordArrowTop;
|
|
27351
27714
|
const arrowLeft = coordArrowLeft;
|
|
27352
27715
|
let left = coordLeft;
|
|
@@ -27356,16 +27719,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
27356
27719
|
let originY = contentOriginY;
|
|
27357
27720
|
let checkSafeAreaLeft = false;
|
|
27358
27721
|
let checkSafeAreaRight = false;
|
|
27722
|
+
let checkSafeAreaTop = false;
|
|
27723
|
+
let checkSafeAreaBottom = false;
|
|
27359
27724
|
const triggerTop = triggerCoordinates
|
|
27360
27725
|
? triggerCoordinates.top + triggerCoordinates.height
|
|
27361
27726
|
: bodyHeight / 2 - contentHeight / 2;
|
|
27362
27727
|
const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
|
|
27363
27728
|
let addPopoverBottomClass = false;
|
|
27729
|
+
const hideArrow = false;
|
|
27364
27730
|
/**
|
|
27365
27731
|
* Adjust popover so it does not
|
|
27366
27732
|
* go off the left of the screen.
|
|
27367
27733
|
*/
|
|
27368
|
-
if (left < bodyPadding +
|
|
27734
|
+
if (left < bodyPadding + safeArea.left) {
|
|
27369
27735
|
left = bodyPadding;
|
|
27370
27736
|
checkSafeAreaLeft = true;
|
|
27371
27737
|
originX = 'left';
|
|
@@ -27374,41 +27740,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
27374
27740
|
* go off the right of the screen.
|
|
27375
27741
|
*/
|
|
27376
27742
|
}
|
|
27377
|
-
else if (contentWidth + bodyPadding + left +
|
|
27743
|
+
else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
|
|
27378
27744
|
checkSafeAreaRight = true;
|
|
27379
27745
|
left = bodyWidth - contentWidth - bodyPadding;
|
|
27380
27746
|
originX = 'right';
|
|
27381
27747
|
}
|
|
27382
27748
|
/**
|
|
27383
27749
|
* Adjust popover so it does not
|
|
27384
|
-
* go off the
|
|
27750
|
+
* go off the bottom of the screen
|
|
27751
|
+
* or overlap the bottom safe area.
|
|
27385
27752
|
* If popover is on the left or the right of
|
|
27386
27753
|
* the trigger, then we should not adjust top
|
|
27387
27754
|
* margins.
|
|
27388
27755
|
*/
|
|
27389
|
-
if (triggerTop + triggerHeight + contentHeight > bodyHeight
|
|
27390
|
-
|
|
27756
|
+
if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
|
|
27757
|
+
(side === 'top' || side === 'bottom')) {
|
|
27758
|
+
/**
|
|
27759
|
+
* Calculate where the popover top would be if flipped
|
|
27760
|
+
* above the trigger. Check whether that position clears
|
|
27761
|
+
* the top safe area with room for bodyPadding.
|
|
27762
|
+
*/
|
|
27763
|
+
const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
|
|
27764
|
+
if (idealFlipTop >= safeArea.top + bodyPadding) {
|
|
27391
27765
|
/**
|
|
27392
|
-
*
|
|
27393
|
-
*
|
|
27394
|
-
*
|
|
27395
|
-
* off the bottom of the screen. However, we do not want to move
|
|
27396
|
-
* the popover up so much that it goes off the top of the screen.
|
|
27397
|
-
*
|
|
27398
|
-
* We chose 12 here so that the popover position looks a bit nicer as
|
|
27399
|
-
* it is not right up against the edge of the screen.
|
|
27766
|
+
* Popover fits above the trigger without overlapping
|
|
27767
|
+
* the top safe area. Use the ideal position directly —
|
|
27768
|
+
* no safe-area CSS vars needed since it clears both edges.
|
|
27400
27769
|
*/
|
|
27401
|
-
top =
|
|
27770
|
+
top = idealFlipTop;
|
|
27402
27771
|
arrowTop = top + contentHeight;
|
|
27403
27772
|
originY = 'bottom';
|
|
27404
27773
|
addPopoverBottomClass = true;
|
|
27405
|
-
/**
|
|
27406
|
-
* If not enough room for popover to appear
|
|
27407
|
-
* above trigger, then cut it off.
|
|
27408
|
-
*/
|
|
27409
27774
|
}
|
|
27410
27775
|
else {
|
|
27776
|
+
/**
|
|
27777
|
+
* Can't flip above the trigger. Constrain the bottom
|
|
27778
|
+
* edge while keeping the top near the trigger. This
|
|
27779
|
+
* creates a scrollable area anchored to the trigger.
|
|
27780
|
+
*/
|
|
27411
27781
|
bottom = bodyPadding;
|
|
27782
|
+
checkSafeAreaBottom = true;
|
|
27783
|
+
/**
|
|
27784
|
+
* When the trigger is near the bottom of the screen,
|
|
27785
|
+
* the calculated top may be at or past the bottom
|
|
27786
|
+
* constraint, leaving zero visible height. In that
|
|
27787
|
+
* case, pull top up to the top safe area so the
|
|
27788
|
+
* popover fills the available space between safe areas.
|
|
27789
|
+
*/
|
|
27790
|
+
const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
|
|
27791
|
+
if (top >= bottomEdge) {
|
|
27792
|
+
top = safeArea.top + bodyPadding;
|
|
27793
|
+
checkSafeAreaTop = true;
|
|
27794
|
+
}
|
|
27412
27795
|
}
|
|
27413
27796
|
}
|
|
27414
27797
|
return {
|
|
@@ -27419,9 +27802,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
27419
27802
|
originY,
|
|
27420
27803
|
checkSafeAreaLeft,
|
|
27421
27804
|
checkSafeAreaRight,
|
|
27805
|
+
checkSafeAreaTop,
|
|
27806
|
+
checkSafeAreaBottom,
|
|
27422
27807
|
arrowTop,
|
|
27423
27808
|
arrowLeft,
|
|
27424
27809
|
addPopoverBottomClass,
|
|
27810
|
+
hideArrow,
|
|
27425
27811
|
};
|
|
27426
27812
|
};
|
|
27427
27813
|
const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
|
|
@@ -27451,6 +27837,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
|
|
|
27451
27837
|
};
|
|
27452
27838
|
|
|
27453
27839
|
const POPOVER_IOS_BODY_PADDING = 5;
|
|
27840
|
+
/**
|
|
27841
|
+
* Minimum edge margin for iOS popovers ensures visual spacing from screen
|
|
27842
|
+
* edges on devices without safe areas (e.g., older iPhones without notches).
|
|
27843
|
+
* Previously this was a hardcoded `safeAreaMargin = 25` that served dual
|
|
27844
|
+
* purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
|
|
27845
|
+
* insets are read dynamically, this floor preserves the visual spacing when
|
|
27846
|
+
* safe-area values are 0.
|
|
27847
|
+
*/
|
|
27848
|
+
const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
|
|
27454
27849
|
/**
|
|
27455
27850
|
* iOS Popover Enter Animation
|
|
27456
27851
|
*/
|
|
@@ -27476,8 +27871,16 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27476
27871
|
};
|
|
27477
27872
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
27478
27873
|
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
27479
|
-
const
|
|
27480
|
-
const
|
|
27874
|
+
const rawSafeArea = getSafeAreaInsets(doc);
|
|
27875
|
+
const safeArea = size === 'cover'
|
|
27876
|
+
? { top: 0, bottom: 0, left: 0, right: 0 }
|
|
27877
|
+
: {
|
|
27878
|
+
top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
27879
|
+
bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
27880
|
+
left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
27881
|
+
right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
27882
|
+
};
|
|
27883
|
+
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, arrowTop, arrowLeft, addPopoverBottomClass} = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
27481
27884
|
const baseAnimation = createAnimation();
|
|
27482
27885
|
const backdropAnimation = createAnimation();
|
|
27483
27886
|
const contentAnimation = createAnimation();
|
|
@@ -27508,10 +27911,14 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27508
27911
|
baseEl.classList.add('popover-bottom');
|
|
27509
27912
|
}
|
|
27510
27913
|
if (bottom !== undefined) {
|
|
27511
|
-
|
|
27914
|
+
let bottomValue = `${bottom}px`;
|
|
27915
|
+
if (checkSafeAreaBottom) {
|
|
27916
|
+
bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
|
|
27917
|
+
}
|
|
27918
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
27512
27919
|
}
|
|
27513
|
-
const safeAreaLeft = ' + var(--ion-safe-area-left,
|
|
27514
|
-
const safeAreaRight = ' - var(--ion-safe-area-right,
|
|
27920
|
+
const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
|
|
27921
|
+
const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
|
|
27515
27922
|
let leftValue = `${left}px`;
|
|
27516
27923
|
if (checkSafeAreaLeft) {
|
|
27517
27924
|
leftValue = `${left}px${safeAreaLeft}`;
|
|
@@ -27519,7 +27926,11 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27519
27926
|
if (checkSafeAreaRight) {
|
|
27520
27927
|
leftValue = `${left}px${safeAreaRight}`;
|
|
27521
27928
|
}
|
|
27522
|
-
|
|
27929
|
+
let topValue = `${top}px`;
|
|
27930
|
+
if (checkSafeAreaTop) {
|
|
27931
|
+
topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
|
|
27932
|
+
}
|
|
27933
|
+
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
|
27523
27934
|
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
27524
27935
|
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
27525
27936
|
if (arrowEl !== null) {
|
|
@@ -27595,7 +28006,23 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27595
28006
|
};
|
|
27596
28007
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
|
|
27597
28008
|
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
|
27598
|
-
|
|
28009
|
+
// MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
|
|
28010
|
+
// This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
|
|
28011
|
+
const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
|
|
28012
|
+
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates);
|
|
28013
|
+
const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
|
|
28014
|
+
const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
|
|
28015
|
+
let leftValue = `${left}px`;
|
|
28016
|
+
if (checkSafeAreaLeft) {
|
|
28017
|
+
leftValue = `${left}px${safeAreaLeftCalc}`;
|
|
28018
|
+
}
|
|
28019
|
+
if (checkSafeAreaRight) {
|
|
28020
|
+
leftValue = `${left}px${safeAreaRightCalc}`;
|
|
28021
|
+
}
|
|
28022
|
+
let topValue = `${top}px`;
|
|
28023
|
+
if (checkSafeAreaTop) {
|
|
28024
|
+
topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
|
|
28025
|
+
}
|
|
27599
28026
|
const baseAnimation = createAnimation();
|
|
27600
28027
|
const backdropAnimation = createAnimation();
|
|
27601
28028
|
const wrapperAnimation = createAnimation();
|
|
@@ -27612,13 +28039,17 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27612
28039
|
contentAnimation
|
|
27613
28040
|
.addElement(contentEl)
|
|
27614
28041
|
.beforeStyles({
|
|
27615
|
-
top: `calc(${
|
|
27616
|
-
left: `calc(${
|
|
28042
|
+
top: `calc(${topValue} + var(--offset-y, 0px))`,
|
|
28043
|
+
left: `calc(${leftValue} + var(--offset-x, 0px))`,
|
|
27617
28044
|
'transform-origin': `${originY} ${originX}`,
|
|
27618
28045
|
})
|
|
27619
28046
|
.beforeAddWrite(() => {
|
|
27620
28047
|
if (bottom !== undefined) {
|
|
27621
|
-
|
|
28048
|
+
let bottomValue = `${bottom}px`;
|
|
28049
|
+
if (checkSafeAreaBottom) {
|
|
28050
|
+
bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
|
|
28051
|
+
}
|
|
28052
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
27622
28053
|
}
|
|
27623
28054
|
})
|
|
27624
28055
|
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|
|
@@ -27630,7 +28061,7 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
|
|
|
27630
28061
|
if (size === 'cover') {
|
|
27631
28062
|
baseEl.style.setProperty('--width', `${contentWidth}px`);
|
|
27632
28063
|
}
|
|
27633
|
-
if (
|
|
28064
|
+
if (addPopoverBottomClass) {
|
|
27634
28065
|
baseEl.classList.add('popover-bottom');
|
|
27635
28066
|
}
|
|
27636
28067
|
})
|
|
@@ -29735,8 +30166,6 @@ class Refresher {
|
|
|
29735
30166
|
this.ionRefresh = createEvent(this, "ionRefresh", 7);
|
|
29736
30167
|
this.ionPull = createEvent(this, "ionPull", 7);
|
|
29737
30168
|
this.ionStart = createEvent(this, "ionStart", 7);
|
|
29738
|
-
this.ionPullStart = createEvent(this, "ionPullStart", 7);
|
|
29739
|
-
this.ionPullEnd = createEvent(this, "ionPullEnd", 7);
|
|
29740
30169
|
this.appliedStyles = false;
|
|
29741
30170
|
this.didStart = false;
|
|
29742
30171
|
this.progress = 0;
|
|
@@ -29791,8 +30220,8 @@ class Refresher {
|
|
|
29791
30220
|
* than `1`. The default value is `1` which is equal to the speed of the cursor.
|
|
29792
30221
|
* If a negative value is passed in, the factor will be `1` instead.
|
|
29793
30222
|
*
|
|
29794
|
-
* For example
|
|
29795
|
-
* `10` pixels, instead of `10` pixels
|
|
30223
|
+
* For example: If the value passed is `1.2` and the content is dragged by
|
|
30224
|
+
* `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
|
|
29796
30225
|
* (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
|
|
29797
30226
|
* will be `8` pixels, less than the amount the cursor has moved.
|
|
29798
30227
|
*
|
|
@@ -29842,9 +30271,6 @@ class Refresher {
|
|
|
29842
30271
|
this.animations = [];
|
|
29843
30272
|
this.progress = 0;
|
|
29844
30273
|
this.state = 1 /* RefresherState.Inactive */;
|
|
29845
|
-
this.ionPullEnd.emit({
|
|
29846
|
-
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
29847
|
-
});
|
|
29848
30274
|
}
|
|
29849
30275
|
async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
|
|
29850
30276
|
this.elementToTransform = this.scrollEl;
|
|
@@ -29877,7 +30303,6 @@ class Refresher {
|
|
|
29877
30303
|
if (!this.didStart) {
|
|
29878
30304
|
this.didStart = true;
|
|
29879
30305
|
this.ionStart.emit();
|
|
29880
|
-
this.ionPullStart.emit();
|
|
29881
30306
|
}
|
|
29882
30307
|
// emit "pulling" on every move
|
|
29883
30308
|
if (this.pointerDown) {
|
|
@@ -29953,7 +30378,6 @@ class Refresher {
|
|
|
29953
30378
|
this.lastVelocityY = ev.velocityY;
|
|
29954
30379
|
},
|
|
29955
30380
|
onEnd: () => {
|
|
29956
|
-
const hadStarted = this.didStart;
|
|
29957
30381
|
this.pointerDown = false;
|
|
29958
30382
|
this.didStart = false;
|
|
29959
30383
|
if (this.needsCompletion) {
|
|
@@ -29963,13 +30387,6 @@ class Refresher {
|
|
|
29963
30387
|
else if (this.didRefresh) {
|
|
29964
30388
|
readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
|
|
29965
30389
|
}
|
|
29966
|
-
else if (hadStarted) {
|
|
29967
|
-
/**
|
|
29968
|
-
* User started pulling but released before reaching the refresh threshold.
|
|
29969
|
-
* Emit ionPullEnd to complete the event pair.
|
|
29970
|
-
*/
|
|
29971
|
-
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
29972
|
-
}
|
|
29973
30390
|
},
|
|
29974
30391
|
});
|
|
29975
30392
|
this.disabledChanged();
|
|
@@ -30016,7 +30433,6 @@ class Refresher {
|
|
|
30016
30433
|
ev.data.animation = animation;
|
|
30017
30434
|
animation.progressStart(false, 0);
|
|
30018
30435
|
this.ionStart.emit();
|
|
30019
|
-
this.ionPullStart.emit();
|
|
30020
30436
|
this.animations.push(animation);
|
|
30021
30437
|
return;
|
|
30022
30438
|
}
|
|
@@ -30039,7 +30455,6 @@ class Refresher {
|
|
|
30039
30455
|
this.animations = [];
|
|
30040
30456
|
this.gesture.enable(true);
|
|
30041
30457
|
this.state = 1 /* RefresherState.Inactive */;
|
|
30042
|
-
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
30043
30458
|
});
|
|
30044
30459
|
return;
|
|
30045
30460
|
}
|
|
@@ -30284,7 +30699,6 @@ class Refresher {
|
|
|
30284
30699
|
if (!this.didStart) {
|
|
30285
30700
|
this.didStart = true;
|
|
30286
30701
|
this.ionStart.emit();
|
|
30287
|
-
this.ionPullStart.emit();
|
|
30288
30702
|
}
|
|
30289
30703
|
// emit "pulling" on every move
|
|
30290
30704
|
this.ionPull.emit();
|
|
@@ -30328,15 +30742,6 @@ class Refresher {
|
|
|
30328
30742
|
* available right away.
|
|
30329
30743
|
*/
|
|
30330
30744
|
this.restoreOverflowStyle();
|
|
30331
|
-
/**
|
|
30332
|
-
* If ionPullStart was emitted, we need to emit ionPullEnd
|
|
30333
|
-
* even though the gesture was aborted before reaching the
|
|
30334
|
-
* pulling threshold.
|
|
30335
|
-
*/
|
|
30336
|
-
if (this.didStart) {
|
|
30337
|
-
this.didStart = false;
|
|
30338
|
-
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
30339
|
-
}
|
|
30340
30745
|
}
|
|
30341
30746
|
}
|
|
30342
30747
|
beginRefresh() {
|
|
@@ -30383,9 +30788,6 @@ class Refresher {
|
|
|
30383
30788
|
if (this.contentFullscreen && this.backgroundContentEl) {
|
|
30384
30789
|
(_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
|
|
30385
30790
|
}
|
|
30386
|
-
this.ionPullEnd.emit({
|
|
30387
|
-
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
30388
|
-
});
|
|
30389
30791
|
}, 600);
|
|
30390
30792
|
// reset the styles on the scroll element
|
|
30391
30793
|
// set that the refresh is actively cancelling/completing
|
|
@@ -30439,7 +30841,7 @@ class Refresher {
|
|
|
30439
30841
|
}
|
|
30440
30842
|
render() {
|
|
30441
30843
|
const mode = getIonMode$1(this);
|
|
30442
|
-
return (hAsync(Host, { key: '
|
|
30844
|
+
return (hAsync(Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
|
|
30443
30845
|
[mode]: true,
|
|
30444
30846
|
// Used internally for styling
|
|
30445
30847
|
[`refresher-${mode}`]: true,
|