fix(animations): only use the WA-polyfill alongside AnimationBuilder (#22143)

This patch removes the need to include the Web Animations API Polyfill
(web-animations-js) as a dependency. Angular will now fallback to using
CSS Keyframes in the event that `element.animate` is no longer supported
by the browser.

In the event that an application does use `AnimationBuilder` then the
web-animations-js polyfill is required to enable programmatic,
position-based access to an animation.

Closes #17496

PR Close #22143
This commit is contained in:
Matias Niemelä
2018-02-08 15:01:43 -08:00
committed by Victor Berchet
parent 9eecb0b27f
commit b2f366b3b7
23 changed files with 1680 additions and 81 deletions

View File

@ -10,5 +10,7 @@ export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationSty
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine_next';
export {CssKeyframesDriver as ɵCssKeyframesDriver} from './render/css_keyframes/css_keyframes_driver';
export {CssKeyframesPlayer as ɵCssKeyframesPlayer} from './render/css_keyframes/css_keyframes_player';
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

View File

@ -33,7 +33,8 @@ export class NoopAnimationDriver implements AnimationDriver {
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): AnimationPlayer {
easing: string, previousPlayers: any[] = [],
scrubberAccessRequested?: boolean): AnimationPlayer {
return new NoopAnimationPlayer(duration, delay);
}
}
@ -56,5 +57,5 @@ export abstract class AnimationDriver {
abstract animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing?: string|null, previousPlayers?: any[]): any;
easing?: string|null, previousPlayers?: any[], scrubberAccessRequested?: boolean): any;
}

View File

@ -0,0 +1,151 @@
/**
* @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 {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle} from '../../util';
import {AnimationDriver} from '../animation_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {CssKeyframesPlayer} from './css_keyframes_player';
import {DirectStylePlayer} from './direct_style_player';
const KEYFRAMES_NAME_PREFIX = 'gen_css_kf_';
const TAB_SPACE = ' ';
export class CssKeyframesDriver implements AnimationDriver {
private _count = 0;
private readonly _head: any = document.querySelector('head');
private _warningIssued = false;
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string {
return (window.getComputedStyle(element) as any)[prop] as string;
}
buildKeyframeElement(element: any, name: string, keyframes: {[key: string]: any}[]): any {
keyframes = keyframes.map(kf => hypenatePropsObject(kf));
let keyframeStr = `@keyframes ${name} {\n`;
let tab = '';
keyframes.forEach(kf => {
tab = TAB_SPACE;
const offset = parseFloat(kf.offset);
keyframeStr += `${tab}${offset * 100}% {\n`;
tab += TAB_SPACE;
Object.keys(kf).forEach(prop => {
const value = kf[prop];
switch (prop) {
case 'offset':
return;
case 'easing':
if (value) {
keyframeStr += `${tab}animation-timing-function: ${value};\n`;
}
return;
default:
keyframeStr += `${tab}${prop}: ${value};\n`;
return;
}
});
keyframeStr += `${tab}}\n`;
});
keyframeStr += `}\n`;
const kfElm = document.createElement('style');
kfElm.innerHTML = keyframeStr;
return kfElm;
}
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
if (scrubberAccessRequested) {
this._notifyFaultyScrubber();
}
const previousCssKeyframePlayers = <CssKeyframesPlayer[]>previousPlayers.filter(
player => player instanceof CssKeyframesPlayer);
const previousStyles: {[key: string]: any} = {};
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousCssKeyframePlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
const finalStyles = flattenKeyframesIntoStyles(keyframes);
// if there is no animation then there is no point in applying
// styles and waiting for an event to get fired. This causes lag.
// It's better to just directly apply the styles to the element
// via the direct styling animation player.
if (duration == 0) {
return new DirectStylePlayer(element, finalStyles);
}
const animationName = `${KEYFRAMES_NAME_PREFIX}${this._count++}`;
const kfElm = this.buildKeyframeElement(element, animationName, keyframes);
document.querySelector('head') !.appendChild(kfElm);
const player = new CssKeyframesPlayer(
element, keyframes, animationName, duration, delay, easing, finalStyles);
player.onDestroy(() => removeElement(kfElm));
return player;
}
private _notifyFaultyScrubber() {
if (!this._warningIssued) {
console.warn(
'@angular/animations: please load the web-animations.js polyfill to allow programmatic access...\n',
' visit http://bit.ly/IWukam to learn more about using the web-animation-js polyfill.');
this._warningIssued = true;
}
}
}
function flattenKeyframesIntoStyles(
keyframes: null | {[key: string]: any} | {[key: string]: any}[]): {[key: string]: any} {
let flatKeyframes: {[key: string]: any} = {};
if (keyframes) {
const kfs = Array.isArray(keyframes) ? keyframes : [keyframes];
kfs.forEach(kf => {
Object.keys(kf).forEach(prop => {
if (prop == 'offset' || prop == 'easing') return;
flatKeyframes[prop] = kf[prop];
});
});
}
return flatKeyframes;
}
function hypenatePropsObject(object: {[key: string]: any}): {[key: string]: any} {
const newObj: {[key: string]: any} = {};
Object.keys(object).forEach(prop => {
const newProp = prop.replace(/([a-z])([A-Z])/g, '$1-$2');
newObj[newProp] = object[prop];
});
return newObj;
}
function removeElement(node: any) {
node.parentNode.removeChild(node);
}

View File

@ -0,0 +1,149 @@
/**
* @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 {AnimationPlayer} from '@angular/animations';
import {computeStyle} from '../../util';
import {ElementAnimationStyleHandler} from './element_animation_style_handler';
const DEFAULT_FILL_MODE = 'forwards';
const DEFAULT_EASING = 'linear';
const ANIMATION_END_EVENT = 'animationend';
export enum AnimatorControlState {
INITIALIZED = 1,
STARTED = 2,
FINISHED = 3,
DESTROYED = 4
}
export class CssKeyframesPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = [];
private _onStartFns: Function[] = [];
private _onDestroyFns: Function[] = [];
private _started = false;
private _styler: ElementAnimationStyleHandler;
public parentPlayer: AnimationPlayer;
public readonly totalTime: number;
public readonly easing: string;
public currentSnapshot: {[key: string]: string} = {};
public state = 0;
constructor(
public readonly element: any, public readonly keyframes: {[key: string]: string | number}[],
public readonly animationName: string, private readonly _duration: number,
private readonly _delay: number, easing: string,
private readonly _finalStyles: {[key: string]: any}) {
this.easing = easing || DEFAULT_EASING;
this.totalTime = _duration + _delay;
this._buildStyler();
}
onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
destroy() {
this.init();
if (this.state >= AnimatorControlState.DESTROYED) return;
this.state = AnimatorControlState.DESTROYED;
this._styler.destroy();
this._flushStartFns();
this._flushDoneFns();
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
private _flushDoneFns() {
this._onDoneFns.forEach(fn => fn());
this._onDoneFns = [];
}
private _flushStartFns() {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
}
finish() {
this.init();
if (this.state >= AnimatorControlState.FINISHED) return;
this.state = AnimatorControlState.FINISHED;
this._styler.finish();
this._flushStartFns();
this._flushDoneFns();
}
setPosition(value: number) { this._styler.setPosition(value); }
getPosition(): number { return this._styler.getPosition(); }
hasStarted(): boolean { return this.state >= AnimatorControlState.STARTED; }
init(): void {
if (this.state >= AnimatorControlState.INITIALIZED) return;
this.state = AnimatorControlState.INITIALIZED;
const elm = this.element;
this._styler.apply();
if (this._delay) {
this._styler.pause();
}
}
play(): void {
this.init();
if (!this.hasStarted()) {
this._flushStartFns();
this.state = AnimatorControlState.STARTED;
}
this._styler.resume();
}
pause(): void {
this.init();
this._styler.pause();
}
restart(): void {
this.reset();
this.play();
}
reset(): void {
this._styler.destroy();
this._buildStyler();
this._styler.apply();
}
private _buildStyler() {
this._styler = new ElementAnimationStyleHandler(
this.element, this.animationName, this._duration, this._delay, this.easing,
DEFAULT_FILL_MODE, () => this.finish());
}
/* @internal */
triggerCallback(phaseName: string): void {
const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns;
methods.forEach(fn => fn());
methods.length = 0;
}
beforeDestroy() {
this.init();
const styles: {[key: string]: string} = {};
if (this.hasStarted()) {
const finished = this.state >= AnimatorControlState.FINISHED;
Object.keys(this._finalStyles).forEach(prop => {
if (prop != 'offset') {
styles[prop] = finished ? this._finalStyles[prop] : computeStyle(this.element, prop);
}
});
}
this.currentSnapshot = styles;
}
}

