mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 18:12:09 +08:00
fix(core): CSS animation parsing (#10245)
This commit is contained in:
@ -257,8 +257,8 @@ allTests['SEGMENTED-BAR'] = segmentedBarTests;
|
||||
import * as lifecycle from './ui/lifecycle/lifecycle-tests';
|
||||
allTests['LIFECYCLE'] = lifecycle;
|
||||
|
||||
// import * as cssAnimationTests from './ui/animation/css-animation-tests';
|
||||
// allTests['CSS-ANIMATION'] = cssAnimationTests;
|
||||
import * as cssAnimationTests from './ui/animation/css-animation-tests';
|
||||
allTests['CSS-ANIMATION'] = cssAnimationTests;
|
||||
|
||||
import * as transitionTests from './navigation/transition-tests';
|
||||
allTests['TRANSITIONS'] = transitionTests;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as TKUnit from '../../tk-unit';
|
||||
import * as styleScope from '@nativescript/core/ui/styling/style-scope';
|
||||
import { StyleScope } from '@nativescript/core/ui/styling/style-scope';
|
||||
import * as keyframeAnimation from '@nativescript/core/ui/animation/keyframe-animation';
|
||||
import { CoreTypes } from '@nativescript/core';
|
||||
import * as helper from '../../ui-helper';
|
||||
@ -13,28 +13,35 @@ const DELTA = 1;
|
||||
const SCALE_DELTA = 0.001;
|
||||
|
||||
function createAnimationFromCSS(css: string, name: string): keyframeAnimation.KeyframeAnimationInfo {
|
||||
let scope = new styleScope.StyleScope();
|
||||
const scope = new StyleScope();
|
||||
scope.css = css;
|
||||
scope.ensureSelectors();
|
||||
let selector = findSelectorInScope(scope, name);
|
||||
if (selector !== undefined) {
|
||||
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||
const selector = findSelectorInScope(scope, name);
|
||||
if (selector) {
|
||||
const animation = scope.getAnimations(selector.ruleset)[0];
|
||||
|
||||
return animation;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findSelectorInScope(scope: styleScope.StyleScope, cssClass: string): SelectorCore {
|
||||
function findSelectorInScope(scope: StyleScope, cssClass: string): SelectorCore {
|
||||
let selectors = scope.query({ cssClasses: new Set([cssClass]) });
|
||||
|
||||
return selectors[0];
|
||||
}
|
||||
|
||||
export function test_ReadAnimationProperties() {
|
||||
let css = '.test { ' + 'animation-name: first; ' + 'animation-duration: 4s; ' + 'animation-timing-function: ease-in; ' + 'animation-delay: 1.5; ' + 'animation-iteration-count: 10; ' + 'animation-direction: reverse; ' + 'animation-fill-mode: forwards; ' + ' }';
|
||||
let animation = createAnimationFromCSS(css, 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-name: first;
|
||||
animation-duration: 4s;
|
||||
animation-timing-function: ease-in;
|
||||
animation-delay: 1.5;
|
||||
animation-iteration-count: 10;
|
||||
animation-direction: reverse;
|
||||
animation-fill-mode: forwards;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.name, 'first');
|
||||
TKUnit.assertEqual(animation.duration, 4000);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeIn);
|
||||
@ -45,7 +52,12 @@ export function test_ReadAnimationProperties() {
|
||||
}
|
||||
|
||||
export function test_ReadTheAnimationProperty() {
|
||||
let animation = createAnimationFromCSS('.test { animation: second 0.2s ease-out 1 2 }', 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation: second 0.2s ease-out 1s 2;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.name, 'second');
|
||||
TKUnit.assertEqual(animation.duration, 200);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeOut);
|
||||
@ -54,53 +66,126 @@ export function test_ReadTheAnimationProperty() {
|
||||
}
|
||||
|
||||
export function test_ReadAnimationCurve() {
|
||||
let animation = createAnimationFromCSS('.test { animation-timing-function: ease-in; }', 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: ease-in;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeIn);
|
||||
animation = createAnimationFromCSS('.test { animation-timing-function: ease-out; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: ease-out;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeOut);
|
||||
animation = createAnimationFromCSS('.test { animation-timing-function: linear; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: linear;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.linear);
|
||||
animation = createAnimationFromCSS('.test { animation-timing-function: ease-in-out; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: ease-in-out;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.easeInOut);
|
||||
animation = createAnimationFromCSS('.test { animation-timing-function: spring; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: spring;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.curve, CoreTypes.AnimationCurve.spring);
|
||||
animation = createAnimationFromCSS('.test { animation-timing-function: cubic-bezier(0.1, 1.0, 0.5, 0.5); }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-timing-function: cubic-bezier(0.1, 1.0, 0.5, 0.5);
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
let curve = animation.curve;
|
||||
TKUnit.assert(curve.x1 === 0.1 && curve.y1 === 1.0 && curve.x2 === 0.5 && curve.y2 === 0.5);
|
||||
}
|
||||
|
||||
export function test_ReadIterations() {
|
||||
let animation = createAnimationFromCSS('.test { animation-iteration-count: 5; }', 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.iterations, 5);
|
||||
animation = createAnimationFromCSS('.test { animation-iteration-count: infinite; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-iteration-count: infinite;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.iterations, Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
export function test_ReadFillMode() {
|
||||
let animation = createAnimationFromCSS('.test { animation-iteration-count: 5; }', 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertFalse(animation.isForwards);
|
||||
animation = createAnimationFromCSS('.test { animation-fill-mode: forwards; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-fill-mode: forwards;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertTrue(animation.isForwards);
|
||||
animation = createAnimationFromCSS('.test { animation-fill-mode: backwards; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-fill-mode: backwards;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertFalse(animation.isForwards);
|
||||
}
|
||||
|
||||
export function test_ReadDirection() {
|
||||
let animation = createAnimationFromCSS('.test { animation-iteration-count: 5; }', 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-iteration-count: 5;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertFalse(animation.isReverse);
|
||||
animation = createAnimationFromCSS('.test { animation-direction: reverse; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-direction: reverse;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertTrue(animation.isReverse);
|
||||
animation = createAnimationFromCSS('.test { animation-direction: normal; }', 'test');
|
||||
animation = createAnimationFromCSS(
|
||||
`.test {
|
||||
animation-direction: normal;
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertFalse(animation.isReverse);
|
||||
}
|
||||
|
||||
export function test_ReadKeyframe() {
|
||||
let scope = new styleScope.StyleScope();
|
||||
scope.css = '.test { animation-name: test; } @keyframes test { from { background-color: red; } to { background-color: blue; } }';
|
||||
scope.ensureSelectors();
|
||||
let selector = findSelectorInScope(scope, 'test');
|
||||
TKUnit.assert(selector !== undefined, 'CSS selector was not created!');
|
||||
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
from { background-color: red; }
|
||||
to { background-color: blue; }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assert(animation !== undefined, 'CSS selector was not created!');
|
||||
TKUnit.assertEqual(animation.name, 'test', 'Wrong animation name!');
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2, 'Keyframes not parsed correctly!');
|
||||
TKUnit.assertEqual(animation.keyframes[0].duration, 0, 'First keyframe duration should be 0');
|
||||
@ -110,8 +195,13 @@ export function test_ReadKeyframe() {
|
||||
}
|
||||
|
||||
export function test_ReadTransformAllSet() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: rotate(10) scaleX(5) translate(100, 200); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: rotate(10) scaleX(5) translate(100, 200); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertAreClose(rotate.z, 10, DELTA);
|
||||
@ -124,8 +214,13 @@ export function test_ReadTransformAllSet() {
|
||||
}
|
||||
|
||||
export function test_ReadTransformNone() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: none; } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: none; }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.z, 0);
|
||||
@ -138,7 +233,13 @@ export function test_ReadTransformNone() {
|
||||
}
|
||||
|
||||
export function test_ReadScale() {
|
||||
const animation = createAnimationFromCSS('.test { animation-name: test; } @keyframes test { to { transform: scale(-5, 12.3pt); } }', 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scale(-5, 12.3pt); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -147,7 +248,13 @@ export function test_ReadScale() {
|
||||
}
|
||||
|
||||
export function test_ReadScaleSingle() {
|
||||
const animation = createAnimationFromCSS('.test { animation-name: test; } @keyframes test { to { transform: scale(2); } }', 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scale(2); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -156,8 +263,13 @@ export function test_ReadScaleSingle() {
|
||||
}
|
||||
|
||||
export function test_ReadScaleXY() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: scaleX(5) scaleY(10); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scaleX(5) scaleY(10); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -166,8 +278,13 @@ export function test_ReadScaleXY() {
|
||||
}
|
||||
|
||||
export function test_ReadScaleX() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: scaleX(12.5); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scaleX(12.5); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -177,8 +294,13 @@ export function test_ReadScaleX() {
|
||||
}
|
||||
|
||||
export function test_ReadScaleY() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: scaleY(10); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scaleY(10); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -188,8 +310,13 @@ export function test_ReadScaleY() {
|
||||
}
|
||||
|
||||
export function test_ReadScale3d() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: scale3d(10, 20, 30); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: scale3d(10, 20, 30); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, 'scale');
|
||||
@ -198,7 +325,13 @@ export function test_ReadScale3d() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslate() {
|
||||
const animation = createAnimationFromCSS('.test { animation-name: test; } @keyframes test { to { transform: translate(100, 20); } }', 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translate(100, 20); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -207,7 +340,13 @@ export function test_ReadTranslate() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslateSingle() {
|
||||
const animation = createAnimationFromCSS('.test { animation-name: test; } @keyframes test { to { transform: translate(30); } }', 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translate(30); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -216,8 +355,13 @@ export function test_ReadTranslateSingle() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslateXY() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: translateX(5) translateY(10); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translateX(5) translateY(10); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -226,8 +370,13 @@ export function test_ReadTranslateXY() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslateX() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: translateX(12.5); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translateX(12.5); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -237,8 +386,13 @@ export function test_ReadTranslateX() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslateY() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: translateY(10); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translateY(10); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -248,8 +402,13 @@ export function test_ReadTranslateY() {
|
||||
}
|
||||
|
||||
export function test_ReadTranslate3d() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: translate3d(10, 20, 30); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: translate3d(10, 20, 30); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, 'translate');
|
||||
@ -258,8 +417,13 @@ export function test_ReadTranslate3d() {
|
||||
}
|
||||
|
||||
export function test_ReadRotate() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: rotate(5); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: rotate(5); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, 'rotate');
|
||||
@ -267,8 +431,13 @@ export function test_ReadRotate() {
|
||||
}
|
||||
|
||||
export function test_ReadRotateDeg() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: rotate(45deg); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: rotate(45deg); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, 'rotate');
|
||||
@ -276,8 +445,13 @@ export function test_ReadRotateDeg() {
|
||||
}
|
||||
|
||||
export function test_ReadRotateRad() {
|
||||
const css = '.test { animation-name: test; } @keyframes test { to { transform: rotate(0.7853981634rad); } }';
|
||||
const animation = createAnimationFromCSS(css, 'test');
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
to { transform: rotate(0.7853981634rad); }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, 'rotate');
|
||||
@ -285,8 +459,16 @@ export function test_ReadRotateRad() {
|
||||
}
|
||||
|
||||
export function test_ReadAnimationWithUnsortedKeyframes() {
|
||||
let css = '.test { animation-name: test; } ' + '@keyframes test { ' + 'from { opacity: 0; } ' + '20%, 60% { opacity: 0.5; } ' + '40%, 80% { opacity: 0.3; } ' + 'to { opacity: 1; } ' + '}';
|
||||
let animation = createAnimationFromCSS(css, 'test');
|
||||
let animation = createAnimationFromCSS(
|
||||
`.test { animation-name: test; }
|
||||
@keyframes test {
|
||||
from { opacity: 0; }
|
||||
20%, 60% { opacity: 0.5; }
|
||||
40%, 80% { opacity: 0.3; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 6);
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 0);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 0.5);
|
||||
@ -303,33 +485,56 @@ export function test_ReadAnimationWithUnsortedKeyframes() {
|
||||
}
|
||||
|
||||
export function test_ReadAnimationsWithCSSImport() {
|
||||
let css = "@import 'ui/animation/test-page.css'; .test { animation-name: test; }";
|
||||
let css = "@import 'ui/animation/test-page.css'; .test { animation-name: test-page-keyframes; }";
|
||||
let animation = createAnimationFromCSS(css, 'test');
|
||||
TKUnit.assertEqual(animation.keyframes.length, 3);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].property, 'backgroundColor');
|
||||
}
|
||||
|
||||
export function test_LoadTwoAnimationsWithTheSameName() {
|
||||
let scope = new styleScope.StyleScope();
|
||||
scope.css = '@keyframes a1 { from { opacity: 0; } to { opacity: 1; } } @keyframes a1 { from { opacity: 0; } to { opacity: 0.5; } } .a { animation-name: a1; }';
|
||||
scope.ensureSelectors();
|
||||
let selector = findSelectorInScope(scope, 'a');
|
||||
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||
const animation = createAnimationFromCSS(
|
||||
`.a { animation-name: a1; }
|
||||
@keyframes a1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes a1 {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.5; } /* this should override the previous one */
|
||||
}`,
|
||||
'a'
|
||||
);
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 0.5);
|
||||
scope = new styleScope.StyleScope();
|
||||
scope.css = '@keyframes k { from { opacity: 0; } to { opacity: 1; } } .a { animation-name: k; animation-duration: 2; } .a { animation-name: k; animation-duration: 3; }';
|
||||
scope.ensureSelectors();
|
||||
selector = findSelectorInScope(scope, 'a');
|
||||
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[0].keyframes.length, 2);
|
||||
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[0].keyframes.length, 2);
|
||||
|
||||
const animation2 = createAnimationFromCSS(
|
||||
`.a {
|
||||
animation-name: k;
|
||||
animation-duration: 2;
|
||||
}
|
||||
.a {
|
||||
animation-name: k;
|
||||
animation-duration: 3;
|
||||
}
|
||||
@keyframes k {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'a'
|
||||
);
|
||||
|
||||
TKUnit.assertEqual(animation2.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation2.keyframes.length, 2);
|
||||
}
|
||||
|
||||
export function test_LoadAnimationProgrammatically() {
|
||||
let stack = new stackModule.StackLayout();
|
||||
helper.buildUIAndRunTest(stack, function (views) {
|
||||
let page = views[1];
|
||||
page.css = '@keyframes a { from { opacity: 1; } to { opacity: 0; } }';
|
||||
page.css = `@keyframes a {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}`;
|
||||
let animation = page.getKeyframeAnimationWithName('a');
|
||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].property, 'opacity');
|
||||
@ -345,7 +550,14 @@ export function test_ExecuteCSSAnimation() {
|
||||
let stackLayout = new stackModule.StackLayout();
|
||||
stackLayout.addChild(label);
|
||||
|
||||
mainPage.css = '@keyframes k { from { background-color: red; } to { background-color: green; } } .l { animation-name: k; animation-duration: 0.1s; animation-fill-mode: forwards; }';
|
||||
mainPage.css = `@keyframes k {
|
||||
from { background-color: red; }
|
||||
to { background-color: green; } }
|
||||
.l {
|
||||
animation-name: k;
|
||||
animation-duration: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}`;
|
||||
mainPage.content = stackLayout;
|
||||
|
||||
TKUnit.waitUntilReady(() => label.isLoaded);
|
||||
@ -384,7 +596,7 @@ export function test_ExecuteCSSAnimation() {
|
||||
//}
|
||||
|
||||
export function test_ReadTwoAnimations() {
|
||||
let scope = new styleScope.StyleScope();
|
||||
let scope = new StyleScope();
|
||||
scope.css = '.test { animation: one 0.2s ease-out 1 2, two 2s ease-in; }';
|
||||
scope.ensureSelectors();
|
||||
let selector = findSelectorInScope(scope, 'test');
|
||||
@ -396,11 +608,16 @@ export function test_ReadTwoAnimations() {
|
||||
}
|
||||
|
||||
export function test_AnimationCurveInKeyframes() {
|
||||
let scope = new styleScope.StyleScope();
|
||||
scope.css = '@keyframes an { from { animation-timing-function: linear; background-color: red; } 50% { background-color: green; } to { background-color: black; } } .test { animation-name: an; animation-timing-function: ease-in; }';
|
||||
scope.ensureSelectors();
|
||||
let selector = findSelectorInScope(scope, 'test');
|
||||
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||
const animation = createAnimationFromCSS(
|
||||
`.test { animation-name: an; animation-timing-function: ease-in; }
|
||||
@keyframes an {
|
||||
from { animation-timing-function: linear; background-color: red; }
|
||||
50% { background-color: green; }
|
||||
to { background-color: black; }
|
||||
}`,
|
||||
'test'
|
||||
);
|
||||
|
||||
TKUnit.assertEqual(animation.keyframes[0].curve, CoreTypes.AnimationCurve.linear);
|
||||
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
||||
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
||||
|
@ -1,4 +1,4 @@
|
||||
@keyframes test {
|
||||
@keyframes test-page-keyframes {
|
||||
from {
|
||||
background-color: red;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
// @ts-nocheck
|
||||
global.WeakRef.prototype.get = global.WeakRef.prototype.deref;
|
||||
global.NativeClass = function () {};
|
||||
global.NSObject = class NSObject {};
|
||||
global.NSString = {
|
||||
stringWithString() {
|
||||
return {
|
||||
|
523
packages/core/ui/styling/css-animation-parser.spec.ts
Normal file
523
packages/core/ui/styling/css-animation-parser.spec.ts
Normal file
@ -0,0 +1,523 @@
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import type { KeyframeAnimationInfo, KeyframeInfo } from '../animation';
|
||||
import { CssAnimationParser, keyframeAnimationsFromCSSProperty } from './css-animation-parser';
|
||||
import { cssTreeParse } from '../../css/css-tree-parser';
|
||||
|
||||
describe('css-animation-parser', () => {
|
||||
describe('shorthand-property-parser', () => {
|
||||
// helper functions
|
||||
function testSingleAnimation(css: string): KeyframeAnimationInfo {
|
||||
const animations: KeyframeAnimationInfo[] = [];
|
||||
keyframeAnimationsFromCSSProperty(css, animations);
|
||||
|
||||
return animations[0];
|
||||
}
|
||||
|
||||
function testMultipleAnimations(css: string): KeyframeAnimationInfo[] {
|
||||
const animations: KeyframeAnimationInfo[] = [];
|
||||
keyframeAnimationsFromCSSProperty(css, animations);
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
it('empty', () => {
|
||||
const animation = testSingleAnimation('');
|
||||
expect(animation).toBeUndefined();
|
||||
});
|
||||
|
||||
// times to test for
|
||||
const times = {
|
||||
'0s': 0,
|
||||
'0ms': 0,
|
||||
'250ms': 250,
|
||||
'0.5s': 500,
|
||||
'1500ms': 1500,
|
||||
'1s': 1000,
|
||||
'3s': 3000,
|
||||
};
|
||||
|
||||
const curves = {
|
||||
ease: CoreTypes.AnimationCurve.ease,
|
||||
linear: CoreTypes.AnimationCurve.linear,
|
||||
'ease-in': CoreTypes.AnimationCurve.easeIn,
|
||||
'ease-out': CoreTypes.AnimationCurve.easeOut,
|
||||
'ease-in-out': CoreTypes.AnimationCurve.easeInOut,
|
||||
spring: CoreTypes.AnimationCurve.spring,
|
||||
'cubic-bezier(0.1, 1.0, 0.5, 0.5)': CoreTypes.AnimationCurve.cubicBezier(0.1, 1.0, 0.5, 0.5),
|
||||
'cubic-bezier(0.42, 0.0, 1.0, 1.0);': CoreTypes.AnimationCurve.cubicBezier(0.42, 0.0, 1.0, 1.0),
|
||||
};
|
||||
|
||||
it('parses duration', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
expect(testSingleAnimation(`${timeString}`).duration).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses delay', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
const animation = testSingleAnimation(`0s ${timeString}`);
|
||||
expect(animation.duration).toBe(0);
|
||||
expect(animation.delay).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses duration and delay', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
const animation = testSingleAnimation(`${timeString} ${timeString}`);
|
||||
expect(animation.duration).toBe(ms);
|
||||
expect(animation.delay).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses curve', () => {
|
||||
Object.entries(curves).forEach(([curveString, curve]) => {
|
||||
const animation = testSingleAnimation(`${curveString}`);
|
||||
expect(animation.curve).toEqual(curve);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses duration, curve and delay', () => {
|
||||
Object.entries(curves).forEach(([curveString, curve]) => {
|
||||
const animation1 = testSingleAnimation(`225ms 300ms ${curveString}`);
|
||||
expect(animation1.duration).toBe(225);
|
||||
expect(animation1.delay).toBe(300);
|
||||
expect(animation1.curve).toEqual(curve);
|
||||
|
||||
// curve and delay can be swapped
|
||||
const animation2 = testSingleAnimation(`225ms ${curveString} 300ms`);
|
||||
expect(animation2.duration).toBe(225);
|
||||
expect(animation2.delay).toBe(300);
|
||||
expect(animation2.curve).toEqual(curve);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses iteration count', () => {
|
||||
expect(testSingleAnimation(`0s 0s ease 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`0s 0s ease 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`0s 0s ease infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`1s 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`1s 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`1s infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`ease 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`ease 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`ease infinite`).iterations).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('parses direction', () => {
|
||||
expect(testSingleAnimation(`1s`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s normal`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease 2 reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease infinite reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s 2 reverse`).isReverse).toBe(true);
|
||||
|
||||
// unsupported values should still work
|
||||
expect(testSingleAnimation(`1s alternate`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s alternate-reverse`).isReverse).toBe(false);
|
||||
});
|
||||
|
||||
it('parses fill-mode', () => {
|
||||
expect(testSingleAnimation(`1s`).isForwards).toBe(false);
|
||||
expect(testSingleAnimation(`1s none`).isForwards).toBe(false);
|
||||
expect(testSingleAnimation(`1s backwards`).isForwards).toBe(false);
|
||||
|
||||
expect(testSingleAnimation(`1s both`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease 2 forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease infinite forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s forwards`).isForwards).toBe(true);
|
||||
});
|
||||
|
||||
it('parses play-state', () => {
|
||||
// TODO: implement play-state?
|
||||
|
||||
expect(testSingleAnimation(`1s`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s running`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease 2 paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease infinite paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s ease paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s ease 1s paused`)).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses animation name', () => {
|
||||
expect(testSingleAnimation(`1s`).name).toBe('');
|
||||
expect(testSingleAnimation(`1s fade`).name).toBe('fade');
|
||||
expect(testSingleAnimation(`1s 'fade'`).name).toBe('fade');
|
||||
expect(testSingleAnimation(`1s "fade"`).name).toBe('fade');
|
||||
|
||||
expect(testSingleAnimation(`1s fade-in`).name).toBe('fade-in');
|
||||
expect(testSingleAnimation(`1s 'fade-in'`).name).toBe('fade-in');
|
||||
expect(testSingleAnimation(`1s "fade-in"`).name).toBe('fade-in');
|
||||
|
||||
expect(testSingleAnimation(`1s fade_in`).name).toBe('fade_in');
|
||||
expect(testSingleAnimation(`1s 'fade_in'`).name).toBe('fade_in');
|
||||
expect(testSingleAnimation(`1s "fade_in"`).name).toBe('fade_in');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s ease-in 1s 2 reverse both paused slidein', () => {
|
||||
const animation = testSingleAnimation(`3s ease-in 1s 2 reverse both paused slidein`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.delay).toBe(1000);
|
||||
expect(animation.curve).toBe(CoreTypes.AnimationCurve.easeIn);
|
||||
expect(animation.iterations).toBe(2);
|
||||
expect(animation.isReverse).toBe(true);
|
||||
expect(animation.isForwards).toBe(true);
|
||||
expect(animation.name).toBe('slidein');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s linear 1s slidein', () => {
|
||||
const animation = testSingleAnimation(`3s linear 1s slidein`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.delay).toBe(1000);
|
||||
expect(animation.curve).toBe(CoreTypes.AnimationCurve.linear);
|
||||
expect(animation.name).toBe('slidein');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s linear slidein, 3s ease-out 5s slideout', () => {
|
||||
const [animation1, animation2] = testMultipleAnimations(`3s linear slidein, 3s ease-out 5s slideout`);
|
||||
|
||||
expect(animation1.duration).toBe(3000);
|
||||
expect(animation1.curve).toBe(CoreTypes.AnimationCurve.linear);
|
||||
expect(animation1.name).toBe('slidein');
|
||||
|
||||
expect(animation2.duration).toBe(3000);
|
||||
expect(animation2.delay).toBe(5000);
|
||||
expect(animation2.curve).toBe(CoreTypes.AnimationCurve.easeOut);
|
||||
expect(animation2.name).toBe('slideout');
|
||||
});
|
||||
|
||||
it('parses SPEC example: 3s none backwards', () => {
|
||||
const animation = testSingleAnimation(`3s none backwards`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.isForwards).toBe(false);
|
||||
expect(animation.name).toBe('backwards');
|
||||
});
|
||||
|
||||
it('parses SPEC example: 3s backwards', () => {
|
||||
const animation = testSingleAnimation(`3s backwards`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.isForwards).toBe(false);
|
||||
expect(animation.name).toBe('');
|
||||
});
|
||||
|
||||
it('does not throw on invalid values', () => {
|
||||
// prettier-ignore
|
||||
const invalidValues = [
|
||||
'asd',
|
||||
'$#-1401;lk',
|
||||
'1 1 1 1 1 1 1 1 1 1',
|
||||
'1s 1s 1s 1s',
|
||||
',,,,',
|
||||
'$,1s-_1:s>',
|
||||
Infinity.toString(),
|
||||
NaN.toString(),
|
||||
null,
|
||||
undefined
|
||||
];
|
||||
|
||||
invalidValues.forEach((value) => {
|
||||
expect(() => testSingleAnimation(value)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframe-parser', () => {
|
||||
// helper function
|
||||
function testKeyframesArrayFromCSS(css: string, expectedName?: string): KeyframeInfo[] {
|
||||
const ast = cssTreeParse(css, 'test.css');
|
||||
const rules = ast.stylesheet.rules;
|
||||
const firstRule = rules[0];
|
||||
|
||||
expect(rules.length).toBe(1);
|
||||
expect(firstRule.type).toBe('keyframes');
|
||||
|
||||
const name = firstRule.name;
|
||||
const keyframes = firstRule.keyframes;
|
||||
|
||||
if (expectedName) {
|
||||
expect(name).toBe(expectedName);
|
||||
}
|
||||
|
||||
return CssAnimationParser.keyframesArrayFromCSS(keyframes);
|
||||
}
|
||||
|
||||
it('parses "from" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [from] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "to" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [to] = res;
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "from/to" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "0%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [from] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "100%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [to] = res;
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "0%/100%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "via" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
50% { opacity: 0.5; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [via] = res;
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
});
|
||||
|
||||
it('parses multiple keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses multiple keyframes with mixed stops', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses duplicate keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
50% { translateX: 100; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(2);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
expect(via.declarations[1].property).toBe('translateX');
|
||||
expect(via.declarations[1].value).toBe(100);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses timing functions in keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; animation-timing-function: ease-in; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.curve).toBe(CoreTypes.AnimationCurve.easeIn);
|
||||
expect(to.curve).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('sorts multiple keyframes with mixed order', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
100% { opacity: 1; }
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -7,13 +7,13 @@ import { transformConverter } from '../styling/style-properties';
|
||||
import { cleanupImportantFlags } from './css-utils';
|
||||
|
||||
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
|
||||
'animation-name': (info: any, value: any) => (info.name = value),
|
||||
'animation-name': (info: any, value: any) => (info.name = value.replace(/['"]/g, '')),
|
||||
'animation-duration': (info: any, value: any) => (info.duration = timeConverter(value)),
|
||||
'animation-delay': (info: any, value: any) => (info.delay = timeConverter(value)),
|
||||
'animation-timing-function': (info: any, value: any) => (info.curve = animationTimingFunctionConverter(value)),
|
||||
'animation-iteration-count': (info: any, value: any) => (info.iterations = value === 'infinite' ? Number.POSITIVE_INFINITY : parseFloat(value)),
|
||||
'animation-direction': (info: any, value: any) => (info.isReverse = value === 'reverse'),
|
||||
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards'),
|
||||
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards' || value === 'both'),
|
||||
});
|
||||
|
||||
export class CssAnimationParser {
|
||||
@ -65,6 +65,7 @@ export class CssAnimationParser {
|
||||
if (current === undefined) {
|
||||
current = <KeyframeInfo>{};
|
||||
current.duration = time;
|
||||
current.declarations = [];
|
||||
parsedKeyframes[time] = current;
|
||||
}
|
||||
for (const declaration of keyframe.declarations) {
|
||||
@ -72,7 +73,7 @@ export class CssAnimationParser {
|
||||
current.curve = animationTimingFunctionConverter(declaration.value);
|
||||
}
|
||||
}
|
||||
current.declarations = declarations;
|
||||
current.declarations = current.declarations.concat(declarations);
|
||||
}
|
||||
}
|
||||
const array = [];
|
||||
@ -87,39 +88,97 @@ export class CssAnimationParser {
|
||||
}
|
||||
}
|
||||
|
||||
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
|
||||
if (typeof value === 'string') {
|
||||
const values = value.split(/[,]+/);
|
||||
for (const parsedValue of values) {
|
||||
const animationInfo = new KeyframeAnimationInfo();
|
||||
const arr = (<string>parsedValue).trim().split(/[ ]+/);
|
||||
/**
|
||||
* @see https://w3c.github.io/csswg-drafts/css-animations/#propdef-animation
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/animation
|
||||
* @internal - exported for testing
|
||||
* @param value
|
||||
* @param animations
|
||||
*/
|
||||
export function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arr.length > 0) {
|
||||
animationInfo.name = arr[0];
|
||||
}
|
||||
if (arr.length > 1) {
|
||||
animationInfo.duration = timeConverter(arr[1]);
|
||||
}
|
||||
if (arr.length > 2) {
|
||||
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
|
||||
}
|
||||
if (arr.length > 3) {
|
||||
animationInfo.delay = timeConverter(arr[3]);
|
||||
}
|
||||
if (arr.length > 4) {
|
||||
animationInfo.iterations = parseInt(arr[4]);
|
||||
}
|
||||
if (arr.length > 5) {
|
||||
animationInfo.isReverse = arr[4] === 'reverse';
|
||||
}
|
||||
if (arr.length > 6) {
|
||||
animationInfo.isForwards = arr[5] === 'forwards';
|
||||
}
|
||||
if (arr.length > 7) {
|
||||
throw new Error('Invalid value for animation: ' + value);
|
||||
}
|
||||
animations.push(animationInfo);
|
||||
if (value.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches whitespace except if the whitespace is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
|
||||
*/
|
||||
const VALUE_SPLIT_RE = /\s(?![^(]*\))/;
|
||||
|
||||
/**
|
||||
* Matches commas except if the comma is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
|
||||
*/
|
||||
const MULTIPLE_SPLIT_RE = /,(?![^(]*\))/;
|
||||
|
||||
const isTime = (v: string) => !!v.match(/\dm?s$/g);
|
||||
const isTimingFunction = (v: string) => !!v.match(/ease|linear|ease-in|ease-out|ease-in-out|spring|cubic-bezier/g);
|
||||
const isIterationCount = (v: string) => !!v.match(/infinite|[\d.]+$/g);
|
||||
const isDirection = (v: string) => !!v.match(/normal|reverse|alternate|alternate-reverse/g);
|
||||
const isFillMode = (v: string) => !!v.match(/none|forwards|backwards|both/g);
|
||||
const isPlayState = (v: string) => !!v.match(/running|paused/g);
|
||||
|
||||
const values = value.split(MULTIPLE_SPLIT_RE);
|
||||
for (const parsedValue of values) {
|
||||
const animationInfo = new KeyframeAnimationInfo();
|
||||
const parts = (<string>parsedValue).trim().split(VALUE_SPLIT_RE);
|
||||
|
||||
const [duration, delay] = parts.filter(isTime);
|
||||
const [timing] = parts.filter(isTimingFunction);
|
||||
const [iterationCount] = parts.filter(isIterationCount);
|
||||
const [direction] = parts.filter(isDirection);
|
||||
const [fillMode] = parts.filter(isFillMode);
|
||||
const [playState] = parts.filter(isPlayState);
|
||||
const [name] = parts.filter((v) => {
|
||||
// filter out "consumed" values
|
||||
return ![duration, delay, timing, iterationCount, direction, fillMode, playState].filter(Boolean).includes(v);
|
||||
});
|
||||
|
||||
// console.log({
|
||||
// duration,
|
||||
// delay,
|
||||
// timing,
|
||||
// iterationCount,
|
||||
// direction,
|
||||
// fillMode,
|
||||
// playState,
|
||||
// name,
|
||||
// });
|
||||
|
||||
if (duration) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-duration'](animationInfo, duration);
|
||||
}
|
||||
if (delay) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-delay'](animationInfo, delay);
|
||||
}
|
||||
if (timing) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-timing-function'](animationInfo, timing);
|
||||
}
|
||||
if (iterationCount) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-iteration-count'](animationInfo, iterationCount);
|
||||
}
|
||||
if (direction) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-direction'](animationInfo, direction);
|
||||
}
|
||||
if (fillMode) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-fill-mode'](animationInfo, fillMode);
|
||||
}
|
||||
if (playState) {
|
||||
// TODO: implement play state? Currently not supported...
|
||||
}
|
||||
if (name) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-name'](animationInfo, name);
|
||||
} else {
|
||||
// based on the SPEC we should set the name to 'none' if no name is provided
|
||||
// however we just don't set the name at all.
|
||||
// perhaps we should set it to 'none' and handle it accordingly.
|
||||
// animationInfo.name = 'none'
|
||||
}
|
||||
|
||||
animations.push(animationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,9 +187,9 @@ export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: Keyframe
|
||||
const property = CssAnimationProperty._getByCssName(unparsedProperty);
|
||||
unparsedValue = cleanupImportantFlags(unparsedValue, property?.cssLocalName);
|
||||
|
||||
if (typeof unparsedProperty === 'string' && property && property._valueConverter) {
|
||||
if (typeof unparsedProperty === 'string' && property?._valueConverter) {
|
||||
declarations[property.name] = property._valueConverter(<string>unparsedValue);
|
||||
} else if (typeof unparsedValue === 'string' && unparsedProperty === 'transform') {
|
||||
} else if (unparsedProperty === 'transform') {
|
||||
const transformations = transformConverter(unparsedValue);
|
||||
Object.assign(declarations, transformations);
|
||||
}
|
||||
|
Reference in New Issue
Block a user