Skip to content

Commit fdc55c0

Browse files
authored
fix(modal): swipe to close on content blocks scroll in ion-nav (#25300)
resolves #25298
1 parent 7111370 commit fdc55c0

File tree

52 files changed

+148
-74
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+148
-74
lines changed

core/src/components/modal/gestures/swipe-to-close.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Animation } from '../../../interface';
22
import { getTimeGivenProgression } from '../../../utils/animation/cubic-bezier';
3-
import { isIonContent } from '../../../utils/content';
3+
import { isIonContent, findClosestIonContent } from '../../../utils/content';
44
import type { GestureDetail } from '../../../utils/gesture';
55
import { createGesture } from '../../../utils/gesture';
6-
import { clamp } from '../../../utils/helpers';
6+
import { clamp, getElementRoot } from '../../../utils/helpers';
77

88
import { calculateSpringStep, handleCanDismiss } from './utils';
99

@@ -12,20 +12,16 @@ export const SwipeToCloseDefaults = {
1212
MIN_PRESENTING_SCALE: 0.93,
1313
};
1414

15-
export const createSwipeToCloseGesture = (
16-
el: HTMLIonModalElement,
17-
contentEl: HTMLElement,
18-
scrollEl: HTMLElement,
19-
animation: Animation,
20-
onDismiss: () => void
21-
) => {
15+
export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: Animation, onDismiss: () => void) => {
2216
const height = el.offsetHeight;
2317
let isOpen = false;
2418
let canDismissBlocksGesture = false;
19+
let contentEl: HTMLElement | null = null;
20+
let scrollEl: HTMLElement | null = null;
2521
const canDismissMaxStep = 0.2;
26-
const hasRefresherInContent = !!contentEl.querySelector('ion-refresher');
22+
let initialScrollY = true;
2723
const getScrollY = () => {
28-
if (isIonContent(contentEl)) {
24+
if (contentEl && isIonContent(contentEl)) {
2925
return (contentEl as HTMLIonContentElement).scrollY;
3026
/**
3127
* Custom scroll containers are intended to be
@@ -36,9 +32,12 @@ export const createSwipeToCloseGesture = (
3632
return true;
3733
}
3834
};
39-
const initialScrollY = getScrollY();
4035

4136
const disableContentScroll = () => {
37+
if (!contentEl) {
38+
return;
39+
}
40+
4241
if (isIonContent(contentEl)) {
4342
(contentEl as HTMLIonContentElement).scrollY = false;
4443
} else {
@@ -47,6 +46,10 @@ export const createSwipeToCloseGesture = (
4746
};
4847

4948
const resetContentScroll = () => {
49+
if (!contentEl) {
50+
return;
51+
}
52+
5053
if (isIonContent(contentEl)) {
5154
(contentEl as HTMLIonContentElement).scrollY = initialScrollY;
5255
} else {
@@ -67,9 +70,17 @@ export const createSwipeToCloseGesture = (
6770
* the content is scrolled all the way
6871
* to the top so that we do not interfere
6972
* with scrolling.
73+
*
74+
* We cannot assume that the `ion-content`
75+
* target will remain consistent between
76+
* swipes. For example, when using
77+
* ion-nav within a card modal it is
78+
* possible to swipe, push a view, and then
79+
* swipe again. The target content will not
80+
* be the same between swipes.
7081
*/
71-
const content = target.closest('ion-content');
72-
if (content) {
82+
contentEl = findClosestIonContent(target);
83+
if (contentEl) {
7384
/**
7485
* The card should never swipe to close
7586
* on the content with a refresher.
@@ -78,8 +89,21 @@ export const createSwipeToCloseGesture = (
7889
* than the refresher gesture as the iOS native
7990
* refresh gesture uses a scroll listener in
8091
* addition to a gesture.
92+
*
93+
* Note: Do not use getScrollElement here
94+
* because we need this to be a synchronous
95+
* operation, and getScrollElement is
96+
* asynchronous.
8197
*/
82-
return !hasRefresherInContent && scrollEl.scrollTop === 0;
98+
if (isIonContent(contentEl)) {
99+
const root = getElementRoot(contentEl);
100+
scrollEl = root.querySelector('.inner-scroll');
101+
} else {
102+
scrollEl = contentEl;
103+
}
104+
105+
const hasRefresherInContent = !!contentEl.querySelector('ion-refresher');
106+
return !hasRefresherInContent && scrollEl!.scrollTop === 0;
83107
}
84108

85109
/**
@@ -96,6 +120,14 @@ export const createSwipeToCloseGesture = (
96120

97121
const onStart = (detail: GestureDetail) => {
98122
const { deltaY } = detail;
123+
124+
/**
125+
* Get the initial scrollY value so
126+
* that we can correctly reset the scrollY
127+
* prop when the gesture ends.
128+
*/
129+
initialScrollY = getScrollY();
130+
99131
/**
100132
* If canDismiss is anything other than `true`
101133
* then users should be able to swipe down

core/src/components/modal/modal.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
OverlayEventDetail,
1616
OverlayInterface,
1717
} from '../../interface';
18-
import { getScrollElement, findIonContent, printIonContentErrorMsg } from '../../utils/content';
18+
import { findIonContent, printIonContentErrorMsg } from '../../utils/content';
1919
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
2020
import { raf } from '../../utils/helpers';
2121
import { KEYBOARD_DID_OPEN } from '../../utils/keyboard/keyboard';
@@ -511,7 +511,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
511511
this.currentTransition = undefined;
512512
}
513513

514-
private async initSwipeToClose() {
514+
private initSwipeToClose() {
515515
if (getIonMode(this) !== 'ios') {
516516
return;
517517
}
@@ -529,9 +529,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
529529
printIonContentErrorMsg(el);
530530
return;
531531
}
532-
const scrollEl = await getScrollElement(contentEl);
533532

534-
this.gesture = createSwipeToCloseGesture(el, contentEl, scrollEl, ani, () => {
533+
this.gesture = createSwipeToCloseGesture(el, ani, () => {
535534
/**
536535
* While the gesture animation is finishing
537536
* it is possible for a user to tap the backdrop.

core/src/components/modal/test/card/index.html

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
ion-modal#custom {
2222
--border-radius: 50px !important;
2323
}
24+
.content-wrapper {
25+
height: 100%;
26+
27+
overflow: hidden;
28+
}
2429
</style>
2530
</head>
2631

@@ -48,71 +53,73 @@
4853
</ion-app>
4954

5055
<script>
56+
const renderContent = () => {
57+
return `<ion-content class="ion-padding">
58+
Hello World!
59+
60+
<br />
61+
62+
<p>
63+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
64+
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
65+
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
66+
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
67+
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
68+
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
69+
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
70+
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
71+
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
72+
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
73+
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
74+
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
75+
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
76+
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
77+
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
78+
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
79+
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
80+
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
81+
scelerisque nisl eu suscipit consectetur.
82+
</p>
83+
84+
<p>
85+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
86+
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
87+
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
88+
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
89+
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
90+
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
91+
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
92+
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
93+
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
94+
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
95+
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
96+
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
97+
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
98+
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
99+
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
100+
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
101+
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
102+
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
103+
scelerisque nisl eu suscipit consectetur.
104+
</p>
105+
106+
</ion-content>`;
107+
};
51108
async function createModal(presentingEl, opts) {
52109
// create component to open
53110
const element = document.createElement('div');
54111
element.innerHTML = `
55112
<ion-header id="modal-header">
56113
<ion-toolbar>
57-
<ion-title>Contacts</ion-title>
58114
<ion-buttons slot="end">
59-
<ion-button class="add">
60-
<ion-icon name="add" slot="icon-only"></ion-icon>
61-
</ion-button>
115+
<ion-button class="add">Add</ion-button>
116+
<ion-button class="dismiss">Close</ion-button>
117+
<ion-button class="replace">Replace</ion-button>
62118
</ion-buttons>
63119
</ion-toolbar>
64120
</ion-header>
65-
<ion-content class="ion-padding">
66-
Hello World!
67-
<ion-button class="dismiss">Dismiss Modal</ion-button>
68-
69-
<br />
70-
71-
<p>
72-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
73-
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
74-
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
75-
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
76-
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
77-
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
78-
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
79-
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
80-
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
81-
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
82-
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
83-
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
84-
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
85-
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
86-
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
87-
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
88-
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
89-
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
90-
scelerisque nisl eu suscipit consectetur.
91-
</p>
92-
93-
<p>
94-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae lobortis felis, eu sodales enim. Nam
95-
risus nibh, placerat at rutrum ac, vehicula vel velit. Lorem ipsum dolor sit amet, consectetur adipiscing
96-
elit. Vestibulum quis elementum ligula, ac aliquet nulla. Mauris non placerat mauris. Aenean dignissim lacinia
97-
porttitor. Praesent fringilla at est et ullamcorper. In ac ante ac massa porta venenatis ut id nibh. Fusce
98-
felis neque, aliquet in velit vitae, venenatis euismod libero. Donec vulputate, urna sed sagittis tempor, mi
99-
arcu tristique lacus, eget fringilla urna sem eget felis. Fusce dignissim lacus a scelerisque vehicula. Nulla
100-
nec enim nunc. Quisque nec dui eu nibh pulvinar bibendum quis ut nunc. Duis ex odio, sollicitudin ac mollis
101-
nec, fringilla non lacus. Maecenas sed tincidunt urna. Nunc feugiat maximus venenatis. Donec porttitor, felis
102-
eget porttitor tempor, quam nulla dapibus nisl, sit amet posuere sapien sapien malesuada tortor. Pellentesque
103-
habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque luctus, sapien nec
104-
tincidunt efficitur, nibh turpis faucibus felis, in sodales massa augue nec erat. Morbi sollicitudin nisi ex,
105-
et gravida nisi euismod eu. Suspendisse hendrerit dapibus orci, non viverra neque vestibulum id. Quisque vitae
106-
interdum ligula, quis consectetur nibh. Phasellus in mi at erat ultrices semper. Fusce sollicitudin at dolor
107-
ac lobortis. Morbi sit amet sem quis nulla pellentesque imperdiet. Nullam eu sem a enim maximus eleifend non
108-
vulputate leo. Proin quis congue lacus. Pellentesque placerat, quam at tempus pulvinar, nisl ligula tempor
109-
risus, quis pretium arcu odio et nulla. Nullam mollis consequat pharetra. Phasellus dictum velit sed purus
110-
mattis maximus. In molestie eget massa ut dignissim. In a interdum elit. In finibus nibh a mauris lobortis
111-
aliquet. Proin rutrum varius consequat. In mollis dapibus nisl, eu finibus urna viverra ac. Quisque
112-
scelerisque nisl eu suscipit consectetur.
113-
</p>
114-
115-
</ion-content>
121+
122+
<div class="content-wrapper">${renderContent()}</div>
116123
117124
<ion-footer>
118125
<ion-toolbar>
@@ -134,6 +141,12 @@
134141
presentModal(topModal, opts);
135142
});
136143

144+
const wrapper = element.querySelector('.content-wrapper');
145+
const replace = element.querySelector('ion-button.replace');
146+
replace.addEventListener('click', () => {
147+
wrapper.innerHTML = renderContent();
148+
});
149+
137150
// present the modal
138151
const modalElement = await modalController.create({
139152
presentingElement: presentingEl,

core/src/components/modal/test/card/modal.e2e.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ test.describe('card modal', () => {
8888
await content.waitForElementState('stable');
8989
expect(modal).toBeVisible();
9090
});
91+
test('it should not swipe to close when swiped on the content but the content is scrolled even when content is replaced', async ({
92+
page,
93+
}) => {
94+
const modal = await cardModalPage.openModalByTrigger('#card');
95+
96+
await page.click('ion-button.replace');
97+
98+
const content = (await page.$('ion-modal ion-content'))!;
99+
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
100+
101+
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false);
102+
103+
await content.waitForElementState('stable');
104+
expect(modal).toBeVisible();
105+
});
91106
test('content should be scrollable after gesture ends', async ({ page }) => {
92107
await cardModalPage.openModalByTrigger('#card');
93108
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false, 20);
@@ -152,6 +167,21 @@ test.describe('card modal', () => {
152167
await content.waitForElementState('stable');
153168
expect(modal).toBeVisible();
154169
});
170+
test('it should not swipe to close when swiped on the content but the content is scrolled even when content is replaced', async ({
171+
page,
172+
}) => {
173+
const modal = await cardModalPage.openModalByTrigger('#card');
174+
175+
await page.click('ion-button.replace');
176+
177+
const content = (await page.$('ion-modal ion-content'))!;
178+
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
179+
180+
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false);
181+
182+
await content.waitForElementState('stable');
183+
expect(modal).toBeVisible();
184+
});
155185
test('content should be scrollable after gesture ends', async ({ page }) => {
156186
await cardModalPage.openModalByTrigger('#card');
157187
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false, 20);
21.1 KB
2.57 KB
10.2 KB
21.5 KB
2.74 KB
10.2 KB

0 commit comments

Comments
 (0)