View File

@ -0,0 +1,45 @@
/**
* @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 {NoopAnimationPlayer} from '@angular/animations';
export class DirectStylePlayer extends NoopAnimationPlayer {
private _startingStyles: {[key: string]: any}|null = {};
private __initialized = false;
constructor(public element: any, private _styles: {[key: string]: any}) { super(); }
init() {
if (this.__initialized || !this._startingStyles) return;
this.__initialized = true;
Object.keys(this._styles).forEach(prop => {
this._startingStyles ![prop] = this.element.style[prop];
});
super.init();
}
play() {
if (!this._startingStyles) return;
this.init();
Object.keys(this._styles).forEach(prop => { this.element.style[prop] = this._styles[prop]; });
super.play();
}
destroy() {
if (!this._startingStyles) return;
Object.keys(this._startingStyles).forEach(prop => {
const value = this._startingStyles ![prop];
if (value) {
this.element.style[prop] = value;
} else {
this.element.style.removeProperty(prop);
}
});
this._startingStyles = null;
super.destroy();
}
}

View File

@ -0,0 +1,147 @@
/**
* @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
*/
const ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
const ANIMATION_PROP = 'animation';
const ANIMATIONEND_EVENT = 'animationend';
const ONE_SECOND = 1000;
export class ElementAnimationStyleHandler {
private readonly _eventFn: (e: any) => any;
private _finished = false;
private _destroyed = false;
private _startTime = 0;
private _position = 0;
constructor(
private readonly _element: any, private readonly _name: string,
private readonly _duration: number, private readonly _delay: number,
private readonly _easing: string, private readonly _fillMode: ''|'both'|'forwards',
private readonly _onDoneFn: () => any) {
this._eventFn = (e) => this._handleCallback(e);
}
apply() {
applyKeyframeAnimation(
this._element,
`${this._duration}ms ${this._easing} ${this._delay}ms 1 normal ${this._fillMode} ${this._name}`);
addRemoveAnimationEvent(this._element, this._eventFn, false);
this._startTime = Date.now();
}
pause() { playPauseAnimation(this._element, this._name, 'paused'); }
resume() { playPauseAnimation(this._element, this._name, 'running'); }
setPosition(position: number) {
const index = findIndexForAnimation(this._element, this._name);
this._position = position * this._duration;
setAnimationStyle(this._element, 'Delay', `-${this._position}ms`, index);
}
getPosition() { return this._position; }
private _handleCallback(event: any) {
const timestamp = event._ngTestManualTimestamp || Date.now();
const elapsedTime =
parseFloat(event.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)) * ONE_SECOND;
if (event.animationName == this._name &&
Math.max(timestamp - this._startTime, 0) >= this._delay && elapsedTime >= this._duration) {
this.finish();
}
}
finish() {
if (this._finished) return;
this._finished = true;
this._onDoneFn();
addRemoveAnimationEvent(this._element, this._eventFn, true);
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
this.finish();
removeKeyframeAnimation(this._element, this._name);
}
}
function playPauseAnimation(element: any, name: string, status: 'running' | 'paused') {
const index = findIndexForAnimation(element, name);
setAnimationStyle(element, 'PlayState', status, index);
}
function applyKeyframeAnimation(element: any, value: string): number {
const anim = getAnimationStyle(element, '').trim();
let index = 0;
if (anim.length) {
index = countChars(anim, ',') + 1;
value = `${anim}, ${value}`;
}
setAnimationStyle(element, '', value);
return index;
}
function removeKeyframeAnimation(element: any, name: string) {
const anim = getAnimationStyle(element, '');
const tokens = anim.split(',');
const index = findMatchingTokenIndex(tokens, name);
if (index >= 0) {
tokens.splice(index, 1);
const newValue = tokens.join(',');
setAnimationStyle(element, '', newValue);
}
}
function findIndexForAnimation(element: any, value: string) {
const anim = getAnimationStyle(element, '');
if (anim.indexOf(',') > 0) {
const tokens = anim.split(',');
return findMatchingTokenIndex(tokens, value);
}
return findMatchingTokenIndex([anim], value);
}
function findMatchingTokenIndex(tokens: string[], searchToken: string): number {
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].indexOf(searchToken) >= 0) {
return i;
}
}
return -1;
}
function addRemoveAnimationEvent(element: any, fn: (e: any) => any, doRemove: boolean) {
doRemove ? element.removeEventListener(ANIMATIONEND_EVENT, fn) :
element.addEventListener(ANIMATIONEND_EVENT, fn);
}
function setAnimationStyle(element: any, name: string, value: string, index?: number) {
const prop = ANIMATION_PROP + name;
if (index != null) {
const oldValue = element.style[prop];
if (oldValue.length) {
const tokens = oldValue.split(',');
tokens[index] = value;
value = tokens.join(',');
}
}
element.style[prop] = value;
}
function getAnimationStyle(element: any, name: string) {
return element.style[ANIMATION_PROP + name];
}
function countChars(value: string, char: string): number {
let count = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charAt(i);
if (c === char) count++;
}
return count;
}

