mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-06 22:29:44 +08:00
fix(animation): play method resolves when animation is stopped (#28264)
Issue number: N/A --------- <!-- 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. --> When trying to fix https://github.com/ionic-team/ionic-framework/issues/20092, I discovered thatac2c8e6c22/core/src/components/menu/menu.tsx (L483)was never resolving when the animation was aborted inac2c8e6c22/core/src/components/menu/menu.tsx (L699). This can happen if `menu.disabled` is set to `true` mid-animation. In order to fix the menu bug, I need this promise to resolve when the animation is stopped. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - The `play` method now correctly resolves when the animation is cancelled. ## 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. --> The `play` method resolves when a particular run of the animation is finished. The `stop` method ensures that this run never finishes which is why I've chosen to have `play` resolve. Note that `onFinish` callbacks should not be fired because the animation run did not complete.
This commit is contained in:
@ -30,6 +30,8 @@ interface AnimationOnFinishCallback {
|
||||
o?: AnimationCallbackOptions;
|
||||
}
|
||||
|
||||
type AnimationOnStopCallback = AnimationOnFinishCallback;
|
||||
|
||||
export const createAnimation = (animationId?: string): Animation => {
|
||||
let _delay: number | undefined;
|
||||
let _duration: number | undefined;
|
||||
@ -63,6 +65,7 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
const id: string | undefined = animationId;
|
||||
const onFinishCallbacks: AnimationOnFinishCallback[] = [];
|
||||
const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = [];
|
||||
const onStopOneTimeCallbacks: AnimationOnStopCallback[] = [];
|
||||
const elements: HTMLElement[] = [];
|
||||
const childAnimations: Animation[] = [];
|
||||
const stylesheets: HTMLElement[] = [];
|
||||
@ -134,6 +137,35 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
return numAnimationsRunning !== 0 && !paused;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Remove a callback from a chosen callback array
|
||||
* @param callbackToRemove: A reference to the callback that should be removed
|
||||
* @param callbackObjects: An array of callbacks that callbackToRemove should be removed from.
|
||||
*/
|
||||
const clearCallback = (
|
||||
callbackToRemove: AnimationLifecycle,
|
||||
callbackObjects: AnimationOnFinishCallback[] | AnimationOnStopCallback[]
|
||||
) => {
|
||||
const index = callbackObjects.findIndex((callbackObject) => callbackObject.c === callbackToRemove);
|
||||
|
||||
if (index > -1) {
|
||||
callbackObjects.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Add a callback to be fired when an animation is stopped/cancelled.
|
||||
* @param callback: A reference to the callback that should be fired
|
||||
* @param opts: Any options associated with this particular callback
|
||||
*/
|
||||
const onStop = (callback: AnimationLifecycle, opts?: AnimationCallbackOptions) => {
|
||||
onStopOneTimeCallbacks.push({ c: callback, o: opts });
|
||||
|
||||
return ani;
|
||||
};
|
||||
|
||||
const onFinish = (callback: AnimationLifecycle, opts?: AnimationCallbackOptions) => {
|
||||
const callbacks = opts?.oneTimeCallback ? onFinishOneTimeCallbacks : onFinishCallbacks;
|
||||
callbacks.push({ c: callback, o: opts });
|
||||
@ -953,7 +985,34 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
shouldCalculateNumAnimations = false;
|
||||
}
|
||||
|
||||
onFinish(() => resolve(), { oneTimeCallback: true });
|
||||
/**
|
||||
* When one of these callbacks fires we
|
||||
* need to clear the other's callback otherwise
|
||||
* you can potentially get these callbacks
|
||||
* firing multiple times if the play method
|
||||
* is subsequently called.
|
||||
* Example:
|
||||
* animation.play() (onStop and onFinish callbacks are registered)
|
||||
* animation.stop() (onStop callback is fired, onFinish is not)
|
||||
* animation.play() (onStop and onFinish callbacks are registered)
|
||||
* Total onStop callbacks: 1
|
||||
* Total onFinish callbacks: 2
|
||||
*/
|
||||
const onStopCallback = () => {
|
||||
clearCallback(onFinishCallback, onFinishOneTimeCallbacks);
|
||||
resolve();
|
||||
};
|
||||
const onFinishCallback = () => {
|
||||
clearCallback(onStopCallback, onStopOneTimeCallbacks);
|
||||
resolve();
|
||||
};
|
||||
|
||||
/**
|
||||
* The play method resolves when an animation
|
||||
* run either finishes or is cancelled.
|
||||
*/
|
||||
onFinish(onFinishCallback, { oneTimeCallback: true });
|
||||
onStop(onStopCallback, { oneTimeCallback: true });
|
||||
|
||||
childAnimations.forEach((animation) => {
|
||||
animation.play();
|
||||
@ -969,6 +1028,14 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops an animation and resets it state to the
|
||||
* beginning. This does not fire any onFinish
|
||||
* callbacks because the animation did not finish.
|
||||
* However, since the animation was not destroyed
|
||||
* (i.e. the animation could run again) we do not
|
||||
* clear the onFinish callbacks.
|
||||
*/
|
||||
const stop = () => {
|
||||
childAnimations.forEach((animation) => {
|
||||
animation.stop();
|
||||
@ -980,6 +1047,9 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
}
|
||||
|
||||
resetFlags();
|
||||
|
||||
onStopOneTimeCallbacks.forEach((onStopCallback) => onStopCallback.c(0, ani));
|
||||
onStopOneTimeCallbacks.length = 0;
|
||||
};
|
||||
|
||||
const from = (property: string, value: any) => {
|
||||
|
||||
@ -4,6 +4,24 @@ import { processKeyframes } from '../animation-utils';
|
||||
import { getTimeGivenProgression } from '../cubic-bezier';
|
||||
|
||||
describe('Animation Class', () => {
|
||||
describe('play()', () => {
|
||||
it('should resolve when the animation is cancelled', async () => {
|
||||
// Tell Jest to expect 1 assertion for async code
|
||||
expect.assertions(1);
|
||||
const el = document.createElement('div');
|
||||
const animation = createAnimation()
|
||||
.addElement(el)
|
||||
.fromTo('transform', 'translateX(0px)', 'translateX(100px)')
|
||||
.duration(100000);
|
||||
|
||||
const animationPromise = animation.play();
|
||||
|
||||
animation.stop();
|
||||
|
||||
// Expect that the promise resolves and returns undefined
|
||||
expect(animationPromise).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe('isRunning()', () => {
|
||||
let animation: Animation;
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user