Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(modal): card modal can now be swiped to close on the content #25185

Merged
merged 22 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
95cc7b4
fix(modal): card modal is now swipeable from content
liamdebeasi Apr 18, 2022
673dc5b
fix(modal): only allow swiping on content if scrollTop is 0
liamdebeasi Apr 18, 2022
76d5338
fix(modal): disable scrolling while swiping on card
liamdebeasi Apr 18, 2022
67a6932
fix(modal): swiping up then down now correctly disables scrolling
liamdebeasi Apr 18, 2022
20206ba
perf(modal): re-enable scrolling sooner so there is no delay on gestu…
liamdebeasi Apr 18, 2022
865055a
Merge remote-tracking branch 'origin/main' into FW-355
liamdebeasi Apr 22, 2022
0053e0b
test(modal): add card swiping tests
liamdebeasi Apr 22, 2022
6c06296
chore(): remove unneeded change
liamdebeasi Apr 25, 2022
50261b0
chore(): remove only
liamdebeasi Apr 25, 2022
69e1269
chore(): lint
liamdebeasi Apr 25, 2022
de35a1e
Merge branch 'main' into FW-355
liamdebeasi Apr 25, 2022
0caa00a
Merge remote-tracking branch 'origin/main' into FW-355
liamdebeasi Apr 25, 2022
45ad6b0
Merge branch 'main' into FW-355
liamdebeasi Apr 25, 2022
63a0d2f
chore(): add updated snapshots
Ionitron Apr 25, 2022
dfff7e8
chore(): fix test type
liamdebeasi Apr 26, 2022
bdcdeef
fix(modal): account for custom scroll target
liamdebeasi Apr 26, 2022
9c37288
chore(): lint
liamdebeasi Apr 26, 2022
c73cf91
Merge branch 'main' into FW-355
liamdebeasi Apr 26, 2022
ea9e167
test(modal): add scroll target tests for card
liamdebeasi Apr 26, 2022
5abb166
chore(): lint
liamdebeasi Apr 26, 2022
4e7a957
chore(): fix test config
liamdebeasi Apr 26, 2022
dc0a433
Merge branch 'main' into FW-355
liamdebeasi Apr 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions core/src/components/modal/gestures/swipe-to-close.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;

/**
Expand Down Expand Up @@ -117,6 +203,8 @@ export const createSwipeToCloseGesture = (el: HTMLIonModalElement, animation: An

gesture.enable(false);

resetContentScroll();

animation
.onFinish(() => {
if (!shouldComplete) {
Expand Down
23 changes: 17 additions & 6 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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.
Expand Down
134 changes: 134 additions & 0 deletions core/src/components/modal/test/card-scroll-target/index.html
Original file line number Diff line number Diff line change
@@ -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>
Loading