View File

@ -44,7 +44,7 @@ export class TimelineAnimationEngine {
const element = i.element;
const keyframes = normalizeKeyframes(
this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, []);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, [], true);
}
create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer {

View File

@ -7,12 +7,17 @@
*/
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, copyStyles} from '../../util';
import {AnimationDriver} from '../animation_driver';
import {CssKeyframesDriver} from '../css_keyframes/css_keyframes_driver';
import {containsElement, invokeQuery, matchesElement, validateStyleProperty} from '../shared';
import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver {
private _isNativeImpl = /\{\s*\[native\s+code\]\s*\}/.test(getElementAnimateFn().toString());
private _cssKeyframesDriver = new CssKeyframesDriver();
validateStyleProperty(prop: string): boolean { return validateStyleProperty(prop); }
matchesElement(element: any, selector: string): boolean {
@ -29,24 +34,46 @@ export class WebAnimationsDriver implements AnimationDriver {
return (window.getComputedStyle(element) as any)[prop] as string;
}
overrideWebAnimationsSupport(supported: boolean) { this._isNativeImpl = supported; }
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
previousPlayers: AnimationPlayer[] = [], scrubberAccessRequested?: boolean): AnimationPlayer {
const useKeyframes = !scrubberAccessRequested && !this._isNativeImpl;
if (useKeyframes) {
return this._cssKeyframesDriver.animate(
element, keyframes, duration, delay, easing, previousPlayers);
}
const fill = delay == 0 ? 'both' : 'forwards';
const playerOptions: {[key: string]: string | number} = {duration, delay, fill};
// we check for this to avoid having a null|undefined value be present
// for the easing (which results in an error for certain browsers #9752)
if (easing) {
playerOptions['easing'] = easing;
}
const previousStyles: {[key: string]: any} = {};
const previousWebAnimationPlayers = <WebAnimationsPlayer[]>previousPlayers.filter(
player => { return player instanceof WebAnimationsPlayer; });
return new WebAnimationsPlayer(element, keyframes, playerOptions, previousWebAnimationPlayers);
player => player instanceof WebAnimationsPlayer);
if (allowPreviousPlayerStylesMerge(duration, delay)) {
previousWebAnimationPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => previousStyles[prop] = styles[prop]);
});
}
keyframes = keyframes.map(styles => copyStyles(styles, false));
keyframes = balancePreviousStylesIntoKeyframes(element, keyframes, previousStyles);
return new WebAnimationsPlayer(element, keyframes, playerOptions);
}
}
export function supportsWebAnimations() {
return typeof Element !== 'undefined' && typeof(<any>Element).prototype['animate'] === 'function';
return typeof getElementAnimateFn() === 'function';
}
function getElementAnimateFn(): any {
return (typeof Element !== 'undefined' && (<any>Element).prototype['animate']) || {};
}

View File

@ -7,7 +7,7 @@
*/
import {AnimationPlayer} from '@angular/animations';
import {allowPreviousPlayerStylesMerge, copyStyles} from '../../util';
import {allowPreviousPlayerStylesMerge, balancePreviousStylesIntoKeyframes, computeStyle, copyStyles} from '../../util';
import {DOMAnimation} from './dom_animation';
@ -27,23 +27,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
public time = 0;
public parentPlayer: AnimationPlayer|null = null;
public previousStyles: {[styleName: string]: string | number} = {};
public currentSnapshot: {[styleName: string]: string | number} = {};
constructor(
public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number},
private previousPlayers: WebAnimationsPlayer[] = []) {
public options: {[key: string]: string | number}) {
this._duration = <number>options['duration'];
this._delay = <number>options['delay'] || 0;
this.time = this._duration + this._delay;
if (allowPreviousPlayerStylesMerge(this._duration, this._delay)) {
previousPlayers.forEach(player => {
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
}
}
private _onFinish() {
@ -63,30 +54,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
if (this._initialized) return;
this._initialized = true;
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = this.previousStyles[prop];
});
if (missingStyleProps.length) {
const self = this;
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) {
kf[prop] = _computeStyle(self.element, prop);
});
}
}
}
const keyframes = this.keyframes;
(this as{domPlayer: DOMAnimation}).domPlayer =
this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
@ -178,7 +146,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') {
styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop);
this._finished ? this._finalKeyframe[prop] : computeStyle(this.element, prop);
}
});
}
@ -192,7 +160,3 @@ export class WebAnimationsPlayer implements AnimationPlayer {
methods.length = 0;
}
}
function _computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}

