@ionic/core 8.6.4-nightly.20250708 → 8.6.4

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,163 @@ 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 presentingElIsCardModal = 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 (!presentingElIsCardModal) {
766
+ // The presenting element is not a card modal, so we do not
767
+ // need to care about layering and modal-specific styles.
768
+ const root = getElementRoot(baseEl);
769
+ const wrapperAnimation = createAnimation()
770
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
771
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
772
+ const backdropAnimation = createAnimation()
773
+ .addElement(root.querySelector('ion-backdrop'))
774
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
775
+ // Animate presentingEl from portrait state back to normal
776
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
777
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
778
+ const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
779
+ presentingAnimation
780
+ .addElement(presentingEl)
781
+ .afterStyles({
782
+ transform: 'translateY(0px) scale(1)',
783
+ 'border-radius': '0px',
784
+ })
785
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
786
+ .fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
787
+ .fromTo('filter', 'contrast(0.85)', 'contrast(1)')
788
+ .fromTo('border-radius', '10px 10px 0 0', '0px');
789
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
790
+ }
791
+ else {
792
+ // The presenting element is a card modal, so we do
793
+ // need to care about layering and modal-specific styles.
794
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
795
+ const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
796
+ const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
797
+ presentingAnimation
798
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
799
+ .afterStyles({
800
+ transform: toTransform,
801
+ })
802
+ .fromTo('transform', fromTransform, toTransform)
803
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
804
+ const shadowAnimation = createAnimation()
805
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
806
+ .afterStyles({
807
+ transform: toTransform,
808
+ })
809
+ .fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
810
+ .fromTo('transform', fromTransform, toTransform);
811
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
812
+ }
813
+ return baseAnimation;
814
+ };
815
+ /**
816
+ * Transition animation from landscape view to portrait view
817
+ * This handles the case where a card modal is open in landscape view
818
+ * and the user switches to portrait view
819
+ */
820
+ const landscapeToPortraitTransition = (baseEl, opts, duration = 300) => {
821
+ const { presentingEl } = opts;
822
+ if (!presentingEl) {
823
+ // No transition needed for non-card modals
824
+ return createAnimation('landscape-to-portrait-transition');
825
+ }
826
+ const presentingElIsCardModal = presentingEl.tagName === 'ION-MODAL' && presentingEl.presentingElement !== undefined;
827
+ const presentingElRoot = getElementRoot(presentingEl);
828
+ const bodyEl = document.body;
829
+ const baseAnimation = createAnimation('landscape-to-portrait-transition')
830
+ .addElement(baseEl)
831
+ .easing('cubic-bezier(0.32,0.72,0,1)')
832
+ .duration(duration);
833
+ const presentingAnimation = createAnimation().beforeStyles({
834
+ transform: 'translateY(0)',
835
+ 'transform-origin': 'top center',
836
+ overflow: 'hidden',
837
+ });
838
+ if (!presentingElIsCardModal) {
839
+ // The presenting element is not a card modal, so we do not
840
+ // need to care about layering and modal-specific styles.
841
+ const root = getElementRoot(baseEl);
842
+ const wrapperAnimation = createAnimation()
843
+ .addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow'))
844
+ .fromTo('opacity', '1', '1'); // Keep wrapper visible
845
+ const backdropAnimation = createAnimation()
846
+ .addElement(root.querySelector('ion-backdrop'))
847
+ .fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
848
+ // Animate presentingEl from normal state to portrait state
849
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
850
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
851
+ const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
852
+ presentingAnimation
853
+ .addElement(presentingEl)
854
+ .beforeStyles({
855
+ transform: 'translateY(0px) scale(1)',
856
+ 'transform-origin': 'top center',
857
+ overflow: 'hidden',
858
+ })
859
+ .afterStyles({
860
+ transform: toTransform,
861
+ 'border-radius': '10px 10px 0 0',
862
+ filter: 'contrast(0.85)',
863
+ overflow: 'hidden',
864
+ 'transform-origin': 'top center',
865
+ })
866
+ .beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
867
+ .keyframes([
868
+ { offset: 0, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '0px' },
869
+ { offset: 0.2, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '10px 10px 0 0' },
870
+ { offset: 1, transform: toTransform, filter: 'contrast(0.85)', borderRadius: '10px 10px 0 0' },
871
+ ]);
872
+ baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
873
+ }
874
+ else {
875
+ // The presenting element is also a card modal, so we need
876
+ // to handle layering and modal-specific styles.
877
+ const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
878
+ const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
879
+ const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
880
+ presentingAnimation
881
+ .addElement(presentingElRoot.querySelector('.modal-wrapper'))
882
+ .afterStyles({
883
+ transform: toTransform,
884
+ })
885
+ .fromTo('transform', fromTransform, toTransform)
886
+ .fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
887
+ const shadowAnimation = createAnimation()
888
+ .addElement(presentingElRoot.querySelector('.modal-shadow'))
889
+ .afterStyles({
890
+ transform: toTransform,
891
+ })
892
+ .fromTo('opacity', '0', '0') // Shadow stays hidden
893
+ .fromTo('transform', fromTransform, toTransform);
894
+ baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
895
+ }
896
+ return baseAnimation;
897
+ };
898
+
742
899
  const createEnterAnimation = () => {
743
900
  const backdropAnimation = createAnimation()
744
901
  .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
@@ -1510,6 +1667,16 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1510
1667
  triggerController.addClickListener(el, trigger);
1511
1668
  }
