@ionic/core 8.7.18-dev.11769546281.18b0f3ba → 8.7.18-dev.11769790854.11895f8f
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/modal.js +220 -12
- package/components/popover.js +83 -11
- package/dist/cjs/ion-modal.cjs.entry.js +220 -12
- package/dist/cjs/ion-popover.cjs.entry.js +83 -11
- package/dist/collection/components/modal/gestures/sheet.js +3 -1
- package/dist/collection/components/modal/gestures/swipe-to-close.js +3 -1
- package/dist/collection/components/modal/modal.ios.css +0 -4
- package/dist/collection/components/modal/modal.js +215 -7
- package/dist/collection/components/modal/modal.md.css +0 -4
- package/dist/collection/components/popover/animations/ios.enter.js +21 -5
- package/dist/collection/components/popover/animations/md.enter.js +30 -5
- package/dist/collection/components/popover/utils.js +32 -1
- package/dist/docs.json +1 -1
- package/dist/esm/ion-modal.entry.js +220 -12
- package/dist/esm/ion-popover.entry.js +83 -11
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-ec9ca3fe.entry.js +4 -0
- package/dist/ionic/p-ef4256eb.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 +47 -0
- package/dist/types/components/popover/utils.d.ts +2 -0
- package/hydrate/index.js +302 -22
- package/hydrate/index.mjs +302 -22
- package/package.json +1 -1
- package/dist/ionic/p-91840a80.entry.js +0 -4
- package/dist/ionic/p-f9061316.entry.js +0 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Host, h, writeTask } from "@stencil/core";
|
|
5
|
+
import { win } from "../../utils/browser/index";
|
|
5
6
|
import { findIonContent, printIonContentErrorMsg } from "../../utils/content/index";
|
|
6
7
|
import { CoreDelegate, attachComponent, detachComponent } from "../../utils/framework-delegate";
|
|
7
8
|
import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from "../../utils/helpers";
|
|
@@ -42,6 +43,10 @@ export class Modal {
|
|
|
42
43
|
this.inline = false;
|
|
43
44
|
// Whether or not modal is being dismissed via gesture
|
|
44
45
|
this.gestureAnimationDismissing = false;
|
|
46
|
+
// Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
|
|
47
|
+
this.skipSafeAreaCoordinateDetection = false;
|
|
48
|
+
// Track previous safe-area state to avoid redundant DOM writes
|
|
49
|
+
this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
|
|
45
50
|
this.presented = false;
|
|
46
51
|
/** @internal */
|
|
47
52
|
this.hasController = false;
|
|
@@ -232,7 +237,10 @@ export class Modal {
|
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
onWindowResize() {
|
|
235
|
-
//
|
|
240
|
+
// Invalidate safe-area cache on resize (device rotation may change values)
|
|
241
|
+
this.cachedSafeAreas = undefined;
|
|
242
|
+
this.updateSafeAreaOverrides();
|
|
243
|
+
// Only handle view transition for iOS card modals when no custom animations are provided
|
|
236
244
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
237
245
|
return;
|
|
238
246
|
}
|
|
@@ -255,6 +263,8 @@ export class Modal {
|
|
|
255
263
|
this.triggerController.removeClickListener();
|
|
256
264
|
this.cleanupViewTransitionListener();
|
|
257
265
|
this.cleanupParentRemovalObserver();
|
|
266
|
+
// Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
|
|
267
|
+
this.resetSafeAreaState();
|
|
258
268
|
}
|
|
259
269
|
componentWillLoad() {
|
|
260
270
|
var _a;
|
|
@@ -414,6 +424,8 @@ export class Modal {
|
|
|
414
424
|
else if (!this.keepContentsMounted) {
|
|
415
425
|
await waitForMount();
|
|
416
426
|
}
|
|
427
|
+
// Predict safe-area needs based on modal configuration to avoid visual snap
|
|
428
|
+
this.setInitialSafeAreaOverrides(presentingElement);
|
|
417
429
|
writeTask(() => this.el.classList.add('show-modal'));
|
|
418
430
|
const hasCardModal = presentingElement !== undefined;
|
|
419
431
|
/**
|
|
@@ -475,6 +487,8 @@ export class Modal {
|
|
|
475
487
|
else if (hasCardModal) {
|
|
476
488
|
this.initSwipeToClose();
|
|
477
489
|
}
|
|
490
|
+
// Now that animation is complete, update safe-area based on actual position
|
|
491
|
+
this.updateSafeAreaOverrides();
|
|
478
492
|
// Initialize view transition listener for iOS card modals
|
|
479
493
|
this.initViewTransitionListener();
|
|
480
494
|
// Initialize parent removal observer
|
|
@@ -526,7 +540,7 @@ export class Modal {
|
|
|
526
540
|
await this.dismiss(undefined, GESTURE);
|
|
527
541
|
this.gestureAnimationDismissing = false;
|
|
528
542
|
});
|
|
529
|
-
});
|
|
543
|
+
}, () => this.updateSafeAreaOverrides());
|
|
530
544
|
this.gesture.enable(true);
|
|
531
545
|
}
|
|
532
546
|
initSheetGesture() {
|
|
@@ -547,7 +561,8 @@ export class Modal {
|
|
|
547
561
|
this.currentBreakpoint = breakpoint;
|
|
548
562
|
this.ionBreakpointDidChange.emit({ breakpoint });
|
|
549
563
|
}
|
|
550
|
-
|
|
564
|
+
this.updateSafeAreaOverrides();
|
|
565
|
+
}, () => this.updateSafeAreaOverrides());
|
|
551
566
|
this.gesture = gesture;
|
|
552
567
|
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
|
|
553
568
|
this.gesture.enable(true);
|
|
@@ -625,6 +640,197 @@ export class Modal {
|
|
|
625
640
|
// Clear the cached reference
|
|
626
641
|
this.cachedPageParent = undefined;
|
|
627
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Sets initial safe-area overrides based on modal configuration before
|
|
645
|
+
* the modal becomes visible. This predicts whether the modal will touch
|
|
646
|
+
* screen edges to avoid a visual snap after animation completes.
|
|
647
|
+
*/
|
|
648
|
+
setInitialSafeAreaOverrides(presentingElement) {
|
|
649
|
+
const style = this.el.style;
|
|
650
|
+
const mode = getIonMode(this);
|
|
651
|
+
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
|
|
652
|
+
// Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
|
|
653
|
+
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
654
|
+
const isTablet = window.innerWidth >= 768;
|
|
655
|
+
// Sheet modals always touch bottom edge, never top/left/right
|
|
656
|
+
if (isSheetModal) {
|
|
657
|
+
style.setProperty('--ion-safe-area-top', '0px');
|
|
658
|
+
style.setProperty('--ion-safe-area-left', '0px');
|
|
659
|
+
style.setProperty('--ion-safe-area-right', '0px');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Card modals have rounded top corners
|
|
663
|
+
if (isCardModal) {
|
|
664
|
+
style.setProperty('--ion-safe-area-top', '0px');
|
|
665
|
+
if (isTablet) {
|
|
666
|
+
// On tablets, card modals are inset from all edges
|
|
667
|
+
this.zeroAllSafeAreas();
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// On phones, card modals still extend to the bottom edge
|
|
671
|
+
style.setProperty('--ion-safe-area-left', '0px');
|
|
672
|
+
style.setProperty('--ion-safe-area-right', '0px');
|
|
673
|
+
this.applyFullscreenSafeArea();
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// 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
|
+
}
|
|
628
834
|
sheetOnDismiss() {
|
|
629
835
|
/**
|
|
630
836
|
* While the gesture animation is finishing
|
|
@@ -717,6 +923,8 @@ export class Modal {
|
|
|
717
923
|
}
|
|
718
924
|
this.currentBreakpoint = undefined;
|
|
719
925
|
this.animation = undefined;
|
|
926
|
+
// Reset safe-area state for potential re-presentation
|
|
927
|
+
this.resetSafeAreaState();
|
|
720
928
|
unlock();
|
|
721
929
|
return dismissed;
|
|
722
930
|
}
|
|
@@ -974,20 +1182,20 @@ export class Modal {
|
|
|
974
1182
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
975
1183
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
976
1184
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
977
|
-
return (h(Host, Object.assign({ key: '
|
|
1185
|
+
return (h(Host, Object.assign({ key: '8f42c71ec9c9270b4218ca7eec57ca998871ac07', "no-router": true,
|
|
978
1186
|
// Allow the modal to be navigable when the handle is focusable
|
|
979
1187
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
980
1188
|
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: '
|
|
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: '2f7b08b019c66cbfceacafa9a68b91f8da1f4084', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '6b0d1ae90e8483332f74371be5dcbd0a7b2661ba', class: "modal-shadow", ref: (el) => (this.shadowEl = el) }), h("div", Object.assign({ key: 'c96f671fb25e5de45231ea65ac52500547b2e262',
|
|
982
1190
|
/*
|
|
983
1191
|
role and aria-modal must be used on the
|
|
984
1192
|
same element. They must also be set inside the
|
|
985
1193
|
shadow DOM otherwise ion-button will not be highlighted
|
|
986
1194
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
987
1195
|
*/
|
|
988
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
1196
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'e07419b01a588c0ba62ed9607c843cac7bd26337', class: "modal-handle",
|
|
989
1197
|
// 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: '
|
|
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: '65f35079df8eb8b6cd262e3a08297866715ce7ea', onSlotchange: this.onSlotChange }))));
|
|
991
1199
|
}
|
|
992
1200
|
static get is() { return "ion-modal"; }
|
|
993
1201
|
static get encapsulation() { return "shadow"; }
|
|
@@ -135,10 +135,6 @@ ion-backdrop {
|
|
|
135
135
|
:host {
|
|
136
136
|
--width: 600px;
|
|
137
137
|
--height: 500px;
|
|
138
|
-
--ion-safe-area-top: 0px;
|
|
139
|
-
--ion-safe-area-bottom: 0px;
|
|
140
|
-
--ion-safe-area-right: 0px;
|
|
141
|
-
--ion-safe-area-left: 0px;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
144
140
|
@media only screen and (min-width: 768px) and (min-height: 768px) {
|
|
@@ -31,7 +31,7 @@ export const iosEnterAnimation = (baseEl, opts) => {
|
|
|
31
31
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
32
32
|
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
33
33
|
const margin = size === 'cover' ? 0 : 25;
|
|
34
|
-
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
34
|
+
const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
|
|
35
35
|
const baseAnimation = createAnimation();
|
|
36
36
|
const backdropAnimation = createAnimation();
|
|
37
37
|
const contentAnimation = createAnimation();
|
|
@@ -61,19 +61,35 @@ export const iosEnterAnimation = (baseEl, opts) => {
|
|
|
61
61
|
if (addPopoverBottomClass) {
|
|
62
62
|
baseEl.classList.add('popover-bottom');
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Safe area CSS variable adjustments.
|
|
66
|
+
* When the popover is positioned near an edge, we add the corresponding
|
|
67
|
+
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
68
|
+
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
69
|
+
*/
|
|
70
|
+
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
71
|
+
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
67
72
|
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
68
73
|
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
74
|
+
let topValue = `${top}px`;
|
|
75
|
+
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
69
76
|
let leftValue = `${left}px`;
|
|
77
|
+
if (checkSafeAreaTop) {
|
|
78
|
+
topValue = `${top}px${safeAreaTop}`;
|
|
79
|
+
}
|
|
80
|
+
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
81
|
+
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
82
|
+
}
|
|
70
83
|
if (checkSafeAreaLeft) {
|
|
71
84
|
leftValue = `${left}px${safeAreaLeft}`;
|
|
72
85
|
}
|
|
73
86
|
if (checkSafeAreaRight) {
|
|
74
87
|
leftValue = `${left}px${safeAreaRight}`;
|
|
75
88
|
}
|
|
76
|
-
|
|
89
|
+
if (bottomValue !== undefined) {
|
|
90
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
91
|
+
}
|
|
92
|
+
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
|
77
93
|
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
78
94
|
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
79
95
|
if (arrowEl !== null) {
|
|
@@ -28,7 +28,32 @@ export const mdEnterAnimation = (baseEl, opts) => {
|
|
|
28
28
|
};
|
|
29
29
|
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
|
|
30
30
|
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
|
31
|
-
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
|
|
31
|
+
const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
|
|
32
|
+
/**
|
|
33
|
+
* Safe area CSS variable adjustments.
|
|
34
|
+
* When the popover is positioned near an edge, we add the corresponding
|
|
35
|
+
* safe-area inset to ensure the popover doesn't overlap with system UI
|
|
36
|
+
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
|
37
|
+
*/
|
|
38
|
+
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
|
39
|
+
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
|
40
|
+
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
41
|
+
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
42
|
+
let topValue = `${top}px`;
|
|
43
|
+
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
|
44
|
+
let leftValue = `${left}px`;
|
|
45
|
+
if (checkSafeAreaTop) {
|
|
46
|
+
topValue = `${top}px${safeAreaTop}`;
|
|
47
|
+
}
|
|
48
|
+
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
|
49
|
+
bottomValue = `${bottom}px${safeAreaBottom}`;
|
|
50
|
+
}
|
|
51
|
+
if (checkSafeAreaLeft) {
|
|
52
|
+
leftValue = `${left}px${safeAreaLeft}`;
|
|
53
|
+
}
|
|
54
|
+
if (checkSafeAreaRight) {
|
|
55
|
+
leftValue = `${left}px${safeAreaRight}`;
|
|
56
|
+
}
|
|
32
57
|
const baseAnimation = createAnimation();
|
|
33
58
|
const backdropAnimation = createAnimation();
|
|
34
59
|
const wrapperAnimation = createAnimation();
|
|
@@ -45,13 +70,13 @@ export const mdEnterAnimation = (baseEl, opts) => {
|
|
|
45
70
|
contentAnimation
|
|
46
71
|
.addElement(contentEl)
|
|
47
72
|
.beforeStyles({
|
|
48
|
-
top: `calc(${
|
|
49
|
-
left: `calc(${
|
|
73
|
+
top: `calc(${topValue} + var(--offset-y, 0px))`,
|
|
74
|
+
left: `calc(${leftValue} + var(--offset-x, 0px))`,
|
|
50
75
|
'transform-origin': `${originY} ${originX}`,
|
|
51
76
|
})
|
|
52
77
|
.beforeAddWrite(() => {
|
|
53
|
-
if (
|
|
54
|
-
contentEl.style.setProperty('bottom',
|
|
78
|
+
if (bottomValue !== undefined) {
|
|
79
|
+
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
|
55
80
|
}
|
|
56
81
|
})
|
|
57
82
|
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|
|
@@ -648,6 +648,8 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
|
|
|
648
648
|
let bottom;
|
|
649
649
|
let originX = contentOriginX;
|
|
650
650
|
let originY = contentOriginY;
|
|
651
|
+
let checkSafeAreaTop = false;
|
|
652
|
+
let checkSafeAreaBottom = false;
|
|
651
653
|
let checkSafeAreaLeft = false;
|
|
652
654
|
let checkSafeAreaRight = false;
|
|
653
655
|
const triggerTop = triggerCoordinates
|
|
@@ -692,10 +694,18 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
|
|
|
692
694
|
* We chose 12 here so that the popover position looks a bit nicer as
|
|
693
695
|
* it is not right up against the edge of the screen.
|
|
694
696
|
*/
|
|
695
|
-
top = Math.max(
|
|
697
|
+
top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
|
|
696
698
|
arrowTop = top + contentHeight;
|
|
697
699
|
originY = 'bottom';
|
|
698
700
|
addPopoverBottomClass = true;
|
|
701
|
+
/**
|
|
702
|
+
* If the popover is positioned near the top edge, account for safe area.
|
|
703
|
+
* This ensures the popover doesn't overlap with status bars or notches.
|
|
704
|
+
*/
|
|
705
|
+
if (top <= bodyPadding + safeAreaMargin) {
|
|
706
|
+
checkSafeAreaTop = true;
|
|
707
|
+
top = bodyPadding;
|
|
708
|
+
}
|
|
699
709
|
/**
|
|
700
710
|
* If not enough room for popover to appear
|
|
701
711
|
* above trigger, then cut it off.
|
|
@@ -703,14 +713,35 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
|
|
|
703
713
|
}
|
|
704
714
|
else {
|
|
705
715
|
bottom = bodyPadding;
|
|
716
|
+
/**
|
|
717
|
+
* When the popover is pinned to the bottom, account for safe area.
|
|
718
|
+
* This ensures the popover doesn't overlap with home indicators
|
|
719
|
+
* or navigation bars (e.g., Android API 36+ edge-to-edge).
|
|
720
|
+
*/
|
|
721
|
+
checkSafeAreaBottom = true;
|
|
706
722
|
}
|
|
707
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Final check: If the popover extends into any safe-area region,
|
|
726
|
+
* ensure the corresponding flag is set regardless of side.
|
|
727
|
+
* This handles cases where a side-positioned popover (left/right)
|
|
728
|
+
* still needs bottom safe-area padding because it extends into that region.
|
|
729
|
+
*/
|
|
730
|
+
const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
|
|
731
|
+
if (popoverBottom + safeAreaMargin > bodyHeight) {
|
|
732
|
+
checkSafeAreaBottom = true;
|
|
733
|
+
}
|
|
734
|
+
if (top < safeAreaMargin) {
|
|
735
|
+
checkSafeAreaTop = true;
|
|
736
|
+
}
|
|
708
737
|
return {
|
|
709
738
|
top,
|
|
710
739
|
left,
|
|
711
740
|
bottom,
|
|
712
741
|
originX,
|
|
713
742
|
originY,
|
|
743
|
+
checkSafeAreaTop,
|
|
744
|
+
checkSafeAreaBottom,
|
|
714
745
|
checkSafeAreaLeft,
|
|
715
746
|
checkSafeAreaRight,
|
|
716
747
|
arrowTop,
|
package/dist/docs.json
CHANGED