View File

@ -235,6 +235,30 @@ export function allowPreviousPlayerStylesMerge(duration: number, delay: number)
return duration === 0 || delay === 0;
}
export function balancePreviousStylesIntoKeyframes(
element: any, keyframes: {[key: string]: any}[], previousStyles: {[key: string]: any}) {
const previousStyleProps = Object.keys(previousStyles);
if (previousStyleProps.length && keyframes.length) {
let startingKeyframe = keyframes[0];
let missingStyleProps: string[] = [];
previousStyleProps.forEach(prop => {
if (!startingKeyframe.hasOwnProperty(prop)) {
missingStyleProps.push(prop);
}
startingKeyframe[prop] = previousStyles[prop];
});
if (missingStyleProps.length) {
// tslint:disable-next-line
for (var i = 1; i < keyframes.length; i++) {
let kf = keyframes[i];
missingStyleProps.forEach(function(prop) { kf[prop] = computeStyle(element, prop); });
}
}
}
return keyframes;
}
export function visitDslNode(
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any): any;
export function visitDslNode(
@ -271,3 +295,7 @@ export function visitDslNode(visitor: any, node: any, context: any): any {
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}
}
export function computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}

View File

@ -10,5 +10,6 @@ ts_library(
"//packages/animations/browser",
"//packages/animations/browser/testing",
"//packages/core",
"//packages/core/testing",
],
)

View File

