Skip to content

Commit

Permalink
fix(animations): ensure the web-animations driver converts style prop…
Browse files Browse the repository at this point in the history
…s 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 angular#9111
  • Loading branch information
matsko committed Jun 9, 2016
1 parent e213939 commit 6330e27
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 63 deletions.
76 changes: 45 additions & 31 deletions modules/@angular/platform-browser/src/dom/web_animations_driver.ts
Expand Up @@ -11,9 +11,12 @@ import {
} from '../../core_private';

import {WebAnimationsPlayer} from './web_animations_player';
import {DomAnimatePlayer} from './dom_animate_player';

import {getDOM} from './dom_adapter';

var DASH_CASE_REGEXP = /-([a-z])/g;

export class WebAnimationsDriver implements AnimationDriver {
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
Expand Down Expand Up @@ -44,21 +47,25 @@ export class WebAnimationsDriver implements AnimationDriver {
formattedSteps = [start, start];
}

var player = anyElm.animate(
formattedSteps,
{'duration': duration, 'delay': delay, 'easing': easing, 'fill': 'forwards'});
var player = this._triggerWebAnimation(anyElm, formattedSteps,
{'duration': duration, 'delay': delay, 'easing': easing, 'fill': 'forwards'});

return new WebAnimationsPlayer(player, duration);
}

_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
return elm.animate(keyframes, options);
}
}

function _populateStyles(element: any, styles: AnimationStyles, defaultStyles: {[key: string]: string|number}) {
var data = {};
styles.styles.forEach((entry) => {
StringMapWrapper.forEach(entry, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
(data as any /** TODO #9100 */)[prop] = val == AUTO_STYLE
? _computeStyle(element, prop)
: val.toString() + _resolveStyleUnit(val, prop);
var formattedProp = dashCaseToCamelCase(prop);
(data as any /** TODO #9100 */)[formattedProp] = val == AUTO_STYLE
? _computeStyle(element, formattedProp)
: val.toString() + _resolveStyleUnit(val, prop, formattedProp);
});
});
StringMapWrapper.forEach(defaultStyles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
Expand All @@ -69,13 +76,13 @@ function _populateStyles(element: any, styles: AnimationStyles, defaultStyles: {
return data;
}

function _resolveStyleUnit(val: string | number, prop: string): string {
function _resolveStyleUnit(val: string | number, userProvidedProp: string, formattedProp: string): string {
var unit = '';
if (_isPixelDimensionStyle(prop) && val != 0 && val != '0') {
if (_isPixelDimensionStyle(formattedProp) && val != 0 && val != '0') {
if (isNumber(val)) {
unit = 'px';
} 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;
Expand All @@ -98,32 +105,32 @@ function _isPixelDimensionStyle(prop: string): boolean {
switch (prop) {
case 'width':
case 'height':
case 'min-width':
case 'min-height':
case 'max-width':
case 'max-height':
case 'minWidth':
case 'minHeight':
case 'maxWidth':
case 'maxHeight':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'font-size':
case 'outline-width':
case 'outline-offset':
case 'padding-top':
case 'padding-left':
case 'padding-bottom':
case 'padding-right':
case 'margin-top':
case 'margin-left':
case 'margin-bottom':
case 'margin-right':
case 'border-radius':
case 'border-width':
case 'border-top-width':
case 'border-left-width':
case 'border-right-width':
case 'border-bottom-width':
case 'text-indent':
case 'fontSize':
case 'outlineWidth':
case 'outlineOffset':
case 'paddingTop':
case 'paddingLeft':
case 'paddingBottom':
case 'paddingRight':
case 'marginTop':
case 'marginLeft':
case 'marginBottom':
case 'marginRight':
case 'borderRadius':
case 'borderWidth':
case 'borderTopWidth':
case 'borderLeftWidth':
case 'borderRightWidth':
case 'borderBottomWidth':
case 'textIndent':
return true;

default:
Expand All @@ -134,3 +141,10 @@ function _isPixelDimensionStyle(prop: string): boolean {
function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop];
}

var DASH_CASE_REGEXP = /-([a-z])/g;
export function dashCaseToCamelCase(input: string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
(m: any /** TODO #9100 */) => { return m[1].toUpperCase(); });
}

@@ -0,0 +1,101 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit,
beforeEachProviders
} from '@angular/core/testing/testing_internal';

import {el} from '@angular/platform-browser/testing';
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_palyer';

import {AnimationKeyframe, AnimationStyles} from '../../core_private';

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');
});
});
}
Expand Up @@ -16,38 +16,7 @@ import {
import {isPresent} from "../../src/facade/lang";
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {DomAnimatePlayer} from '../../src/dom/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; }
}
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_palyer';

export function main() {
function makePlayer(): {[key: string]: any} {
Expand Down
@@ -0,0 +1,34 @@
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 = () => {};
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; }
}

0 comments on commit 6330e27

Please sign in to comment.