Ionic 7 deprecated swipeToClose.
Re: swipeToClose being deprecated -- The current thinking is that modals should only be swipeable when either using the card modal or the sheet modal. The fact that non-card/sheet modals could be swipeable was a mistake on our end. This change is being done to better align with native iOS and Android behavior.
https://github.com/ionic-team/ionic-framework/issues/25362#issuecomment-1139804151
To align with native iOS and Android behavior, modals other than card and sheet modals can no longer be dismissed with a swipe. That said, for some use cases—like a Twitter-style image viewer—swiping to close is more convenient for users, and it is a pattern many users already expect. So let us implement swipeToClose behavior ourselves. This is an Angular implementation.
Alternative implementation
Implement it on the page component used inside the Modal. The basic approach is to grab the page component itself with ElementRef, then listen for touchstart and touchend on that element. Compare the clientY values from touchmove when touchstart and touchend fire, and dismiss when touchstart is above touchend. The full implementation looks like this.
import { Component, ElementRef, OnInit } from '@angular/core';
import { IonicSlides, ModalController } from '@ionic/angular';
import { fromEvent, zipWith, withLatestFrom } from 'rxjs';
@Component({...})
export class ModalPage implements OnInit {
constructor(private modalCtrl: ModalController, private elementRef: ElementRef) {}
public ngOnInit() {
const watchSwipe$ = fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchstart')
.pipe(
zipWith(
fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchend').pipe(
withLatestFrom(fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchmove')),
),
),
)
.subscribe(([touchstart, [_, touchmove]]) => {
const touchstartClientY = touchstart.touches
? touchstart.touches[0].clientY
: touchstart.detail[1].clientY;
const touchmoveClientY = touchmove.touches ? touchmove.touches[0].clientY : touchmove.detail[1].clientY;
const yDiff = touchstartClientY - touchmoveClientY;
const threshold = touchmove.touches ? -50 : -5;
if (yDiff < threshold && touchstart.timeStamp <= touchmove.timeStamp) {
watchSwipe$.unsubscribe();
this.modalCtrl.dismiss();
}
});
}
}
fromEvent watches touchstart and touchend, and zipWith pairs them. I use withLatestFrom to combine touchend with touchmove. When touchstart and touchend are paired, I compare their clientY values and dismiss when touchstart is above touchend. Because touchmove fires between touchstart and touchend, I also compare their timeStamp values and dismiss the modal only when touchstart fired before touchmove.
Also, touchmove.touches ? -50 : -5 adjusts the threshold because mouse and touch behave differently—when touchmove comes from the mouse, clientY grows larger. Tune the threshold smaller or larger if you want. This targets the whole Modal; if you only want ion-header, change what you listen on.
A simpler alternative
The code above keeps the Modal looking the same. If a sheet-like appearance is acceptable, you can get close to the same behavior by creating the modal like this. Pick the approach that fits your intent and use case.
this.modal = await this.modalCtrl.create({
backdropDismiss: true,
backdropBreakpoint: 0,
breakpoints: [0, 1],
initialBreakpoint: 1,
handle: false,
showBackdrop: true,
canDismiss: true,
component: MyModalComponent,
mode: 'ios'
});
Summary
With RxJS, this stays simpler than an addEventListener implementation, does it not? See you next time.