fix(animations): ensure enter/leave cancellations work (#15323)
Closes #15315 Closes #15323 PR Close #15323
This commit is contained in:
parent
de3d2eeeba
commit
9bf2fb4a74
|
@ -217,9 +217,9 @@ export class DomAnimationEngine {
|
||||||
// we first run this so that the previous animation player
|
// we first run this so that the previous animation player
|
||||||
// data can be passed into the successive animation players
|
// data can be passed into the successive animation players
|
||||||
let totalTime = 0;
|
let totalTime = 0;
|
||||||
const players = instruction.timelines.map(timelineInstruction => {
|
const players = instruction.timelines.map((timelineInstruction, i) => {
|
||||||
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
|
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
|
||||||
return this._buildPlayer(element, timelineInstruction, previousPlayers);
|
return this._buildPlayer(element, timelineInstruction, previousPlayers, i);
|
||||||
});
|
});
|
||||||
|
|
||||||
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
|
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
|
||||||
|
@ -253,8 +253,8 @@ export class DomAnimationEngine {
|
||||||
public animateTimeline(
|
public animateTimeline(
|
||||||
element: any, instructions: AnimationTimelineInstruction[],
|
element: any, instructions: AnimationTimelineInstruction[],
|
||||||
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
||||||
const players = instructions.map(instruction => {
|
const players = instructions.map((instruction, i) => {
|
||||||
const player = this._buildPlayer(element, instruction, previousPlayers);
|
const player = this._buildPlayer(element, instruction, previousPlayers, i);
|
||||||
player.onDestroy(
|
player.onDestroy(
|
||||||
() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
|
() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
|
||||||
player.init();
|
player.init();
|
||||||
|
@ -266,8 +266,14 @@ export class DomAnimationEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildPlayer(
|
private _buildPlayer(
|
||||||
element: any, instruction: AnimationTimelineInstruction,
|
element: any, instruction: AnimationTimelineInstruction, previousPlayers: AnimationPlayer[],
|
||||||
previousPlayers: AnimationPlayer[]): AnimationPlayer {
|
index: number = 0): AnimationPlayer {
|
||||||
|
// only the very first animation can absorb the previous styles. This
|
||||||
|
// is here to prevent the an overlap situation where a group animation
|
||||||
|
// absorbs previous styles multiple times for the same element.
|
||||||
|
if (index && previousPlayers.length) {
|
||||||
|
previousPlayers = [];
|
||||||
|
}
|
||||||
return this._driver.animate(
|
return this._driver.animate(
|
||||||
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
|
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
|
||||||
instruction.delay, instruction.easing, previousPlayers);
|
instruction.delay, instruction.easing, previousPlayers);
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||||
let startingKeyframe = keyframes[0];
|
let startingKeyframe = keyframes[0];
|
||||||
let missingStyleProps: string[] = [];
|
let missingStyleProps: string[] = [];
|
||||||
previousStyleProps.forEach(prop => {
|
previousStyleProps.forEach(prop => {
|
||||||
if (startingKeyframe[prop] != null) {
|
if (!startingKeyframe.hasOwnProperty(prop)) {
|
||||||
missingStyleProps.push(prop);
|
missingStyleProps.push(prop);
|
||||||
}
|
}
|
||||||
startingKeyframe[prop] = this.previousStyles[prop];
|
startingKeyframe[prop] = this.previousStyles[prop];
|
||||||
|
@ -102,7 +102,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||||
_triggerWebAnimation(element: any, keyframes: any[], options: any): DOMAnimation {
|
_triggerWebAnimation(element: any, keyframes: any[], options: any): DOMAnimation {
|
||||||
// jscompiler doesn't seem to know animate is a native property because it's not fully
|
// jscompiler doesn't seem to know animate is a native property because it's not fully
|
||||||
// supported yet across common browsers (we polyfill it for Edge/Safari) [CL #143630929]
|
// supported yet across common browsers (we polyfill it for Edge/Safari) [CL #143630929]
|
||||||
return <DOMAnimation>element['animate'](keyframes, options);
|
return element['animate'](keyframes, options) as DOMAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
get domPlayer() { return this._player; }
|
get domPlayer() { return this._player; }
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import {DOMAnimation} from '../../src/render/web_animations/dom_animation';
|
||||||
|
import {WebAnimationsPlayer} from '../../src/render/web_animations/web_animations_player';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe('WebAnimationsPlayer', function() {
|
||||||
|
// these tests are only mean't to be run within the DOM
|
||||||
|
if (typeof Element == 'undefined') return;
|
||||||
|
|
||||||
|
let element: any;
|
||||||
|
beforeEach(() => {
|
||||||
|
element = document.createElement('div');
|
||||||
|
document.body.appendChild(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => { document.body.removeChild(element); });
|
||||||
|
|
||||||
|
it('should properly balance any previous player styles into the animation keyframes', () => {
|
||||||
|
element.style.height = '666px';
|
||||||
|
element.style.width = '333px';
|
||||||
|
|
||||||
|
const prevPlayer1 = new MockWebAnimationsPlayer(
|
||||||
|
element, [{width: '0px', offset: 0}, {width: '200px', offset: 1}], {});
|
||||||
|
prevPlayer1.play();
|
||||||
|
prevPlayer1.finish();
|
||||||
|
|
||||||
|
const prevPlayer2 = new MockWebAnimationsPlayer(
|
||||||
|
element, [{height: '0px', offset: 0}, {height: '200px', offset: 1}], {});
|
||||||
|
prevPlayer2.play();
|
||||||
|
prevPlayer2.finish();
|
||||||
|
|
||||||
|
// what needs to happen here is the player below should
|
||||||
|
// examine which styles are present in the provided previous
|
||||||
|
// players and use them as input data for the keyframes of
|
||||||
|
// the new player. Given that the players are in their finished
|
||||||
|
// state, the styles are copied over as the starting keyframe
|
||||||
|
// for the animation and if the styles are missing in later keyframes
|
||||||
|
// then the styling is resolved by computing the styles
|
||||||
|
const player = new MockWebAnimationsPlayer(
|
||||||
|
element, [{width: '100px', offset: 0}, {width: '500px', offset: 1}], {},
|
||||||
|
[prevPlayer1, prevPlayer2]);
|
||||||
|
|
||||||
|
player.init();
|
||||||
|
expect(player.capturedKeyframes).toEqual([
|
||||||
|
{height: '200px', width: '200px', offset: 0},
|
||||||
|
{height: '666px', width: '500px', offset: 1}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockWebAnimationsPlayer extends WebAnimationsPlayer {
|
||||||
|
capturedKeyframes: any[];
|
||||||
|
|
||||||
|
_triggerWebAnimation(element: any, keyframes: any[], options: any): any {
|
||||||
|
this.capturedKeyframes = keyframes;
|
||||||
|
return new MockDOMAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockDOMAnimation implements DOMAnimation {
|
||||||
|
onfinish = (callback: (e: any) => any) => {};
|
||||||
|
position = 0;
|
||||||
|
currentTime = 0;
|
||||||
|
|
||||||
|
cancel(): void {}
|
||||||
|
play(): void {}
|
||||||
|
pause(): void {}
|
||||||
|
finish(): void {}
|
||||||
|
addEventListener(eventName: string, handler: (event: any) => any): any { return null; }
|
||||||
|
dispatchEvent(eventName: string): any { return null; }
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
|
import {AUTO_STYLE, AnimationEvent, animate, group, keyframes, state, style, transition, trigger} from '@angular/animations';
|
||||||
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver} from '@angular/animations/browser';
|
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver} from '@angular/animations/browser';
|
||||||
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
|
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
|
||||||
import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core';
|
import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core';
|
||||||
|
@ -404,7 +404,8 @@ export function main() {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel and merge in mid-animation styles into the follow-up animation', () => {
|
it('should cancel and merge in mid-animation styles into the follow-up animation, but only for animation keyframes that start right away',
|
||||||
|
() => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ani-cmp',
|
selector: 'ani-cmp',
|
||||||
template: `
|
template: `
|
||||||
|
@ -422,8 +423,14 @@ export function main() {
|
||||||
transition(
|
transition(
|
||||||
'b => c',
|
'b => c',
|
||||||
[
|
[
|
||||||
style({'width': '0'}),
|
group([
|
||||||
animate(500, style({'width': '100px'})),
|
animate(500, style({'width': '100px'})),
|
||||||
|
animate(500, style({'height': '100px'})),
|
||||||
|
]),
|
||||||
|
animate(500, keyframes([
|
||||||
|
style({'opacity': '0'}),
|
||||||
|
style({'opacity': '1'})
|
||||||
|
]))
|
||||||
]),
|
]),
|
||||||
])],
|
])],
|
||||||
})
|
})
|
||||||
|
@ -452,10 +459,13 @@ export function main() {
|
||||||
cmp.exp = 'c';
|
cmp.exp = 'c';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
engine.flush();
|
engine.flush();
|
||||||
expect(getLog().length).toEqual(1);
|
|
||||||
|
|
||||||
const data = getLog().pop();
|
const players = getLog();
|
||||||
expect(data.previousStyles).toEqual({opacity: AUTO_STYLE});
|
expect(players.length).toEqual(3);
|
||||||
|
const [p1, p2, p3] = players;
|
||||||
|
expect(p1.previousStyles).toEqual({opacity: AUTO_STYLE});
|
||||||
|
expect(p2.previousStyles).toEqual({});
|
||||||
|
expect(p3.previousStyles).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should invoke an animation trigger that is state-less', () => {
|
it('should invoke an animation trigger that is state-less', () => {
|
||||||
|
|
Loading…
Reference in New Issue