fix(modal): card modal can now be swiped to close on the content (#25185)
resolves #22046
@@ -1,5 +1,6 @@
|
||||
import type { Animation } from '../../../interface';
|
||||
import { getTimeGivenProgression } from '../../../utils/animation/cubic-bezier';
|
||||
import { isIonContent } from '../../../utils/content';
|
||||
import type { GestureDetail } from '../../../utils/gesture';
|
||||
import { createGesture } from '../../../utils/gesture';
|
||||
import { clamp } from '../../../utils/helpers';
|
||||
@@ -11,11 +12,46 @@ export const SwipeToCloseDefaults = {
|
||||
MIN_PRESENTING_SCALE: 0.93,
|
||||
};
|
||||
|
||||
export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: Animation, onDismiss: () => void) => {
|
||||
export const createSwipeToCloseGesture = (
|
||||
el: HTMLIonModalElement,
|
||||
contentEl: HTMLElement,
|
||||
scrollEl: HTMLElement,
|
||||
animation: Animation,
|
||||
onDismiss: () => void
|
||||
) => {
|
||||
const height = el.offsetHeight;
|
||||
let isOpen = false;
|
||||
let canDismissBlocksGesture = false;
|
||||
const canDismissMaxStep = 0.2;
|
||||
const getScrollY = () => {
|
||||
if (isIonContent(contentEl)) {
|
||||
return (contentEl as HTMLIonContentElement).scrollY;
|
||||
/**
|
||||
* Custom scroll containers are intended to be
|
||||
* used with virtual scrolling, so we assume
|
||||
* there is scrolling in this case.
|
||||
*/
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const initialScrollY = getScrollY();
|
||||
|
||||
const disableContentScroll = () => {
|
||||
if (isIonContent(contentEl)) {
|
||||
(contentEl as HTMLIonContentElement).scrollY = false;
|
||||
} else {
|
||||
contentEl.style.setProperty('overflow', 'hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetContentScroll = () => {
|
||||
if (isIonContent(contentEl)) {
|
||||
(contentEl as HTMLIonContentElement).scrollY = initialScrollY;
|
||||
} else {
|
||||
contentEl.style.removeProperty('overflow');
|
||||
}
|
||||
};
|
||||
|
||||
const canStart = (detail: GestureDetail) => {
|
||||
const target = detail.event.target as HTMLElement | null;
|
||||
@@ -24,17 +60,32 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
|
||||
return true;
|
||||
}
|
||||
|
||||
const contentOrFooter = target.closest('ion-content, ion-footer');
|
||||
if (contentOrFooter === null) {
|
||||
/**
|
||||
* If we are swiping on the content,
|
||||
* swiping should only be possible if
|
||||
* the content is scrolled all the way
|
||||
* to the top so that we do not interfere
|
||||
* with scrolling.
|
||||
*/
|
||||
const content = target.closest('ion-content');
|
||||
if (content) {
|
||||
return scrollEl.scrollTop === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Card should be swipeable on all
|
||||
* parts of the modal except for the footer.
|
||||
*/
|
||||
const footer = target.closest('ion-footer');
|
||||
if (footer === null) {
|
||||
return true;
|
||||
}
|
||||
// Target is in the content or the footer so do not start the gesture.
|
||||
// We could be more nuanced here and allow it for content that
|
||||
// does not need to scroll.
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onStart = () => {
|
||||
const onStart = (detail: GestureDetail) => {
|
||||
const { deltaY } = detail;
|
||||
/**
|
||||
* If canDismiss is anything other than `true`
|
||||
* then users should be able to swipe down
|
||||
@@ -43,11 +94,46 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
|
||||
* TODO (FW-937)
|
||||
* Remove undefined check
|
||||
*/
|
||||
|
||||
canDismissBlocksGesture = el.canDismiss !== undefined && el.canDismiss !== true;
|
||||
|
||||
/**
|
||||
* If we are pulling down, then
|
||||
* it is possible we are pulling on the
|
||||
* content. We do not want scrolling to
|
||||
* happen at the same time as the gesture.
|
||||
*/
|
||||
if (deltaY > 0) {
|
||||
disableContentScroll();
|
||||
}
|
||||
|
||||
animation.progressStart(true, isOpen ? 1 : 0);
|
||||
};
|
||||
|
||||
const onMove = (detail: GestureDetail) => {
|
||||
const { deltaY } = detail;
|
||||
|
||||
/**
|
||||
* If we are pulling down, then
|
||||
* it is possible we are pulling on the
|
||||
* content. We do not want scrolling to
|
||||
* happen at the same time as the gesture.
|
||||
*/
|
||||
if (deltaY > 0) {
|
||||
disableContentScroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are swiping on the content
|
||||
* then the swipe gesture should only
|
||||
* happen if we are pulling down.
|
||||
*
|
||||
* However, if we pull up and
|
||||
* then down such that the scroll position
|
||||
* returns to 0, we should be able to swipe
|
||||
* the card.
|
||||
*/
|
||||
|
||||
const step = detail.deltaY / height;
|
||||
|
||||
/**
|
||||
@@ -117,6 +203,8 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An
|
||||
|
||||
gesture.enable(false);
|
||||
|
||||
resetContentScroll();
|
||||
|
||||
animation
|
||||
.onFinish(() => {
|
||||
if (!shouldComplete) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
OverlayEventDetail,
|
||||
OverlayInterface,
|
||||
} from '../../interface';
|
||||
import { getScrollElement, findIonContent, printIonContentErrorMsg } from '../../utils/content';
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { raf } from '../../utils/helpers';
|
||||
import { KEYBOARD_DID_OPEN } from '../../utils/keyboard/keyboard';
|
||||
@@ -283,11 +284,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
@Watch('swipeToClose')
|
||||
swipeToCloseChanged(enable: boolean) {
|
||||
async swipeToCloseChanged(enable: boolean) {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(enable);
|
||||
} else if (enable) {
|
||||
this.initSwipeToClose();
|
||||
await this.initSwipeToClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +475,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
* not run canDismiss on swipe as there would be no swipe gesture created.
|
||||
*/
|
||||
} else if (this.swipeToClose || (this.canDismiss !== undefined && this.presentingElement !== undefined)) {
|
||||
this.initSwipeToClose();
|
||||
await this.initSwipeToClose();
|
||||
}
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
@@ -504,17 +505,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
this.currentTransition = undefined;
|
||||
}
|
||||
|
||||
private initSwipeToClose() {
|
||||
private async initSwipeToClose() {
|
||||
if (getIonMode(this) !== 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { el } = this;
|
||||
|
||||
// All of the elements needed for the swipe gesture
|
||||
// should be in the DOM and referenced by now, except
|
||||
// for the presenting el
|
||||
const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
|
||||
const ani = (this.animation = animationBuilder(this.el, { presentingEl: this.presentingElement }));
|
||||
this.gesture = createSwipeToCloseGesture(this.el, ani, () => {
|
||||
const ani = (this.animation = animationBuilder(el, { presentingEl: this.presentingElement }));
|
||||
|
||||
const contentEl = findIonContent(el);
|
||||
if (!contentEl) {
|
||||
printIonContentErrorMsg(el);
|
||||
return;
|
||||
}
|
||||
const scrollEl = await getScrollElement(contentEl);
|
||||
|
||||
this.gesture = createSwipeToCloseGesture(el, contentEl, scrollEl, ani, () => {
|
||||
/**
|
||||
* While the gesture animation is finishing
|
||||
* it is possible for a user to tap the backdrop.
|
||||
|
||||
134
core/src/components/modal/test/card-scroll-target/index.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Modal - Card</title>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<script type="module">
|
||||
import { modalController } from '../../../../../dist/ionic/index.esm.js';
|
||||
window.modalController = modalController;
|
||||
</script>
|
||||
<style>
|
||||
#content {
|
||||
position: relative;
|
||||
|
||||
display: block;
|
||||
flex: 1;
|
||||
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
contain: size style;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<div class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button expand="block" id="card" onclick="presentModal(document.querySelectorAll('.ion-page')[1])"
|
||||
>Card Modal</ion-button
|
||||
>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
async function createModal(presentingEl, opts) {
|
||||
// create component to open
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = `
|
||||
<ion-header id="modal-header">
|
||||
<ion-toolbar>
|
||||
<ion-title>Contacts</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button class="add">
|
||||
<ion-icon name="add" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content scroll-y="false">
|
||||
<div id="content" class="ion-padding ion-content-scroll-host">
|
||||
Hello World!
|
||||
<ion-button class="dismiss">Dismiss Modal</ion-button>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
|
||||
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
|
||||
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
|
||||
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
|
||||
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
|
||||
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
|
||||
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
|
||||
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
|
||||
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
|
||||
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
|
||||
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
|
||||
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
|
||||
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
|
||||
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
|
||||
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
|
||||
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
|
||||
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
|
||||
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
|
||||
scelerisque nisl eu suscipit consectetur.
|
||||
</p>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`;
|
||||
|
||||
// listen for close event
|
||||
const button = element.querySelector('ion-button.dismiss');
|
||||
button.addEventListener('click', () => {
|
||||
modalController.dismiss();
|
||||
});
|
||||
|
||||
const create = element.querySelector('ion-button.add');
|
||||
create.addEventListener('click', async () => {
|
||||
const topModal = await modalController.getTop();
|
||||
|
||||
presentModal(topModal, opts);
|
||||
});
|
||||
|
||||
// present the modal
|
||||
const modalElement = await modalController.create({
|
||||
presentingElement: presentingEl,
|
||||
component: element,
|
||||
swipeToClose: true,
|
||||
...opts,
|
||||
});
|
||||
return modalElement;
|
||||
}
|
||||
|
||||
async function presentModal(presentingEl, opts) {
|
||||
const modal = await createModal(presentingEl, opts);
|
||||
await modal.present();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('card modal - scroll target', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.metadata.mode !== 'ios', 'Card style modal is only available on iOS');
|
||||
|
||||
await page.goto('/src/components/modal/test/card-scroll-target');
|
||||
});
|
||||
test.describe('card modal: swipe to close', () => {
|
||||
test('it should swipe to close when swiped on the header', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const header = await page.locator('ion-modal ion-header');
|
||||
await dragElementBy(header, page, 0, 500);
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
test('it should swipe to close when swiped on the content', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const content = await page.locator('ion-modal .ion-content-scroll-host');
|
||||
await dragElementBy(content, page, 0, 500);
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
test('it should not swipe to close when swiped on the content but the content is scrolled', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = await page.locator('ion-modal');
|
||||
const content = (await page.$('ion-modal .ion-content-scroll-host'))!;
|
||||
|
||||
await content.evaluate((el: HTMLElement) => (el.scrollTop = 500));
|
||||
|
||||
await dragElementBy(content, page, 0, 500);
|
||||
|
||||
await content.waitForElementState('stable');
|
||||
|
||||
expect(modal).toBeVisible();
|
||||
});
|
||||
test('content should be scrollable after gesture ends', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const content = await page.locator('ion-modal .ion-content-scroll-host');
|
||||
await dragElementBy(content, page, 0, 20);
|
||||
|
||||
expect(content).not.toHaveCSS('overflow', 'hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,39 @@
|
||||
<ion-content class="ion-padding">
|
||||
Hello World!
|
||||
<ion-button class="dismiss">Dismiss Modal</ion-button>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
|
||||
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
|
||||
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
|
||||
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
|
||||
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
|
||||
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
|
||||
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
|
||||
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
|
||||
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
|
||||
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
|
||||
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
|
||||
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
|
||||
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
|
||||
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
|
||||
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
|
||||
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
|
||||
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
|
||||
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
|
||||
scelerisque nisl eu suscipit consectetur.
|
||||
</p>
|
||||
|
||||
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`;
|
||||
|
||||
// listen for close event
|
||||
|
||||
@@ -1,51 +1,108 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import { dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('card modal', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.metadata.mode !== 'ios', 'Card style modal is only available on iOS');
|
||||
|
||||
test.describe('card modal: rendering', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/card');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-present-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should not have visual regressions with custom modal', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/card');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
test.describe('card modal: rendering', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card-custom');
|
||||
await page.click('#card');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-custom-present-${page.getSnapshotSettings()}.png`);
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-present-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should not have visual regressions with custom modal', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card-custom');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-custom-present-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should not have visual regressions with stacked cards', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.add');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-stacked-present-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should not have visual regressions with stacked custom cards', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card-custom');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.add');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(
|
||||
`modal-card-custom-stacked-present-${page.getSnapshotSettings()}.png`
|
||||
);
|
||||
});
|
||||
});
|
||||
test('should not have visual regressions with stacked cards', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/card');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
test.describe('card modal: swipe to close', () => {
|
||||
test('it should swipe to close when swiped on the header', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.add');
|
||||
await ionModalDidPresent.next();
|
||||
const header = await page.locator('ion-modal ion-header');
|
||||
await dragElementBy(header, page, 0, 500);
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`modal-card-stacked-present-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should not have visual regressions with stacked custom cards', async ({ page }) => {
|
||||
await page.goto('/src/components/modal/test/card');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
test('it should swipe to close when swiped on the content', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
await page.click('#card-custom');
|
||||
await ionModalDidPresent.next();
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.click('.add');
|
||||
await ionModalDidPresent.next();
|
||||
const content = await page.locator('ion-modal ion-content');
|
||||
await dragElementBy(content, page, 0, 500);
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(
|
||||
`modal-card-custom-stacked-present-${page.getSnapshotSettings()}.png`
|
||||
);
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
test('it should not swipe to close when swiped on the content but the content is scrolled', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = await page.locator('ion-modal');
|
||||
const content = (await page.$('ion-modal ion-content'))!;
|
||||
|
||||
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
|
||||
|
||||
await dragElementBy(content, page, 0, 500);
|
||||
|
||||
await content.waitForElementState('stable');
|
||||
|
||||
expect(modal).toBeVisible();
|
||||
});
|
||||
test('content should be scrollable after gesture ends', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#card');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const content = await page.locator('ion-modal ion-content');
|
||||
await dragElementBy(content, page, 0, 20);
|
||||
|
||||
expect(content).toHaveJSProperty('scrollY', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 222 KiB |
@@ -13,7 +13,7 @@ const ION_CONTENT_CLASS_SELECTOR = '.ion-content-scroll-host';
|
||||
*/
|
||||
const ION_CONTENT_SELECTOR = `${ION_CONTENT_ELEMENT_SELECTOR}, ${ION_CONTENT_CLASS_SELECTOR}`;
|
||||
|
||||
const isIonContent = (el: Element) => el && el.tagName === ION_CONTENT_TAG_NAME;
|
||||
export const isIonContent = (el: Element) => el && el.tagName === ION_CONTENT_TAG_NAME;
|
||||
|
||||
/**
|
||||
* Waits for the element host fully initialize before
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import type { ElementHandle, Locator } from '@playwright/test';
|
||||
|
||||
import type { E2EPage } from './';
|
||||
|
||||
export const dragElementBy = async (el: Locator, page: E2EPage, dragByX = 0, dragByY = 0) => {
|
||||
export const dragElementBy = async (
|
||||
el: Locator | ElementHandle<SVGElement | HTMLElement>,
|
||||
page: E2EPage,
|
||||
dragByX = 0,
|
||||
dragByY = 0
|
||||
) => {
|
||||
const boundingBox = await el.boundingBox();
|
||||
|
||||
if (!boundingBox) {
|
||||
|
||||