@ionic/core 8.7.18-dev.11769790854.11895f8f → 8.7.18-dev.11770316329.126ef4af
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 +32 -3
- package/components/modal.js +12 -220
- package/components/popover.js +11 -83
- package/dist/cjs/ion-modal.cjs.entry.js +12 -220
- package/dist/cjs/ion-popover.cjs.entry.js +11 -83
- package/dist/cjs/ion-refresher_2.cjs.entry.js +32 -3
- package/dist/collection/components/modal/gestures/sheet.js +1 -3
- package/dist/collection/components/modal/gestures/swipe-to-close.js +1 -3
- package/dist/collection/components/modal/modal.ios.css +4 -0
- package/dist/collection/components/modal/modal.js +7 -215
- package/dist/collection/components/modal/modal.md.css +4 -0
- package/dist/collection/components/popover/animations/ios.enter.js +5 -21
- package/dist/collection/components/popover/animations/md.enter.js +5 -30
- package/dist/collection/components/popover/utils.js +1 -32
- package/dist/collection/components/refresher/refresher.js +70 -4
- package/dist/docs.json +49 -4
- package/dist/esm/ion-modal.entry.js +12 -220
- package/dist/esm/ion-popover.entry.js +11 -83
- package/dist/esm/ion-refresher_2.entry.js +32 -3
- package/dist/html.html-data.json +1 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-90f2d3ff.entry.js +4 -0
- package/dist/ionic/p-91840a80.entry.js +4 -0
- package/dist/ionic/p-f9061316.entry.js +4 -0
- package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
- package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
- package/dist/types/components/modal/modal.d.ts +0 -47
- package/dist/types/components/popover/utils.d.ts +0 -2
- package/dist/types/components/refresher/refresher-interface.d.ts +7 -0
- package/dist/types/components/refresher/refresher.d.ts +16 -3
- package/dist/types/components.d.ts +16 -5
- package/dist/types/interface.d.ts +1 -1
- package/hydrate/index.js +54 -305
- package/hydrate/index.mjs +54 -305
- package/package.json +2 -4
- package/dist/ionic/p-639dd543.entry.js +0 -4
- package/dist/ionic/p-ec9ca3fe.entry.js +0 -4
- package/dist/ionic/p-ef4256eb.entry.js +0 -4
|
@@ -660,8 +660,6 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
660
660
|
let bottom;
|
|
661
661
|
let originX = contentOriginX;
|
|
662
662
|
let originY = contentOriginY;
|
|
663
|
-
let checkSafeAreaTop = false;
|
|
664
|
-
let checkSafeAreaBottom = false;
|
|
665
663
|
let checkSafeAreaLeft = false;
|
|
666
664
|
let checkSafeAreaRight = false;
|
|
667
665
|
const triggerTop = triggerCoordinates
|
|
@@ -706,18 +704,10 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
706
704
|
* We chose 12 here so that the popover position looks a bit nicer as
|
|
707
705
|
* it is not right up against the edge of the screen.
|
|
708
706
|
*/
|
|
709
|
-
top = Math.max(
|
|
707
|
+
top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
|
|
710
708
|
arrowTop = top + contentHeight;
|
|
711
709
|
originY = 'bottom';
|
|
712
710
|
addPopoverBottomClass = true;
|
|
713
|
-
/**
|
|
714
|
-
* If the popover is positioned near the top edge, account for safe area.
|
|
715
|
-
* This ensures the popover doesn't overlap with status bars or notches.
|
|
716
|
-
*/
|
|
717
|
-
if (top <= bodyPadding + safeAreaMargin) {
|
|
718
|
-
checkSafeAreaTop = true;
|
|
719
|
-
top = bodyPadding;
|
|
720
|
-
}
|
|
721
711
|
/**
|
|
722
712
|
* If not enough room for popover to appear
|
|
723
713
|
* above trigger, then cut it off.
|
|
@@ -725,35 +715,14 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
725
715
|
}
|
|
726
716
|
else {
|
|
727
717
|
bottom = bodyPadding;
|
|
728
|
-
/**
|
|
729
|
-
* When the popover is pinned to the bottom, account for safe area.
|
|
730
|
-
* This ensures the popover doesn't overlap with home indicators
|
|
731
|
-
* or navigation bars (e.g., Android API 36+ edge-to-edge).
|
|
732
|
-
*/
|
|
733
|
-
checkSafeAreaBottom = true;
|
|
734
718
|
}
|
|
735
719
|
}
|
|
736
|
-
/**
|
|
737
|
-
* Final check: If the popover extends into any safe-area region,
|
|
738
|
-
* ensure the corresponding flag is set regardless of side.
|
|
739
|
-
* This handles cases where a side-positioned popover (left/right)
|
|
740
|
-
* still needs bottom safe-area padding because it extends into that region.
|
|
741
|
-
*/
|
|
742
|
-
const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
|
|
743
|
-
if (popoverBottom + safeAreaMargin > bodyHeight) {
|
|
744
|
-
checkSafeAreaBottom = true;
|
|
745
|
-
}
|
|
746
|
-
if (top < safeAreaMargin) {
|
|
747
|
-
checkSafeAreaTop = true;
|
|
748
|
-
}
|
|
749
720
|
return {
|
|
750
721
|
top,
|
|
751
722
|
left,
|
|
752
723
|
bottom,
|
|
753
724
|
originX,
|
|
754
725
|
originY,
|
|
755
|
-
checkSafeAreaTop,
|
|
756
|
-
checkSafeAreaBottom,
|
|
757
726
|
checkSafeAreaLeft,
|
|
758
727
|
checkSafeAreaRight,
|
|
759
728
|
arrowTop,
|
|
@@ -814,7 +783,7 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
814
783
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
815
784
|
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
816
785
|
const margin = size === 'cover' ? 0 : 25;
|
|
817
|
-
const { originX, originY, top, left, bottom,
|
|
786
|
+
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
818
787
|
const baseAnimation = animation.createAnimation();
|
|
819
788
|
const backdropAnimation = animation.createAnimation();
|
|
820
789
|
const contentAnimation = animation.createAnimation();
|
|
@@ -844,35 +813,19 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
844
813
|
if (addPopoverBottomClass) {
|
|
845
814
|
baseEl.classList.add('popover-bottom');
|
|
846
815
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
851
|
-
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
852
|
-
*/
|
|
853
|
-
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
854
|
-
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
816
|
+
if (bottom !== undefined) {
|
|
817
|
+
contentEl.style.setProperty('bottom', `${bottom}px`);
|
|
818
|
+
}
|
|
855
819
|
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
856
820
|
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
857
|
-
let topValue = `${top}px`;
|
|
858
|
-
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
859
821
|
let leftValue = `${left}px`;
|
|
860
|
-
if (checkSafeAreaTop) {
|
|
861
|
-
topValue = `${top}px${safeAreaTop}`;
|
|
862
|
-
}
|
|
863
|
-
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
864
|
-
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
865
|
-
}
|
|
866
822
|
if (checkSafeAreaLeft) {
|
|
867
823
|
leftValue = `${left}px${safeAreaLeft}`;
|
|
868
824
|
}
|
|
869
825
|
if (checkSafeAreaRight) {
|
|
870
826
|
leftValue = `${left}px${safeAreaRight}`;
|
|
871
827
|
}
|
|
872
|
-
|
|
873
|
-
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
874
|
-
}
|
|
875
|
-
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
|
828
|
+
contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
|
|
876
829
|
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
877
830
|
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
878
831
|
if (arrowEl !== null) {
|
|
@@ -948,32 +901,7 @@ const mdEnterAnimation = (baseEl, opts) => {
|
|
|
948
901
|
};
|
|
949
902
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
|
|
950
903
|
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
|
951
|
-
const { originX, originY, top, left, bottom
|
|
952
|
-
/**
|
|
953
|
-
* Safe area CSS variable adjustments.
|
|
954
|
-
* When the popover is positioned near an edge, we add the corresponding
|
|
955
|
-
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
956
|
-
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
957
|
-
*/
|
|
958
|
-
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
959
|
-
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
960
|
-
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
961
|
-
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
962
|
-
let topValue = `${top}px`;
|
|
963
|
-
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
964
|
-
let leftValue = `${left}px`;
|
|
965
|
-
if (checkSafeAreaTop) {
|
|
966
|
-
topValue = `${top}px${safeAreaTop}`;
|
|
967
|
-
}
|
|
968
|
-
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
969
|
-
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
970
|
-
}
|
|
971
|
-
if (checkSafeAreaLeft) {
|
|
972
|
-
leftValue = `${left}px${safeAreaLeft}`;
|
|
973
|
-
}
|
|
974
|
-
if (checkSafeAreaRight) {
|
|
975
|
-
leftValue = `${left}px${safeAreaRight}`;
|
|
976
|
-
}
|
|
904
|
+
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
|
|
977
905
|
const baseAnimation = animation.createAnimation();
|
|
978
906
|
const backdropAnimation = animation.createAnimation();
|
|
979
907
|
const wrapperAnimation = animation.createAnimation();
|
|
@@ -990,13 +918,13 @@ const mdEnterAnimation = (baseEl, opts) => {
|
|
|
990
918
|
contentAnimation
|
|
991
919
|
.addElement(contentEl)
|
|
992
920
|
.beforeStyles({
|
|
993
|
-
top: `calc(${
|
|
994
|
-
left: `calc(${
|
|
921
|
+
top: `calc(${top}px + var(--offset-y, 0px))`,
|
|
922
|
+
left: `calc(${left}px + var(--offset-x, 0px))`,
|
|
995
923
|
'transform-origin': `${originY} ${originX}`,
|
|
996
924
|
})
|
|
997
925
|
.beforeAddWrite(() => {
|
|
998
|
-
if (
|
|
999
|
-
contentEl.style.setProperty('bottom',
|
|
926
|
+
if (bottom !== undefined) {
|
|
927
|
+
contentEl.style.setProperty('bottom', `${bottom}px`);
|
|
1000
928
|
}
|
|
1001
929
|
})
|
|
1002
930
|
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|
|
@@ -214,6 +214,8 @@ const Refresher = class {
|
|
|
214
214
|
this.ionRefresh = index.createEvent(this, "ionRefresh", 7);
|
|
215
215
|
this.ionPull = index.createEvent(this, "ionPull", 7);
|
|
216
216
|
this.ionStart = index.createEvent(this, "ionStart", 7);
|
|
217
|
+
this.ionPullStart = index.createEvent(this, "ionPullStart", 7);
|
|
218
|
+
this.ionPullEnd = index.createEvent(this, "ionPullEnd", 7);
|
|
217
219
|
this.appliedStyles = false;
|
|
218
220
|
this.didStart = false;
|
|
219
221
|
this.progress = 0;
|
|
@@ -268,8 +270,8 @@ const Refresher = class {
|
|
|
268
270
|
* than `1`. The default value is `1` which is equal to the speed of the cursor.
|
|
269
271
|
* If a negative value is passed in, the factor will be `1` instead.
|
|
270
272
|
*
|
|
271
|
-
* For example
|
|
272
|
-
* `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
|
|
273
|
+
* For example, If the value passed is `1.2` and the content is dragged by
|
|
274
|
+
* `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
|
|
273
275
|
* (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
|
|
274
276
|
* will be `8` pixels, less than the amount the cursor has moved.
|
|
275
277
|
*
|
|
@@ -319,6 +321,9 @@ const Refresher = class {
|
|
|
319
321
|
this.animations = [];
|
|
320
322
|
this.progress = 0;
|
|
321
323
|
this.state = 1 /* RefresherState.Inactive */;
|
|
324
|
+
this.ionPullEnd.emit({
|
|
325
|
+
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
326
|
+
});
|
|
322
327
|
}
|
|
323
328
|
async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
|
|
324
329
|
this.elementToTransform = this.scrollEl;
|
|
@@ -351,6 +356,7 @@ const Refresher = class {
|
|
|
351
356
|
if (!this.didStart) {
|
|
352
357
|
this.didStart = true;
|
|
353
358
|
this.ionStart.emit();
|
|
359
|
+
this.ionPullStart.emit();
|
|
354
360
|
}
|
|
355
361
|
// emit "pulling" on every move
|
|
356
362
|
if (this.pointerDown) {
|
|
@@ -426,6 +432,7 @@ const Refresher = class {
|
|
|
426
432
|
this.lastVelocityY = ev.velocityY;
|
|
427
433
|
},
|
|
428
434
|
onEnd: () => {
|
|
435
|
+
const hadStarted = this.didStart;
|
|
429
436
|
this.pointerDown = false;
|
|
430
437
|
this.didStart = false;
|
|
431
438
|
if (this.needsCompletion) {
|
|
@@ -435,6 +442,13 @@ const Refresher = class {
|
|
|
435
442
|
else if (this.didRefresh) {
|
|
436
443
|
index.readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
|
|
437
444
|
}
|
|
445
|
+
else if (hadStarted) {
|
|
446
|
+
/**
|
|
447
|
+
* User started pulling but released before reaching the refresh threshold.
|
|
448
|
+
* Emit ionPullEnd to complete the event pair.
|
|
449
|
+
*/
|
|
450
|
+
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
451
|
+
}
|
|
438
452
|
},
|
|
439
453
|
});
|
|
440
454
|
this.disabledChanged();
|
|
@@ -481,6 +495,7 @@ const Refresher = class {
|
|
|
481
495
|
ev.data.animation = animation;
|
|
482
496
|
animation.progressStart(false, 0);
|
|
483
497
|
this.ionStart.emit();
|
|
498
|
+
this.ionPullStart.emit();
|
|
484
499
|
this.animations.push(animation);
|
|
485
500
|
return;
|
|
486
501
|
}
|
|
@@ -503,6 +518,7 @@ const Refresher = class {
|
|
|
503
518
|
this.animations = [];
|
|
504
519
|
this.gesture.enable(true);
|
|
505
520
|
this.state = 1 /* RefresherState.Inactive */;
|
|
521
|
+
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
506
522
|
});
|
|
507
523
|
return;
|
|
508
524
|
}
|
|
@@ -747,6 +763,7 @@ const Refresher = class {
|
|
|
747
763
|
if (!this.didStart) {
|
|
748
764
|
this.didStart = true;
|
|
749
765
|
this.ionStart.emit();
|
|
766
|
+
this.ionPullStart.emit();
|
|
750
767
|
}
|
|
751
768
|
// emit "pulling" on every move
|
|
752
769
|
this.ionPull.emit();
|
|
@@ -790,6 +807,15 @@ const Refresher = class {
|
|
|
790
807
|
* available right away.
|
|
791
808
|
*/
|
|
792
809
|
this.restoreOverflowStyle();
|
|
810
|
+
/**
|
|
811
|
+
* If ionPullStart was emitted, we need to emit ionPullEnd
|
|
812
|
+
* even though the gesture was aborted before reaching the
|
|
813
|
+
* pulling threshold.
|
|
814
|
+
*/
|
|
815
|
+
if (this.didStart) {
|
|
816
|
+
this.didStart = false;
|
|
817
|
+
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
818
|
+
}
|
|
793
819
|
}
|
|
794
820
|
}
|
|
795
821
|
beginRefresh() {
|
|
@@ -836,6 +862,9 @@ const Refresher = class {
|
|
|
836
862
|
if (this.contentFullscreen && this.backgroundContentEl) {
|
|
837
863
|
(_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
|
|
838
864
|
}
|
|
865
|
+
this.ionPullEnd.emit({
|
|
866
|
+
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
867
|
+
});
|
|
839
868
|
}, 600);
|
|
840
869
|
// reset the styles on the scroll element
|
|
841
870
|
// set that the refresh is actively cancelling/completing
|
|
@@ -889,7 +918,7 @@ const Refresher = class {
|
|
|
889
918
|
}
|
|
890
919
|
render() {
|
|
891
920
|
const mode = ionicGlobal.getIonMode(this);
|
|
892
|
-
return (index.h(index.Host, { key: '
|
|
921
|
+
return (index.h(index.Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
|
|
893
922
|
[mode]: true,
|
|
894
923
|
// Used internally for styling
|
|
895
924
|
[`refresher-${mode}`]: true,
|
|
@@ -7,7 +7,7 @@ import { clamp, getElementRoot, raf } from "../../../utils/helpers";
|
|
|
7
7
|
import { FOCUS_TRAP_DISABLE_CLASS } from "../../../utils/overlays";
|
|
8
8
|
import { getBackdropValueForSheet } from "../utils";
|
|
9
9
|
import { calculateSpringStep, handleCanDismiss } from "./utils";
|
|
10
|
-
export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange
|
|
10
|
+
export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
|
|
11
11
|
// Defaults for the sheet swipe animation
|
|
12
12
|
const defaultBackdrop = [
|
|
13
13
|
{ offset: 0, opacity: 'var(--backdrop-opacity)' },
|
|
@@ -338,8 +338,6 @@ export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpo
|
|
|
338
338
|
: step;
|
|
339
339
|
offset = clamp(0.0001, processedStep, maxStep);
|
|
340
340
|
animation.progressStep(offset);
|
|
341
|
-
// Notify modal of position change for safe-area updates
|
|
342
|
-
onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
|
|
343
341
|
};
|
|
344
342
|
const onEnd = (detail) => {
|
|
345
343
|
/**
|
|
@@ -12,7 +12,7 @@ import { calculateSpringStep, handleCanDismiss } from "./utils";
|
|
|
12
12
|
export const SwipeToCloseDefaults = {
|
|
13
13
|
MIN_PRESENTING_SCALE: 0.915,
|
|
14
14
|
};
|
|
15
|
-
export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss
|
|
15
|
+
export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
|
|
16
16
|
/**
|
|
17
17
|
* The step value at which a card modal
|
|
18
18
|
* is eligible for dismissing via gesture.
|
|
@@ -169,8 +169,6 @@ export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismi
|
|
|
169
169
|
const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
|
|
170
170
|
const clampedStep = clamp(0.0001, processedStep, maxStep);
|
|
171
171
|
animation.progressStep(clampedStep);
|
|
172
|
-
// Notify modal of position change for safe-area updates
|
|
173
|
-
onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
|
|
174
172
|
/**
|
|
175
173
|
* When swiping down half way, the status bar style
|
|
176
174
|
* should be reset to its default value.
|
|
@@ -135,6 +135,10 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
+
--ion-safe-area-top: 0px;
|
|
139
|
+
--ion-safe-area-bottom: 0px;
|
|
140
|
+
--ion-safe-area-right: 0px;
|
|
141
|
+
--ion-safe-area-left: 0px;
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Host, h, writeTask } from "@stencil/core";
|
|
5
|
-
import { win } from "../../utils/browser/index";
|
|
6
5
|
import { findIonContent, printIonContentErrorMsg } from "../../utils/content/index";
|
|
7
6
|
import { CoreDelegate, attachComponent, detachComponent } from "../../utils/framework-delegate";
|
|
8
7
|
import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from "../../utils/helpers";
|
|
@@ -43,10 +42,6 @@ export class Modal {
|
|
|
43
42
|
this.inline = false;
|
|
44
43
|
// Whether or not modal is being dismissed via gesture
|
|
45
44
|
this.gestureAnimationDismissing = false;
|
|
46
|
-
// Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
|
|
47
|
-
this.skipSafeAreaCoordinateDetection = false;
|
|
48
|
-
// Track previous safe-area state to avoid redundant DOM writes
|
|
49
|
-
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
50
45
|
this.presented = false;
|
|
51
46
|
/** @internal */
|
|
52
47
|
this.hasController = false;
|
|
@@ -237,10 +232,7 @@ export class Modal {
|
|
|
237
232
|
}
|
|
238
233
|
}
|
|
239
234
|
onWindowResize() {
|
|
240
|
-
//
|
|
241
|
-
this.cachedSafeAreas = undefined;
|
|
242
|
-
this.updateSafeAreaOverrides();
|
|
243
|
-
// Only handle view transition for iOS card modals when no custom animations are provided
|
|
235
|
+
// Only handle resize for iOS card modals when no custom animations are provided
|
|
244
236
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
245
237
|
return;
|
|
246
238
|
}
|
|
@@ -263,8 +255,6 @@ export class Modal {
|
|
|
263
255
|
this.triggerController.removeClickListener();
|
|
264
256
|
this.cleanupViewTransitionListener();
|
|
265
257
|
this.cleanupParentRemovalObserver();
|
|
266
|
-
// Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
|
|
267
|
-
this.resetSafeAreaState();
|
|
268
258
|
}
|
|
269
259
|
componentWillLoad() {
|
|
270
260
|
var _a;
|
|
@@ -424,8 +414,6 @@ export class Modal {
|
|
|
424
414
|
else if (!this.keepContentsMounted) {
|
|
425
415
|
await waitForMount();
|
|
426
416
|
}
|
|
427
|
-
// Predict safe-area needs based on modal configuration to avoid visual snap
|
|
428
|
-
this.setInitialSafeAreaOverrides(presentingElement);
|
|
429
417
|
writeTask(() => this.el.classList.add('show-modal'));
|
|
430
418
|
const hasCardModal = presentingElement !== undefined;
|
|
431
419
|
/**
|
|
@@ -487,8 +475,6 @@ export class Modal {
|
|
|
487
475
|
else if (hasCardModal) {
|
|
488
476
|
this.initSwipeToClose();
|
|
489
477
|
}
|
|
490
|
-
// Now that animation is complete, update safe-area based on actual position
|
|
491
|
-
this.updateSafeAreaOverrides();
|
|
492
478
|
// Initialize view transition listener for iOS card modals
|
|
493
479
|
this.initViewTransitionListener();
|
|
494
480
|
// Initialize parent removal observer
|
|
@@ -540,7 +526,7 @@ export class Modal {
|
|
|
540
526
|
await this.dismiss(undefined, GESTURE);
|
|
541
527
|
this.gestureAnimationDismissing = false;
|
|
542
528
|
});
|
|
543
|
-
}
|
|
529
|
+
});
|
|
544
530
|
this.gesture.enable(true);
|
|
545
531
|
}
|
|
546
532
|
initSheetGesture() {
|
|
@@ -561,8 +547,7 @@ export class Modal {
|
|
|
561
547
|
this.currentBreakpoint = breakpoint;
|
|
562
548
|
this.ionBreakpointDidChange.emit({ breakpoint });
|
|
563
549
|
}
|
|
564
|
-
|
|
565
|
-
}, () => this.updateSafeAreaOverrides());
|
|
550
|
+
});
|
|
566
551
|
this.gesture = gesture;
|
|
567
552
|
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
|
|
568
553
|
this.gesture.enable(true);
|
|
@@ -640,197 +625,6 @@ export class Modal {
|
|
|
640
625
|
// Clear the cached reference
|
|
641
626
|
this.cachedPageParent = undefined;
|
|
642
627
|
}
|
|
643
|
-
/**
|
|
644
|
-
* Sets initial safe-area overrides based on modal configuration before
|
|
645
|
-
* the modal becomes visible. This predicts whether the modal will touch
|
|
646
|
-
* screen edges to avoid a visual snap after animation completes.
|
|
647
|
-
*/
|
|
648
|
-
setInitialSafeAreaOverrides(presentingElement) {
|
|
649
|
-
const style = this.el.style;
|
|
650
|
-
const mode = getIonMode(this);
|
|
651
|
-
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
652
|
-
// Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
|
|
653
|
-
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
654
|
-
const isTablet = window.innerWidth >= 768;
|
|
655
|
-
// Sheet modals always touch bottom edge, never top/left/right
|
|
656
|
-
if (isSheetModal) {
|
|
657
|
-
style.setProperty('--ion-safe-area-top', '0px');
|
|
658
|
-
style.setProperty('--ion-safe-area-left', '0px');
|
|
659
|
-
style.setProperty('--ion-safe-area-right', '0px');
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
// Card modals have rounded top corners
|
|
663
|
-
if (isCardModal) {
|
|
664
|
-
style.setProperty('--ion-safe-area-top', '0px');
|
|
665
|
-
if (isTablet) {
|
|
666
|
-
// On tablets, card modals are inset from all edges
|
|
667
|
-
this.zeroAllSafeAreas();
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
// On phones, card modals still extend to the bottom edge
|
|
671
|
-
style.setProperty('--ion-safe-area-left', '0px');
|
|
672
|
-
style.setProperty('--ion-safe-area-right', '0px');
|
|
673
|
-
this.applyFullscreenSafeArea();
|
|
674
|
-
}
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
// Check if modal is fullscreen via CSS custom properties
|
|
678
|
-
// This applies to both phone and tablet sizes - custom modals may have
|
|
679
|
-
// non-fullscreen dimensions even on phones (e.g., --height: 70%)
|
|
680
|
-
const computedStyle = getComputedStyle(this.el);
|
|
681
|
-
const width = computedStyle.getPropertyValue('--width').trim();
|
|
682
|
-
const height = computedStyle.getPropertyValue('--height').trim();
|
|
683
|
-
const isFullscreen = width === '100%' && height === '100%';
|
|
684
|
-
if (isFullscreen) {
|
|
685
|
-
this.applyFullscreenSafeArea();
|
|
686
|
-
}
|
|
687
|
-
else if (isTablet) {
|
|
688
|
-
// Centered dialog on tablet doesn't touch edges
|
|
689
|
-
this.zeroAllSafeAreas();
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
692
|
-
// Non-fullscreen modal on phone - use coordinate-based detection
|
|
693
|
-
// to determine which edges it touches (e.g., bottom-aligned custom modals)
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Applies safe-area handling for fullscreen modals.
|
|
698
|
-
* Adds wrapper padding when no footer is present to prevent
|
|
699
|
-
* content from overlapping system navigation areas.
|
|
700
|
-
*/
|
|
701
|
-
applyFullscreenSafeArea() {
|
|
702
|
-
this.skipSafeAreaCoordinateDetection = true;
|
|
703
|
-
this.updateFooterPadding();
|
|
704
|
-
// Watch for dynamic footer additions/removals (e.g., async data loading)
|
|
705
|
-
// Use subtree:true to support wrapped footers in framework components
|
|
706
|
-
// (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
|
|
707
|
-
if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
|
|
708
|
-
this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
|
|
709
|
-
this.footerObserver.observe(this.el, { childList: true, subtree: true });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Updates wrapper and shadow padding based on footer presence.
|
|
714
|
-
* Called initially and when footer is dynamically added/removed.
|
|
715
|
-
* Both elements must be styled identically to prevent visual mismatches.
|
|
716
|
-
*/
|
|
717
|
-
updateFooterPadding() {
|
|
718
|
-
if (!this.wrapperEl)
|
|
719
|
-
return;
|
|
720
|
-
const hasFooter = this.el.querySelector('ion-footer') !== null;
|
|
721
|
-
// Apply to both wrapper and shadow to keep them in sync
|
|
722
|
-
const elements = [this.wrapperEl, this.shadowEl].filter(Boolean);
|
|
723
|
-
if (hasFooter) {
|
|
724
|
-
elements.forEach((el) => {
|
|
725
|
-
el.style.removeProperty('padding-bottom');
|
|
726
|
-
el.style.removeProperty('box-sizing');
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
elements.forEach((el) => {
|
|
731
|
-
el.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
732
|
-
el.style.setProperty('box-sizing', 'border-box');
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Sets all safe-area CSS variables to 0px for modals that
|
|
738
|
-
* don't touch screen edges.
|
|
739
|
-
*/
|
|
740
|
-
zeroAllSafeAreas() {
|
|
741
|
-
const style = this.el.style;
|
|
742
|
-
style.setProperty('--ion-safe-area-top', '0px');
|
|
743
|
-
style.setProperty('--ion-safe-area-bottom', '0px');
|
|
744
|
-
style.setProperty('--ion-safe-area-left', '0px');
|
|
745
|
-
style.setProperty('--ion-safe-area-right', '0px');
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Resets all safe-area related state and styles.
|
|
749
|
-
* Called during dismiss and disconnectedCallback to ensure clean state
|
|
750
|
-
* for re-presentation of inline modals.
|
|
751
|
-
*/
|
|
752
|
-
resetSafeAreaState() {
|
|
753
|
-
var _a;
|
|
754
|
-
this.skipSafeAreaCoordinateDetection = false;
|
|
755
|
-
this.cachedSafeAreas = undefined;
|
|
756
|
-
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
757
|
-
(_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
758
|
-
this.footerObserver = undefined;
|
|
759
|
-
// Clear wrapper and shadow styles that may have been set for safe-area handling
|
|
760
|
-
[this.wrapperEl, this.shadowEl].forEach((el) => {
|
|
761
|
-
if (el) {
|
|
762
|
-
el.style.removeProperty('padding-bottom');
|
|
763
|
-
el.style.removeProperty('box-sizing');
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
// Clear safe-area CSS variable overrides
|
|
767
|
-
const style = this.el.style;
|
|
768
|
-
style.removeProperty('--ion-safe-area-top');
|
|
769
|
-
style.removeProperty('--ion-safe-area-bottom');
|
|
770
|
-
style.removeProperty('--ion-safe-area-left');
|
|
771
|
-
style.removeProperty('--ion-safe-area-right');
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Gets the root safe-area values from the document element.
|
|
775
|
-
* Uses cached values during gestures to avoid getComputedStyle calls.
|
|
776
|
-
*/
|
|
777
|
-
getSafeAreaValues() {
|
|
778
|
-
if (!this.cachedSafeAreas) {
|
|
779
|
-
const rootStyle = getComputedStyle(document.documentElement);
|
|
780
|
-
this.cachedSafeAreas = {
|
|
781
|
-
top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
|
|
782
|
-
bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
|
|
783
|
-
left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
|
|
784
|
-
right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
return this.cachedSafeAreas;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Updates safe-area CSS variable overrides based on whether the modal
|
|
791
|
-
* extends into each safe-area region. Called after animation
|
|
792
|
-
* and during gestures to handle dynamic position changes.
|
|
793
|
-
*
|
|
794
|
-
* Optimized to avoid redundant DOM writes by tracking previous state.
|
|
795
|
-
*/
|
|
796
|
-
updateSafeAreaOverrides() {
|
|
797
|
-
if (this.skipSafeAreaCoordinateDetection) {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const wrapper = this.wrapperEl;
|
|
801
|
-
if (!wrapper) {
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
const rect = wrapper.getBoundingClientRect();
|
|
805
|
-
const safeAreas = this.getSafeAreaValues();
|
|
806
|
-
const extendsIntoTop = rect.top < safeAreas.top;
|
|
807
|
-
const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
|
|
808
|
-
const extendsIntoLeft = rect.left < safeAreas.left;
|
|
809
|
-
const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
|
|
810
|
-
// Only update DOM when state actually changes
|
|
811
|
-
const prev = this.prevSafeAreaState;
|
|
812
|
-
const style = this.el.style;
|
|
813
|
-
if (extendsIntoTop !== prev.top) {
|
|
814
|
-
extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
|
|
815
|
-
prev.top = extendsIntoTop;
|
|
816
|
-
}
|
|
817
|
-
if (extendsIntoBottom !== prev.bottom) {
|
|
818
|
-
extendsIntoBottom
|
|
819
|
-
? style.removeProperty('--ion-safe-area-bottom')
|
|
820
|
-
: style.setProperty('--ion-safe-area-bottom', '0px');
|
|
821
|
-
prev.bottom = extendsIntoBottom;
|
|
822
|
-
}
|
|
823
|
-
if (extendsIntoLeft !== prev.left) {
|
|
824
|
-
extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
|
|
825
|
-
prev.left = extendsIntoLeft;
|
|
826
|
-
}
|
|
827
|
-
if (extendsIntoRight !== prev.right) {
|
|
828
|
-
extendsIntoRight
|
|
829
|
-
? style.removeProperty('--ion-safe-area-right')
|
|
830
|
-
: style.setProperty('--ion-safe-area-right', '0px');
|
|
831
|
-
prev.right = extendsIntoRight;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
628
|
sheetOnDismiss() {
|
|
835
629
|
/**
|
|
836
630
|
* While the gesture animation is finishing
|
|
@@ -923,8 +717,6 @@ export class Modal {
|
|
|
923
717
|
}
|
|
924
718
|
this.currentBreakpoint = undefined;
|
|
925
719
|
this.animation = undefined;
|
|
926
|
-
// Reset safe-area state for potential re-presentation
|
|
927
|
-
this.resetSafeAreaState();
|
|
928
720
|
unlock();
|
|
929
721
|
return dismissed;
|
|
930
722
|
}
|
|
@@ -1182,20 +974,20 @@ export class Modal {
|
|
|
1182
974
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
1183
975
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
1184
976
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
1185
|
-
return (h(Host, Object.assign({ key: '
|
|
977
|
+
return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
|
|
1186
978
|
// Allow the modal to be navigable when the handle is focusable
|
|
1187
979
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
1188
980
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
1189
|
-
}, 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: '
|
|
981
|
+
}, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
|
|
1190
982
|
/*
|
|
1191
983
|
role and aria-modal must be used on the
|
|
1192
984
|
same element. They must also be set inside the
|
|
1193
985
|
shadow DOM otherwise ion-button will not be highlighted
|
|
1194
986
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
1195
987
|
*/
|
|
1196
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
988
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
|
|
1197
989
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
1198
|
-
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: '
|
|
990
|
+
tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
|
|
1199
991
|
}
|
|
1200
992
|
static get is() { return "ion-modal"; }
|
|
1201
993
|
static get encapsulation() { return "shadow"; }
|
|
@@ -135,6 +135,10 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
+
--ion-safe-area-top: 0px;
|
|
139
|
+
--ion-safe-area-bottom: 0px;
|
|
140
|
+
--ion-safe-area-right: 0px;
|
|
141
|
+
--ion-safe-area-left: 0px;
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|