mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 15:51:16 +08:00
fix(modal): support iOS card view transitions for viewport changes (#30520)
Issue number: resolves #30296 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Currently, there is no support for moving between an iOS card view (mobile, portrait modal with presenting element) to a non-card view when the resolution changes (e.g., the device goes from a portrait layout to landscape). This causes issues both way because modals that should be card modals when the user transitions to a portrait view stay as non-card modals and modals that were card modals when they were opened but the user goes to landscape view end up with a black box stuck around the edges of the screen. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> With this change, we now fully support transitioning between the two modal views when the resolution changes. This should fix the issue where the background could become stuck and should be a nicer experience for users switching between the two orientations while using modals. I also took the time to clean up the terminology in use here to refer to "mobile view" (as it was meant here) to be portrait view and the other view to be referred to as landscape view. I did this because I had accidentally mixed them up while working on this and I had to do a refactor to fix it, so I'm hoping that by clarifying the terminology now it helps prevent similar mistakes for others in the future. ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> [Relevant test screen](https://ionic-framework-git-fw-6596-ionic1.vercel.app/src/components/modal/test/card?ionic:mode=ios) Dev build: `8.6.3-dev.11751378808.12cc4a5c`
This commit is contained in:
@ -48,7 +48,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (presentingEl) {
|
if (presentingEl) {
|
||||||
const isMobile = window.innerWidth < 768;
|
const isPortrait = window.innerWidth < 768;
|
||||||
const hasCardModal =
|
const hasCardModal =
|
||||||
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||||
const presentingElRoot = getElementRoot(presentingEl);
|
const presentingElRoot = getElementRoot(presentingEl);
|
||||||
@ -61,7 +61,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
|||||||
|
|
||||||
const bodyEl = document.body;
|
const bodyEl = document.body;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isPortrait) {
|
||||||
/**
|
/**
|
||||||
* Fallback for browsers that does not support `max()` (ex: Firefox)
|
* Fallback for browsers that does not support `max()` (ex: Firefox)
|
||||||
* No need to worry about statusbar padding since engines like Gecko
|
* No need to worry about statusbar padding since engines like Gecko
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
|||||||
.addAnimation(wrapperAnimation);
|
.addAnimation(wrapperAnimation);
|
||||||
|
|
||||||
if (presentingEl) {
|
if (presentingEl) {
|
||||||
const isMobile = window.innerWidth < 768;
|
const isPortrait = window.innerWidth < 768;
|
||||||
const hasCardModal =
|
const hasCardModal =
|
||||||
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||||
const presentingElRoot = getElementRoot(presentingEl);
|
const presentingElRoot = getElementRoot(presentingEl);
|
||||||
@ -61,7 +61,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
|||||||
|
|
||||||
const bodyEl = document.body;
|
const bodyEl = document.body;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isPortrait) {
|
||||||
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
||||||
const modalTransform = hasCardModal ? '-10px' : transformOffset;
|
const modalTransform = hasCardModal ? '-10px' : transformOffset;
|
||||||
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
|||||||
198
core/src/components/modal/animations/ios.transition.ts
Normal file
198
core/src/components/modal/animations/ios.transition.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import { createAnimation } from '@utils/animation/animation';
|
||||||
|
import { getElementRoot } from '@utils/helpers';
|
||||||
|
|
||||||
|
import type { Animation } from '../../../interface';
|
||||||
|
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
|
||||||
|
import type { ModalAnimationOptions } from '../modal-interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition animation from portrait view to landscape view
|
||||||
|
* This handles the case where a card modal is open in portrait view
|
||||||
|
* and the user switches to landscape view
|
||||||
|
*/
|
||||||
|
export const portraitToLandscapeTransition = (
|
||||||
|
baseEl: HTMLElement,
|
||||||
|
opts: ModalAnimationOptions,
|
||||||
|
duration = 300
|
||||||
|
): Animation => {
|
||||||
|
const { presentingEl } = opts;
|
||||||
|
|
||||||
|
if (!presentingEl) {
|
||||||
|
// No transition needed for non-card modals
|
||||||
|
return createAnimation('portrait-to-landscape-transition');
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentingElIsCardModal =
|
||||||
|
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||||
|
const presentingElRoot = getElementRoot(presentingEl);
|
||||||
|
const bodyEl = document.body;
|
||||||
|
|
||||||
|
const baseAnimation = createAnimation('portrait-to-landscape-transition')
|
||||||
|
.addElement(baseEl)
|
||||||
|
.easing('cubic-bezier(0.32,0.72,0,1)')
|
||||||
|
.duration(duration);
|
||||||
|
|
||||||
|
const presentingAnimation = createAnimation().beforeStyles({
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
'transform-origin': 'top center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!presentingElIsCardModal) {
|
||||||
|
// The presenting element is not a card modal, so we do not
|
||||||
|
// need to care about layering and modal-specific styles.
|
||||||
|
const root = getElementRoot(baseEl);
|
||||||
|
const wrapperAnimation = createAnimation()
|
||||||
|
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||||
|
.fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
|
||||||
|
|
||||||
|
const backdropAnimation = createAnimation()
|
||||||
|
.addElement(root.querySelector('ion-backdrop')!)
|
||||||
|
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
||||||
|
|
||||||
|
// Animate presentingEl from portrait state back to normal
|
||||||
|
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
||||||
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
||||||
|
|
||||||
|
presentingAnimation
|
||||||
|
.addElement(presentingEl)
|
||||||
|
.afterStyles({
|
||||||
|
transform: 'translateY(0px) scale(1)',
|
||||||
|
'border-radius': '0px',
|
||||||
|
})
|
||||||
|
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
|
||||||
|
.fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
|
||||||
|
.fromTo('filter', 'contrast(0.85)', 'contrast(1)')
|
||||||
|
.fromTo('border-radius', '10px 10px 0 0', '0px');
|
||||||
|
|
||||||
|
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
||||||
|
} else {
|
||||||
|
// The presenting element is a card modal, so we do
|
||||||
|
// need to care about layering and modal-specific styles.
|
||||||
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
||||||
|
const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
||||||
|
|
||||||
|
presentingAnimation
|
||||||
|
.addElement(presentingElRoot.querySelector('.modal-wrapper')!)
|
||||||
|
.afterStyles({
|
||||||
|
transform: toTransform,
|
||||||
|
})
|
||||||
|
.fromTo('transform', fromTransform, toTransform)
|
||||||
|
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
||||||
|
|
||||||
|
const shadowAnimation = createAnimation()
|
||||||
|
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
|
||||||
|
.afterStyles({
|
||||||
|
transform: toTransform,
|
||||||
|
})
|
||||||
|
.fromTo('opacity', '0', '0') // Shadow stays hidden in landscape for card modals
|
||||||
|
.fromTo('transform', fromTransform, toTransform);
|
||||||
|
|
||||||
|
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseAnimation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition animation from landscape view to portrait view
|
||||||
|
* This handles the case where a card modal is open in landscape view
|
||||||
|
* and the user switches to portrait view
|
||||||
|
*/
|
||||||
|
export const landscapeToPortraitTransition = (
|
||||||
|
baseEl: HTMLElement,
|
||||||
|
opts: ModalAnimationOptions,
|
||||||
|
duration = 300
|
||||||
|
): Animation => {
|
||||||
|
const { presentingEl } = opts;
|
||||||
|
|
||||||
|
if (!presentingEl) {
|
||||||
|
// No transition needed for non-card modals
|
||||||
|
return createAnimation('landscape-to-portrait-transition');
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentingElIsCardModal =
|
||||||
|
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||||
|
const presentingElRoot = getElementRoot(presentingEl);
|
||||||
|
const bodyEl = document.body;
|
||||||
|
|
||||||
|
const baseAnimation = createAnimation('landscape-to-portrait-transition')
|
||||||
|
.addElement(baseEl)
|
||||||
|
.easing('cubic-bezier(0.32,0.72,0,1)')
|
||||||
|
.duration(duration);
|
||||||
|
|
||||||
|
const presentingAnimation = createAnimation().beforeStyles({
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
'transform-origin': 'top center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!presentingElIsCardModal) {
|
||||||
|
// The presenting element is not a card modal, so we do not
|
||||||
|
// need to care about layering and modal-specific styles.
|
||||||
|
const root = getElementRoot(baseEl);
|
||||||
|
const wrapperAnimation = createAnimation()
|
||||||
|
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||||
|
.fromTo('opacity', '1', '1'); // Keep wrapper visible
|
||||||
|
|
||||||
|
const backdropAnimation = createAnimation()
|
||||||
|
.addElement(root.querySelector('ion-backdrop')!)
|
||||||
|
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
|
||||||
|
|
||||||
|
// Animate presentingEl from normal state to portrait state
|
||||||
|
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
|
||||||
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
|
||||||
|
|
||||||
|
presentingAnimation
|
||||||
|
.addElement(presentingEl)
|
||||||
|
.beforeStyles({
|
||||||
|
transform: 'translateY(0px) scale(1)',
|
||||||
|
'transform-origin': 'top center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
})
|
||||||
|
.afterStyles({
|
||||||
|
transform: toTransform,
|
||||||
|
'border-radius': '10px 10px 0 0',
|
||||||
|
filter: 'contrast(0.85)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'transform-origin': 'top center',
|
||||||
|
})
|
||||||
|
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
|
||||||
|
.keyframes([
|
||||||
|
{ offset: 0, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '0px' },
|
||||||
|
{ offset: 0.2, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '10px 10px 0 0' },
|
||||||
|
{ offset: 1, transform: toTransform, filter: 'contrast(0.85)', borderRadius: '10px 10px 0 0' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
|
||||||
|
} else {
|
||||||
|
// The presenting element is also a card modal, so we need
|
||||||
|
// to handle layering and modal-specific styles.
|
||||||
|
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
||||||
|
const toTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
||||||
|
|
||||||
|
presentingAnimation
|
||||||
|
.addElement(presentingElRoot.querySelector('.modal-wrapper')!)
|
||||||
|
.afterStyles({
|
||||||
|
transform: toTransform,
|
||||||
|
})
|
||||||
|
.fromTo('transform', fromTransform, toTransform)
|
||||||
|
.fromTo('filter', 'contrast(0.85)', 'contrast(0.85)'); // Keep same contrast for card
|
||||||
|
|
||||||
|
const shadowAnimation = createAnimation()
|
||||||
|
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
|
||||||
|
.afterStyles({
|
||||||
|
transform: toTransform,
|
||||||
|
})
|
||||||
|
.fromTo('opacity', '0', '0') // Shadow stays hidden
|
||||||
|
.fromTo('transform', fromTransform, toTransform);
|
||||||
|
|
||||||
|
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseAnimation;
|
||||||
|
};
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
|
import { Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
|
||||||
import { findIonContent, printIonContentErrorMsg } from '@utils/content';
|
import { findIonContent, printIonContentErrorMsg } from '@utils/content';
|
||||||
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
|
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
|
||||||
import { raf, inheritAttributes, hasLazyBuild } from '@utils/helpers';
|
import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from '@utils/helpers';
|
||||||
import type { Attributes } from '@utils/helpers';
|
import type { Attributes } from '@utils/helpers';
|
||||||
import { createLockController } from '@utils/lock-controller';
|
import { createLockController } from '@utils/lock-controller';
|
||||||
import { printIonWarning } from '@utils/logging';
|
import { printIonWarning } from '@utils/logging';
|
||||||
@ -37,11 +37,12 @@ import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
|||||||
|
|
||||||
import { iosEnterAnimation } from './animations/ios.enter';
|
import { iosEnterAnimation } from './animations/ios.enter';
|
||||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||||
|
import { portraitToLandscapeTransition, landscapeToPortraitTransition } from './animations/ios.transition';
|
||||||
import { mdEnterAnimation } from './animations/md.enter';
|
import { mdEnterAnimation } from './animations/md.enter';
|
||||||
import { mdLeaveAnimation } from './animations/md.leave';
|
import { mdLeaveAnimation } from './animations/md.leave';
|
||||||
import type { MoveSheetToBreakpointOptions } from './gestures/sheet';
|
import type { MoveSheetToBreakpointOptions } from './gestures/sheet';
|
||||||
import { createSheetGesture } from './gestures/sheet';
|
import { createSheetGesture } from './gestures/sheet';
|
||||||
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
|
import { createSwipeToCloseGesture, SwipeToCloseDefaults } from './gestures/swipe-to-close';
|
||||||
import type { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from './modal-interface';
|
import type { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from './modal-interface';
|
||||||
import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
|
import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
|
||||||
|
|
||||||
@ -90,6 +91,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
// Whether or not modal is being dismissed via gesture
|
// Whether or not modal is being dismissed via gesture
|
||||||
private gestureAnimationDismissing = false;
|
private gestureAnimationDismissing = false;
|
||||||
|
|
||||||
|
// View transition properties for handling portrait/landscape switches
|
||||||
|
private currentViewIsPortrait?: boolean;
|
||||||
|
private viewTransitionAnimation?: Animation;
|
||||||
|
private resizeTimeout?: any;
|
||||||
|
|
||||||
lastFocus?: HTMLElement;
|
lastFocus?: HTMLElement;
|
||||||
animation?: Animation;
|
animation?: Animation;
|
||||||
|
|
||||||
@ -261,6 +267,19 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Listen('resize', { target: 'window' })
|
||||||
|
onWindowResize() {
|
||||||
|
// Only handle resize for iOS card modals when no custom animations are provided
|
||||||
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(this.resizeTimeout);
|
||||||
|
this.resizeTimeout = setTimeout(() => {
|
||||||
|
this.handleViewTransition();
|
||||||
|
}, 50); // Debounce to avoid excessive calls during active resizing
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `true`, the component passed into `ion-modal` will
|
* If `true`, the component passed into `ion-modal` will
|
||||||
* automatically be mounted when the modal is created. The
|
* automatically be mounted when the modal is created. The
|
||||||
@ -378,6 +397,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.triggerController.removeClickListener();
|
this.triggerController.removeClickListener();
|
||||||
|
this.cleanupViewTransitionListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
@ -619,6 +639,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
this.initSwipeToClose();
|
this.initSwipeToClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize view transition listener for iOS card modals
|
||||||
|
this.initViewTransitionListener();
|
||||||
|
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,6 +839,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
if (this.gesture) {
|
if (this.gesture) {
|
||||||
this.gesture.destroy();
|
this.gesture.destroy();
|
||||||
}
|
}
|
||||||
|
this.cleanupViewTransitionListener();
|
||||||
}
|
}
|
||||||
this.currentBreakpoint = undefined;
|
this.currentBreakpoint = undefined;
|
||||||
this.animation = undefined;
|
this.animation = undefined;
|
||||||
@ -963,6 +987,134 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private initViewTransitionListener() {
|
||||||
|
// Only enable for iOS card modals when no custom animations are provided
|
||||||
|
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial view state
|
||||||
|
this.currentViewIsPortrait = window.innerWidth < 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleViewTransition() {
|
||||||
|
const isPortrait = window.innerWidth < 768;
|
||||||
|
|
||||||
|
// Only transition if view state actually changed
|
||||||
|
if (this.currentViewIsPortrait === isPortrait) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any ongoing transition animation
|
||||||
|
if (this.viewTransitionAnimation) {
|
||||||
|
this.viewTransitionAnimation.destroy();
|
||||||
|
this.viewTransitionAnimation = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { presentingElement } = this;
|
||||||
|
if (!presentingElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transition animation
|
||||||
|
let transitionAnimation: Animation;
|
||||||
|
if (this.currentViewIsPortrait && !isPortrait) {
|
||||||
|
// Portrait to landscape transition
|
||||||
|
transitionAnimation = portraitToLandscapeTransition(this.el, {
|
||||||
|
presentingEl: presentingElement,
|
||||||
|
currentBreakpoint: this.currentBreakpoint,
|
||||||
|
backdropBreakpoint: this.backdropBreakpoint,
|
||||||
|
expandToScroll: this.expandToScroll,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Landscape to portrait transition
|
||||||
|
transitionAnimation = landscapeToPortraitTransition(this.el, {
|
||||||
|
presentingEl: presentingElement,
|
||||||
|
currentBreakpoint: this.currentBreakpoint,
|
||||||
|
backdropBreakpoint: this.backdropBreakpoint,
|
||||||
|
expandToScroll: this.expandToScroll,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state and play animation
|
||||||
|
this.currentViewIsPortrait = isPortrait;
|
||||||
|
this.viewTransitionAnimation = transitionAnimation;
|
||||||
|
|
||||||
|
transitionAnimation.play().then(() => {
|
||||||
|
this.viewTransitionAnimation = undefined;
|
||||||
|
|
||||||
|
// After orientation transition, recreate the swipe-to-close gesture
|
||||||
|
// with updated animation that reflects the new presenting element state
|
||||||
|
this.reinitSwipeToClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupViewTransitionListener() {
|
||||||
|
// Clear any pending resize timeout
|
||||||
|
if (this.resizeTimeout) {
|
||||||
|
clearTimeout(this.resizeTimeout);
|
||||||
|
this.resizeTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.viewTransitionAnimation) {
|
||||||
|
this.viewTransitionAnimation.destroy();
|
||||||
|
this.viewTransitionAnimation = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reinitSwipeToClose() {
|
||||||
|
// Only reinitialize if we have a presenting element and are on iOS
|
||||||
|
if (getIonMode(this) !== 'ios' || !this.presentingElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up existing gesture and animation
|
||||||
|
if (this.gesture) {
|
||||||
|
this.gesture.destroy();
|
||||||
|
this.gesture = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.animation) {
|
||||||
|
// Properly end the progress-based animation at initial state before destroying
|
||||||
|
// to avoid leaving modal in intermediate swipe position
|
||||||
|
this.animation.progressEnd(0, 0, 0);
|
||||||
|
this.animation.destroy();
|
||||||
|
this.animation = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force the modal back to the correct position or it could end up
|
||||||
|
// in a weird state after destroying the animation
|
||||||
|
raf(() => {
|
||||||
|
this.ensureCorrectModalPosition();
|
||||||
|
this.initSwipeToClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCorrectModalPosition() {
|
||||||
|
const { el, presentingElement } = this;
|
||||||
|
const root = getElementRoot(el);
|
||||||
|
|
||||||
|
const wrapperEl = root.querySelector('.modal-wrapper') as HTMLElement | null;
|
||||||
|
if (wrapperEl) {
|
||||||
|
wrapperEl.style.transform = 'translateY(0vh)';
|
||||||
|
wrapperEl.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentingElement) {
|
||||||
|
const isPortrait = window.innerWidth < 768;
|
||||||
|
|
||||||
|
if (isPortrait) {
|
||||||
|
const transformOffset = !CSS.supports('width', 'max(0px, 1px)')
|
||||||
|
? '30px'
|
||||||
|
: 'max(30px, var(--ion-safe-area-top))';
|
||||||
|
const scale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
|
||||||
|
presentingElement.style.transform = `translateY(${transformOffset}) scale(${scale})`;
|
||||||
|
} else {
|
||||||
|
presentingElement.style.transform = 'translateY(0px) scale(1)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
handle,
|
handle,
|
||||||
|
|||||||
Reference in New Issue
Block a user