From ab436dbfe67f4750c77230527154858faaf9da6c Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 22 Mar 2023 22:16:15 +0100 Subject: [PATCH] fix(core): CSS animation parsing (#10245) --- apps/automated/src/test-runner.ts | 4 +- .../src/ui/animation/css-animation-tests.ts | 385 ++++++++++--- apps/automated/src/ui/animation/test-page.css | 2 +- packages/core/jest.setup.ts | 2 + .../ui/styling/css-animation-parser.spec.ts | 523 ++++++++++++++++++ .../core/ui/styling/css-animation-parser.ts | 131 +++-- 6 files changed, 924 insertions(+), 123 deletions(-) create mode 100644 packages/core/ui/styling/css-animation-parser.spec.ts diff --git a/apps/automated/src/test-runner.ts b/apps/automated/src/test-runner.ts index 85cd7c892..32c226958 100644 --- a/apps/automated/src/test-runner.ts +++ b/apps/automated/src/test-runner.ts @@ -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; diff --git a/apps/automated/src/ui/animation/css-animation-tests.ts b/apps/automated/src/ui/animation/css-animation-tests.ts index ae4206ff8..edb41b114 100644 --- a/apps/automated/src/ui/animation/css-animation-tests.ts +++ b/apps/automated/src/ui/animation/css-animation-tests.ts @@ -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); diff --git a/apps/automated/src/ui/animation/test-page.css b/apps/automated/src/ui/animation/test-page.css index 921e1ab52..2aaa1b034 100644 --- a/apps/automated/src/ui/animation/test-page.css +++ b/apps/automated/src/ui/animation/test-page.css @@ -1,4 +1,4 @@ -@keyframes test { +@keyframes test-page-keyframes { from { background-color: red; } diff --git a/packages/core/jest.setup.ts b/packages/core/jest.setup.ts index 14449f493..82cadf289 100644 --- a/packages/core/jest.setup.ts +++ b/packages/core/jest.setup.ts @@ -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 { diff --git a/packages/core/ui/styling/css-animation-parser.spec.ts b/packages/core/ui/styling/css-animation-parser.spec.ts new file mode 100644 index 000000000..6a36a22e8 --- /dev/null +++ b/packages/core/ui/styling/css-animation-parser.spec.ts @@ -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); + }); + }); +}); diff --git a/packages/core/ui/styling/css-animation-parser.ts b/packages/core/ui/styling/css-animation-parser.ts index 30c3d4004..eefcd342d 100644 --- a/packages/core/ui/styling/css-animation-parser.ts +++ b/packages/core/ui/styling/css-animation-parser.ts @@ -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 = {}; 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 = (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 = (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(unparsedValue); - } else if (typeof unparsedValue === 'string' && unparsedProperty === 'transform') { + } else if (unparsedProperty === 'transform') { const transformations = transformConverter(unparsedValue); Object.assign(declarations, transformations); }