mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
feat(toast): add swipe to dismiss functionality (#28442)
Issue number: resolves #21769 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Toast does not support swipe gestures to dismiss. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Added a `swipeGesture` property that allows users to swipe toasts closed. Note: This is a combination of previous PRs https://github.com/ionic-team/ionic-framework/pull/28380 and https://github.com/ionic-team/ionic-framework/pull/28402 ⚠️ There is a visual glitch on iOS where dragging and having the toast animate back to its opened position causes a flicker. This is an iOS 17 regression and is being tracked in https://github.com/ionic-team/ionic-framework/issues/28467. This bug has been reported to and confirmed by Apple. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> ⚠️ Give co-author credit to author in https://github.com/ionic-team/ionic-framework/pull/23124 --------- Co-authored-by: evgeniy-skakun <evgeniy-skakun@users.noreply.github.com>
This commit is contained in:
@ -1,10 +1,12 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { State, Watch, Component, Element, Event, h, Host, Method, Prop } from '@stencil/core';
|
||||
import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
|
||||
import type { Gesture } from '@utils/gesture';
|
||||
import { raf } from '@utils/helpers';
|
||||
import { createLockController } from '@utils/lock-controller';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import {
|
||||
GESTURE,
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
dismiss,
|
||||
@ -29,6 +31,7 @@ import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
import { mdEnterAnimation } from './animations/md.enter';
|
||||
import { mdLeaveAnimation } from './animations/md.leave';
|
||||
import { getAnimationPosition } from './animations/utils';
|
||||
import { createSwipeToDismissGesture } from './gestures/swipe-to-dismiss';
|
||||
import type {
|
||||
ToastButton,
|
||||
ToastPosition,
|
||||
@ -36,6 +39,7 @@ import type {
|
||||
ToastPresentOptions,
|
||||
ToastDismissOptions,
|
||||
ToastAnimationPosition,
|
||||
ToastSwipeGestureDirection,
|
||||
} from './toast-interface';
|
||||
|
||||
// TODO(FW-2832): types
|
||||
@ -64,6 +68,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
private readonly triggerController = createTriggerController();
|
||||
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
|
||||
private durationTimeout?: ReturnType<typeof setTimeout>;
|
||||
private gesture?: Gesture;
|
||||
|
||||
/**
|
||||
* Holds the position of the toast calculated in the present
|
||||
@ -193,6 +198,45 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() htmlAttributes?: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* If set to 'vertical', the Toast can be dismissed with
|
||||
* a swipe gesture. The swipe direction is determined by
|
||||
* the value of the `position` property:
|
||||
* `top`: The Toast can be swiped up to dismiss.
|
||||
* `bottom`: The Toast can be swiped down to dismiss.
|
||||
* `middle`: The Toast can be swiped up or down to dismiss.
|
||||
*/
|
||||
@Prop() swipeGesture?: ToastSwipeGestureDirection;
|
||||
@Watch('swipeGesture')
|
||||
swipeGestureChanged() {
|
||||
/**
|
||||
* If the Toast is presented, then we need to destroy
|
||||
* any actives gestures before a new gesture is potentially
|
||||
* created below.
|
||||
*
|
||||
* If the Toast is dismissed, then no gesture should be available
|
||||
* since the Toast is not visible. This case should never
|
||||
* happen since the "dismiss" method handles destroying
|
||||
* any active swipe gestures, but we keep this code
|
||||
* around to handle the first case.
|
||||
*/
|
||||
this.destroySwipeGesture();
|
||||
|
||||
/**
|
||||
* A new swipe gesture should only be created
|
||||
* if the Toast is presented. If the Toast is not
|
||||
* yet presented then the "present" method will
|
||||
* handle calling the swipe gesture setup function.
|
||||
*/
|
||||
if (this.presented && this.prefersSwipeGesture()) {
|
||||
/**
|
||||
* If the Toast is presented then
|
||||
* lastPresentedPosition is defined.
|
||||
*/
|
||||
this.createSwipeGesture(this.lastPresentedPosition!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, the toast will open. If `false`, the toast will close.
|
||||
* Use this if you need finer grained control over presentation, otherwise
|
||||
@ -326,6 +370,15 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
this.durationTimeout = setTimeout(() => this.dismiss(undefined, 'timeout'), this.duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the Toast has a swipe gesture then we can
|
||||
* create the gesture so users can swipe the
|
||||
* presented Toast.
|
||||
*/
|
||||
if (this.prefersSwipeGesture()) {
|
||||
this.createSwipeGesture(animationPosition);
|
||||
}
|
||||
|
||||
unlock();
|
||||
}
|
||||
|
||||
@ -373,6 +426,13 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
this.lastPresentedPosition = undefined;
|
||||
|
||||
/**
|
||||
* If the Toast has a swipe gesture then we can
|
||||
* safely destroy it now that it is dismissed.
|
||||
*/
|
||||
this.destroySwipeGesture();
|
||||
|
||||
unlock();
|
||||
|
||||
return dismissed;
|
||||
@ -486,6 +546,48 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new swipe gesture so Toast
|
||||
* can be swiped to dismiss.
|
||||
*/
|
||||
private createSwipeGesture = (toastPosition: ToastAnimationPosition) => {
|
||||
const gesture = (this.gesture = createSwipeToDismissGesture(this.el, toastPosition, () => {
|
||||
/**
|
||||
* If the gesture completed then
|
||||
* we should dismiss the toast.
|
||||
*/
|
||||
this.dismiss(undefined, GESTURE);
|
||||
}));
|
||||
|
||||
gesture.enable(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy an existing swipe gesture
|
||||
* so Toast can no longer be swiped to dismiss.
|
||||
*/
|
||||
private destroySwipeGesture = () => {
|
||||
const { gesture } = this;
|
||||
|
||||
if (gesture === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `true` if swipeGesture
|
||||
* is configured to a value that enables the swipe behavior.
|
||||
* Returns `false` otherwise.
|
||||
*/
|
||||
private prefersSwipeGesture = () => {
|
||||
const { swipeGesture } = this;
|
||||
|
||||
return swipeGesture === 'vertical';
|
||||
};
|
||||
|
||||
renderButtons(buttons: ToastButton[], side: 'start' | 'end') {
|
||||
if (buttons.length === 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user