1512
1669
  }
1670
+ onWindowResize() {
1671
+ // Only handle resize for iOS card modals when no custom animations are provided
1672
+ if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
1673
+ return;
1674
+ }
1675
+ clearTimeout(this.resizeTimeout);
1676
+ this.resizeTimeout = setTimeout(() => {
1677
+ this.handleViewTransition();
1678
+ }, 50); // Debounce to avoid excessive calls during active resizing
1679
+ }
1513
1680
  breakpointsChanged(breakpoints) {
1514
1681
  if (breakpoints !== undefined) {
1515
1682
  this.sortedBreakpoints = breakpoints.sort((a, b) => a - b);
@@ -1522,6 +1689,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1522
1689
  }
1523
1690
  disconnectedCallback() {
1524
1691
  this.triggerController.removeClickListener();
1692
+ this.cleanupViewTransitionListener();
1525
1693
  }
1526
1694
  componentWillLoad() {
1527
1695
  var _a;
@@ -1732,6 +1900,8 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1732
1900
  else if (hasCardModal) {
1733
1901
  this.initSwipeToClose();
1734
1902
  }
1903
+ // Initialize view transition listener for iOS card modals
1904
+ this.initViewTransitionListener();
1735
1905
  unlock();
1736
1906
  }
1737
1907
  initSwipeToClose() {
@@ -1885,6 +2055,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1885
2055
  if (this.gesture) {
1886
2056
  this.gesture.destroy();
1887
2057
  }
2058
+ this.cleanupViewTransitionListener();
1888
2059
  }
1889
2060
  this.currentBreakpoint = undefined;
1890
2061
  this.animation = undefined;
@@ -1960,6 +2131,108 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1960
2131
  await this.setCurrentBreakpoint(nextBreakpoint);
1961
2132
  return true;
1962
2133
  }
