mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
chore(merge): merging main into next
This commit is contained in:
@ -1,6 +1,9 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('item-sliding: async'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@ -35,5 +38,85 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
await expect(itemSlidingEl).toHaveClass(/item-sliding-active-slide/);
|
||||
});
|
||||
|
||||
// NOTE: This test uses the CDN version of Ionic.
|
||||
// If this test fails, it is likely due to a regression in the published package.
|
||||
test('should not throw errors when adding multiple items with side="end" using the Ionic CDN', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/29499',
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
page.on('pageerror', (error) => {
|
||||
errors.push(error.message);
|
||||
});
|
||||
|
||||
// This issue only happens when using a CDN version of Ionic
|
||||
// so we need to use the CDN by passing the `importIonicFromCDN` option
|
||||
// to setContent.
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Item Sliding</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button id="addItem" onclick="addItem()">ADD ITEM</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list id="list"></ion-list>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
let itemList = [];
|
||||
function generateItem() {
|
||||
const currentItem = itemList.length + 1;
|
||||
const item = \`
|
||||
<ion-item-sliding>
|
||||
<ion-item>
|
||||
<ion-label>Sliding Item \${currentItem}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-options side="end">
|
||||
<ion-item-option>Delete</ion-item-option>
|
||||
</ion-item-options>
|
||||
</ion-item-sliding>
|
||||
\`;
|
||||
itemList.push(item);
|
||||
return item;
|
||||
}
|
||||
function addItem() {
|
||||
const list = document.getElementById('list');
|
||||
list.innerHTML += generateItem();
|
||||
const currentItem = itemList.length;
|
||||
}
|
||||
</script>
|
||||
`,
|
||||
{ ...config, importIonicFromCDN: true }
|
||||
);
|
||||
|
||||
// Click the button enough times to reproduce the issue
|
||||
const addButton = page.locator('#addItem');
|
||||
await addButton.click();
|
||||
await addButton.click();
|
||||
await addButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// Check that the items have been added
|
||||
const items = page.locator('ion-item-sliding');
|
||||
expect(await items.count()).toBe(3);
|
||||
|
||||
// Check that no errors have been logged
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,7 +48,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
}
|
||||
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const isPortrait = window.innerWidth < 768;
|
||||
const hasCardModal =
|
||||
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||
const presentingElRoot = getElementRoot(presentingEl);
|
||||
@ -61,7 +61,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
|
||||
const bodyEl = document.body;
|
||||
|
||||
if (isMobile) {
|
||||
if (isPortrait) {
|
||||
/**
|
||||
* Fallback for browsers that does not support `max()` (ex: Firefox)
|
||||
* No need to worry about statusbar padding since engines like Gecko
|
||||
|
||||
@ -35,7 +35,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
.addAnimation(wrapperAnimation);
|
||||
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const isPortrait = window.innerWidth < 768;
|
||||
const hasCardModal =
|
||||
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
|
||||
const presentingElRoot = getElementRoot(presentingEl);
|
||||
@ -61,7 +61,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
|
||||
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 modalTransform = hasCardModal ? '-10px' : transformOffset;
|
||||
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 { 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 { 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 { createLockController } from '@utils/lock-controller';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
@ -37,11 +37,12 @@ import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
|
||||
import { iosEnterAnimation } from './animations/ios.enter';
|
||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
import { portraitToLandscapeTransition, landscapeToPortraitTransition } from './animations/ios.transition';
|
||||
import { mdEnterAnimation } from './animations/md.enter';
|
||||
import { mdLeaveAnimation } from './animations/md.leave';
|
||||
import type { MoveSheetToBreakpointOptions } 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 { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
|
||||
|
||||
@ -92,6 +93,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
// Whether or not modal is being dismissed via gesture
|
||||
private gestureAnimationDismissing = false;
|
||||
|
||||
// View transition properties for handling portrait/landscape switches
|
||||
private currentViewIsPortrait?: boolean;
|
||||
private viewTransitionAnimation?: Animation;
|
||||
private resizeTimeout?: any;
|
||||
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
|
||||
@ -263,6 +269,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
|
||||
* automatically be mounted when the modal is created. The
|
||||
@ -389,6 +408,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
disconnectedCallback() {
|
||||
this.triggerController.removeClickListener();
|
||||
this.cleanupViewTransitionListener();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
@ -631,6 +651,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
this.initSwipeToClose();
|
||||
}
|
||||
|
||||
// Initialize view transition listener for iOS card modals
|
||||
this.initViewTransitionListener();
|
||||
|
||||
unlock();
|
||||
}
|
||||
|
||||
@ -830,6 +853,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
}
|
||||
this.cleanupViewTransitionListener();
|
||||
}
|
||||
this.currentBreakpoint = undefined;
|
||||
this.animation = undefined;
|
||||
@ -993,6 +1017,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() {
|
||||
const {
|
||||
handle,
|
||||
|
||||
Reference in New Issue
Block a user