@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.
@@ -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)')
@@ -1522,6 +1657,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1522
1657
  }
1523
1658
  disconnectedCallback() {
1524
1659
  this.triggerController.removeClickListener();
1660
+ this.cleanupViewTransitionListener();
1525
1661
  }
1526
1662
  componentWillLoad() {
1527
1663
  var _a;
@@ -1732,6 +1868,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1732
1868
  else if (hasCardModal) {
1733
1869
  this.initSwipeToClose();
1734
1870
  }
1871
+ // Initialize view transition listener for iOS card modals
1872
+ this.initViewTransitionListener();
1735
1873
  unlock();
1736
1874
  }
1737
1875
  initSwipeToClose() {
@@ -1885,6 +2023,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1885
2023
  if (this.gesture) {
1886
2024
  this.gesture.destroy();
1887
2025
  }
2026
+ this.cleanupViewTransitionListener();
1888
2027
  }
1889
2028
  this.currentBreakpoint = undefined;
1890
2029
  this.animation = undefined;
@@ -1960,6 +2099,67 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1960
2099
  await this.setCurrentBreakpoint(nextBreakpoint);
1961
2100
  return true;
1962
2101
  }
2102
+ initViewTransitionListener() {
2103
+ // Only enable for iOS card modals when no custom animations are provided
2104
+ if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
2105
+ return;
2106
+ }
2107
+ // Set initial view state
2108
+ this.currentViewIsPortrait = window.innerWidth < 768;
2109
+ // Create debounced resize handler
2110
+ let resizeTimeout;
2111
+ this.resizeListener = () => {
2112
+ clearTimeout(resizeTimeout);
2113
+ resizeTimeout = setTimeout(() => {
2114
+ this.handleViewTransition();
2115
+ }, 100); // Debounce for 100ms to avoid excessive calls
2116
+ };
2117
+ window.addEventListener('resize', this.resizeListener);
2118
+ }
2119
+ handleViewTransition() {
2120
+ const isPortrait = window.innerWidth < 768;
2121
+ // Only transition if view state actually changed
2122
+ if (this.currentViewIsPortrait === isPortrait) {
2123
+ return;
2124
+ }
2125
+ // Cancel any ongoing transition animation
2126
+ if (this.viewTransitionAnimation) {
2127
+ this.viewTransitionAnimation.destroy();
2128
+ this.viewTransitionAnimation = undefined;
2129
+ }
2130
+ const { presentingElement } = this;
2131
+ if (!presentingElement) {
2132
+ return;
2133
+ }
2134
+ // Create transition animation
2135
+ let transitionAnimation;
2136
+ if (this.currentViewIsPortrait && !isPortrait) {
2137
+ // Portrait to landscape transition
2138
+ transitionAnimation = portraitToLandscapeTransition(this.el, {
2139
+ presentingEl: presentingElement});
2140
+ }
2141
+ else {
2142
+ // Landscape to portrait transition
2143
+ transitionAnimation = landscapeToPortraitTransition(this.el, {
2144
+ presentingEl: presentingElement});
2145
+ }
2146
+ // Update state and play animation
2147
+ this.currentViewIsPortrait = isPortrait;
2148
+ this.viewTransitionAnimation = transitionAnimation;
2149
+ transitionAnimation.play().then(() => {
2150
+ this.viewTransitionAnimation = undefined;
2151
+ });
2152
+ }
2153
+ cleanupViewTransitionListener() {
2154
+ if (this.resizeListener) {
2155
+ window.removeEventListener('resize', this.resizeListener);
2156
+ this.resizeListener = undefined;
2157
+ }
2158
+ if (this.viewTransitionAnimation) {
2159
+ this.viewTransitionAnimation.destroy();
2160
+ this.viewTransitionAnimation = undefined;
2161
+ }
2162
+ }
1963
2163
  render() {
1964
2164
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
1965
2165
  const showHandle = handle !== false && isSheetModal;
@@ -1967,20 +2167,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1967
2167
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1968
2168
  const isHandleCycle = handleBehavior === 'cycle';
1969
2169
  const isSheetModalWithHandle = isSheetModal && showHandle;
1970
- return (h(Host, Object.assign({ key: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
2170
+ return (h(Host, Object.assign({ key: 'd5bcaa588471573e1bcbd99997b306c8ecdd124f', "no-router": true,
1971
2171
  // Allow the modal to be navigable when the handle is focusable
1972
2172
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1973
2173
  zIndex: `${20000 + this.overlayIndex}`,
1974
- }, 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',
2174
+ }, 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',
1975
2175
  /*
1976
2176
  role and aria-modal must be used on the
1977
2177
  same element. They must also be set inside the
1978
2178
  shadow DOM otherwise ion-button will not be highlighted
1979
2179
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1980
2180
  */
1981
- 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",
2181
+ 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",
1982
2182
  // Prevents the handle from receiving keyboard focus when it does not cycle
1983
- 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' }))));
2183
+ 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' }))));
1984
2184
  }
1985
2185
  get el() { return this; }
1986
2186
  static get watchers() { return {
@@ -589,7 +589,7 @@ const iosEnterAnimation = (baseEl, opts) => {
589
589
  baseAnimation.addAnimation(contentAnimation);
590
590
  }
591
591
  if (presentingEl) {
592
- const isMobile = window.innerWidth < 768;
592
+ const isPortrait = window.innerWidth < 768;
593
593
  const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
594
594
  const presentingElRoot = helpers.getElementRoot(presentingEl);
595
595
  const presentingAnimation = animation.createAnimation().beforeStyles({
@@ -598,7 +598,7 @@ const iosEnterAnimation = (baseEl, opts) => {
598
598
  overflow: 'hidden',
599
599
  });
600
600
  const bodyEl = document.body;
601
- if (isMobile) {
601
+ if (isPortrait) {
602
602
  /**
603
603
  * Fallback for browsers that does not support `max()` (ex: Firefox)
604
604
  * No need to worry about statusbar padding since engines like Gecko
@@ -676,7 +676,7 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
676
676
  .duration(duration)
677
677
  .addAnimation(wrapperAnimation);
678
678
  if (presentingEl) {
679
- const isMobile = window.innerWidth < 768;
679
+ const isPortrait = window.innerWidth < 768;
680
680
  const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
681
681
  const presentingElRoot = helpers.getElementRoot(presentingEl);
682
682
  const presentingAnimation = animation.createAnimation()
@@ -694,7 +694,7 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
694
694
  }
695
695
  });
696
696
  const bodyEl = document.body;
697
- if (isMobile) {
697
+ if (isPortrait) {
698
698
  const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
699
699
  const modalTransform = hasCardModal ? '-10px' : transformOffset;
700
700
  const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
@@ -741,6 +741,141 @@ const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
741
741
  return baseAnimation;
742
742
  };
743
743
 
744
+ /**
745
+ * Transition animation from portrait view to landscape view
746
+ * This handles the case where a card modal is open in portrait view
747
+ * and the user switches to landscape view
748
+ */
749
+ const portraitToLandscapeTransition = (baseEl, opts, duration = 300) => {
750
+ const { presentingEl } = opts;
751
+ if (!presentingEl) {
752
+ // No transition needed for non-card modals
753
+ return animation.createAnimation('portrait-to-landscape-transition');
754
+ }
755
+ const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
756
+ const presentingElRoot = helpers.getElementRoot(presentingEl);
757
+ const bodyEl = document.body;
758
+ const baseAnimation = animation.createAnimation('portrait-to-landscape-transition')
759
+ .addElement(baseEl)
760
+ .easing('cubic-bezier(0.32,0.72,0,1)')
761
+ .duration(duration);
762
+ const presentingAnimation = animation.createAnimation().beforeStyles({
763
+ transform: 'translateY(0)',
764
+ 'transform-origin': 'top center',
765
+ overflow: 'hidden',
766
+ });
767
+ if (!hasCardModal) {
768
+ // Non-card modal: transition from portrait state to landscape state
769
+ // Portrait: presentingEl has transform and body has black background
770
+ // Landscape: no transform, no body background, modal wrapper opacity changes
771
+ const root = helpers.getElementRoot(baseEl);
772
+ const wrapperAnimation = animation.createAnimation()
773
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
774
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
775
+ const backdropAnimation = animation.createAnimation()
776
+ .addElement(root.querySelector('ion-backdrop'))
777
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
778
+ // Animate presentingEl from portrait state back to normal
779
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
780
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
781
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
782
+ presentingAnimation
783
+ .addElement(presentingEl)
784
+ .afterStyles({
785
+ transform: 'translateY(0px) scale(1)',
786
+ 'border-radius': '0px',
787
+ })
788
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
789
+ .fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
790
+ .fromTo('filter', 'contrast(0.85)', 'contrast(1)')
791
+ .fromTo('border-radius', '10px 10px 0 0', '0px');
792
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
793
+ }
794
+ else {
795
+ // Card modal: transition from portrait card state to landscape card state
796
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
797
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
798
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
799
+ const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
800
+ presentingAnimation
801
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
802
+ .fromTo('transform', fromTransform, toTransform)
803
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
804
+ const shadowAnimation = animation.createAnimation()
805
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
806
+ .fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
807
+ .fromTo('transform', fromTransform, toTransform);
808
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
809
+ }
810
+ return baseAnimation;
811
+ };
812
+ /**
813
+ * Transition animation from landscape view to portrait view
814
+ * This handles the case where a card modal is open in landscape view
815
+ * and the user switches to portrait view
816
+ */
817
+ const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
818
+ const { presentingEl } = opts;
819
+ if (!presentingEl) {
820
+ // No transition needed for non-card modals
821
+ return animation.createAnimation('landscape-to-portrait-transition');
822
+ }
823
+ const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
824
+ const presentingElRoot = helpers.getElementRoot(presentingEl);
825
+ const bodyEl = document.body;
826
+ const baseAnimation = animation.createAnimation('landscape-to-portrait-transition')
827
+ .addElement(baseEl)
828
+ .easing('cubic-bezier(0.32,0.72,0,1)')
829
+ .duration(duration);
830
+ const presentingAnimation = animation.createAnimation().beforeStyles({
831
+ transform: 'translateY(0)',
832
+ 'transform-origin': 'top center',
833
+ overflow: 'hidden',
834
+ });
835
+ if (!hasCardModal) {
836
+ // Non-card modal: transition from landscape state to portrait state
837
+ const root = helpers.getElementRoot(baseEl);
838
+ const wrapperAnimation = animation.createAnimation()
839
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
840
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible
841
+ const backdropAnimation = animation.createAnimation()
842
+ .addElement(root.querySelector('ion-backdrop'))
843
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
844
+ // Animate presentingEl from normal state to portrait state
845
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
846
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
847
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
848
+ presentingAnimation
849
+ .addElement(presentingEl)
850
+ .afterStyles({
851
+ transform: toTransform,
852
+ 'border-radius': '10px 10px 0 0',
853
+ })
854
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
855
+ .fromTo('transform', 'translateY(0px) scale(1)', toTransform)
856
+ .fromTo('filter', 'contrast(1)', 'contrast(0.85)')
857
+ .fromTo('border-radius', '0px', '10px 10px 0 0');
858
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
859
+ }
860
+ else {
861
+ // Card modal: transition from landscape card state to portrait card state
862
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
863
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
864
+ const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
865
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
866
+ presentingAnimation
867
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
868
+ .fromTo('transform', fromTransform, toTransform)
869
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
870
+ const shadowAnimation = animation.createAnimation()
871
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
872
+ .fromTo('opacity', '0', '0') // Shadow stays hidden
873
+ .fromTo('transform', fromTransform, toTransform);
874
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
875
+ }
876
+ return baseAnimation;
877
+ };
878
+
744
879
  const createEnterAnimation = () => {
745
880
  const backdropAnimation = animation.createAnimation()
746
881
  .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
@@ -1522,6 +1657,7 @@ const Modal = class {
1522
1657
  }
1523
1658
  disconnectedCallback() {
1524
1659
  this.triggerController.removeClickListener();
1660
+ this.cleanupViewTransitionListener();
1525
1661
  }
1526
1662
  componentWillLoad() {
1527
1663
  var _a;
@@ -1732,6 +1868,8 @@ const Modal = class {
1732
1868
  else if (hasCardModal) {
1733
1869
  this.initSwipeToClose();
1734
1870
  }
1871
+ // Initialize view transition listener for iOS card modals
1872
+ this.initViewTransitionListener();
1735
1873
  unlock();
1736
1874
  }
1737
1875
  initSwipeToClose() {
@@ -1885,6 +2023,7 @@ const Modal = class {
1885
2023
  if (this.gesture) {
1886
2024
  this.gesture.destroy();
1887
2025
  }
2026
+ this.cleanupViewTransitionListener();
1888
2027
  }
1889
2028
  this.currentBreakpoint = undefined;
1890
2029
  this.animation = undefined;
@@ -1960,6 +2099,67 @@ const Modal = class {
1960
2099
  await this.setCurrentBreakpoint(nextBreakpoint);
1961
2100
  return true;
1962
2101
  }
2102
+ initViewTransitionListener() {
2103
+ // Only enable for iOS card modals when no custom animations are provided
2104
+ if (index$3.getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
2105
+ return;
2106
+ }
2107
+ // Set initial view state
2108
+ this.currentViewIsPortrait = window.innerWidth < 768;
2109
+ // Create debounced resize handler
2110
+ let resizeTimeout;
2111
+ this.resizeListener = () => {
2112
+ clearTimeout(resizeTimeout);
2113
+ resizeTimeout = setTimeout(() => {
2114
+ this.handleViewTransition();
2115
+ }, 100); // Debounce for 100ms to avoid excessive calls
2116
+ };
2117
+ window.addEventListener('resize', this.resizeListener);
2118
+ }
2119
+ handleViewTransition() {
2120
+ const isPortrait = window.innerWidth < 768;
2121
+ // Only transition if view state actually changed
2122
+ if (this.currentViewIsPortrait === isPortrait) {
2123
+ return;
2124
+ }
2125
+ // Cancel any ongoing transition animation
2126
+ if (this.viewTransitionAnimation) {
2127
+ this.viewTransitionAnimation.destroy();
2128
+ this.viewTransitionAnimation = undefined;
2129
+ }
2130
+ const { presentingElement } = this;
2131
+ if (!presentingElement) {
2132
+ return;
2133
+ }
2134
+ // Create transition animation
2135
+ let transitionAnimation;
2136
+ if (this.currentViewIsPortrait && !isPortrait) {
2137
+ // Portrait to landscape transition
2138
+ transitionAnimation = portraitToLandscapeTransition(this.el, {
2139
+ presentingEl: presentingElement});
2140
+ }
2141
+ else {
2142
+ // Landscape to portrait transition
2143
+ transitionAnimation = landscapeToPortraitTransition(this.el, {
2144
+ presentingEl: presentingElement});
2145
+ }
2146
+ // Update state and play animation
2147
+ this.currentViewIsPortrait = isPortrait;
2148
+ this.viewTransitionAnimation = transitionAnimation;
2149
+ transitionAnimation.play().then(() => {
2150
+ this.viewTransitionAnimation = undefined;
2151
+ });
2152
+ }
2153
+ cleanupViewTransitionListener() {
2154
+ if (this.resizeListener) {
2155
+ window.removeEventListener('resize', this.resizeListener);
2156
+ this.resizeListener = undefined;
2157
+ }
2158
+ if (this.viewTransitionAnimation) {
2159
+ this.viewTransitionAnimation.destroy();
2160
+ this.viewTransitionAnimation = undefined;
2161
+ }
2162
+ }
1963
2163
  render() {
1964
2164
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
1965
2165
  const showHandle = handle !== false && isSheetModal;
@@ -1967,20 +2167,20 @@ const Modal = class {
1967
2167
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1968
2168
  const isHandleCycle = handleBehavior === 'cycle';
1969
2169
  const isSheetModalWithHandle = isSheetModal && showHandle;
1970
- return (index$3.h(index$3.Host, Object.assign({ key: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
2170
+ return (index$3.h(index$3.Host, Object.assign({ key: 'd5bcaa588471573e1bcbd99997b306c8ecdd124f', "no-router": true,
1971
2171
  // Allow the modal to be navigable when the handle is focusable
1972
2172
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1973
2173
  zIndex: `${20000 + this.overlayIndex}`,
1974
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '90a6605a9564a699d6f66cf71cf6b506796a2963', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'a97d071395333bf803c0a9347bda000cf7500d8d', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'e7b7985c7414a13e3ba8dcecf497b76e92edf53e',
2174
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '78b954a1c50a0325eb51db04314e86701c66d840', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '21887227c272f5eba1b3d164be3572bd710d415c', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'a40472a9065c04ad12fb7b2b2a05e5021f077ad5',
1975
2175
  /*
1976
2176
  role and aria-modal must be used on the
1977
2177
  same element. They must also be set inside the
1978
2178
  shadow DOM otherwise ion-button will not be highlighted
1979
2179
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1980
2180
  */
1981
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '8258b65570b11a8ee9c9df2537d6419cd2e34536', class: "modal-handle",
2181
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: 'a4973f66587546031d1cc0197e1ba7c2521a3fbc', class: "modal-handle",
1982
2182
  // Prevents the handle from receiving keyboard focus when it does not cycle
1983
- 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) })), index$3.h("slot", { key: '394370d0ed03ee03152f8f8abae7ff7664ca5c13' }))));
2183
+ 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) })), index$3.h("slot", { key: 'ee5e007e3a4e592d98f1b5a5d31ad55e6faa3bdb' }))));
1984
2184
  }
1985
2185
  get el() { return index$3.getElement(this); }
1986
2186
  static get watchers() { return {
@@ -36,7 +36,7 @@ export const iosEnterAnimation = (baseEl, opts) => {
36
36
  baseAnimation.addAnimation(contentAnimation);
37
37
  }
38
38
  if (presentingEl) {
39
- const isMobile = window.innerWidth < 768;
39
+ const isPortrait = window.innerWidth < 768;
40
40
  const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
41
41
  const presentingElRoot = getElementRoot(presentingEl);
42
42
  const presentingAnimation = createAnimation().beforeStyles({
@@ -45,7 +45,7 @@ export const iosEnterAnimation = (baseEl, opts) => {
45
45
  overflow: 'hidden',
46
46
  });
47
47
  const bodyEl = document.body;
48
- if (isMobile) {
48
+ if (isPortrait) {
49
49
  /**
50
50
  * Fallback for browsers that does not support `max()` (ex: Firefox)
51
51
  * No need to worry about statusbar padding since engines like Gecko
@@ -25,7 +25,7 @@ export const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
25
25
  .duration(duration)
26
26
  .addAnimation(wrapperAnimation);
27
27
  if (presentingEl) {
28
- const isMobile = window.innerWidth < 768;
28
+ const isPortrait = window.innerWidth < 768;
29
29
  const hasCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
30
30
  const presentingElRoot = getElementRoot(presentingEl);
31
31
  const presentingAnimation = createAnimation()
@@ -43,7 +43,7 @@ export const iosLeaveAnimation = (baseEl, opts, duration = 500) => {
43
43
  }
44
44
  });
45
45
  const bodyEl = document.body;
46
- if (isMobile) {
46
+ if (isPortrait) {
47
47
  const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
48
48
  const modalTransform = hasCardModal ? '-10px' : transformOffset;
49
49
  const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;