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;
|
o?: AnimationCallbackOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnimationOnStopCallback = AnimationOnFinishCallback;
|
||||||
|
|
||||||
export const createAnimation = (animationId?: string): Animation => {
|
export const createAnimation = (animationId?: string): Animation => {
|
||||||
let _delay: number | undefined;
|
let _delay: number | undefined;
|
||||||
let _duration: number | undefined;
|
let _duration: number | undefined;
|
||||||
@ -63,6 +65,7 @@ export const createAnimation = (animationId?: string): Animation => {
|
|||||||
const id: string | undefined = animationId;
|
const id: string | undefined = animationId;
|
||||||
const onFinishCallbacks: AnimationOnFinishCallback[] = [];
|
const onFinishCallbacks: AnimationOnFinishCallback[] = [];
|
||||||
const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = [];
|
const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = [];
|
||||||
|
const onStopOneTimeCallbacks: AnimationOnStopCallback[] = [];
|
||||||
const elements: HTMLElement[] = [];
|
const elements: HTMLElement[] = [];
|
||||||
const childAnimations: Animation[] = [];
|
const childAnimations: Animation[] = [];
|
||||||
const stylesheets: HTMLElement[] = [];
|
const stylesheets: HTMLElement[] = [];
|
||||||
@ -134,6 +137,35 @@ export const createAnimation = (animationId?: string): Animation => {
|
|||||||
return numAnimationsRunning !== 0 && !paused;
|
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 onFinish = (callback: AnimationLifecycle, opts?: AnimationCallbackOptions) => {
|
||||||
const callbacks = opts?.oneTimeCallback ? onFinishOneTimeCallbacks : onFinishCallbacks;
|
const callbacks = opts?.oneTimeCallback ? onFinishOneTimeCallbacks : onFinishCallbacks;
|
||||||
callbacks.push({ c: callback, o: opts });
|
callbacks.push({ c: callback, o: opts });
|
||||||
@ -953,7 +985,34 @@ export const createAnimation = (animationId?: string): Animation => {
|
|||||||
shouldCalculateNumAnimations = false;
|
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) => {
|
childAnimations.forEach((animation) => {
|
||||||
animation.play();
|
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 = () => {
|
const stop = () => {
|
||||||
childAnimations.forEach((animation) => {
|
childAnimations.forEach((animation) => {
|
||||||
animation.stop();
|
animation.stop();
|
||||||
@ -980,6 +1047,9 @@ export const createAnimation = (animationId?: string): Animation => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetFlags();
|
resetFlags();
|
||||||
|
|
||||||
|
onStopOneTimeCallbacks.forEach((onStopCallback) => onStopCallback.c(0, ani));
|
||||||
|
onStopOneTimeCallbacks.length = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const from = (property: string, value: any) => {
|
const from = (property: string, value: any) => {
|
||||||
|
|||||||
@ -4,6 +4,24 @@ import { processKeyframes } from '../animation-utils';
|
|||||||
import { getTimeGivenProgression } from '../cubic-bezier';
|
import { getTimeGivenProgression } from '../cubic-bezier';
|
||||||
|
|
||||||
describe('Animation Class', () => {
|
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()', () => {
|
describe('isRunning()', () => {
|
||||||
let animation: Animation;
|
let animation: Animation;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user