2134
+ initViewTransitionListener() {
2135
+ // Only enable for iOS card modals when no custom animations are provided
2136
+ if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
2137
+ return;
2138
+ }
2139
+ // Set initial view state
2140
+ this.currentViewIsPortrait = window.innerWidth < 768;
2141
+ }
2142
+ handleViewTransition() {
2143
+ const isPortrait = window.innerWidth < 768;
2144
+ // Only transition if view state actually changed
2145
+ if (this.currentViewIsPortrait === isPortrait) {
2146
+ return;
2147
+ }
2148
+ // Cancel any ongoing transition animation
2149
+ if (this.viewTransitionAnimation) {
2150
+ this.viewTransitionAnimation.destroy();
2151
+ this.viewTransitionAnimation = undefined;
2152
+ }
2153
+ const { presentingElement } = this;
2154
+ if (!presentingElement) {
2155
+ return;
2156
+ }
2157
+ // Create transition animation
2158
+ let transitionAnimation;
2159
+ if (this.currentViewIsPortrait && !isPortrait) {
2160
+ // Portrait to landscape transition
2161
+ transitionAnimation = portraitToLandscapeTransition(this.el, {
2162
+ presentingEl: presentingElement});
2163
+ }
2164
+ else {
2165
+ // Landscape to portrait transition
2166
+ transitionAnimation = landscapeToPortraitTransition(this.el, {
2167
+ presentingEl: presentingElement});
2168
+ }
2169
+ // Update state and play animation
2170
+ this.currentViewIsPortrait = isPortrait;
2171
+ this.viewTransitionAnimation = transitionAnimation;
2172
+ transitionAnimation.play().then(() => {
2173
+ this.viewTransitionAnimation = undefined;
2174
+ // After orientation transition, recreate the swipe-to-close gesture
2175
+ // with updated animation that reflects the new presenting element state
2176
+ this.reinitSwipeToClose();
2177
+ });
2178
+ }
2179
+ cleanupViewTransitionListener() {
2180
+ // Clear any pending resize timeout
2181
+ if (this.resizeTimeout) {
2182
+ clearTimeout(this.resizeTimeout);
2183
+ this.resizeTimeout = undefined;
2184
+ }
2185
+ if (this.viewTransitionAnimation) {
2186
+ this.viewTransitionAnimation.destroy();
2187
+ this.viewTransitionAnimation = undefined;
2188
+ }
2189
+ }
2190
+ reinitSwipeToClose() {
2191
+ // Only reinitialize if we have a presenting element and are on iOS
2192
+ if (getIonMode(this) !== 'ios' || !this.presentingElement) {
2193
+ return;
2194
+ }
2195
+ // Clean up existing gesture and animation
2196
+ if (this.gesture) {
2197
+ this.gesture.destroy();
2198
+ this.gesture = undefined;
2199
+ }
2200
+ if (this.animation) {
2201
+ // Properly end the progress-based animation at initial state before destroying
2202
+ // to avoid leaving modal in intermediate swipe position
2203
+ this.animation.progressEnd(0, 0, 0);
2204
+ this.animation.destroy();
2205
+ this.animation = undefined;
2206
+ }
2207
+ // Force the modal back to the correct position or it could end up
2208
+ // in a weird state after destroying the animation
2209
+ raf(() => {
2210
+ this.ensureCorrectModalPosition();
2211
+ this.initSwipeToClose();
2212
+ });
2213
+ }
2214
+ ensureCorrectModalPosition() {
2215
+ const { el, presentingElement } = this;
2216
+ const root = getElementRoot(el);
2217
+ const wrapperEl = root.querySelector('.modal-wrapper');
2218
+ if (wrapperEl) {
2219
+ wrapperEl.style.transform = 'translateY(0vh)';
2220
+ wrapperEl.style.opacity = '1';
2221
+ }
2222
+ if (presentingElement) {
2223
+ const isPortrait = window.innerWidth < 768;
2224
+ if (isPortrait) {
2225
+ const transformOffset = !CSS.supports('width', 'max(0px, 1px)')
2226
+ ? '30px'
2227
+ : 'max(30px, var(--ion-safe-area-top))';
2228
+ const scale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
2229
+ presentingElement.style.transform = `translateY(${transformOffset}) scale(${scale})`;
2230
+ }
2231
+ else {
2232
+ presentingElement.style.transform = 'translateY(0px) scale(1)';
2233
+ }
2234
+ }
2235
+ }
1963
2236
  render() {
1964
2237
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
1965
2238
  const showHandle = handle !== false && isSheetModal;
@@ -1967,20 +2240,20 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
1967
2240
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1968
2241
  const isHandleCycle = handleBehavior === 'cycle';
1969
2242
  const isSheetModalWithHandle = isSheetModal && showHandle;
1970
- return (h(Host, Object.assign({ key: '8add05bb43a2cdb5e3cf180147d31eb85a018fe0', "no-router": true,
2243
+ return (h(Host, Object.assign({ key: '1980fa23331381c568a2be8091d888e09754fc52', "no-router": true,
1971
2244
  // Allow the modal to be navigable when the handle is focusable
1972
2245
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1973
2246
  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',
2247
+ }, 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: 'ba94b055c064e2907eabbe6d7a43cb52adff1048', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '991f47859250d2143275ebb9b0b01a6ea8c491c0', class: "modal-shadow" }), h("div", Object.assign({ key: '02ecf8ac6a5bdb309ff993cc74a3911e99502a89',
1975
2248
  /*
1976
2249
  role and aria-modal must be used on the
1977
2250
  same element. They must also be set inside the
1978
2251
  shadow DOM otherwise ion-button will not be highlighted
1979
2252
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1980
2253
  */
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",
2254
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '0180a4d6952e41bfd736272d1a49d47d86ca7fef', class: "modal-handle",
1982
2255
  // 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' }))));
2256
+ 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: 'd062f330675f730ad70c23267baed200ca9b43b0' }))));
1984
2257
  }
1985
2258
  get el() { return this; }
1986
2259
  static get watchers() { return {
@@ -2024,7 +2297,7 @@ const Modal = /*@__PURE__*/ proxyCustomElement(class Modal extends HTMLElement {
2024
2297
  "onWillDismiss": [64],
2025
2298
  "setCurrentBreakpoint": [64],
2026
2299
  "getCurrentBreakpoint": [64]
2027
- }, undefined, {
2300
+ }, [[9, "resize", "onWindowResize"]], {
2028
2301
  "isOpen": ["onIsOpenChange"],
2029
2302
  "trigger": ["triggerChanged"]
2030
2303
  }]);