@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.
@@ -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: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
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: '90a6605a9564a699d6f66cf71cf6b506796a2963', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'a97d071395333bf803c0a9347bda000cf7500d8d', class: "modal-shadow" }), h("div", Object.assign({ key: 'e7b7985c7414a13e3ba8dcecf497b76e92edf53e',
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: '8258b65570b11a8ee9c9df2537d6419cd2e34536', class: "modal-handle",
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: '394370d0ed03ee03152f8f8abae7ff7664ca5c13' }))));
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2025-06-26T21:00:10",
2
+ "timestamp": "2025-06-30T17:52:09",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.33.1",
@@ -587,7 +587,7 @@ const iosEnterAnimation = (baseEl, opts) => {
587
587
  baseAnimation.addAnimation(contentAnimation);
588
588
  }
589
589
  if (presentingEl) {
590
- const isMobile = window.innerWidth < 768;
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 (isMobile) {
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 isMobile = window.innerWidth < 768;
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 (isMobile) {
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: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
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: '90a6605a9564a699d6f66cf71cf6b506796a2963', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'a97d071395333bf803c0a9347bda000cf7500d8d', class: "modal-shadow" }), h("div", Object.assign({ key: 'e7b7985c7414a13e3ba8dcecf497b76e92edf53e',
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: '8258b65570b11a8ee9c9df2537d6419cd2e34536', class: "modal-handle",
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: '394370d0ed03ee03152f8f8abae7ff7664ca5c13' }))));
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 {