fix(animations): ensure the web-animations driver converts style props to camel-case
The web animations API now requires that all styles are converted to camel case. Chrome has already made this breaking change and hyphenated styles are not functional anymore. Closes #9111 Closes #9112
This commit is contained in:
parent
7d9c1e1225
commit
4d51158b1a
|
@ -5,6 +5,8 @@ import {StringMapWrapper} from '../facade/collection';
|
||||||
import {StringWrapper, isNumber, isPresent} from '../facade/lang';
|
import {StringWrapper, isNumber, isPresent} from '../facade/lang';
|
||||||
|
|
||||||
import {getDOM} from './dom_adapter';
|
import {getDOM} from './dom_adapter';
|
||||||
|
import {DomAnimatePlayer} from './dom_animate_player';
|
||||||
|
import {dashCaseToCamelCase} from './util';
|
||||||
import {WebAnimationsPlayer} from './web_animations_player';
|
import {WebAnimationsPlayer} from './web_animations_player';
|
||||||
|
|
||||||
export class WebAnimationsDriver implements AnimationDriver {
|
export class WebAnimationsDriver implements AnimationDriver {
|
||||||
|
@ -13,7 +15,7 @@ export class WebAnimationsDriver implements AnimationDriver {
|
||||||
duration: number, delay: number, easing: string): AnimationPlayer {
|
duration: number, delay: number, easing: string): AnimationPlayer {
|
||||||
var anyElm = <any>element;
|
var anyElm = <any>element;
|
||||||
|
|
||||||
var formattedSteps: any[] /** TODO #9100 */ = [];
|
var formattedSteps: {[key: string]: string | number}[] = [];
|
||||||
var startingStyleLookup: {[key: string]: string | number} = {};
|
var startingStyleLookup: {[key: string]: string | number} = {};
|
||||||
if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
|
if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
|
||||||
startingStyleLookup = _populateStyles(anyElm, startingStyles, {});
|
startingStyleLookup = _populateStyles(anyElm, startingStyles, {});
|
||||||
|
@ -23,7 +25,7 @@ export class WebAnimationsDriver implements AnimationDriver {
|
||||||
|
|
||||||
keyframes.forEach((keyframe: AnimationKeyframe) => {
|
keyframes.forEach((keyframe: AnimationKeyframe) => {
|
||||||
let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup);
|
let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup);
|
||||||
(data as any /** TODO #9100 */)['offset'] = keyframe.offset;
|
data['offset'] = keyframe.offset;
|
||||||
formattedSteps.push(data);
|
formattedSteps.push(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,44 +35,52 @@ export class WebAnimationsDriver implements AnimationDriver {
|
||||||
// start/end values is suitable enough for the web-animations API
|
// start/end values is suitable enough for the web-animations API
|
||||||
if (formattedSteps.length == 1) {
|
if (formattedSteps.length == 1) {
|
||||||
var start = formattedSteps[0];
|
var start = formattedSteps[0];
|
||||||
start.offset = null;
|
start['offset'] = null;
|
||||||
formattedSteps = [start, start];
|
formattedSteps = [start, start];
|
||||||
}
|
}
|
||||||
|
|
||||||
var player = anyElm.animate(
|
var player = this._triggerWebAnimation(
|
||||||
formattedSteps,
|
anyElm, formattedSteps,
|
||||||
{'duration': duration, 'delay': delay, 'easing': easing, 'fill': 'forwards'});
|
{'duration': duration, 'delay': delay, 'easing': easing, 'fill': 'forwards'});
|
||||||
|
|
||||||
return new WebAnimationsPlayer(player, duration);
|
return new WebAnimationsPlayer(player, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
|
||||||
|
return elm.animate(keyframes, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _populateStyles(
|
function _populateStyles(
|
||||||
element: any, styles: AnimationStyles, defaultStyles: {[key: string]: string | number}) {
|
element: any, styles: AnimationStyles,
|
||||||
var data = {};
|
defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} {
|
||||||
|
var data: {[key: string]: string | number} = {};
|
||||||
styles.styles.forEach((entry) => {
|
styles.styles.forEach((entry) => {
|
||||||
StringMapWrapper.forEach(entry, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
|
StringMapWrapper.forEach(entry, (val: any, prop: string) => {
|
||||||
(data as any /** TODO #9100 */)[prop] = val == AUTO_STYLE ?
|
var formattedProp = dashCaseToCamelCase(prop);
|
||||||
_computeStyle(element, prop) :
|
data[formattedProp] = val == AUTO_STYLE ?
|
||||||
val.toString() + _resolveStyleUnit(val, prop);
|
_computeStyle(element, formattedProp) :
|
||||||
|
val.toString() + _resolveStyleUnit(val, prop, formattedProp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
StringMapWrapper.forEach(
|
StringMapWrapper.forEach(defaultStyles, (value: string, prop: string) => {
|
||||||
defaultStyles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
|
if (!isPresent(data[prop])) {
|
||||||
if (!isPresent((data as any /** TODO #9100 */)[prop])) {
|
data[prop] = value;
|
||||||
(data as any /** TODO #9100 */)[prop] = value;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resolveStyleUnit(val: string | number, prop: string): string {
|
function _resolveStyleUnit(
|
||||||
|
val: string | number, userProvidedProp: string, formattedProp: string): string {
|
||||||
var unit = '';
|
var unit = '';
|
||||||
if (_isPixelDimensionStyle(prop) && val != 0 && val != '0') {
|
if (_isPixelDimensionStyle(formattedProp) && val != 0 && val != '0') {
|
||||||
if (isNumber(val)) {
|
if (isNumber(val)) {
|
||||||
unit = 'px';
|
unit = 'px';
|
||||||
} else if (_findDimensionalSuffix(val.toString()).length == 0) {
|
} else if (_findDimensionalSuffix(val.toString()).length == 0) {
|
||||||
throw new BaseException('Please provide a CSS unit value for ' + prop + ':' + val);
|
throw new BaseException(
|
||||||
|
'Please provide a CSS unit value for ' + userProvidedProp + ':' + val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unit;
|
return unit;
|
||||||
|
@ -93,32 +103,32 @@ function _isPixelDimensionStyle(prop: string): boolean {
|
||||||
switch (prop) {
|
switch (prop) {
|
||||||
case 'width':
|
case 'width':
|
||||||
case 'height':
|
case 'height':
|
||||||
case 'min-width':
|
case 'minWidth':
|
||||||
case 'min-height':
|
case 'minHeight':
|
||||||
case 'max-width':
|
case 'maxWidth':
|
||||||
case 'max-height':
|
case 'maxHeight':
|
||||||
case 'left':
|
case 'left':
|
||||||
case 'top':
|
case 'top':
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
case 'right':
|
case 'right':
|
||||||
case 'font-size':
|
case 'fontSize':
|
||||||
case 'outline-width':
|
case 'outlineWidth':
|
||||||
case 'outline-offset':
|
case 'outlineOffset':
|
||||||
case 'padding-top':
|
case 'paddingTop':
|
||||||
case 'padding-left':
|
case 'paddingLeft':
|
||||||
case 'padding-bottom':
|
case 'paddingBottom':
|
||||||
case 'padding-right':
|
case 'paddingRight':
|
||||||
case 'margin-top':
|
case 'marginTop':
|
||||||
case 'margin-left':
|
case 'marginLeft':
|
||||||
case 'margin-bottom':
|
case 'marginBottom':
|
||||||
case 'margin-right':
|
case 'marginRight':
|
||||||
case 'border-radius':
|
case 'borderRadius':
|
||||||
case 'border-width':
|
case 'borderWidth':
|
||||||
case 'border-top-width':
|
case 'borderTopWidth':
|
||||||
case 'border-left-width':
|
case 'borderLeftWidth':
|
||||||
case 'border-right-width':
|
case 'borderRightWidth':
|
||||||
case 'border-bottom-width':
|
case 'borderBottomWidth':
|
||||||
case 'text-indent':
|
case 'textIndent':
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
|
||||||
|
import {el} from '@angular/platform-browser/testing';
|
||||||
|
|
||||||
|
import {AnimationKeyframe, AnimationStyles} from '../../core_private';
|
||||||
|
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
|
||||||
|
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
|
||||||
|
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
|
||||||
|
|
||||||
|
class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
|
||||||
|
public log: {[key: string]: any}[] = [];
|
||||||
|
|
||||||
|
constructor() { super(); }
|
||||||
|
|
||||||
|
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
|
||||||
|
this.log.push({'elm': elm, 'keyframes': keyframes, 'options': options});
|
||||||
|
return new MockDomAnimatePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeStyles(styles: {[key: string]: string | number}): AnimationStyles {
|
||||||
|
return new AnimationStyles([styles]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeKeyframe(
|
||||||
|
offset: number, styles: {[key: string]: string | number}): AnimationKeyframe {
|
||||||
|
return new AnimationKeyframe(offset, _makeStyles(styles));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe('WebAnimationsDriver', () => {
|
||||||
|
var driver: ExtendedWebAnimationsDriver;
|
||||||
|
var elm: HTMLElement;
|
||||||
|
beforeEach(() => {
|
||||||
|
driver = new ExtendedWebAnimationsDriver();
|
||||||
|
elm = el('<div></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert all styles to camelcase', () => {
|
||||||
|
var startingStyles = _makeStyles({'border-top-right': '40px'});
|
||||||
|
var styles = [
|
||||||
|
_makeKeyframe(0, {'max-width': '100px', 'height': '200px'}),
|
||||||
|
_makeKeyframe(1, {'font-size': '555px'})
|
||||||
|
];
|
||||||
|
|
||||||
|
driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||||
|
var details = driver.log.pop();
|
||||||
|
var startKeyframe = details['keyframes'][0];
|
||||||
|
var firstKeyframe = details['keyframes'][1];
|
||||||
|
var lastKeyframe = details['keyframes'][2];
|
||||||
|
|
||||||
|
expect(startKeyframe['borderTopRight']).toEqual('40px');
|
||||||
|
|
||||||
|
expect(firstKeyframe['maxWidth']).toEqual('100px');
|
||||||
|
expect(firstKeyframe['max-width']).toBeFalsy();
|
||||||
|
expect(firstKeyframe['height']).toEqual('200px');
|
||||||
|
|
||||||
|
expect(lastKeyframe['fontSize']).toEqual('555px');
|
||||||
|
expect(lastKeyframe['font-size']).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto prefix numeric properties with a `px` value', () => {
|
||||||
|
var startingStyles = _makeStyles({'borderTopWidth': 40});
|
||||||
|
var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})];
|
||||||
|
|
||||||
|
driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
|
||||||
|
var details = driver.log.pop();
|
||||||
|
var startKeyframe = details['keyframes'][0];
|
||||||
|
var firstKeyframe = details['keyframes'][1];
|
||||||
|
var lastKeyframe = details['keyframes'][2];
|
||||||
|
|
||||||
|
expect(startKeyframe['borderTopWidth']).toEqual('40px');
|
||||||
|
|
||||||
|
expect(firstKeyframe['fontSize']).toEqual('100px');
|
||||||
|
|
||||||
|
expect(lastKeyframe['height']).toEqual('555em');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,40 +1,7 @@
|
||||||
import {AsyncTestCompleter, MockAnimationPlayer, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
|
import {AsyncTestCompleter, MockAnimationPlayer, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
|
||||||
|
|
||||||
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
|
|
||||||
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
|
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
|
||||||
import {isPresent} from '../../src/facade/lang';
|
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
|
||||||
|
|
||||||
export class MockDomAnimatePlayer implements DomAnimatePlayer {
|
|
||||||
public captures: {[key: string]: any[]} = {};
|
|
||||||
private _position: number = 0;
|
|
||||||
private _onfinish = () => {};
|
|
||||||
public currentTime: number;
|
|
||||||
|
|
||||||
_capture(method: string, data: any) {
|
|
||||||
if (!isPresent(this.captures[method])) {
|
|
||||||
this.captures[method] = [];
|
|
||||||
}
|
|
||||||
this.captures[method].push(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() { this._capture('cancel', null); }
|
|
||||||
play() { this._capture('play', null); }
|
|
||||||
pause() { this._capture('pause', null); }
|
|
||||||
finish() {
|
|
||||||
this._capture('finish', null);
|
|
||||||
this._onfinish();
|
|
||||||
}
|
|
||||||
set onfinish(fn) {
|
|
||||||
this._capture('onfinish', fn);
|
|
||||||
this._onfinish = fn;
|
|
||||||
}
|
|
||||||
get onfinish() { return this._onfinish; }
|
|
||||||
set position(val) {
|
|
||||||
this._capture('position', val);
|
|
||||||
this._position = val;
|
|
||||||
}
|
|
||||||
get position() { return this._position; }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
function makePlayer(): {[key: string]: any} {
|
function makePlayer(): {[key: string]: any} {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {DomAnimatePlayer} from '../src/dom/dom_animate_player';
|
||||||
|
import {isPresent} from '../src/facade/lang';
|
||||||
|
|
||||||
|
export class MockDomAnimatePlayer implements DomAnimatePlayer {
|
||||||
|
public captures: {[key: string]: any[]} = {};
|
||||||
|
private _position: number = 0;
|
||||||
|
private _onfinish: Function = () => {};
|
||||||
|
public currentTime: number;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_capture(method: string, data: any): void {
|
||||||
|
if (!isPresent(this.captures[method])) {
|
||||||
|
this.captures[method] = [];
|
||||||
|
}
|
||||||
|
this.captures[method].push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void { this._capture('cancel', null); }
|
||||||
|
play(): void { this._capture('play', null); }
|
||||||
|
pause(): void { this._capture('pause', null); }
|
||||||
|
finish(): void {
|
||||||
|
this._capture('finish', null);
|
||||||
|
this._onfinish();
|
||||||
|
}
|
||||||
|
set onfinish(fn: Function) {
|
||||||
|
this._capture('onfinish', fn);
|
||||||
|
this._onfinish = fn;
|
||||||
|
}
|
||||||
|
get onfinish(): Function { return this._onfinish; }
|
||||||
|
set position(val: number) {
|
||||||
|
this._capture('position', val);
|
||||||
|
this._position = val;
|
||||||
|
}
|
||||||
|
get position(): number { return this._position; }
|
||||||
|
}
|
Loading…
Reference in New Issue