@ionic/core 8.6.3-dev.11750971489.140836b0 → 8.6.3-dev.11751315648.1da06a67

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,140 @@
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().beforeStyles({
26
+ transform: 'translateY(0)',
27
+ 'transform-origin': 'top center',
28
+ overflow: 'hidden',
29
+ });
30
+ if (!hasCardModal) {
31
+ // Non-card modal: transition from portrait state to landscape state
32
+ // Portrait: presentingEl has transform and body has black background
33
+ // Landscape: no transform, no body background, modal wrapper opacity changes
34
+ const root = getElementRoot(baseEl);
35
+ const wrapperAnimation = createAnimation()
36
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
37
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
38
+ const backdropAnimation = createAnimation()
39
+ .addElement(root.querySelector('ion-backdrop'))
40
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
41
+ // Animate presentingEl from portrait state back to normal
42
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
43
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
44
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
45
+ presentingAnimation
46
+ .addElement(presentingEl)
47
+ .afterStyles({
48
+ transform: 'translateY(0px) scale(1)',
49
+ 'border-radius': '0px',
50
+ })
51
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
52
+ .fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
53
+ .fromTo('filter', 'contrast(0.85)', 'contrast(1)')
54
+ .fromTo('border-radius', '10px 10px 0 0', '0px');
55
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
56
+ }
57
+ else {
58
+ // Card modal: transition from portrait card state to landscape card state
59
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
60
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
61
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
62
+ const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
63
+ presentingAnimation
64
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
65
+ .fromTo('transform', fromTransform, toTransform)
66
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
67
+ const shadowAnimation = createAnimation()
68
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
69
+ .fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
70
+ .fromTo('transform', fromTransform, toTransform);
71
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
72
+ }
73
+ return baseAnimation;
74
+ };
75
+ /**
76
+ * Transition animation from landscape view to portrait view
77
+ * This handles the case where a card modal is open in landscape view
78
+ * and the user switches to portrait view
79
+ */
80
+ export const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
81
+ const { presentingEl } = opts;
82
+ if (!presentingEl) {
83
+ // No transition needed for non-card modals
84
+ return createAnimation('landscape-to-portrait-transition');
85
+ }
86
+ const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
87
+ const presentingElRoot = getElementRoot(presentingEl);
88
+ const bodyEl = document.body;
89
+ const baseAnimation = createAnimation('landscape-to-portrait-transition')
90
+ .addElement(baseEl)
91
+ .easing('cubic-bezier(0.32,0.72,0,1)')
92
+ .duration(duration);
93
+ const presentingAnimation = createAnimation().beforeStyles({
94
+ transform: 'translateY(0)',
95
+ 'transform-origin': 'top center',
96
+ overflow: 'hidden',
97
+ });
98
+ if (!hasCardModal) {
99
+ // Non-card modal: transition from landscape state to portrait state
100
+ const root = getElementRoot(baseEl);
101
+ const wrapperAnimation = createAnimation()
102
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
103
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible
104
+ const backdropAnimation = createAnimation()
105
+ .addElement(root.querySelector('ion-backdrop'))
106
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
107
+ // Animate presentingEl from normal state to portrait state
108
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
109
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
110
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
111
+ presentingAnimation
112
+ .addElement(presentingEl)
113
+ .afterStyles({
114
+ transform: toTransform,
115
+ 'border-radius': '10px 10px 0 0',
116
+ })
117
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
118
+ .fromTo('transform', 'translateY(0px) scale(1)', toTransform)
119
+ .fromTo('filter', 'contrast(1)', 'contrast(0.85)')
120
+ .fromTo('border-radius', '0px', '10px 10px 0 0');
121
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
122
+ }
123
+ else {
124
+ // Card modal: transition from landscape card state to portrait card state
125
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
126
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
127
+ const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
128
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
129
+ presentingAnimation
130
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
131
+ .fromTo('transform', fromTransform, toTransform)
132
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
133
+ const shadowAnimation = createAnimation()
134
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
135
+ .fromTo('opacity', '0', '0') // Shadow stays hidden
136
+ .fromTo('transform', fromTransform, toTransform);
137
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
138
+ }
139
+ return baseAnimation;
140
+ };
@@ -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-30T20:35:47",
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,141 @@ 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().beforeStyles({
761
+ transform: 'translateY(0)',
762
+ 'transform-origin': 'top center',
763
+ overflow: 'hidden',
764
+ });
765
+ if (!hasCardModal) {
766
+ // Non-card modal: transition from portrait state to landscape state
767
+ // Portrait: presentingEl has transform and body has black background
768
+ // Landscape: no transform, no body background, modal wrapper opacity changes
769
+ const root = getElementRoot(baseEl);
770
+ const wrapperAnimation = createAnimation()
771
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
772
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
773
+ const backdropAnimation = createAnimation()
774
+ .addElement(root.querySelector('ion-backdrop'))
775
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
776
+ // Animate presentingEl from portrait state back to normal
777
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
778
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
779
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
780
+ presentingAnimation
781
+ .addElement(presentingEl)
782
+ .afterStyles({
783
+ transform: 'translateY(0px) scale(1)',
784
+ 'border-radius': '0px',
785
+ })
786
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
787
+ .fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
788
+ .fromTo('filter', 'contrast(0.85)', 'contrast(1)')
789
+ .fromTo('border-radius', '10px 10px 0 0', '0px');
790
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
791
+ }
792
+ else {
793
+ // Card modal: transition from portrait card state to landscape card state
794
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
795
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
796
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
797
+ const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
798
+ presentingAnimation
799
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
800
+ .fromTo('transform', fromTransform, toTransform)
801
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
802
+ const shadowAnimation = createAnimation()
803
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
804
+ .fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
805
+ .fromTo('transform', fromTransform, toTransform);
806
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
807
+ }
808
+ return baseAnimation;
809
+ };
810
+ /**
811
+ * Transition animation from landscape view to portrait view
812
+ * This handles the case where a card modal is open in landscape view
813
+ * and the user switches to portrait view
814
+ */
815
+ const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
816
+ const { presentingEl } = opts;
817
+ if (!presentingEl) {
818
+ // No transition needed for non-card modals
819
+ return createAnimation('landscape-to-portrait-transition');
820
+ }
821
+ const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
822
+ const presentingElRoot = getElementRoot(presentingEl);
823
+ const bodyEl = document.body;
824
+ const baseAnimation = createAnimation('landscape-to-portrait-transition')
825
+ .addElement(baseEl)
826
+ .easing('cubic-bezier(0.32,0.72,0,1)')
827
+ .duration(duration);
828
+ const presentingAnimation = createAnimation().beforeStyles({
829
+ transform: 'translateY(0)',
830
+ 'transform-origin': 'top center',
831
+ overflow: 'hidden',
832
+ });
833
+ if (!hasCardModal) {
834
+ // Non-card modal: transition from landscape state to portrait state
835
+ const root = getElementRoot(baseEl);
836
+ const wrapperAnimation = createAnimation()
837
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
838
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible
839
+ const backdropAnimation = createAnimation()
840
+ .addElement(root.querySelector('ion-backdrop'))
841
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
842
+ // Animate presentingEl from normal state to portrait state
843
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
844
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
845
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
846
+ presentingAnimation
847
+ .addElement(presentingEl)
848
+ .afterStyles({
849
+ transform: toTransform,
850
+ 'border-radius': '10px 10px 0 0',
851
+ })
852
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
853
+ .fromTo('transform', 'translateY(0px) scale(1)', toTransform)
854
+ .fromTo('filter', 'contrast(1)', 'contrast(0.85)')
855
+ .fromTo('border-radius', '0px', '10px 10px 0 0');
856
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
857
+ }
858
+ else {
859
+ // Card modal: transition from landscape card state to portrait card state
860
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
861
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
862
+ const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
863
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
864
+ presentingAnimation
865
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
866
+ .fromTo('transform', fromTransform, toTransform)
867
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
868
+ const shadowAnimation = createAnimation()
869
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
870
+ .fromTo('opacity', '0', '0') // Shadow stays hidden
871
+ .fromTo('transform', fromTransform, toTransform);
872
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
873
+ }
874
+ return baseAnimation;
875
+ };
876
+
742
877
  const createEnterAnimation = () => {
743
878
  const backdropAnimation = createAnimation()
744
879
  .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
@@ -1520,6 +1655,7 @@ const Modal = class {
1520
1655
  }
1521
1656
  disconnectedCallback() {
1522
1657
  this.triggerController.removeClickListener();
1658
+ this.cleanupViewTransitionListener();
1523
1659
  }
1524
1660
  componentWillLoad() {
1525
1661
  var _a;
@@ -1730,6 +1866,8 @@ const Modal = class {
1730
1866
  else if (hasCardModal) {
1731
1867
  this.initSwipeToClose();
1732
1868
  }
1869
+ // Initialize view transition listener for iOS card modals
1870
+ this.initViewTransitionListener();
1733
1871
  unlock();
1734
1872
  }
1735
1873
  initSwipeToClose() {
@@ -1883,6 +2021,7 @@ const Modal = class {
1883
2021
  if (this.gesture) {
1884
2022
  this.gesture.destroy();
1885
2023
  }
2024
+ this.cleanupViewTransitionListener();
1886
2025
  }
1887
2026
  this.currentBreakpoint = undefined;
1888
2027
  this.animation = undefined;
@@ -1958,6 +2097,67 @@ const Modal = class {
1958
2097
  await this.setCurrentBreakpoint(nextBreakpoint);
1959
2098
  return true;
1960
2099
  }
2100
+ initViewTransitionListener() {
2101
+ // Only enable for iOS card modals when no custom animations are provided
2102
+ if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
2103
+ return;
2104
+ }
2105
+ // Set initial view state
2106
+ this.currentViewIsPortrait = window.innerWidth < 768;
2107
+ // Create debounced resize handler
2108
+ let resizeTimeout;
2109
+ this.resizeListener = () => {
2110
+ clearTimeout(resizeTimeout);
2111
+ resizeTimeout = setTimeout(() => {
2112
+ this.handleViewTransition();
2113
+ }, 100); // Debounce for 100ms to avoid excessive calls
2114
+ };
2115
+ window.addEventListener('resize', this.resizeListener);
2116
+ }
2117
+ handleViewTransition() {
2118
+ const isPortrait = window.innerWidth < 768;
2119
+ // Only transition if view state actually changed
2120
+ if (this.currentViewIsPortrait === isPortrait) {
2121
+ return;
2122
+ }
2123
+ // Cancel any ongoing transition animation
2124
+ if (this.viewTransitionAnimation) {
2125
+ this.viewTransitionAnimation.destroy();
2126
+ this.viewTransitionAnimation = undefined;
2127
+ }
2128
+ const { presentingElement } = this;
2129
+ if (!presentingElement) {
2130
+ return;
2131
+ }
2132
+ // Create transition animation
2133
+ let transitionAnimation;
2134
+ if (this.currentViewIsPortrait && !isPortrait) {
2135
+ // Portrait to landscape transition
2136
+ transitionAnimation = portraitToLandscapeTransition(this.el, {
2137
+ presentingEl: presentingElement});
2138
+ }
2139
+ else {
2140
+ // Landscape to portrait transition
2141
+ transitionAnimation = landscapeToPortraitTransition(this.el, {
2142
+ presentingEl: presentingElement});
2143
+ }
2144
+ // Update state and play animation
2145
+ this.currentViewIsPortrait = isPortrait;
2146
+ this.viewTransitionAnimation = transitionAnimation;
2147
+ transitionAnimation.play().then(() => {
2148
+ this.viewTransitionAnimation = undefined;
2149
+ });
2150
+ }
2151
+ cleanupViewTransitionListener() {
2152
+ if (this.resizeListener) {
2153
+ window.removeEventListener('resize', this.resizeListener);
2154
+ this.resizeListener = undefined;
2155
+ }
2156
+ if (this.viewTransitionAnimation) {
2157
+ this.viewTransitionAnimation.destroy();
2158
+ this.viewTransitionAnimation = undefined;
2159
+ }
2160
+ }
1961
2161
  render() {
1962
2162
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
1963
2163
  const showHandle = handle !== false && isSheetModal;
@@ -1965,20 +2165,20 @@ const Modal = class {
1965
2165
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1966
2166
  const isHandleCycle = handleBehavior === 'cycle';
1967
2167
  const isSheetModalWithHandle = isSheetModal && showHandle;
1968
- return (h(Host, Object.assign({ key: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
2168
+ return (h(Host, Object.assign({ key: 'd5bcaa588471573e1bcbd99997b306c8ecdd124f', "no-router": true,
1969
2169
  // Allow the modal to be navigable when the handle is focusable
1970
2170
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1971
2171
  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',
2172
+ }, 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
2173
  /*
1974
2174
  role and aria-modal must be used on the
1975
2175
  same element. They must also be set inside the
1976
2176
  shadow DOM otherwise ion-button will not be highlighted
1977
2177
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1978
2178
  */
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",
2179
+ 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
2180
  // 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' }))));
2181
+ 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
2182
  }
1983
2183
  get el() { return getElement(this); }
1984
2184
  static get watchers() { return {