@ -0,0 +1,400 @@
/**
* @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 {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {CssKeyframesDriver} from '../../../src/render/css_keyframes/css_keyframes_driver';
import {CssKeyframesPlayer} from '../../../src/render/css_keyframes/css_keyframes_player';
import {DirectStylePlayer} from '../../../src/render/css_keyframes/direct_style_player';
import {assertElementExistsInDom, createElement, findKeyframeDefinition, forceReflow, makeAnimationEvent, supportsAnimationEventCreation} from './shared';
const CSS_KEYFRAME_RULE_TYPE = 7;
describe('CssKeyframesDriver tests', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined' ||
typeof(window as any)['AnimationEvent'] == 'undefined')
return;
describe('building keyframes', () => {
it('should build CSS keyframe style object containing the keyframe styles', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const kfElm = animator.buildKeyframeElement(elm, 'myKfAnim', [
{opacity: 0, width: '0px', offset: 0},
{opacity: 0.5, width: '100px', offset: 0.5},
{opacity: 1, width: '200px', offset: 1},
]);
const head = document.querySelector('head') !;
head.appendChild(kfElm);
forceReflow();
const sheet = kfElm.sheet;
const kfRule = findKeyframeDefinition(sheet);
expect(kfRule.name).toEqual('myKfAnim');
expect(kfRule.type).toEqual(CSS_KEYFRAME_RULE_TYPE);
const keyframeCssRules = kfRule.cssRules;
expect(keyframeCssRules.length).toEqual(3);
const [from, mid, to] = keyframeCssRules;
expect(from.keyText).toEqual('0%');
expect(mid.keyText).toEqual('50%');
expect(to.keyText).toEqual('100%');
const fromStyles = from.style;
expect(fromStyles.opacity).toEqual('0');
expect(fromStyles.width).toEqual('0px');
const midStyles = mid.style;
expect(midStyles.opacity).toEqual('0.5');
expect(midStyles.width).toEqual('100px');
const toStyles = to.style;
expect(toStyles.opacity).toEqual('1');
expect(toStyles.width).toEqual('200px');
});
it('should convert numeric values into px-suffixed data', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const kfElm = animator.buildKeyframeElement(elm, 'myKfAnim', [
{width: '0px', offset: 0},
{width: '100px', offset: 0.5},
{width: '200px', offset: 1},
]);
const head = document.querySelector('head') !;
head.appendChild(kfElm);
forceReflow();
const sheet = kfElm.sheet;
const kfRule = findKeyframeDefinition(sheet);
const keyframeCssRules = kfRule.cssRules;
const [from, mid, to] = keyframeCssRules;
expect(from.style.width).toEqual('0px');
expect(mid.style.width).toEqual('100px');
expect(to.style.width).toEqual('200px');
});
});
describe('when animating', () => {
it('should set an animation on the element that matches the generated animation', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const sheet: any = document.styleSheets[document.styleSheets.length - 1];
const kfRule = findKeyframeDefinition(sheet);
player.init();
const {animationName, duration, easing} = parseElementAnimationStyle(elm);
expect(animationName).toEqual(kfRule.name);
expect(duration).toEqual(1234);
expect(easing).toEqual('ease-out');
});
it('should animate until the `animationend` method is emitted, but stil retain the <style> method and the element animation details',
fakeAsync(() => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const matchingStyleElm = findStyleObjectWithKeyframes();
player.play();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
let completed = false;
player.onDone(() => completed = true);
expect(completed).toBeFalsy();
flushMicrotasks();
expect(completed).toBeFalsy();
const event = makeAnimationEvent('end', player.animationName, 1234);
elm.dispatchEvent(event);
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
}));
it('should animate until finish() is called, but stil retain the <style> method and the element animation details',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
const matchingStyleElm = findStyleObjectWithKeyframes();
player.play();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
let completed = false;
player.onDone(() => completed = true);
expect(completed).toBeFalsy();
flushMicrotasks();
expect(completed).toBeFalsy();
player.finish();
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 1234);
assertElementExistsInDom(matchingStyleElm, true);
}));
it('should animate until the destroy method is called and cleanup the element animation details',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
player.play();
assertExistingAnimationDuration(elm, 1234);
let completed = false;
player.onDone(() => completed = true);
flushMicrotasks();
expect(completed).toBeFalsy();
player.destroy();
flushMicrotasks();
expect(completed).toBeTruthy();
assertExistingAnimationDuration(elm, 0);
}));
it('should return an instance of a direct style player if an animation has a duration of 0',
() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
assertExistingAnimationDuration(elm, 0);
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
0, 0, 'ease-out');
expect(player instanceof DirectStylePlayer).toBeTruthy();
});
it('should cleanup the associated <style> object when the animation is destroyed',
fakeAsync(() => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = animator.animate(
elm,
[
{width: '0px', offset: 0},
{width: '200px', offset: 1},
],
1234, 0, 'ease-out');
player.play();
const matchingStyleElm = findStyleObjectWithKeyframes();
assertElementExistsInDom(matchingStyleElm, true);
player.destroy();
flushMicrotasks();
assertElementExistsInDom(matchingStyleElm, false);
}));
it('should return the final styles when capture() is called', () => {
const elm = createElement();
const animator = new CssKeyframesDriver();
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{color: 'red', width: '111px', height: '111px', offset: 0},
{color: 'blue', height: '999px', width: '999px', offset: 1},
],
2000, 0, 'ease-out');
player.play();
player.finish();
player.beforeDestroy !();
expect(player.currentSnapshot).toEqual({
width: '999px',
height: '999px',
color: 'blue',
});
});
it('should return the intermediate styles when capture() is called in the middle of the animation',
() => {
const elm = createElement();
document.body.appendChild(elm); // this is required so GCS works
const animator = new CssKeyframesDriver();
const player = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', height: '0px', offset: 0},
{height: '100px', width: '100px', offset: 1},
],
2000, 0, 'ease-out');
player.play();
player.setPosition(0.5);
player.beforeDestroy();
const result = player.currentSnapshot;
expect(parseFloat(result['width'])).toBeGreaterThan(0);
expect(parseFloat(result['height'])).toBeGreaterThan(0);
});
it('should capture existing keyframe player styles in and merge in the styles into the follow up player\'s keyframes',
() => {
// IE cannot modify the position of an animation...
// note that this feature is only for testing purposes
if (isIE()) return;
const elm = createElement();
elm.style.border = '1px solid black';
document.body.appendChild(elm); // this is required so GCS works
const animator = new CssKeyframesDriver();
const p1 = <CssKeyframesPlayer>animator.animate(
elm,
[
{width: '0px', lineHeight: '20px', offset: 0},
{width: '100px', lineHeight: '50px', offset: 1},
],
2000, 0, 'ease-out');
const p2 = <CssKeyframesPlayer>animator.animate(
elm,
[
{height: '100px', offset: 0},
{height: '300px', offset: 1},
],
2000, 0, 'ease-out');
p1.play();
p1.setPosition(0.5);
p1.beforeDestroy();
p2.play();
p2.setPosition(0.5);
p2.beforeDestroy();
const p3 = <CssKeyframesPlayer>animator.animate(
elm,
[
{height: '0px', width: '0px', offset: 0},
{height: '400px', width: '400px', offset: 0.5},
{height: '500px', width: '500px', offset: 1},
],
2000, 0, 'ease-out', [p1, p2]);
p3.init();
const [k1, k2, k3] = p3.keyframes;
const offset = k1.offset;
expect(offset).toEqual(0);
const width = parseInt(k1['width'] as string);
expect(width).toBeGreaterThan(0);
expect(width).toBeLessThan(100);
const bWidth = parseInt(k1['lineHeight'] as string);
expect(bWidth).toBeGreaterThan(20);
expect(bWidth).toBeLessThan(50);
const height = parseFloat(k1['height'] as string);
expect(height).toBeGreaterThan(100);
expect(height).toBeLessThan(300);
// since the lineHeight wasn't apart of the follow-up animation,
// it's values were copied over into all the keyframes
const b1 = bWidth;
const b2 = parseInt(k2['lineHeight'] as string);
const b3 = parseInt(k3['lineHeight'] as string);
expect(b1).toEqual(b2);
expect(b2).toEqual(b3);
// we delete the lineHeight values because they are float-based
// and each browser has a different value based on precision...
// therefore we can't assert it directly below (asserting it above
// on the first keyframe was all that was needed since they are the same)
delete k2['lineHeight'];
delete k3['lineHeight'];
expect(k2).toEqual({width: '400px', height: '400px', offset: 0.5});
expect(k3).toEqual({width: '500px', height: '500px', offset: 1});
});
});
});
function assertExistingAnimationDuration(element: any, duration: number) {
expect(parseElementAnimationStyle(element).duration).toEqual(duration);
}
function findStyleObjectWithKeyframes(): any|null {
const sheetWithKeyframes = document.styleSheets[document.styleSheets.length - 1];
const styleElms = Array.from(document.querySelectorAll('head style') as any as any[]);
return styleElms.find(elm => elm.sheet == sheetWithKeyframes) || null;
}
function parseElementAnimationStyle(element: any):
{duration: number, delay: number, easing: string, animationName: string} {
const style = element.style;
const duration = parseInt(style.animationDuration || 0);
const delay = style.animationDelay;
const easing = style.animationTimingFunction;
const animationName = style.animationName;
return {duration, delay, easing, animationName};
}
function isIE() {
// note that this only applies to older IEs (not edge)
return (window as any).document['documentMode'] ? true : false;
}

View File

@ -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 {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {DirectStylePlayer} from '../../../src/render/css_keyframes/direct_style_player';
import {assertStyle, createElement} from './shared';
const CSS_KEYFRAME_RULE_TYPE = 7;
describe('DirectStylePlayer tests', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined') return;
it('should apply the styling to the given element when the animation starts and remove when destroyed',
() => {
const element = createElement();
const player = new DirectStylePlayer(element, {opacity: 0.5});
assertStyle(element, 'opacity', '');
player.play();
assertStyle(element, 'opacity', '0.5');
player.finish();
assertStyle(element, 'opacity', '0.5');
player.destroy();
assertStyle(element, 'opacity', '');
});
it('should finish the animation after one tick', fakeAsync(() => {
const element = createElement();
const player = new DirectStylePlayer(element, {opacity: 0.5});
let done = false;
player.onDone(() => done = true);
expect(done).toBeFalsy();
player.play();
expect(done).toBeFalsy();
flushMicrotasks();
expect(done).toBeTruthy();
}));
it('should restore existing element styles once the animation is destroyed', fakeAsync(() => {
const element = createElement();
element.style['width'] = '100px';
element.style['height'] = '200px';
const player = new DirectStylePlayer(element, {width: '500px', opacity: 0.5});
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
player.init();
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
player.play();
assertStyle(element, 'width', '500px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '0.5');
player.destroy();
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '200px');
assertStyle(element, 'opacity', '');
}));
});

View File

@ -0,0 +1,233 @@
/**
* @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 {ElementAnimationStyleHandler} from '../../../src/render/css_keyframes/element_animation_style_handler';
import {computeStyle} from '../../../src/util';
import {assertStyle, createElement, makeAnimationEvent, supportsAnimationEventCreation} from './shared';
const EMPTY_FN = () => {};
{
describe('ElementAnimationStyleHandler', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined' ||
typeof(window as any)['AnimationEvent'] == 'undefined')
return;
it('should add and remove an animation on to an element\'s styling', () => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'someAnimation', 1234, 999, 'ease-in', 'forwards', EMPTY_FN);
assertStyle(element, 'animation-name', '');
assertStyle(element, 'animation-duration', '');
assertStyle(element, 'animation-delay', '');
assertStyle(element, 'animation-timing-function', '');
assertStyle(element, 'animation-fill-mode', '');
handler.apply();
assertStyle(element, 'animation-name', 'someAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '999ms');
assertStyle(element, 'animation-timing-function', 'ease-in');
assertStyle(element, 'animation-fill-mode', 'forwards');
handler.finish();
assertStyle(element, 'animation-name', 'someAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '999ms');
assertStyle(element, 'animation-timing-function', 'ease-in');
assertStyle(element, 'animation-fill-mode', 'forwards');
handler.destroy();
assertStyle(element, 'animation-name', '');
assertStyle(element, 'animation-duration', '');
assertStyle(element, 'animation-delay', '');
assertStyle(element, 'animation-timing-function', '');
assertStyle(element, 'animation-fill-mode', '');
});
it('should respect existing animation styling on an element', () => {
const element = createElement();
document.body.appendChild(element);
element.style.setProperty('animation', 'fooAnimation 1s ease-out forwards');
assertStyle(element, 'animation-name', 'fooAnimation');
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', 'fooAnimation');
handler.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect animation styling that is prefixed after a handler is applied on an element',
() => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', '');
handler.apply();
assertStyle(element, 'animation-name', 'barAnimation');
const anim = element.style.animation;
element.style.setProperty('animation', `${anim}, fooAnimation 1s ease-out forwards`);
assertStyle(element, 'animation-name', 'barAnimation, fooAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect animation styling that is suffixed after a handler is applied on an element',
() => {
const element = createElement();
document.body.appendChild(element);
const handler = new ElementAnimationStyleHandler(
element, 'barAnimation', 1234, 555, 'ease-out', 'both', EMPTY_FN);
assertStyle(element, 'animation-name', '');
handler.apply();
assertStyle(element, 'animation-name', 'barAnimation');
const anim = element.style.animation;
element.style.setProperty('animation', `fooAnimation 1s ease-out forwards, ${anim}`);
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
handler.destroy();
assertStyle(element, 'animation-name', 'fooAnimation');
});
it('should respect existing animation handlers on an element', () => {
const element = createElement();
document.body.appendChild(element);
assertStyle(element, 'animation-name', '');
const h1 = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', EMPTY_FN);
h1.apply();
assertStyle(element, 'animation-name', 'fooAnimation');
assertStyle(element, 'animation-duration', '1234ms');
assertStyle(element, 'animation-delay', '333ms');
const h2 = new ElementAnimationStyleHandler(
element, 'barAnimation', 5678, 666, 'ease-out', 'both', EMPTY_FN);
h2.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation');
assertStyle(element, 'animation-duration', '1234ms, 5678ms');
assertStyle(element, 'animation-delay', '333ms, 666ms');
const h3 = new ElementAnimationStyleHandler(
element, 'bazAnimation', 90, 999, 'ease-out', 'both', EMPTY_FN);
h3.apply();
assertStyle(element, 'animation-name', 'fooAnimation, barAnimation, bazAnimation');
assertStyle(element, 'animation-duration', '1234ms, 5678ms, 90ms');
assertStyle(element, 'animation-delay', '333ms, 666ms, 999ms');
h2.destroy();
assertStyle(element, 'animation-name', 'fooAnimation, bazAnimation');
assertStyle(element, 'animation-duration', '1234ms, 90ms');
assertStyle(element, 'animation-delay', '333ms, 999ms');
h1.destroy();
assertStyle(element, 'animation-name', 'bazAnimation');
assertStyle(element, 'animation-duration', '90ms');
assertStyle(element, 'animation-delay', '999ms');
});
it('should fire the onDone method when .finish() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.finish();
expect(done).toBeTruthy();
});
it('should fire the onDone method only once when .finish() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let doneCount = 0;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => doneCount++);
expect(doneCount).toEqual(0);
handler.finish();
expect(doneCount).toEqual(1);
handler.finish();
expect(doneCount).toEqual(1);
});
it('should fire the onDone method when .destroy() is called on the handler', () => {
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.destroy();
expect(done).toBeTruthy();
});
it('should fire the onDone method when the matching animationend event is emitted', () => {
// IE10 and IE11 cannot create an instanceof AnimationEvent
if (!supportsAnimationEventCreation()) return;
const element = createElement();
document.body.appendChild(element);
let done = false;
const handler = new ElementAnimationStyleHandler(
element, 'fooAnimation', 1234, 333, 'ease-out', 'both', () => done = true);
expect(done).toBeFalsy();
handler.apply();
expect(done).toBeFalsy();
let event = makeAnimationEvent('end', 'fooAnimation', 100);
element.dispatchEvent(event);
expect(done).toBeFalsy();
event = makeAnimationEvent('end', 'fooAnimation', 1234);
element.dispatchEvent(event);
expect(done).toBeFalsy();
const timestampAfterDelay = Date.now() + 500;
event = makeAnimationEvent('end', 'fakeAnimation', 1234, timestampAfterDelay);
element.dispatchEvent(event);
expect(done).toBeFalsy();
event = makeAnimationEvent('end', 'fooAnimation', 1234, timestampAfterDelay);
element.dispatchEvent(event);
expect(done).toBeTruthy();
});
});
}

View File

@ -0,0 +1,50 @@
/**
* @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
*/
export function forceReflow() {
(document.body as any)['_reflow'] = document.body.clientWidth;
}
export function makeAnimationEvent(
startOrEnd: 'start' | 'end', animationName: string, elapsedTime: number, timestamp?: number) {
const e = new AnimationEvent('animation' + startOrEnd, {animationName, elapsedTime});
if (timestamp) {
(e as any)._ngTestManualTimestamp = timestamp;
}
return e;
}
export function supportsAnimationEventCreation() {
let supported = false;
try {
makeAnimationEvent('end', 'test', 0);
supported = true;
} catch (e) {
}
return supported;
}
export function findKeyframeDefinition(sheet: any): any|null {
return sheet.cssRules[0] || null;
}
export function createElement() {
return document.createElement('div');
}
export function assertStyle(element: any, prop: string, value: string) {
expect(element.style[prop] || '').toEqual(value);
}
export function assertElementExistsInDom(element: any, yes?: boolean) {
const exp = expect(element.parentNode);
if (yes) {
exp.toBeTruthy();
} else {
exp.toBeFalsy();
}
}

