@ionic/core 8.7.18-dev.11770318664.15f47a5e → 8.7.18-dev.11770674094.18396f54
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 +248 -13
- package/components/popover.js +152 -30
- package/dist/cjs/ion-modal.cjs.entry.js +248 -13
- 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 +0 -4
- package/dist/collection/components/modal/modal.js +127 -11
- package/dist/collection/components/modal/modal.md.css +0 -4
- package/dist/collection/components/modal/safe-area-utils.js +123 -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 +248 -13
- 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-36d78f36.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 +23 -0
- package/dist/types/components/modal/safe-area-utils.d.ts +55 -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 +403 -75
- package/hydrate/index.mjs +403 -75
- 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
|
@@ -16,6 +16,58 @@ require('./index-DkNv4J_i.js');
|
|
|
16
16
|
require('./hardware-back-button-VCK4V3mG.js');
|
|
17
17
|
require('./gesture-controller-dtqlP_q4.js');
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Shared per-frame cache for safe-area insets. Avoids creating a temporary
|
|
21
|
+
* DOM element and forcing a synchronous reflow on every call within the same
|
|
22
|
+
* frame (e.g., multiple popovers presenting simultaneously). Invalidated
|
|
23
|
+
* after the next animation frame so values stay fresh across orientation
|
|
24
|
+
* changes and viewport resizes.
|
|
25
|
+
*/
|
|
26
|
+
let cachedInsets = null;
|
|
27
|
+
let cacheInvalidationScheduled = false;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the current --ion-safe-area-* CSS custom property values
|
|
30
|
+
* to actual pixel numbers. A temporary element is needed because
|
|
31
|
+
* getComputedStyle on :root returns the declared value of custom
|
|
32
|
+
* properties (e.g. "env(safe-area-inset-top)") rather than a
|
|
33
|
+
* resolved number. By assigning them to standard padding properties,
|
|
34
|
+
* the browser resolves the value.
|
|
35
|
+
*
|
|
36
|
+
* Results are cached for the current frame to avoid repeated reflows.
|
|
37
|
+
*/
|
|
38
|
+
const getSafeAreaInsets = (doc) => {
|
|
39
|
+
if (cachedInsets !== null) {
|
|
40
|
+
return cachedInsets;
|
|
41
|
+
}
|
|
42
|
+
if (doc.body === null) {
|
|
43
|
+
return { top: 0, bottom: 0, left: 0, right: 0 };
|
|
44
|
+
}
|
|
45
|
+
const el = doc.createElement('div');
|
|
46
|
+
el.style.cssText =
|
|
47
|
+
'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
|
|
48
|
+
'padding-top:var(--ion-safe-area-top,0px);' +
|
|
49
|
+
'padding-bottom:var(--ion-safe-area-bottom,0px);' +
|
|
50
|
+
'padding-left:var(--ion-safe-area-left,0px);' +
|
|
51
|
+
'padding-right:var(--ion-safe-area-right,0px);';
|
|
52
|
+
doc.body.appendChild(el);
|
|
53
|
+
const style = getComputedStyle(el);
|
|
54
|
+
const insets = {
|
|
55
|
+
top: parseFloat(style.paddingTop) || 0,
|
|
56
|
+
bottom: parseFloat(style.paddingBottom) || 0,
|
|
57
|
+
left: parseFloat(style.paddingLeft) || 0,
|
|
58
|
+
right: parseFloat(style.paddingRight) || 0,
|
|
59
|
+
};
|
|
60
|
+
el.remove();
|
|
61
|
+
cachedInsets = insets;
|
|
62
|
+
if (!cacheInvalidationScheduled) {
|
|
63
|
+
cacheInvalidationScheduled = true;
|
|
64
|
+
helpers.raf(() => {
|
|
65
|
+
cachedInsets = null;
|
|
66
|
+
cacheInvalidationScheduled = false;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return insets;
|
|
70
|
+
};
|
|
19
71
|
/**
|
|
20
72
|
* Returns the dimensions of the popover
|
|
21
73
|
* arrow on `ios` mode. If arrow is disabled
|
|
@@ -651,8 +703,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
|
|
|
651
703
|
* Adjusts popover positioning coordinates
|
|
652
704
|
* such that popover does not appear offscreen
|
|
653
705
|
* or overlapping safe area bounds.
|
|
706
|
+
*
|
|
707
|
+
* @internal - This is not part of the public API.
|
|
654
708
|
*/
|
|
655
|
-
const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight,
|
|
709
|
+
const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
|
|
656
710
|
let arrowTop = coordArrowTop;
|
|
657
711
|
const arrowLeft = coordArrowLeft;
|
|
658
712
|
let left = coordLeft;
|
|
@@ -662,16 +716,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
662
716
|
let originY = contentOriginY;
|
|
663
717
|
let checkSafeAreaLeft = false;
|
|
664
718
|
let checkSafeAreaRight = false;
|
|
719
|
+
let checkSafeAreaTop = false;
|
|
720
|
+
let checkSafeAreaBottom = false;
|
|
665
721
|
const triggerTop = triggerCoordinates
|
|
666
722
|
? triggerCoordinates.top + triggerCoordinates.height
|
|
667
723
|
: bodyHeight / 2 - contentHeight / 2;
|
|
668
724
|
const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
|
|
669
725
|
let addPopoverBottomClass = false;
|
|
726
|
+
const hideArrow = false;
|
|
670
727
|
/**
|
|
671
728
|
* Adjust popover so it does not
|
|
672
729
|
* go off the left of the screen.
|
|
673
730
|
*/
|
|
674
|
-
if (left < bodyPadding +
|
|
731
|
+
if (left < bodyPadding + safeArea.left) {
|
|
675
732
|
left = bodyPadding;
|
|
676
733
|
checkSafeAreaLeft = true;
|
|
677
734
|
originX = 'left';
|
|
@@ -680,41 +737,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
680
737
|
* go off the right of the screen.
|
|
681
738
|
*/
|
|
682
739
|
}
|
|
683
|
-
else if (contentWidth + bodyPadding + left +
|
|
740
|
+
else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
|
|
684
741
|
checkSafeAreaRight = true;
|
|
685
742
|
left = bodyWidth - contentWidth - bodyPadding;
|
|
686
743
|
originX = 'right';
|
|
687
744
|
}
|
|
688
745
|
/**
|
|
689
746
|
* Adjust popover so it does not
|
|
690
|
-
* go off the
|
|
747
|
+
* go off the bottom of the screen
|
|
748
|
+
* or overlap the bottom safe area.
|
|
691
749
|
* If popover is on the left or the right of
|
|
692
750
|
* the trigger, then we should not adjust top
|
|
693
751
|
* margins.
|
|
694
752
|
*/
|
|
695
|
-
if (triggerTop + triggerHeight + contentHeight > bodyHeight
|
|
696
|
-
|
|
753
|
+
if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
|
|
754
|
+
(side === 'top' || side === 'bottom')) {
|
|
755
|
+
/**
|
|
756
|
+
* Calculate where the popover top would be if flipped
|
|
757
|
+
* above the trigger. Check whether that position clears
|
|
758
|
+
* the top safe area with room for bodyPadding.
|
|
759
|
+
*/
|
|
760
|
+
const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
|
|
761
|
+
if (idealFlipTop >= safeArea.top + bodyPadding) {
|
|
697
762
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* off the bottom of the screen. However, we do not want to move
|
|
702
|
-
* the popover up so much that it goes off the top of the screen.
|
|
703
|
-
*
|
|
704
|
-
* We chose 12 here so that the popover position looks a bit nicer as
|
|
705
|
-
* it is not right up against the edge of the screen.
|
|
763
|
+
* Popover fits above the trigger without overlapping
|
|
764
|
+
* the top safe area. Use the ideal position directly —
|
|
765
|
+
* no safe-area CSS vars needed since it clears both edges.
|
|
706
766
|
*/
|
|
707
|
-
top =
|
|
767
|
+
top = idealFlipTop;
|
|
708
768
|
arrowTop = top + contentHeight;
|
|
709
769
|
originY = 'bottom';
|
|
710
770
|
addPopoverBottomClass = true;
|
|
711
|
-
/**
|
|
712
|
-
* If not enough room for popover to appear
|
|
713
|
-
* above trigger, then cut it off.
|
|
714
|
-
*/
|
|
715
771
|
}
|
|
716
772
|
else {
|
|
773
|
+
/**
|
|
774
|
+
* Can't flip above the trigger. Constrain the bottom
|
|
775
|
+
* edge while keeping the top near the trigger. This
|
|
776
|
+
* creates a scrollable area anchored to the trigger.
|
|
777
|
+
*/
|
|
717
778
|
bottom = bodyPadding;
|
|
779
|
+
checkSafeAreaBottom = true;
|
|
780
|
+
/**
|
|
781
|
+
* When the trigger is near the bottom of the screen,
|
|
782
|
+
* the calculated top may be at or past the bottom
|
|
783
|
+
* constraint, leaving zero visible height. In that
|
|
784
|
+
* case, pull top up to the top safe area so the
|
|
785
|
+
* popover fills the available space between safe areas.
|
|
786
|
+
*/
|
|
787
|
+
const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
|
|
788
|
+
if (top >= bottomEdge) {
|
|
789
|
+
top = safeArea.top + bodyPadding;
|
|
790
|
+
checkSafeAreaTop = true;
|
|
791
|
+
}
|
|
718
792
|
}
|
|
719
793
|
}
|
|
720
794
|
return {
|
|
@@ -725,9 +799,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
|
|
|
725
799
|
originY,
|
|
726
800
|
checkSafeAreaLeft,
|
|
727
801
|
checkSafeAreaRight,
|
|
802
|
+
checkSafeAreaTop,
|
|
803
|
+
checkSafeAreaBottom,
|
|
728
804
|
arrowTop,
|
|
729
805
|
arrowLeft,
|
|
730
806
|
addPopoverBottomClass,
|
|
807
|
+
hideArrow,
|
|
731
808
|
};
|
|
732
809
|
};
|
|
733
810
|
const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
|
|
@@ -757,6 +834,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
|
|
|
757
834
|
};
|
|
758
835
|
|
|
759
836
|
const POPOVER_IOS_BODY_PADDING = 5;
|
|
837
|
+
/**
|
|
838
|
+
* Minimum edge margin for iOS popovers ensures visual spacing from screen
|
|
839
|
+
* edges on devices without safe areas (e.g., older iPhones without notches).
|
|
840
|
+
* Previously this was a hardcoded `safeAreaMargin = 25` that served dual
|
|
841
|
+
* purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
|
|
842
|
+
* insets are read dynamically, this floor preserves the visual spacing when
|
|
843
|
+
* safe-area values are 0.
|
|
844
|
+
*/
|
|
845
|
+
const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
|
|
760
846
|
/**
|
|
761
847
|
* iOS Popover Enter Animation
|
|
762
848
|
*/
|
|
@@ -782,8 +868,16 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
782
868
|
};
|
|
783
869
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
784
870
|
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
785
|
-
const
|
|
786
|
-
const
|
|
871
|
+
const rawSafeArea = getSafeAreaInsets(doc);
|
|
872
|
+
const safeArea = size === 'cover'
|
|
873
|
+
? { top: 0, bottom: 0, left: 0, right: 0 }
|
|
874
|
+
: {
|
|
875
|
+
top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
876
|
+
bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
877
|
+
left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
878
|
+
right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
|
|
879
|
+
};
|
|
880
|
+
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);
|
|
787
881
|
const baseAnimation = animation.createAnimation();
|
|
788
882
|
const backdropAnimation = animation.createAnimation();
|
|
789
883
|
const contentAnimation = animation.createAnimation();
|
|
@@ -814,10 +908,14 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
814
908
|
baseEl.classList.add('popover-bottom');
|
|
815
909
|
}
|
|
816
910
|
if (bottom !== undefined) {
|
|
817
|
-
|
|
911
|
+
let bottomValue = `${bottom}px`;
|
|
912
|
+
if (checkSafeAreaBottom) {
|
|
913
|
+
bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
|
|
914
|
+
}
|
|
915
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
818
916
|
}
|
|
819
|
-
const safeAreaLeft = ' + var(--ion-safe-area-left,
|
|
820
|
-
const safeAreaRight = ' - var(--ion-safe-area-right,
|
|
917
|
+
const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
|
|
918
|
+
const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
|
|
821
919
|
let leftValue = `${left}px`;
|
|
822
920
|
if (checkSafeAreaLeft) {
|
|
823
921
|
leftValue = `${left}px${safeAreaLeft}`;
|
|
@@ -825,7 +923,11 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
825
923
|
if (checkSafeAreaRight) {
|
|
826
924
|
leftValue = `${left}px${safeAreaRight}`;
|
|
827
925
|
}
|
|
828
|
-
|
|
926
|
+
let topValue = `${top}px`;
|
|
927
|
+
if (checkSafeAreaTop) {
|
|
928
|
+
topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
|
|
929
|
+
}
|
|
930
|
+
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
|
829
931
|
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
830
932
|
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
831
933
|
if (arrowEl !== null) {
|
|
@@ -901,7 +1003,23 @@ const mdEnterAnimation = (baseEl, opts) => {
|
|
|
901
1003
|
};
|
|
902
1004
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
|
|
903
1005
|
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
|
904
|
-
|
|
1006
|
+
// MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
|
|
1007
|
+
// This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
|
|
1008
|
+
const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
|
|
1009
|
+
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);
|
|
1010
|
+
const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
|
|
1011
|
+
const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
|
|
1012
|
+
let leftValue = `${left}px`;
|
|
1013
|
+
if (checkSafeAreaLeft) {
|
|
1014
|
+
leftValue = `${left}px${safeAreaLeftCalc}`;
|
|
1015
|
+
}
|
|
1016
|
+
if (checkSafeAreaRight) {
|
|
1017
|
+
leftValue = `${left}px${safeAreaRightCalc}`;
|
|
1018
|
+
}
|
|
1019
|
+
let topValue = `${top}px`;
|
|
1020
|
+
if (checkSafeAreaTop) {
|
|
1021
|
+
topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
|
|
1022
|
+
}
|
|
905
1023
|
const baseAnimation = animation.createAnimation();
|
|
906
1024
|
const backdropAnimation = animation.createAnimation();
|
|
907
1025
|
const wrapperAnimation = animation.createAnimation();
|
|
@@ -918,13 +1036,17 @@ const mdEnterAnimation = (baseEl, opts) => {
|
|
|
918
1036
|
contentAnimation
|
|
919
1037
|
.addElement(contentEl)
|
|
920
1038
|
.beforeStyles({
|
|
921
|
-
top: `calc(${
|
|
922
|
-
left: `calc(${
|
|
1039
|
+
top: `calc(${topValue} + var(--offset-y, 0px))`,
|
|
1040
|
+
left: `calc(${leftValue} + var(--offset-x, 0px))`,
|
|
923
1041
|
'transform-origin': `${originY} ${originX}`,
|
|
924
1042
|
})
|
|
925
1043
|
.beforeAddWrite(() => {
|
|
926
1044
|
if (bottom !== undefined) {
|
|
927
|
-
|
|
1045
|
+
let bottomValue = `${bottom}px`;
|
|
1046
|
+
if (checkSafeAreaBottom) {
|
|
1047
|
+
bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
|
|
1048
|
+
}
|
|
1049
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
928
1050
|
}
|
|
929
1051
|
})
|
|
930
1052
|
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|
|
@@ -936,7 +1058,7 @@ const mdEnterAnimation = (baseEl, opts) => {
|
|
|
936
1058
|
if (size === 'cover') {
|
|
937
1059
|
baseEl.style.setProperty('--width', `${contentWidth}px`);
|
|
938
1060
|
}
|
|
939
|
-
if (
|
|
1061
|
+
if (addPopoverBottomClass) {
|
|
940
1062
|
baseEl.classList.add('popover-bottom');
|
|
941
1063
|
}
|
|
942
1064
|
})
|
|
@@ -214,8 +214,6 @@ 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);
|
|
219
217
|
this.appliedStyles = false;
|
|
220
218
|
this.didStart = false;
|
|
221
219
|
this.progress = 0;
|
|
@@ -270,8 +268,8 @@ const Refresher = class {
|
|
|
270
268
|
* than `1`. The default value is `1` which is equal to the speed of the cursor.
|
|
271
269
|
* If a negative value is passed in, the factor will be `1` instead.
|
|
272
270
|
*
|
|
273
|
-
* For example
|
|
274
|
-
* `10` pixels, instead of `10` pixels
|
|
271
|
+
* For example: If the value passed is `1.2` and the content is dragged by
|
|
272
|
+
* `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
|
|
275
273
|
* (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
|
|
276
274
|
* will be `8` pixels, less than the amount the cursor has moved.
|
|
277
275
|
*
|
|
@@ -321,9 +319,6 @@ const Refresher = class {
|
|
|
321
319
|
this.animations = [];
|
|
322
320
|
this.progress = 0;
|
|
323
321
|
this.state = 1 /* RefresherState.Inactive */;
|
|
324
|
-
this.ionPullEnd.emit({
|
|
325
|
-
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
326
|
-
});
|
|
327
322
|
}
|
|
328
323
|
async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
|
|
329
324
|
this.elementToTransform = this.scrollEl;
|
|
@@ -356,7 +351,6 @@ const Refresher = class {
|
|
|
356
351
|
if (!this.didStart) {
|
|
357
352
|
this.didStart = true;
|
|
358
353
|
this.ionStart.emit();
|
|
359
|
-
this.ionPullStart.emit();
|
|
360
354
|
}
|
|
361
355
|
// emit "pulling" on every move
|
|
362
356
|
if (this.pointerDown) {
|
|
@@ -432,7 +426,6 @@ const Refresher = class {
|
|
|
432
426
|
this.lastVelocityY = ev.velocityY;
|
|
433
427
|
},
|
|
434
428
|
onEnd: () => {
|
|
435
|
-
const hadStarted = this.didStart;
|
|
436
429
|
this.pointerDown = false;
|
|
437
430
|
this.didStart = false;
|
|
438
431
|
if (this.needsCompletion) {
|
|
@@ -442,13 +435,6 @@ const Refresher = class {
|
|
|
442
435
|
else if (this.didRefresh) {
|
|
443
436
|
index.readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
|
|
444
437
|
}
|
|
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
|
-
}
|
|
452
438
|
},
|
|
453
439
|
});
|
|
454
440
|
this.disabledChanged();
|
|
@@ -495,7 +481,6 @@ const Refresher = class {
|
|
|
495
481
|
ev.data.animation = animation;
|
|
496
482
|
animation.progressStart(false, 0);
|
|
497
483
|
this.ionStart.emit();
|
|
498
|
-
this.ionPullStart.emit();
|
|
499
484
|
this.animations.push(animation);
|
|
500
485
|
return;
|
|
501
486
|
}
|
|
@@ -518,7 +503,6 @@ const Refresher = class {
|
|
|
518
503
|
this.animations = [];
|
|
519
504
|
this.gesture.enable(true);
|
|
520
505
|
this.state = 1 /* RefresherState.Inactive */;
|
|
521
|
-
this.ionPullEnd.emit({ reason: 'cancel' });
|
|
522
506
|
});
|
|
523
507
|
return;
|
|
524
508
|
}
|
|
@@ -763,7 +747,6 @@ const Refresher = class {
|
|
|
763
747
|
if (!this.didStart) {
|
|
764
748
|
this.didStart = true;
|
|
765
749
|
this.ionStart.emit();
|
|
766
|
-
this.ionPullStart.emit();
|
|
767
750
|
}
|
|
768
751
|
// emit "pulling" on every move
|
|
769
752
|
this.ionPull.emit();
|
|
@@ -807,15 +790,6 @@ const Refresher = class {
|
|
|
807
790
|
* available right away.
|
|
808
791
|
*/
|
|
809
792
|
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
|
-
}
|
|
819
793
|
}
|
|
820
794
|
}
|
|
821
795
|
beginRefresh() {
|
|
@@ -862,9 +836,6 @@ const Refresher = class {
|
|
|
862
836
|
if (this.contentFullscreen && this.backgroundContentEl) {
|
|
863
837
|
(_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
|
|
864
838
|
}
|
|
865
|
-
this.ionPullEnd.emit({
|
|
866
|
-
reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
|
|
867
|
-
});
|
|
868
839
|
}, 600);
|
|
869
840
|
// reset the styles on the scroll element
|
|
870
841
|
// set that the refresh is actively cancelling/completing
|
|
@@ -918,7 +889,7 @@ const Refresher = class {
|
|
|
918
889
|
}
|
|
919
890
|
render() {
|
|
920
891
|
const mode = ionicGlobal.getIonMode(this);
|
|
921
|
-
return (index.h(index.Host, { key: '
|
|
892
|
+
return (index.h(index.Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
|
|
922
893
|
[mode]: true,
|
|
923
894
|
// Used internally for styling
|
|
924
895
|
[`refresher-${mode}`]: true,
|
|
@@ -135,10 +135,6 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
-
--ion-safe-area-top: 0px;
|
|
139
|
-
--ion-safe-area-bottom: 0px;
|
|
140
|
-
--ion-safe-area-right: 0px;
|
|
141
|
-
--ion-safe-area-left: 0px;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|
|
@@ -21,6 +21,7 @@ import { mdEnterAnimation } from "./animations/md.enter";
|
|
|
21
21
|
import { mdLeaveAnimation } from "./animations/md.leave";
|
|
22
22
|
import { createSheetGesture } from "./gestures/sheet";
|
|
23
23
|
import { createSwipeToCloseGesture, SwipeToCloseDefaults } from "./gestures/swipe-to-close";
|
|
24
|
+
import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, } from "./safe-area-utils";
|
|
24
25
|
import { setCardStatusBarDark, setCardStatusBarDefault } from "./utils";
|
|
25
26
|
// TODO(FW-2832): types
|
|
26
27
|
/**
|
|
@@ -255,6 +256,9 @@ export class Modal {
|
|
|
255
256
|
this.triggerController.removeClickListener();
|
|
256
257
|
this.cleanupViewTransitionListener();
|
|
257
258
|
this.cleanupParentRemovalObserver();
|
|
259
|
+
// Also called in dismiss() — intentional dual cleanup covers both
|
|
260
|
+
// dismiss-then-remove and direct DOM removal without dismiss.
|
|
261
|
+
this.cleanupSafeAreaOverrides();
|
|
258
262
|
}
|
|
259
263
|
componentWillLoad() {
|
|
260
264
|
var _a;
|
|
@@ -415,6 +419,11 @@ export class Modal {
|
|
|
415
419
|
await waitForMount();
|
|
416
420
|
}
|
|
417
421
|
writeTask(() => this.el.classList.add('show-modal'));
|
|
422
|
+
// Recalculate isSheetModal before safe-area setup because framework
|
|
423
|
+
// bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
|
|
424
|
+
this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
425
|
+
// Set initial safe-area overrides before animation
|
|
426
|
+
this.setInitialSafeAreaOverrides();
|
|
418
427
|
const hasCardModal = presentingElement !== undefined;
|
|
419
428
|
/**
|
|
420
429
|
* We need to change the status bar at the
|
|
@@ -432,6 +441,10 @@ export class Modal {
|
|
|
432
441
|
backdropBreakpoint: this.backdropBreakpoint,
|
|
433
442
|
expandToScroll: this.expandToScroll,
|
|
434
443
|
});
|
|
444
|
+
// Update safe-area based on actual position after animation
|
|
445
|
+
this.updateSafeAreaOverrides();
|
|
446
|
+
// Apply fullscreen safe-area padding if needed
|
|
447
|
+
this.applyFullscreenSafeArea();
|
|
435
448
|
/* tslint:disable-next-line */
|
|
436
449
|
if (typeof window !== 'undefined') {
|
|
437
450
|
/**
|
|
@@ -463,13 +476,7 @@ export class Modal {
|
|
|
463
476
|
};
|
|
464
477
|
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
|
465
478
|
}
|
|
466
|
-
|
|
467
|
-
* Recalculate isSheetModal because framework bindings (e.g., Angular)
|
|
468
|
-
* may not have been applied when componentWillLoad ran.
|
|
469
|
-
*/
|
|
470
|
-
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
471
|
-
this.isSheetModal = isSheetModal;
|
|
472
|
-
if (isSheetModal) {
|
|
479
|
+
if (this.isSheetModal) {
|
|
473
480
|
this.initSheetGesture();
|
|
474
481
|
}
|
|
475
482
|
else if (hasCardModal) {
|
|
@@ -546,6 +553,8 @@ export class Modal {
|
|
|
546
553
|
if (this.currentBreakpoint !== breakpoint) {
|
|
547
554
|
this.currentBreakpoint = breakpoint;
|
|
548
555
|
this.ionBreakpointDidChange.emit({ breakpoint });
|
|
556
|
+
// Update safe-area overrides based on new position
|
|
557
|
+
this.updateSafeAreaOverrides();
|
|
549
558
|
}
|
|
550
559
|
});
|
|
551
560
|
this.gesture = gesture;
|
|
@@ -713,6 +722,7 @@ export class Modal {
|
|
|
713
722
|
}
|
|
714
723
|
this.cleanupViewTransitionListener();
|
|
715
724
|
this.cleanupParentRemovalObserver();
|
|
725
|
+
this.cleanupSafeAreaOverrides();
|
|
716
726
|
this.cleanupChildRoutePassthrough();
|
|
717
727
|
}
|
|
718
728
|
this.currentBreakpoint = undefined;
|
|
@@ -841,6 +851,9 @@ export class Modal {
|
|
|
841
851
|
this.viewTransitionAnimation = transitionAnimation;
|
|
842
852
|
transitionAnimation.play().then(() => {
|
|
843
853
|
this.viewTransitionAnimation = undefined;
|
|
854
|
+
// Wait for a layout pass after the transition so getBoundingClientRect()
|
|
855
|
+
// in getPositionBasedSafeAreaConfig() reflects the new dimensions.
|
|
856
|
+
raf(() => this.updateSafeAreaOverrides());
|
|
844
857
|
// After orientation transition, recreate the swipe-to-close gesture
|
|
845
858
|
// with updated animation that reflects the new presenting element state
|
|
846
859
|
this.reinitSwipeToClose();
|
|
@@ -967,6 +980,109 @@ export class Modal {
|
|
|
967
980
|
(_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
968
981
|
this.parentRemovalObserver = undefined;
|
|
969
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Creates the context object for safe-area utilities.
|
|
985
|
+
*/
|
|
986
|
+
getSafeAreaContext() {
|
|
987
|
+
return {
|
|
988
|
+
isSheetModal: this.isSheetModal,
|
|
989
|
+
isCardModal: this.presentingElement !== undefined && getIonMode(this) === 'ios',
|
|
990
|
+
presentingElement: this.presentingElement,
|
|
991
|
+
breakpoints: this.breakpoints,
|
|
992
|
+
currentBreakpoint: this.currentBreakpoint,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Sets initial safe-area overrides before modal animation.
|
|
997
|
+
* Called in present() before animation starts.
|
|
998
|
+
*/
|
|
999
|
+
setInitialSafeAreaOverrides() {
|
|
1000
|
+
const context = this.getSafeAreaContext();
|
|
1001
|
+
const safeAreaConfig = getInitialSafeAreaConfig(context);
|
|
1002
|
+
applySafeAreaOverrides(this.el, safeAreaConfig);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Updates safe-area overrides during dynamic state changes.
|
|
1006
|
+
* Called after animations, during gestures, and on orientation changes.
|
|
1007
|
+
*/
|
|
1008
|
+
updateSafeAreaOverrides() {
|
|
1009
|
+
const { wrapperEl, el } = this;
|
|
1010
|
+
const context = this.getSafeAreaContext();
|
|
1011
|
+
// Sheet modals: the wrapper extends beyond the viewport and is translated
|
|
1012
|
+
// via breakpoint gestures, making getBoundingClientRect unreliable for
|
|
1013
|
+
// edge detection. Instead, use breakpoint value to determine top safe-area.
|
|
1014
|
+
if (context.isSheetModal) {
|
|
1015
|
+
const needsTopSafeArea = context.currentBreakpoint === 1;
|
|
1016
|
+
applySafeAreaOverrides(el, {
|
|
1017
|
+
top: needsTopSafeArea ? 'inherit' : '0px',
|
|
1018
|
+
bottom: 'inherit',
|
|
1019
|
+
left: '0px',
|
|
1020
|
+
right: '0px',
|
|
1021
|
+
});
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
// Card modals have fixed safe-area requirements set by initial prediction.
|
|
1025
|
+
if (context.isCardModal)
|
|
1026
|
+
return;
|
|
1027
|
+
// wrapperEl is required for position-based detection below
|
|
1028
|
+
if (!wrapperEl)
|
|
1029
|
+
return;
|
|
1030
|
+
// Regular modals: use position-based detection to correctly handle both
|
|
1031
|
+
// fullscreen modals and centered dialogs with custom dimensions.
|
|
1032
|
+
const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
|
|
1033
|
+
applySafeAreaOverrides(el, safeAreaConfig);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Applies padding-bottom to fullscreen modal wrapper to prevent
|
|
1037
|
+
* content from overlapping system navigation bar.
|
|
1038
|
+
*/
|
|
1039
|
+
applyFullscreenSafeArea() {
|
|
1040
|
+
const { wrapperEl, el } = this;
|
|
1041
|
+
if (!wrapperEl)
|
|
1042
|
+
return;
|
|
1043
|
+
const context = this.getSafeAreaContext();
|
|
1044
|
+
if (context.isSheetModal || context.isCardModal)
|
|
1045
|
+
return;
|
|
1046
|
+
// Check for standard Ionic layout children (ion-content, ion-footer),
|
|
1047
|
+
// searching one level deep for wrapped components (e.g.,
|
|
1048
|
+
// <app-footer><ion-footer>...</ion-footer></app-footer>).
|
|
1049
|
+
let hasContent = false;
|
|
1050
|
+
let hasFooter = false;
|
|
1051
|
+
for (const child of Array.from(el.children)) {
|
|
1052
|
+
if (child.tagName === 'ION-CONTENT')
|
|
1053
|
+
hasContent = true;
|
|
1054
|
+
if (child.tagName === 'ION-FOOTER')
|
|
1055
|
+
hasFooter = true;
|
|
1056
|
+
for (const grandchild of Array.from(child.children)) {
|
|
1057
|
+
if (grandchild.tagName === 'ION-CONTENT')
|
|
1058
|
+
hasContent = true;
|
|
1059
|
+
if (grandchild.tagName === 'ION-FOOTER')
|
|
1060
|
+
hasFooter = true;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
// Only apply wrapper padding for standard Ionic layouts (has ion-content
|
|
1064
|
+
// but no ion-footer). Custom modals with raw HTML are fully
|
|
1065
|
+
// developer-controlled and should not be modified.
|
|
1066
|
+
if (!hasContent || hasFooter)
|
|
1067
|
+
return;
|
|
1068
|
+
// Reduce wrapper height by safe-area and add equivalent padding so the
|
|
1069
|
+
// total visual size stays the same but the flex content area shrinks.
|
|
1070
|
+
// Using height + padding instead of box-sizing: border-box avoids
|
|
1071
|
+
// breaking custom modals that set --border-width (border-box would
|
|
1072
|
+
// include the border inside the height, changing the layout).
|
|
1073
|
+
wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
|
|
1074
|
+
wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Clears all safe-area overrides and padding from wrapper.
|
|
1078
|
+
*/
|
|
1079
|
+
cleanupSafeAreaOverrides() {
|
|
1080
|
+
clearSafeAreaOverrides(this.el);
|
|
1081
|
+
if (this.wrapperEl) {
|
|
1082
|
+
this.wrapperEl.style.removeProperty('height');
|
|
1083
|
+
this.wrapperEl.style.removeProperty('padding-bottom');
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
970
1086
|
render() {
|
|
971
1087
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
972
1088
|
const showHandle = handle !== false && isSheetModal;
|
|
@@ -974,20 +1090,20 @@ export class Modal {
|
|
|
974
1090
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
975
1091
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
976
1092
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
977
|
-
return (h(Host, Object.assign({ key: '
|
|
1093
|
+
return (h(Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
|
|
978
1094
|
// Allow the modal to be navigable when the handle is focusable
|
|
979
1095
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
980
1096
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
981
|
-
}, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: '
|
|
1097
|
+
}, 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: '6866fb2a265d40c761ed6bfb661a217948c571ef', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '00788de9267f1c064f16aa55f35250de16522ae6', class: "modal-shadow" }), h("div", Object.assign({ key: 'a575e1755bd25a7996cdb04b014a91d538e7bc94',
|
|
982
1098
|
/*
|
|
983
1099
|
role and aria-modal must be used on the
|
|
984
1100
|
same element. They must also be set inside the
|
|
985
1101
|
shadow DOM otherwise ion-button will not be highlighted
|
|
986
1102
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
987
1103
|
*/
|
|
988
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
1104
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'b99c64717037e9241479ea2f5bb84e79a466d918', class: "modal-handle",
|
|
989
1105
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
990
|
-
tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: '
|
|
1106
|
+
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: 'ee672afd537b281aeeb73a55d61ceeb5dfae212a', onSlotchange: this.onSlotChange }))));
|
|
991
1107
|
}
|
|
992
1108
|
static get is() { return "ion-modal"; }
|
|
993
1109
|
static get encapsulation() { return "shadow"; }
|
|
@@ -135,10 +135,6 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
-
--ion-safe-area-top: 0px;
|
|
139
|
-
--ion-safe-area-bottom: 0px;
|
|
140
|
-
--ion-safe-area-right: 0px;
|
|
141
|
-
--ion-safe-area-left: 0px;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|