Skip to content

Commit

Permalink
sx: Add keyframes support
Browse files Browse the repository at this point in the history
This commit adds a keyframes map to StyleCollector and exports a new
keyframes function. The keyframes function parses the animation object
and generates a hashed name based on the input. Then it passes
the generated style and hash to the StyleCollector. The StyleCollector
reports back if the animation already exists. If it does not exists
and we are in the browser, the function appends the animation to
the SX stylesheet definitions.
  • Loading branch information
tbergquist-godaddy committed Nov 12, 2020
1 parent 6fc66cf commit cd71541
Show file tree
Hide file tree
Showing 15 changed files with 394 additions and 27 deletions.
36 changes: 36 additions & 0 deletions src/sx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ In conventional applications, CSS rules are duplicated throughout the stylesheet
- [Multiple stylesheets precedence](#multiple-stylesheets-precedence)
- [Pseudo CSS classes and elements](#pseudo-css-classes-and-elements)
- [`@media` and `@supports`](#media-and-supports)
- [Keyframes](#keyframes)
- [Precise Flow types](#precise-flow-types)
- [Production usage considerations](#production-usage-considerations)
- [Server-side rendering](#server-side-rendering)
Expand Down Expand Up @@ -227,6 +228,41 @@ const styles = sx.create({
});
```
### Keyframes
SX also has support for keyframes, it exports a function that generates an animation name from the input you give it. You can use it like this:
```jsx
export function AnimatedComponent() {
return <div className={styles('text')}>text</div>;
}
const fadeIn = sx.keyframes({
'0%': {
opacity: 0,
},
'50%, 55%': {
opacity: 0.3,
},
'100%': {
opacity: 1,
},
});
const styles = sx.create({
text: {
animationName: fadeIn,
animationDuration: '2s',
},
});
```
It also supports `from` and `to` for simpler animations.
```js
const simple = sx.keyframes({ from: { opacity: 0 }, to: { opacity: 1 } });
```
### Precise Flow types
SX knows about almost every property or rule which exists in CSS and tries to help with mistakes when writing the styles.
Expand Down
2 changes: 1 addition & 1 deletion src/sx/__flowtests__/css-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ sx.create({
unknownProperty: 'red',
},
UnknownPropertyInsideMedia: {
// $FlowExpectedError[incompatible-call]
'@media print': {
// $FlowExpectedError[incompatible-call]
unknownProperty: 'red',
},
},
Expand Down
3 changes: 2 additions & 1 deletion src/sx/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import create from './src/create';
import keyframes from './src/keyframes';
import renderPageWithSX from './src/renderPageWithSX';

export { create, renderPageWithSX };
export { create, keyframes, renderPageWithSX };
1 change: 1 addition & 0 deletions src/sx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"change-case": "^4.1.1",
"css-tree": "^1.0.1",
"fast-levenshtein": "^3.0.0",
"json-stable-stringify": "^1.0.1",
"mdn-data": "^2.0.14",
"prettier": "^2.1.2"
},
Expand Down
13 changes: 13 additions & 0 deletions src/sx/src/StyleCollector.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type StyleBufferType = Map<string, StyleCollectorNodeInterface>;

class StyleCollector {
#styleBuffer: StyleBufferType = new Map();
#keyframes: Map<string, string> = new Map();

collect(baseStyleSheet: {|
+[sheetName: string]: $FlowFixMe,
Expand Down Expand Up @@ -109,11 +110,23 @@ class StyleCollector {
this.#styleBuffer.forEach((node) => {
sxStyle += node.printNodes().join('');
});
this.#keyframes.forEach((node) => {
sxStyle += node;
});
return sxStyle;
}

addKeyframe(name: string, value: string): boolean {
if (this.#keyframes.has(name)) {
return true;
}
this.#keyframes.set(name, value);
return false;
}

reset(): void {
this.#styleBuffer.clear();
this.#keyframes.clear();
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/sx/src/__tests__/StyleCollector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,13 @@ it('works with mediaQueries', () => {
}
`);
});

it('works with keyframes', () => {
expect(
StyleCollector.addKeyframe('_1kFWtB', `@keyframes _1kFWtB {from {opacity: 0}to {opacity:1}}`),
).toBe(false);

expect(
StyleCollector.addKeyframe('_1kFWtB', `@keyframes _1kFWtB {from {opacity: 0}to {opacity:1}}`),
).toBe(true);
});
27 changes: 27 additions & 0 deletions src/sx/src/__tests__/SxDomTests.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,30 @@ it('handles background:none specificity correctly', () => {
const test2 = screen.getByText('test_2');
expect(test2).toHaveStyle(`background-color:${normalizeColor('blue')}`);
});

it('works with keyframes', () => {
const animation = sx.keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
const styles = sx.create({
myClass: {
animationName: animation,
animationDuration: '2s',
},
});

const { container } = render(
<>
{sx.renderPageWithSX(jest.fn()).styles}
<div className={styles('myClass')}>test_1</div>
</>,
);
expect(container.querySelector('[data-adeira-sx]')).toMatchInlineSnapshot(`
<style
data-adeira-sx="true"
>
.P4y5l{animation-name:_2rMlJa}.HDQox{animation-duration:2s}@keyframes _2rMlJa {from {opacity:0;}to {opacity:1;}}
</style>
`);
});
59 changes: 59 additions & 0 deletions src/sx/src/__tests__/__snapshots__/fixtures.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,65 @@ exports[`matches expected output: @unknown.js 1`] = `
Invariant Violation: Unsupported rule "@unknown"
`;

exports[`matches expected output: keyframes-from-to.js 1`] = `
~~~~~~~~~~ INPUT ~~~~~~~~~~
{
"aaa": {
"animationName": "_2rMlJa"
}
}
~~~~~~~~~~ OUTPUT ~~~~~~~~~~
.P4y5l {
animation-name: _2rMlJa;
}
@keyframes _2rMlJa {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
~~~~~~~~~~ USAGE ~~~~~~~~~~
className={styles('aaa')}
↓ ↓ ↓
class="P4y5l"
`;

exports[`matches expected output: keyframes-percentage.js 1`] = `
~~~~~~~~~~ INPUT ~~~~~~~~~~
{
"aaa": {
"animationName": "gg1lu"
}
}
~~~~~~~~~~ OUTPUT ~~~~~~~~~~
._4BANPA {
animation-name: gg1lu;
}
@keyframes gg1lu {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
25% {
opacity: 0.7;
}
}
~~~~~~~~~~ USAGE ~~~~~~~~~~
className={styles('aaa')}
↓ ↓ ↓
class="_4BANPA"
`;

exports[`matches expected output: media-queries.js 1`] = `
~~~~~~~~~~ INPUT ~~~~~~~~~~
{
Expand Down
19 changes: 19 additions & 0 deletions src/sx/src/__tests__/fixtures/keyframes-from-to.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @flow

import type { SheetDefinitions } from '../../create';
import keyframes from '../../keyframes';

const animation = keyframes({
from: {
opacity: 0,
},
to: {
opacity: 1,
},
});

export default ({
aaa: {
animationName: animation,
},
}: SheetDefinitions);
22 changes: 22 additions & 0 deletions src/sx/src/__tests__/fixtures/keyframes-percentage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @flow

import type { SheetDefinitions } from '../../create';
import keyframes from '../../keyframes';

const animation = keyframes({
'0%': {
opacity: 0,
},
'25%': {
opacity: 0.7,
},
'100%': {
opacity: 1,
},
});

export default ({
aaa: {
animationName: animation,
},
}: SheetDefinitions);
94 changes: 94 additions & 0 deletions src/sx/src/__tests__/keyframes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// @flow

import keyframes from '../keyframes';
import styleCollector from '../StyleCollector';

it('returns hashed name of the keyframe', () => {
const spy = jest.spyOn(styleCollector, 'addKeyframe');
const hashedName = keyframes({
from: {
opacity: 0,
transform: 'translateX(-300px)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
expect(spy).toHaveBeenCalledWith(
hashedName,
'@keyframes wOIjT {from {opacity:0;transform:translateX(-300px);}to {opacity:1;transform:translateX(0);}}',
);
const hashedName2 = keyframes({
from: {
transform: 'translateX(-300px)',
},
to: {
transform: 'translateX(0)',
},
});
expect(spy).toHaveBeenNthCalledWith(
2,
hashedName2,
'@keyframes _1kFWtB {from {transform:translateX(-300px);}to {transform:translateX(0);}}',
);
expect(hashedName).toMatchInlineSnapshot(`"wOIjT"`);
expect(hashedName2).toMatchInlineSnapshot(`"_1kFWtB"`);
spy.mockReset();
});

it('works with percentages', () => {
const spy = jest.spyOn(styleCollector, 'addKeyframe');

const hashedName = keyframes({
'0%': {
transform: 'translateX(-300px)',
},
'75%, 80%': {
transform: 'translateX(50px)',
},
'100%': {
transform: 'translateX(0)',
},
});
expect(spy).toHaveBeenCalledWith(
hashedName,
'@keyframes _3B4dOq {0% {transform:translateX(-300px);}100% {transform:translateX(0);}75%,80% {transform:translateX(50px);}}',
);
spy.mockReset();
});

it('generates same hash for similar object', () => {
const hashedName = keyframes({
from: {
opacity: 0,
transform: 'translateX(-300px)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
const hashedName2 = keyframes({
from: {
opacity: 0,
transform: 'translateX(-300px)',
},
to: {
opacity: 1,
transform: 'translateX(0)',
},
});
const hashedName3 = keyframes({
from: {
transform: 'translateX(-300px)',
opacity: 0,
},
to: {
transform: 'translateX(0)',
opacity: 1,
},
});
expect(hashedName).toEqual(hashedName2);
expect(hashedName2).toEqual(hashedName3);
});
9 changes: 1 addition & 8 deletions src/sx/src/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,8 @@ type MediaQueries = {|
+[string]: MediaQueries, // media queries can be recursively nested
|};

// https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes
type KeyFrames = {|
+from: AllCSSPropertyTypes,
+to: AllCSSPropertyTypes,
+[number]: AllCSSPropertyTypes, // percentages
|};

// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
type AtRules = MediaQueries | KeyFrames;
type AtRules = MediaQueries;

type AllCSSProperties = {|
...AllCSSPropertyTypes,
Expand Down

0 comments on commit cd71541

Please sign in to comment.