View File

@ -0,0 +1,61 @@
/**
* @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 {CssKeyframesPlayer} from '../../../src/render/css_keyframes/css_keyframes_player';
import {DOMAnimation} from '../../../src/render/web_animations/dom_animation';
import {WebAnimationsDriver} from '../../../src/render/web_animations/web_animations_driver';
import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animations_player';
{
describe('WebAnimationsDriver', () => {
if (typeof Element == 'undefined' || typeof document == 'undefined') return;
describe('when web-animations are not supported natively', () => {
it('should return an instance of a CssKeyframePlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(false);
const player = driver.animate(element, [], 1000, 1000, '', [], false);
expect(player instanceof CssKeyframesPlayer).toBeTruthy();
});
it('should return an instance of a WebAnimationsPlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(false);
const player = driver.animate(element, [], 1000, 1000, '', [], true);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
});
describe('when web-animations are supported natively', () => {
it('should return an instance of a WebAnimationsPlayer if scrubbing is not requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(true);
const player = driver.animate(element, [], 1000, 1000, '', [], false);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
it('should return an instance of a WebAnimationsPlayer if scrubbing is requested', () => {
const element = createElement();
const driver = makeDriver();
driver.overrideWebAnimationsSupport(true);
const player = driver.animate(element, [], 1000, 1000, '', [], true);
expect(player instanceof WebAnimationsPlayer).toBeTruthy();
});
});
});
}
function makeDriver() {
return new WebAnimationsDriver();
}
function createElement() {
return document.createElement('div');
}

View File

@ -33,24 +33,6 @@ import {WebAnimationsPlayer} from '../../../src/render/web_animations/web_animat
expect(p.log).toEqual(['pause', 'play']);
});
it('should allow an empty set of keyframes with a set of previous styles', () => {
const previousKeyframes = [
{opacity: 0, offset: 0},
{opacity: 1, offset: 1},
];
const previousPlayer = new WebAnimationsPlayer(element, previousKeyframes, {duration: 1000});
previousPlayer.play();
previousPlayer.finish();
previousPlayer.beforeDestroy();
const EMPTY_KEYFRAMES: any[] = [];
const player =
new WebAnimationsPlayer(element, EMPTY_KEYFRAMES, {duration: 1000}, [previousPlayer]);
player.play();
player.destroy();
});
it('should not pause the player if created and started before initialized', () => {
const keyframes = [
{opacity: 0, offset: 0},

View File

@ -0,0 +1,274 @@
/**
* @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 {animate, group, keyframes, query, state, style, transition, trigger} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵCssKeyframesDriver as CssKeyframesDriver, ɵCssKeyframesPlayer as CssKeyframesPlayer} from '@angular/animations/browser';
import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_group_player';
import {Component, ViewChild} from '@angular/core';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {TestBed} from '../../testing';
(function() {
// these tests are only mean't to be run within the DOM (for now)
// Buggy in Chromium 39, see https://github.com/angular/angular/issues/15793
if (typeof Element == 'undefined') return;
describe('animation integration tests using css keyframe animations', function() {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{provide: AnimationDriver, useClass: CssKeyframesDriver}],
imports: [BrowserAnimationsModule]
});
});
it('should compute (*) animation styles for a container that is being removed', () => {
@Component({
selector: 'ani-cmp',
template: `
<div @auto *ngIf="exp">
<div style="line-height:20px;">1</div>
<div style="line-height:20px;">2</div>
<div style="line-height:20px;">3</div>
<div style="line-height:20px;">4</div>
<div style="line-height:20px;">5</div>
</div>
`,
animations: [trigger(
'auto',
[
state('void', style({height: '0px'})),
state('*', style({height: '*'})),
transition('* => *', animate(1000)),
])]
})
class Cmp {
public exp: boolean = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
expect(engine.players.length).toEqual(1);
let player = getPlayer(engine) as CssKeyframesPlayer;
expect(player.keyframes).toEqual([{height: '0px', offset: 0}, {height: '100px', offset: 1}]);
player.finish();
if (browserDetection.isOldChrome) return;
cmp.exp = false;
fixture.detectChanges();
player = getPlayer(engine) as CssKeyframesPlayer;
expect(player.keyframes).toEqual([{height: '100px', offset: 0}, {height: '0px', offset: 1}]);
});
it('should cleanup all existing @keyframe <style> objects after the animation has finished',
() => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="myAnimationExp">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>
`,
animations: [trigger(
'myAnimation',
[
transition(
'* => go',
[
query(
'div',
[
style({opacity: 0}),
animate('1s', style({opacity: 0})),
]),
]),
])]
})
class Cmp {
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.myAnimationExp = 'go';
fixture.detectChanges();
const webPlayer = <AnimationGroupPlayer>getPlayer(engine);
const players = webPlayer.players as CssKeyframesPlayer[];
expect(players.length).toEqual(5);
const head = document.querySelector('head') !;
const sheets: any[] = [];
for (let i = 0; i < 5; i++) {
const sheet = findStyleObjectWithKeyframes(i);
expect(head.contains(sheet)).toBeTruthy();
sheets.push(sheet);
}
cmp.myAnimationExp = 'go-back';
fixture.detectChanges();
for (let i = 0; i < 5; i++) {
expect(head.contains(sheets[i])).toBeFalsy();
}
});
it('should properly handle easing values that are apart of the sequence', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #elm [@myAnimation]="myAnimationExp"></div>
`,
animations: [
trigger(
'myAnimation',
[
transition(
'* => goSteps',
[
style({opacity: 0}),
animate('1s ease-out', style({opacity: 1})),
]),
transition(
'* => goKeyframes',
[
animate('1s cubic-bezier(0.5, 1, 0.5, 1)', keyframes([
style({opacity: 0}),
style({opacity: 0.5}),
style({opacity: 1}),
])),
]),
]),
]
})
class Cmp {
@ViewChild('elm') public element: any;
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.myAnimationExp = 'goSteps';
fixture.detectChanges();
let kfElm = findStyleObjectWithKeyframes();
const [r1, r2] = kfElm.sheet.cssRules[0].cssRules;
assertEasing(r1, 'ease-out');
assertEasing(r2, '');
const element = cmp.element.nativeElement;
const webPlayer = getPlayer(engine);
cmp.myAnimationExp = 'goKeyframes';
fixture.detectChanges();
assertEasing(element, 'cubic-bezier(0.5,1,0.5,1)');
});
it('should restore existing style values once the animation completes', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #elm [@myAnimation]="myAnimationExp"></div>
`,
animations: [
trigger(
'myAnimation',
[
state('go', style({width: '200px'})),
transition(
'* => go',
[
style({height: '100px', width: '100px'}), group([
animate('1s', style({height: '200px'})),
animate('1s', style({width: '200px'}))
])
]),
]),
]
})
class Cmp {
@ViewChild('elm') public element: any;
public myAnimationExp = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
const element = cmp.element.nativeElement;
element.style['width'] = '50px';
element.style['height'] = '50px';
assertStyle(element, 'width', '50px');
assertStyle(element, 'height', '50px');
cmp.myAnimationExp = 'go';
fixture.detectChanges();
const player = getPlayer(engine);
assertStyle(element, 'width', '100px');
assertStyle(element, 'height', '100px');
player.finish();
assertStyle(element, 'width', '200px');
assertStyle(element, 'height', '50px');
});
});
})();
function approximate(value: number, target: number) {
return Math.abs(target - value) / value;
}
function getPlayer(engine: AnimationEngine, index = 0) {
return (engine.players[index] as any) !.getRealPlayer();
}
function findStyleObjectWithKeyframes(index?: number): any|null {
const sheetWithKeyframes = document.styleSheets[document.styleSheets.length - (index || 1)];
const styleElms = Array.from(document.querySelectorAll('head style') as any as any[]);
return styleElms.find(elm => elm.sheet == sheetWithKeyframes) || null;
}
function assertEasing(node: any, easing: string) {
expect((node.style.animationTimingFunction || '').replace(/\s+/g, '')).toEqual(easing);
}
function assertStyle(node: any, prop: string, value: string) {
expect(node.style[prop] || '').toEqual(value);
}

View File

@ -60,7 +60,6 @@ import {TestBed} from '../../testing';
cmp.exp = true;
fixture.detectChanges();
engine.flush();
expect(engine.players.length).toEqual(1);
let webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer;
@ -69,6 +68,8 @@ import {TestBed} from '../../testing';
{height: '0px', offset: 0}, {height: '100px', offset: 1}
]);
webPlayer.finish();
if (!browserDetection.isOldChrome) {
cmp.exp = false;
fixture.detectChanges();
@ -378,9 +379,9 @@ import {TestBed} from '../../testing';
player = engine.players[0] !;
webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
expect(approximate(parseFloat(webPlayer.previousStyles['width'] as string), 150))
expect(approximate(parseFloat(webPlayer.keyframes[0]['width'] as string), 150))
.toBeLessThan(0.05);
expect(approximate(parseFloat(webPlayer.previousStyles['height'] as string), 300))
expect(approximate(parseFloat(webPlayer.keyframes[0]['height'] as string), 300))
.toBeLessThan(0.05);
});
@ -445,9 +446,9 @@ import {TestBed} from '../../testing';
expect(players.length).toEqual(5);
for (let i = 0; i < players.length; i++) {
const p = players[i] as ɵWebAnimationsPlayer;
expect(approximate(parseFloat(p.previousStyles['width'] as string), 250))
expect(approximate(parseFloat(p.keyframes[0]['width'] as string), 250))
.toBeLessThan(0.05);
expect(approximate(parseFloat(p.previousStyles['height'] as string), 500))
expect(approximate(parseFloat(p.keyframes[0]['height'] as string), 500))
.toBeLessThan(0.05);
}
});

View File

@ -7,7 +7,7 @@
*/
import {AnimationBuilder} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵNoopAnimationDriver as NoopAnimationDriver, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵCssKeyframesDriver as CssKeyframesDriver, ɵNoopAnimationDriver as NoopAnimationDriver, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
@ -22,10 +22,7 @@ export class InjectableAnimationEngine extends AnimationEngine {
}
export function instantiateSupportedAnimationDriver() {
if (supportsWebAnimations()) {
return new WebAnimationsDriver();
}
return new NoopAnimationDriver();
return supportsWebAnimations() ? new WebAnimationsDriver() : new CssKeyframesDriver();
}
export function instantiateDefaultStyleNormalizer() {