@ionic/core 8.6.3-dev.11750971489.140836b0 → 8.6.3-dev.11751305829.1112929c
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 +192 -8
- package/dist/cjs/ion-modal.cjs.entry.js +192 -8
- package/dist/collection/components/modal/animations/ios.enter.js +2 -2
- package/dist/collection/components/modal/animations/ios.leave.js +2 -2
- package/dist/collection/components/modal/animations/ios.transition.js +124 -0
- package/dist/collection/components/modal/modal.js +78 -4
- package/dist/docs.json +1 -1
- package/dist/esm/ion-modal.entry.js +192 -8
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-0df5b2a5.entry.js +4 -0
- package/dist/types/components/modal/animations/ios.transition.d.ts +14 -0
- package/dist/types/components/modal/modal.d.ts +6 -0
- package/hydrate/index.js +192 -8
- package/hydrate/index.mjs +192 -8
- package/package.json +2 -2
- package/dist/ionic/p-9e32212d.entry.js +0 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
|
+
*/
|
|
4
|
+
import { createAnimation } from "../../../utils/animation/animation";
|
|
5
|
+
import { getElementRoot } from "../../../utils/helpers";
|
|
6
|
+
import { SwipeToCloseDefaults } from "../gestures/swipe-to-close";
|
|
7
|
+
/**
|
|
8
|
+
* Transition animation from portrait view to landscape view
|
|
9
|
+
* This handles the case where a card modal is open in portrait view
|
|
10
|
+
* and the user switches to landscape view
|
|
11
|
+
*/
|
|
12
|
+
export const portraitToLandscapeTransition = (baseEl, opts, duration = 300) => {
|
|
13
|
+
const { presentingEl } = opts;
|
|
14
|
+
if (!presentingEl) {
|
|
15
|
+
// No transition needed for non-card modals
|
|
16
|
+
return createAnimation('portrait-to-landscape-transition');
|
|
17
|
+
}
|
|
18
|
+
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
19
|
+
const presentingElRoot = getElementRoot(presentingEl);
|
|
20
|
+
const bodyEl = document.body;
|
|
21
|
+
const baseAnimation = createAnimation('portrait-to-landscape-transition')
|
|
22
|
+
.addElement(baseEl)
|
|
23
|
+
.easing('cubic-bezier(0.32,0.72,0,1)')
|
|
24
|
+
.duration(duration);
|
|
25
|
+
const presentingAnimation = createAnimation();
|
|
26
|
+
if (!hasCardModal) {
|
|
27
|
+
// Non-card modal: transition from portrait state to landscape state
|
|
28
|
+
// Portrait: presentingEl has transform and body has black background
|
|
29
|
+
// Landscape: no transform, no body background, modal wrapper opacity changes
|
|
30
|
+
const root = getElementRoot(baseEl);
|
|
31
|
+
const wrapperAnimation = createAnimation()
|
|
32
|
+
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
|
|
33
|
+
.fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
|
|
34
|
+
const backdropAnimation = createAnimation()
|
|
35
|
+
.addElement(root.querySelector('ion-backdrop'))
|
|
36
|
+
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
|
37
|
+
// Animate presentingEl from portrait state back to normal
|
|
38
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
39
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
40
|
+
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
41
|
+
presentingAnimation
|
|
42
|
+
.addElement(presentingEl)
|
|
43
|
+
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
|
|
44
|
+
.fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
|
|
45
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(1)')
|
|
46
|
+
.fromTo('border-radius', '10px 10px 0 0', '0px');
|
|
47
|
+
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Card modal: transition from portrait card state to landscape card state
|
|
51
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
52
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
53
|
+
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
54
|
+
const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
|
55
|
+
presentingAnimation
|
|
56
|
+
.addElement(presentingElRoot.querySelector('.modal-wrapper'))
|
|
57
|
+
.fromTo('transform', fromTransform, toTransform)
|
|
58
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
|
59
|
+
const shadowAnimation = createAnimation()
|
|
60
|
+
.addElement(presentingElRoot.querySelector('.modal-shadow'))
|
|
61
|
+
.fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
|
|
62
|
+
.fromTo('transform', fromTransform, toTransform);
|
|
63
|
+
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
|
64
|
+
}
|
|
65
|
+
return baseAnimation;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Transition animation from landscape view to portrait view
|
|
69
|
+
* This handles the case where a card modal is open in landscape view
|
|
70
|
+
* and the user switches to portrait view
|
|
71
|
+
*/
|
|
72
|
+
export const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
|
|
73
|
+
const { presentingEl } = opts;
|
|
74
|
+
if (!presentingEl) {
|
|
75
|
+
// No transition needed for non-card modals
|
|
76
|
+
return createAnimation('landscape-to-portrait-transition');
|
|
77
|
+
}
|
|
78
|
+
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
79
|
+
const presentingElRoot = getElementRoot(presentingEl);
|
|
80
|
+
const bodyEl = document.body;
|
|
81
|
+
const baseAnimation = createAnimation('landscape-to-portrait-transition')
|
|
82
|
+
.addElement(baseEl)
|
|
83
|
+
.easing('cubic-bezier(0.32,0.72,0,1)')
|
|
84
|
+
.duration(duration);
|
|
85
|
+
const presentingAnimation = createAnimation();
|
|
86
|
+
if (!hasCardModal) {
|
|
87
|
+
// Non-card modal: transition from landscape state to portrait state
|
|
88
|
+
const root = getElementRoot(baseEl);
|
|
89
|
+
const wrapperAnimation = createAnimation()
|
|
90
|
+
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
|
|
91
|
+
.fromTo('opacity', '1', '1'); // Keep wrapper visible
|
|
92
|
+
const backdropAnimation = createAnimation()
|
|
93
|
+
.addElement(root.querySelector('ion-backdrop'))
|
|
94
|
+
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
|
95
|
+
// Animate presentingEl from normal state to portrait state
|
|
96
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
97
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
98
|
+
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
99
|
+
presentingAnimation
|
|
100
|
+
.addElement(presentingEl)
|
|
101
|
+
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
|
|
102
|
+
.fromTo('transform', 'translateY(0px) scale(1)', toTransform)
|
|
103
|
+
.fromTo('filter', 'contrast(1)', 'contrast(0.85)')
|
|
104
|
+
.fromTo('border-radius', '0px', '10px 10px 0 0');
|
|
105
|
+
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Card modal: transition from landscape card state to portrait card state
|
|
109
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
110
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
111
|
+
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
|
112
|
+
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
113
|
+
presentingAnimation
|
|
114
|
+
.addElement(presentingElRoot.querySelector('.modal-wrapper'))
|
|
115
|
+
.fromTo('transform', fromTransform, toTransform)
|
|
116
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
|
117
|
+
const shadowAnimation = createAnimation()
|
|
118
|
+
.addElement(presentingElRoot.querySelector('.modal-shadow'))
|
|
119
|
+
.fromTo('opacity', '0', '0') // Shadow stays hidden
|
|
120
|
+
.fromTo('transform', fromTransform, toTransform);
|
|
121
|
+
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
|
122
|
+
}
|
|
123
|
+
return baseAnimation;
|
|
124
|
+
};
|
|
@@ -16,6 +16,7 @@ import { getIonMode } from "../../global/ionic-global";
|
|
|
16
16
|
import { KEYBOARD_DID_OPEN } from "../../utils/keyboard/keyboard";
|
|
17
17
|
import { iosEnterAnimation } from "./animations/ios.enter";
|
|
18
18
|
import { iosLeaveAnimation } from "./animations/ios.leave";
|
|
19
|
+
import { portraitToLandscapeTransition, landscapeToPortraitTransition } from "./animations/ios.transition";
|
|
19
20
|
import { mdEnterAnimation } from "./animations/md.enter";
|
|
20
21
|
import { mdLeaveAnimation } from "./animations/md.leave";
|
|
21
22
|
import { createSheetGesture } from "./gestures/sheet";
|
|
@@ -222,6 +223,7 @@ export class Modal {
|
|
|
222
223
|
}
|
|
223
224
|
disconnectedCallback() {
|
|
224
225
|
this.triggerController.removeClickListener();
|
|
226
|
+
this.cleanupViewTransitionListener();
|
|
225
227
|
}
|
|
226
228
|
componentWillLoad() {
|
|
227
229
|
var _a;
|
|
@@ -432,6 +434,8 @@ export class Modal {
|
|
|
432
434
|
else if (hasCardModal) {
|
|
433
435
|
this.initSwipeToClose();
|
|
434
436
|
}
|
|
437
|
+
// Initialize view transition listener for iOS card modals
|
|
438
|
+
this.initViewTransitionListener();
|
|
435
439
|
unlock();
|
|
436
440
|
}
|
|
437
441
|
initSwipeToClose() {
|
|
@@ -585,6 +589,7 @@ export class Modal {
|
|
|
585
589
|
if (this.gesture) {
|
|
586
590
|
this.gesture.destroy();
|
|
587
591
|
}
|
|
592
|
+
this.cleanupViewTransitionListener();
|
|
588
593
|
}
|
|
589
594
|
this.currentBreakpoint = undefined;
|
|
590
595
|
this.animation = undefined;
|
|
@@ -660,6 +665,75 @@ export class Modal {
|
|
|
660
665
|
await this.setCurrentBreakpoint(nextBreakpoint);
|
|
661
666
|
return true;
|
|
662
667
|
}
|
|
668
|
+
initViewTransitionListener() {
|
|
669
|
+
// Only enable for iOS card modals when no custom animations are provided
|
|
670
|
+
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// Set initial view state
|
|
674
|
+
this.currentViewIsPortrait = window.innerWidth < 768;
|
|
675
|
+
// Create debounced resize handler
|
|
676
|
+
let resizeTimeout;
|
|
677
|
+
this.resizeListener = () => {
|
|
678
|
+
clearTimeout(resizeTimeout);
|
|
679
|
+
resizeTimeout = setTimeout(() => {
|
|
680
|
+
this.handleViewTransition();
|
|
681
|
+
}, 100); // Debounce for 100ms to avoid excessive calls
|
|
682
|
+
};
|
|
683
|
+
window.addEventListener('resize', this.resizeListener);
|
|
684
|
+
}
|
|
685
|
+
handleViewTransition() {
|
|
686
|
+
const isPortrait = window.innerWidth < 768;
|
|
687
|
+
// Only transition if view state actually changed
|
|
688
|
+
if (this.currentViewIsPortrait === isPortrait) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// Cancel any ongoing transition animation
|
|
692
|
+
if (this.viewTransitionAnimation) {
|
|
693
|
+
this.viewTransitionAnimation.destroy();
|
|
694
|
+
this.viewTransitionAnimation = undefined;
|
|
695
|
+
}
|
|
696
|
+
const { presentingElement } = this;
|
|
697
|
+
if (!presentingElement) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// Create transition animation
|
|
701
|
+
let transitionAnimation;
|
|
702
|
+
if (this.currentViewIsPortrait && !isPortrait) {
|
|
703
|
+
// Portrait to landscape transition
|
|
704
|
+
transitionAnimation = portraitToLandscapeTransition(this.el, {
|
|
705
|
+
presentingEl: presentingElement,
|
|
706
|
+
currentBreakpoint: this.currentBreakpoint,
|
|
707
|
+
backdropBreakpoint: this.backdropBreakpoint,
|
|
708
|
+
expandToScroll: this.expandToScroll,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
// Landscape to portrait transition
|
|
713
|
+
transitionAnimation = landscapeToPortraitTransition(this.el, {
|
|
714
|
+
presentingEl: presentingElement,
|
|
715
|
+
currentBreakpoint: this.currentBreakpoint,
|
|
716
|
+
backdropBreakpoint: this.backdropBreakpoint,
|
|
717
|
+
expandToScroll: this.expandToScroll,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
// Update state and play animation
|
|
721
|
+
this.currentViewIsPortrait = isPortrait;
|
|
722
|
+
this.viewTransitionAnimation = transitionAnimation;
|
|
723
|
+
transitionAnimation.play().then(() => {
|
|
724
|
+
this.viewTransitionAnimation = undefined;
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
cleanupViewTransitionListener() {
|
|
728
|
+
if (this.resizeListener) {
|
|
729
|
+
window.removeEventListener('resize', this.resizeListener);
|
|
730
|
+
this.resizeListener = undefined;
|
|
731
|
+
}
|
|
732
|
+
if (this.viewTransitionAnimation) {
|
|
733
|
+
this.viewTransitionAnimation.destroy();
|
|
734
|
+
this.viewTransitionAnimation = undefined;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
663
737
|
render() {
|
|
664
738
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
665
739
|
const showHandle = handle !== false && isSheetModal;
|
|
@@ -667,20 +741,20 @@ export class Modal {
|
|
|
667
741
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
668
742
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
669
743
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
670
|
-
return (h(Host, Object.assign({ key: '
|
|
744
|
+
return (h(Host, Object.assign({ key: 'd5bcaa588471573e1bcbd99997b306c8ecdd124f', "no-router": true,
|
|
671
745
|
// Allow the modal to be navigable when the handle is focusable
|
|
672
746
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
673
747
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
674
|
-
}, 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: '
|
|
748
|
+
}, 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: '78b954a1c50a0325eb51db04314e86701c66d840', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '21887227c272f5eba1b3d164be3572bd710d415c', class: "modal-shadow" }), h("div", Object.assign({ key: 'a40472a9065c04ad12fb7b2b2a05e5021f077ad5',
|
|
675
749
|
/*
|
|
676
750
|
role and aria-modal must be used on the
|
|
677
751
|
same element. They must also be set inside the
|
|
678
752
|
shadow DOM otherwise ion-button will not be highlighted
|
|
679
753
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
680
754
|
*/
|
|
681
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
755
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'a4973f66587546031d1cc0197e1ba7c2521a3fbc', class: "modal-handle",
|
|
682
756
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
683
|
-
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: '
|
|
757
|
+
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: 'ee5e007e3a4e592d98f1b5a5d31ad55e6faa3bdb' }))));
|
|
684
758
|
}
|
|
685
759
|
static get is() { return "ion-modal"; }
|
|
686
760
|
static get encapsulation() { return "shadow"; }
|
package/dist/docs.json
CHANGED
|
@@ -587,7 +587,7 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
587
587
|
baseAnimation.addAnimation(contentAnimation);
|
|
588
588
|
}
|
|
589
589
|
if (presentingEl) {
|
|
590
|
-
const
|
|
590
|
+
const isPortrait = window.innerWidth < 768;
|
|
591
591
|
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
592
592
|
const presentingElRoot = getElementRoot(presentingEl);
|
|
593
593
|
const presentingAnimation = createAnimation().beforeStyles({
|
|
@@ -596,7 +596,7 @@ const iosEnterAnimation = (baseEl, opts) => {
|
|
|
596
596
|
overflow: 'hidden',
|
|
597
597
|
});
|
|
598
598
|
const bodyEl = document.body;
|
|
599
|
-
if (
|
|
599
|
+
if (isPortrait) {
|
|
600
600
|
/**
|
|
601
601
|
* Fallback for browsers that does not support `max()` (ex: Firefox)
|
|
602
602
|
* No need to worry about statusbar padding since engines like Gecko
|
|
@@ -674,7 +674,7 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
|
|
|
674
674
|
.duration(duration)
|
|
675
675
|
.addAnimation(wrapperAnimation);
|
|
676
676
|
if (presentingEl) {
|
|
677
|
-
const
|
|
677
|
+
const isPortrait = window.innerWidth < 768;
|
|
678
678
|
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
679
679
|
const presentingElRoot = getElementRoot(presentingEl);
|
|
680
680
|
const presentingAnimation = createAnimation()
|
|
@@ -692,7 +692,7 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
|
|
|
692
692
|
}
|
|
693
693
|
});
|
|
694
694
|
const bodyEl = document.body;
|
|
695
|
-
if (
|
|
695
|
+
if (isPortrait) {
|
|
696
696
|
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
697
697
|
const modalTransform = hasCardModal ? '-10px' : transformOffset;
|
|
698
698
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
@@ -739,6 +739,125 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
|
|
|
739
739
|
return baseAnimation;
|
|
740
740
|
};
|
|
741
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Transition animation from portrait view to landscape view
|
|
744
|
+
* This handles the case where a card modal is open in portrait view
|
|
745
|
+
* and the user switches to landscape view
|
|
746
|
+
*/
|
|
747
|
+
const portraitToLandscapeTransition = (baseEl, opts, duration = 300) => {
|
|
748
|
+
const { presentingEl } = opts;
|
|
749
|
+
if (!presentingEl) {
|
|
750
|
+
// No transition needed for non-card modals
|
|
751
|
+
return createAnimation('portrait-to-landscape-transition');
|
|
752
|
+
}
|
|
753
|
+
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
754
|
+
const presentingElRoot = getElementRoot(presentingEl);
|
|
755
|
+
const bodyEl = document.body;
|
|
756
|
+
const baseAnimation = createAnimation('portrait-to-landscape-transition')
|
|
757
|
+
.addElement(baseEl)
|
|
758
|
+
.easing('cubic-bezier(0.32,0.72,0,1)')
|
|
759
|
+
.duration(duration);
|
|
760
|
+
const presentingAnimation = createAnimation();
|
|
761
|
+
if (!hasCardModal) {
|
|
762
|
+
// Non-card modal: transition from portrait state to landscape state
|
|
763
|
+
// Portrait: presentingEl has transform and body has black background
|
|
764
|
+
// Landscape: no transform, no body background, modal wrapper opacity changes
|
|
765
|
+
const root = getElementRoot(baseEl);
|
|
766
|
+
const wrapperAnimation = createAnimation()
|
|
767
|
+
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
|
|
768
|
+
.fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
|
|
769
|
+
const backdropAnimation = createAnimation()
|
|
770
|
+
.addElement(root.querySelector('ion-backdrop'))
|
|
771
|
+
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
|
772
|
+
// Animate presentingEl from portrait state back to normal
|
|
773
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
774
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
775
|
+
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
776
|
+
presentingAnimation
|
|
777
|
+
.addElement(presentingEl)
|
|
778
|
+
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
|
|
779
|
+
.fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
|
|
780
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(1)')
|
|
781
|
+
.fromTo('border-radius', '10px 10px 0 0', '0px');
|
|
782
|
+
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
// Card modal: transition from portrait card state to landscape card state
|
|
786
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
787
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
788
|
+
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
789
|
+
const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
|
790
|
+
presentingAnimation
|
|
791
|
+
.addElement(presentingElRoot.querySelector('.modal-wrapper'))
|
|
792
|
+
.fromTo('transform', fromTransform, toTransform)
|
|
793
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
|
794
|
+
const shadowAnimation = createAnimation()
|
|
795
|
+
.addElement(presentingElRoot.querySelector('.modal-shadow'))
|
|
796
|
+
.fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
|
|
797
|
+
.fromTo('transform', fromTransform, toTransform);
|
|
798
|
+
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
|
799
|
+
}
|
|
800
|
+
return baseAnimation;
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Transition animation from landscape view to portrait view
|
|
804
|
+
* This handles the case where a card modal is open in landscape view
|
|
805
|
+
* and the user switches to portrait view
|
|
806
|
+
*/
|
|
807
|
+
const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
|
|
808
|
+
const { presentingEl } = opts;
|
|
809
|
+
if (!presentingEl) {
|
|
810
|
+
// No transition needed for non-card modals
|
|
811
|
+
return createAnimation('landscape-to-portrait-transition');
|
|
812
|
+
}
|
|
813
|
+
const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
|
|
814
|
+
const presentingElRoot = getElementRoot(presentingEl);
|
|
815
|
+
const bodyEl = document.body;
|
|
816
|
+
const baseAnimation = createAnimation('landscape-to-portrait-transition')
|
|
817
|
+
.addElement(baseEl)
|
|
818
|
+
.easing('cubic-bezier(0.32,0.72,0,1)')
|
|
819
|
+
.duration(duration);
|
|
820
|
+
const presentingAnimation = createAnimation();
|
|
821
|
+
if (!hasCardModal) {
|
|
822
|
+
// Non-card modal: transition from landscape state to portrait state
|
|
823
|
+
const root = getElementRoot(baseEl);
|
|
824
|
+
const wrapperAnimation = createAnimation()
|
|
825
|
+
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
|
|
826
|
+
.fromTo('opacity', '1', '1'); // Keep wrapper visible
|
|
827
|
+
const backdropAnimation = createAnimation()
|
|
828
|
+
.addElement(root.querySelector('ion-backdrop'))
|
|
829
|
+
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
|
830
|
+
// Animate presentingEl from normal state to portrait state
|
|
831
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
832
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
833
|
+
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
834
|
+
presentingAnimation
|
|
835
|
+
.addElement(presentingEl)
|
|
836
|
+
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
|
|
837
|
+
.fromTo('transform', 'translateY(0px) scale(1)', toTransform)
|
|
838
|
+
.fromTo('filter', 'contrast(1)', 'contrast(0.85)')
|
|
839
|
+
.fromTo('border-radius', '0px', '10px 10px 0 0');
|
|
840
|
+
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
// Card modal: transition from landscape card state to portrait card state
|
|
844
|
+
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
|
845
|
+
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
|
846
|
+
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
|
847
|
+
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
|
848
|
+
presentingAnimation
|
|
849
|
+
.addElement(presentingElRoot.querySelector('.modal-wrapper'))
|
|
850
|
+
.fromTo('transform', fromTransform, toTransform)
|
|
851
|
+
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
|
852
|
+
const shadowAnimation = createAnimation()
|
|
853
|
+
.addElement(presentingElRoot.querySelector('.modal-shadow'))
|
|
854
|
+
.fromTo('opacity', '0', '0') // Shadow stays hidden
|
|
855
|
+
.fromTo('transform', fromTransform, toTransform);
|
|
856
|
+
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
|
857
|
+
}
|
|
858
|
+
return baseAnimation;
|
|
859
|
+
};
|
|
860
|
+
|
|
742
861
|
const createEnterAnimation = () => {
|
|
743
862
|
const backdropAnimation = createAnimation()
|
|
744
863
|
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
|
@@ -1520,6 +1639,7 @@ const Modal = class {
|
|
|
1520
1639
|
}
|
|
1521
1640
|
disconnectedCallback() {
|
|
1522
1641
|
this.triggerController.removeClickListener();
|
|
1642
|
+
this.cleanupViewTransitionListener();
|
|
1523
1643
|
}
|
|
1524
1644
|
componentWillLoad() {
|
|
1525
1645
|
var _a;
|
|
@@ -1730,6 +1850,8 @@ const Modal = class {
|
|
|
1730
1850
|
else if (hasCardModal) {
|
|
1731
1851
|
this.initSwipeToClose();
|
|
1732
1852
|
}
|
|
1853
|
+
// Initialize view transition listener for iOS card modals
|
|
1854
|
+
this.initViewTransitionListener();
|
|
1733
1855
|
unlock();
|
|
1734
1856
|
}
|
|
1735
1857
|
initSwipeToClose() {
|
|
@@ -1883,6 +2005,7 @@ const Modal = class {
|
|
|
1883
2005
|
if (this.gesture) {
|
|
1884
2006
|
this.gesture.destroy();
|
|
1885
2007
|
}
|
|
2008
|
+
this.cleanupViewTransitionListener();
|
|
1886
2009
|
}
|
|
1887
2010
|
this.currentBreakpoint = undefined;
|
|
1888
2011
|
this.animation = undefined;
|
|
@@ -1958,6 +2081,67 @@ const Modal = class {
|
|
|
1958
2081
|
await this.setCurrentBreakpoint(nextBreakpoint);
|
|
1959
2082
|
return true;
|
|
1960
2083
|
}
|
|
2084
|
+
initViewTransitionListener() {
|
|
2085
|
+
// Only enable for iOS card modals when no custom animations are provided
|
|
2086
|
+
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
// Set initial view state
|
|
2090
|
+
this.currentViewIsPortrait = window.innerWidth < 768;
|
|
2091
|
+
// Create debounced resize handler
|
|
2092
|
+
let resizeTimeout;
|
|
2093
|
+
this.resizeListener = () => {
|
|
2094
|
+
clearTimeout(resizeTimeout);
|
|
2095
|
+
resizeTimeout = setTimeout(() => {
|
|
2096
|
+
this.handleViewTransition();
|
|
2097
|
+
}, 100); // Debounce for 100ms to avoid excessive calls
|
|
2098
|
+
};
|
|
2099
|
+
window.addEventListener('resize', this.resizeListener);
|
|
2100
|
+
}
|
|
2101
|
+
handleViewTransition() {
|
|
2102
|
+
const isPortrait = window.innerWidth < 768;
|
|
2103
|
+
// Only transition if view state actually changed
|
|
2104
|
+
if (this.currentViewIsPortrait === isPortrait) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
// Cancel any ongoing transition animation
|
|
2108
|
+
if (this.viewTransitionAnimation) {
|
|
2109
|
+
this.viewTransitionAnimation.destroy();
|
|
2110
|
+
this.viewTransitionAnimation = undefined;
|
|
2111
|
+
}
|
|
2112
|
+
const { presentingElement } = this;
|
|
2113
|
+
if (!presentingElement) {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
// Create transition animation
|
|
2117
|
+
let transitionAnimation;
|
|
2118
|
+
if (this.currentViewIsPortrait && !isPortrait) {
|
|
2119
|
+
// Portrait to landscape transition
|
|
2120
|
+
transitionAnimation = portraitToLandscapeTransition(this.el, {
|
|
2121
|
+
presentingEl: presentingElement});
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
// Landscape to portrait transition
|
|
2125
|
+
transitionAnimation = landscapeToPortraitTransition(this.el, {
|
|
2126
|
+
presentingEl: presentingElement});
|
|
2127
|
+
}
|
|
2128
|
+
// Update state and play animation
|
|
2129
|
+
this.currentViewIsPortrait = isPortrait;
|
|
2130
|
+
this.viewTransitionAnimation = transitionAnimation;
|
|
2131
|
+
transitionAnimation.play().then(() => {
|
|
2132
|
+
this.viewTransitionAnimation = undefined;
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
cleanupViewTransitionListener() {
|
|
2136
|
+
if (this.resizeListener) {
|
|
2137
|
+
window.removeEventListener('resize', this.resizeListener);
|
|
2138
|
+
this.resizeListener = undefined;
|
|
2139
|
+
}
|
|
2140
|
+
if (this.viewTransitionAnimation) {
|
|
2141
|
+
this.viewTransitionAnimation.destroy();
|
|
2142
|
+
this.viewTransitionAnimation = undefined;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1961
2145
|
render() {
|
|
1962
2146
|
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
|
|
1963
2147
|
const showHandle = handle !== false && isSheetModal;
|
|
@@ -1965,20 +2149,20 @@ const Modal = class {
|
|
|
1965
2149
|
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
|
1966
2150
|
const isHandleCycle = handleBehavior === 'cycle';
|
|
1967
2151
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
|
1968
|
-
return (h(Host, Object.assign({ key: '
|
|
2152
|
+
return (h(Host, Object.assign({ key: 'd5bcaa588471573e1bcbd99997b306c8ecdd124f', "no-router": true,
|
|
1969
2153
|
// Allow the modal to be navigable when the handle is focusable
|
|
1970
2154
|
tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
|
|
1971
2155
|
zIndex: `${20000 + this.overlayIndex}`,
|
|
1972
|
-
}, 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: '
|
|
2156
|
+
}, 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: '78b954a1c50a0325eb51db04314e86701c66d840', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '21887227c272f5eba1b3d164be3572bd710d415c', class: "modal-shadow" }), h("div", Object.assign({ key: 'a40472a9065c04ad12fb7b2b2a05e5021f077ad5',
|
|
1973
2157
|
/*
|
|
1974
2158
|
role and aria-modal must be used on the
|
|
1975
2159
|
same element. They must also be set inside the
|
|
1976
2160
|
shadow DOM otherwise ion-button will not be highlighted
|
|
1977
2161
|
when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
|
|
1978
2162
|
*/
|
|
1979
|
-
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '
|
|
2163
|
+
role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'a4973f66587546031d1cc0197e1ba7c2521a3fbc', class: "modal-handle",
|
|
1980
2164
|
// Prevents the handle from receiving keyboard focus when it does not cycle
|
|
1981
|
-
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: '
|
|
2165
|
+
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: 'ee5e007e3a4e592d98f1b5a5d31ad55e6faa3bdb' }))));
|
|
1982
2166
|
}
|
|
1983
2167
|
get el() { return getElement(this); }
|
|
1984
2168
|
static get watchers() { return {
|