diff --git a/README.md b/README.md
index 8d05be9..f9932cc 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,15 @@ All of these CSS properties are supported. You can pass either a string or a num
- `alignContent`
- `alignItems`
- `alignSelf`
+- `animation`
+- `animationDelay`
+- `animationDirection`
+- `animationDuration`
+- `animationFillMode`
+- `animationIterationCount`
+- `animationName`
+- `animationPlayState`
+- `animationTimingFunction`
- `background`
- `backgroundBlendMode`
- `backgroundClip`
@@ -133,6 +142,7 @@ All of these CSS properties are supported. You can pass either a string or a num
- `clear`
- `color`
- `columnGap`
+- `content`
- `cursor`
- `display`
- `flex`
@@ -269,6 +279,35 @@ Utility function for filtering out `Box` props. Returns an `{ matchedProps, rema
Type: `object`
+#### keyframes(name, timeline)
+
+Function for generating an animation keyframe name and injecting the provided styles into the stylesheet. The `timeline` object is in the shape of `{ 'from' | 'to' | [0-100]: CssProps }` which define the styles to apply at each position of the animation. Returns the generated name for use with the `animation` or `animationName` props.
+
+```tsx
+import Box, { keyframes } from 'ui-box'
+
+const openAnimation = keyframes('openAnimation', {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-120%)'
+ },
+ to: {
+ transform: 'translateY(0)'
+ }
+})
+
+const AnimatedBox = () =>
+
+// Equivalent using individual props:
+const AnimatedBox = () => (
+
+)
+```
+
#### propTypes
Object of all the `Box` CSS property `propTypes`.
@@ -289,6 +328,7 @@ Object of all the CSS property enhancers (the methods that generate the class na
These enhancer groups are also exported. They're all objects with `{ propTypes, propAliases, propEnhancers }` properties. They're mainly useful for if you want to inherit a subset of the `Box` CSS propTypes in your own components.
+- `animation`
- `background`
- `borderRadius`
- `borders`
diff --git a/package.json b/package.json
index c707d80..6f27030 100644
--- a/package.json
+++ b/package.json
@@ -161,7 +161,7 @@
"size-limit": [
{
"path": "dist/src/index.js",
- "limit": "50 KB",
+ "limit": "55 KB",
"running": false,
"gzip": false
}
diff --git a/src/enhance-props.ts b/src/enhance-props.ts
index 5ef4748..4e775c5 100644
--- a/src/enhance-props.ts
+++ b/src/enhance-props.ts
@@ -7,7 +7,7 @@ import { BoxPropValue, EnhancerProps } from './types/enhancers'
type PreservedProps = Without, keyof EnhancerProps>
-interface EnhancedPropsResult {
+interface EnhancePropsResult {
className: string
enhancedProps: PreservedProps
}
@@ -21,7 +21,7 @@ export default function enhanceProps(
props: EnhancerProps & React.ComponentPropsWithoutRef,
selectorHead = '',
parentProperty = ''
-): EnhancedPropsResult {
+): EnhancePropsResult {
const propsMap = expandAliases(props)
const preservedProps: PreservedProps = {}
let className: string = props.className || ''
diff --git a/src/enhancers/animation.ts b/src/enhancers/animation.ts
new file mode 100644
index 0000000..041bfe0
--- /dev/null
+++ b/src/enhancers/animation.ts
@@ -0,0 +1,95 @@
+import PropTypes from 'prop-types'
+import getCss from '../get-css'
+import { PropValidators, PropTypesMapping, PropEnhancerValueType, PropAliases, PropEnhancers } from '../types/enhancers'
+
+export const propTypes: PropTypesMapping = {
+ animation: PropTypes.string,
+ animationDelay: PropTypes.string,
+ animationDirection: PropTypes.string,
+ animationDuration: PropTypes.string,
+ animationFillMode: PropTypes.string,
+ animationIterationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ animationName: PropTypes.string,
+ animationPlayState: PropTypes.string,
+ animationTimingFunction: PropTypes.string
+}
+
+export const propAliases: PropAliases = {}
+
+export const propValidators: PropValidators = {}
+
+const animation = {
+ className: 'a',
+ cssName: 'animation',
+ jsName: 'animation',
+ complexValue: true
+}
+
+const animationDelay = {
+ className: 'a-dly',
+ cssName: 'animation-delay',
+ jsName: 'animationDelay',
+ defaultUnit: 'ms'
+}
+
+const animationDirection = {
+ className: 'a-dir',
+ cssName: 'animation-direction',
+ jsName: 'animationDirection',
+ safeValue: true
+}
+
+const animationDuration = {
+ className: 'a-dur',
+ cssName: 'animation-duration',
+ jsName: 'animationDuration',
+ defaultUnit: 'ms'
+}
+
+const animationFillMode = {
+ className: 'a-fill-md',
+ cssName: 'animation-fill-mode',
+ jsName: 'animationFillMode',
+ safeValue: true
+}
+
+const animationIterationCount = {
+ className: 'a-itr-ct',
+ cssName: 'animation-iteration-count',
+ jsName: 'animationIterationCount',
+ defaultUnit: ''
+}
+
+const animationName = {
+ className: 'a-nm',
+ cssName: 'animation-name',
+ jsName: 'animationName'
+}
+
+const animationPlayState = {
+ className: 'a-ply-ste',
+ cssName: 'animation-play-state',
+ jsName: 'animationPlayState',
+ safeValue: true
+}
+
+const animationTimingFunction = {
+ className: 'a-tmng-fn',
+ cssName: 'animation-timing-function',
+ jsName: 'animationTimingFunction',
+ complexValue: true
+}
+
+export const propEnhancers: PropEnhancers = {
+ animation: (value: PropEnhancerValueType, selector: string) => getCss(animation, value, selector),
+ animationDelay: (value: PropEnhancerValueType, selector: string) => getCss(animationDelay, value, selector),
+ animationDirection: (value: PropEnhancerValueType, selector: string) => getCss(animationDirection, value, selector),
+ animationDuration: (value: PropEnhancerValueType, selector: string) => getCss(animationDuration, value, selector),
+ animationFillMode: (value: PropEnhancerValueType, selector: string) => getCss(animationFillMode, value, selector),
+ animationIterationCount: (value: PropEnhancerValueType, selector: string) =>
+ getCss(animationIterationCount, value, selector),
+ animationName: (value: PropEnhancerValueType, selector: string) => getCss(animationName, value, selector),
+ animationPlayState: (value: PropEnhancerValueType, selector: string) => getCss(animationPlayState, value, selector),
+ animationTimingFunction: (value: PropEnhancerValueType, selector: string) =>
+ getCss(animationTimingFunction, value, selector)
+}
diff --git a/src/enhancers/index.ts b/src/enhancers/index.ts
index ef24bd5..332436b 100644
--- a/src/enhancers/index.ts
+++ b/src/enhancers/index.ts
@@ -1,3 +1,4 @@
+import * as animation from './animation'
import * as background from './background'
import * as borderRadius from './border-radius'
import * as borders from './borders'
@@ -20,6 +21,7 @@ import * as transition from './transition'
import { PropValidators, PropEnhancers, PropAliases, PropTypesMapping } from '../types/enhancers'
export {
+ animation,
background,
borderRadius,
borders,
@@ -42,6 +44,7 @@ export {
}
export const propTypes: PropTypesMapping = {
+ ...animation.propTypes,
...background.propTypes,
...borderRadius.propTypes,
...borders.propTypes,
@@ -66,6 +69,7 @@ export const propTypes: PropTypesMapping = {
export const propNames = Object.keys(propTypes)
export const propAliases: PropAliases = {
+ ...animation.propAliases,
...background.propAliases,
...borderRadius.propAliases,
...borders.propAliases,
@@ -88,6 +92,7 @@ export const propAliases: PropAliases = {
}
export const propValidators: PropValidators = {
+ ...animation.propValidators,
...background.propValidators,
...borderRadius.propValidators,
...borders.propValidators,
@@ -110,6 +115,7 @@ export const propValidators: PropValidators = {
}
export const propEnhancers: PropEnhancers = {
+ ...animation.propEnhancers,
...background.propEnhancers,
...borderRadius.propEnhancers,
...borders.propEnhancers,
diff --git a/src/enhancers/layout.ts b/src/enhancers/layout.ts
index 3c834ce..72b2d8e 100644
--- a/src/enhancers/layout.ts
+++ b/src/enhancers/layout.ts
@@ -2,11 +2,13 @@ import PropTypes from 'prop-types'
import getCss from '../get-css'
import { getClassNamePrefix } from '../get-class-name'
import { PropEnhancerValueType, PropValidators, PropEnhancers, PropTypesMapping, PropAliases } from '../types/enhancers'
+import { Rule } from '../prefixer'
export const propTypes: PropTypesMapping = {
boxSizing: PropTypes.string,
clear: PropTypes.string,
clearfix: PropTypes.bool,
+ content: PropTypes.string,
display: PropTypes.string,
float: PropTypes.string,
zIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
@@ -22,18 +24,21 @@ const display = {
safeValue: true,
isPrefixed: true
}
+
const float = {
className: 'flt',
cssName: 'float',
jsName: 'float',
safeValue: true
}
+
const clear = {
className: 'clr',
cssName: 'clear',
jsName: 'clear',
safeValue: true
}
+
const zIndex = {
className: 'z-idx',
cssName: 'z-index',
@@ -41,6 +46,7 @@ const zIndex = {
safeValue: true,
defaultUnit: ''
}
+
const boxSizing = {
className: 'box-szg',
cssName: 'box-sizing',
@@ -48,18 +54,30 @@ const boxSizing = {
safeValue: true
}
+const clearfix = () => {
+ const className = `${getClassNamePrefix()}clearfix`
+ const rules: Rule[] = [
+ { property: 'display', value: 'table' },
+ { property: 'clear', value: 'both' },
+ { property: 'content', value: '""' }
+ ]
+ const concatenatedRules = rules.map(rule => ` ${rule.property}: ${rule.value};`).join('\n')
+ const styles = `\n.${className}:before, .${className}:after {\n${concatenatedRules}\n}`
+ return { className, rules, styles }
+}
+
+const content = {
+ className: 'cnt',
+ cssName: 'content',
+ jsName: 'content',
+ complexValue: true
+}
+
export const propEnhancers: PropEnhancers = {
boxSizing: (value: PropEnhancerValueType, selector: string) => getCss(boxSizing, value, selector),
clear: (value: PropEnhancerValueType, selector: string) => getCss(clear, value, selector),
- clearfix: () => ({
- className: `${getClassNamePrefix()}clearfix`,
- styles: `
-.${getClassNamePrefix()}clearfix:before, .${getClassNamePrefix()}clearfix:after {
- display: table;
- clear: both;
- content: "";
-}`
- }),
+ clearfix,
+ content: (value: PropEnhancerValueType, selector: string) => getCss(content, value, selector),
display: (value: PropEnhancerValueType, selector: string) => getCss(display, value, selector),
float: (value: PropEnhancerValueType, selector: string) => getCss(float, value, selector),
zIndex: (value: PropEnhancerValueType, selector: string) => getCss(zIndex, value, selector)
diff --git a/src/get-css.ts b/src/get-css.ts
index 1ceb366..f743e35 100644
--- a/src/get-css.ts
+++ b/src/get-css.ts
@@ -2,6 +2,7 @@ import prefixer, { Rule } from './prefixer'
import valueToString from './value-to-string'
import getClassName, { PropertyInfo } from './get-class-name'
import { EnhancedProp } from './types/enhancers'
+import isProduction from './utils/is-production'
/**
* Generates the class name and styles.
@@ -34,7 +35,8 @@ export default function getCss(propertyInfo: PropertyInfo, value: string | numbe
}
let styles: string
- if (process.env.NODE_ENV === 'production') {
+
+ if (isProduction()) {
const rulesString = rules.map(rule => `${rule.property}:${rule.value}`).join(';')
styles = `.${className}${selector}{${rulesString}}`
} else {
@@ -45,5 +47,5 @@ ${rulesString}
}`
}
- return { className, styles }
+ return { className, styles, rules }
}
diff --git a/src/index.tsx b/src/index.tsx
index 6040406..cd6677c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,11 +2,18 @@ import * as cache from './cache'
import * as styles from './styles'
export { default } from './box'
+export { default as keyframes } from './keyframes'
export { default as splitProps } from './utils/split-props'
export { default as splitBoxProps } from './utils/split-box-props'
export { setClassNamePrefix } from './get-class-name'
export { configureSafeHref } from './utils/safeHref'
export { BoxProps, BoxOwnProps, EnhancerProps, PropsOf, PolymorphicBoxProps, BoxComponent } from './types/box-types'
+export {
+ KeyframesPercentageKey,
+ KeyframesPositionalKey,
+ KeyframesTimeline,
+ KeyframesTimelineKey
+} from './types/keyframes'
export {
background,
diff --git a/src/keyframes.ts b/src/keyframes.ts
new file mode 100644
index 0000000..269ab5d
--- /dev/null
+++ b/src/keyframes.ts
@@ -0,0 +1,128 @@
+import { KeyframesTimeline, KeyframesTimelineKey } from './types/keyframes'
+import hash from '@emotion/hash'
+import flattenObject from './utils/flatten-object'
+import { propEnhancers } from './enhancers'
+import { BoxCssProps, CssProps } from './types/enhancers'
+import { Rule } from './prefixer'
+import isProduction from './utils/is-production'
+import * as stylesheet from './styles'
+import * as cache from './cache'
+
+/**
+ * Generates a unique keyframe name and injects the styles into the stylesheet.
+ * @returns Generated name of the keyframe to use in animation props, i.e. `openAnimation_65p985`
+ *
+ * @example
+ * const openAnimation = keyframes('openAnimation', {
+ * from: {
+ * opacity: 0,
+ * transform: 'translateY(-120%)'
+ * },
+ * to: {
+ * transform: 'translateY(0)'
+ * }
+ * })
+ *
+ *
+ */
+const keyframes = (friendlyName: string, timeline: KeyframesTimeline): string => {
+ const hashedValue = hash(flattenObject(timeline))
+ const name = `${friendlyName}_${hashedValue}`
+
+ // Check for styles in cache before continuing
+ const cachedStyles = cache.get(friendlyName, hashedValue, 'keyframe')
+ if (cachedStyles != null) {
+ return name
+ }
+
+ const keys = Object.keys(timeline) as KeyframesTimelineKey[]
+ const timelineStyles = keys.map(key => getStylesForTimelineKey(key, timeline[key] || {}))
+
+ const styles = getKeyframesStyles(name, timelineStyles)
+
+ cache.set(friendlyName, hashedValue, styles, 'keyframe')
+ stylesheet.add(styles)
+
+ return name
+}
+
+const flatten = (values: T[][]): T[] => {
+ const flattenedValues: T[] = []
+ return flattenedValues.concat(...values)
+}
+
+/**
+ * Returns the style string with the rules for the given timeline key
+ * @example
+ * ```
+ * from {
+ * opacity: 0;
+ * transform: translateY(-120%);
+ * }```
+ */
+const getStylesForTimelineKey = (timelineKey: KeyframesTimelineKey, cssProps: BoxCssProps): string => {
+ const cssPropKeys = Object.keys(cssProps) as Array>
+ const rules = flatten(cssPropKeys.map(cssPropKey => getRulesForKey(cssPropKey, cssProps)))
+ const key = timelineKeyToString(timelineKey)
+ const rulesString = rules
+ .map(rule => {
+ const { property, value } = rule
+ if (isProduction()) {
+ return `${property}:${value};`
+ }
+
+ return ` ${property}: ${value};`
+ })
+ .join(isProduction() ? '' : '\n')
+
+ if (isProduction()) {
+ return `${key} {${rulesString}}`
+ }
+
+ return ` ${key} {\n${rulesString}\n }`
+}
+
+const getRulesForKey = (key: keyof BoxCssProps, cssProps: BoxCssProps): Rule[] => {
+ const value = cssProps[key]
+ const enhancer = propEnhancers[key]
+
+ if (enhancer == null || value == null || value === false) {
+ return []
+ }
+
+ const enhancedProp = enhancer(value, '')
+ if (enhancedProp == null) {
+ return []
+ }
+
+ return enhancedProp.rules
+}
+
+/**
+ * Returns keyframes style string for insertion into the stylesheet
+ * @example
+ * ```@keyframes openAnimation_65p985 {
+ * from {
+ * opacity: 0;
+ * transform: translateY(-120%);
+ * }
+ * to {
+ * transform: translateY(0);
+ * }
+ * }```
+ */
+const getKeyframesStyles = (name: string, rules: string[]): string => {
+ const separator = isProduction() ? '' : '\n'
+ const openBrace = `{${separator}`
+ const closeBrace = `${separator}}`
+ const concatenatedRules = rules.join(separator)
+
+ return `@keyframes ${name} ${openBrace}${concatenatedRules}${closeBrace}`
+}
+
+const timelineKeyToString = (timelineKey: KeyframesTimelineKey): string => {
+ const isNumber = !isNaN(Number(timelineKey))
+ return isNumber ? `${timelineKey}%` : timelineKey.toString()
+}
+
+export default keyframes
diff --git a/src/types/enhancers.ts b/src/types/enhancers.ts
index e57037d..143c526 100644
--- a/src/types/enhancers.ts
+++ b/src/types/enhancers.ts
@@ -1,11 +1,21 @@
import PropTypes from 'prop-types'
import * as CSS from 'csstype'
+import { Rule } from '../prefixer'
-type CssProps = Pick<
+export type CssProps = Pick<
CSS.StandardProperties,
| 'alignContent'
| 'alignItems'
| 'alignSelf'
+ | 'animation'
+ | 'animationDelay'
+ | 'animationDirection'
+ | 'animationDuration'
+ | 'animationFillMode'
+ | 'animationIterationCount'
+ | 'animationName'
+ | 'animationPlayState'
+ | 'animationTimingFunction'
| 'background'
| 'backgroundBlendMode'
| 'backgroundClip'
@@ -46,6 +56,7 @@ type CssProps = Pick<
| 'clear'
| 'color'
| 'columnGap'
+ | 'content'
| 'cursor'
| 'display'
| 'flex'
@@ -141,7 +152,7 @@ type CssProps = Pick<
> &
Pick
-type BoxCssProps = {
+export type BoxCssProps = {
// Enhance the CSS props with the ui-box supported values.
// `string` isn't added because it'll ruin props with string literal types (e.g textAlign)
[P in keyof CP]: CP[P] | number | false | null | undefined
@@ -206,6 +217,18 @@ export interface PropValidators {
}
export interface EnhancedProp {
+ /**
+ * Generated class name representing the styles
+ */
className: string
+
+ /**
+ * Collection of css property/value objects
+ */
+ rules: Rule[]
+
+ /**
+ * Full style string in the format of `.className[:selector] { property: value; }`
+ */
styles: string
}
diff --git a/src/types/keyframes.ts b/src/types/keyframes.ts
new file mode 100644
index 0000000..5600a72
--- /dev/null
+++ b/src/types/keyframes.ts
@@ -0,0 +1,110 @@
+import { BoxCssProps, CssProps } from './enhancers'
+
+export type KeyframesPercentageKey =
+ | 0
+ | 1
+ | 2
+ | 3
+ | 4
+ | 5
+ | 6
+ | 7
+ | 8
+ | 9
+ | 10
+ | 11
+ | 12
+ | 13
+ | 14
+ | 15
+ | 16
+ | 17
+ | 18
+ | 19
+ | 20
+ | 21
+ | 22
+ | 23
+ | 24
+ | 25
+ | 26
+ | 27
+ | 28
+ | 29
+ | 30
+ | 31
+ | 32
+ | 33
+ | 34
+ | 35
+ | 36
+ | 37
+ | 38
+ | 39
+ | 40
+ | 41
+ | 42
+ | 43
+ | 44
+ | 45
+ | 46
+ | 47
+ | 48
+ | 49
+ | 50
+ | 51
+ | 52
+ | 53
+ | 54
+ | 55
+ | 56
+ | 57
+ | 58
+ | 59
+ | 60
+ | 61
+ | 62
+ | 63
+ | 64
+ | 65
+ | 66
+ | 67
+ | 68
+ | 69
+ | 70
+ | 71
+ | 72
+ | 73
+ | 74
+ | 75
+ | 76
+ | 77
+ | 78
+ | 79
+ | 80
+ | 81
+ | 82
+ | 83
+ | 84
+ | 85
+ | 86
+ | 87
+ | 88
+ | 89
+ | 90
+ | 91
+ | 92
+ | 93
+ | 94
+ | 95
+ | 96
+ | 97
+ | 98
+ | 99
+ | 100
+
+export type KeyframesPositionalKey = 'from' | 'to'
+
+export type KeyframesTimelineKey = KeyframesPositionalKey | KeyframesPercentageKey
+
+export type KeyframesTimeline = Partial>>
diff --git a/src/utils/flatten-object.ts b/src/utils/flatten-object.ts
new file mode 100644
index 0000000..fc2c0b7
--- /dev/null
+++ b/src/utils/flatten-object.ts
@@ -0,0 +1,24 @@
+/**
+ * Flattens an object into a string representation in the form of `key:type:value`
+ */
+const flattenObject = (object: Record): string => {
+ const keys = Object.keys(object)
+ return keys
+ .map(key => {
+ const value = object[key]
+ const type = typeof value
+
+ if (Array.isArray(value)) {
+ return `${key}:array:[${value.map((value, index) => flattenObject({ [index]: value }))}]`
+ }
+
+ if (value != null && type === 'object') {
+ return `${key}:${type}:${flattenObject(value)}`
+ }
+
+ return `${key}:${type}:${value}`
+ })
+ .join(';')
+}
+
+export default flattenObject
diff --git a/src/utils/is-production.ts b/src/utils/is-production.ts
new file mode 100644
index 0000000..ac89b51
--- /dev/null
+++ b/src/utils/is-production.ts
@@ -0,0 +1,3 @@
+const isProduction = (): boolean => process.env.NODE_ENV === 'production'
+
+export default isProduction
diff --git a/src/utils/style-sheet.ts b/src/utils/style-sheet.ts
index bd0f6ab..3b991e7 100644
--- a/src/utils/style-sheet.ts
+++ b/src/utils/style-sheet.ts
@@ -1,5 +1,10 @@
-// This file is based off glamor's StyleSheet
-// https://github.com/threepointone/glamor/blob/v2.20.40/src/sheet.js
+import isProduction from './is-production'
+
+/**
+ * This file is based off glamor's StyleSheet
+ * @see https://github.com/threepointone/glamor/blob/v2.20.40/src/sheet.js
+ */
+
const isBrowser = typeof window !== 'undefined'
function last(arr: T[]) {
@@ -30,16 +35,14 @@ function makeStyleTag() {
return tag
}
-type Writeable = {
- -readonly [P in keyof T]: T[P]
-}
+type Writeable = { -readonly [P in keyof T]: T[P] }
interface SSCSSRule {
cssText: string
}
interface ServerSideStyleSheet {
- cssRules: SSCSSRule[],
+ cssRules: SSCSSRule[]
insertRule(rule: string): any
}
@@ -58,9 +61,7 @@ export default class CustomStyleSheet {
constructor(options: Options = {}) {
// The big drawback here is that the css won't be editable in devtools
- this.isSpeedy = options.speedy === undefined
- ? process.env.NODE_ENV === 'production'
- : options.speedy
+ this.isSpeedy = options.speedy === undefined ? isProduction() : options.speedy
this.maxLength = options.maxLength || 65000
}
@@ -83,7 +84,7 @@ export default class CustomStyleSheet {
insertRule: (rule: string) => {
// Enough 'spec compliance' to be able to extract the rules later
// in other words, just the cssText field
- (this.sheet!.cssRules as SSCSSRule[]).push({ cssText: rule })
+ ;(this.sheet!.cssRules as SSCSSRule[]).push({ cssText: rule })
}
}
}
diff --git a/test/get-css.ts b/test/get-css.ts
index 883a65d..5882dc9 100644
--- a/test/get-css.ts
+++ b/test/get-css.ts
@@ -18,7 +18,8 @@ test('supports basic prop + value', t => {
styles: `
.ub-min-w_10px {
min-width: 10px;
-}`
+}`,
+ rules: [{ property: 'min-width', value: '10px' }]
})
})
@@ -34,7 +35,8 @@ test('supports number value', t => {
styles: `
.ub-min-w_10px {
min-width: 10px;
-}`
+}`,
+ rules: [{ property: 'min-width', value: '10px' }]
})
})
@@ -87,6 +89,7 @@ test('appends selector when present', t => {
styles: `
.ub-bg-clr_nfznl2:hover {
background-color: blue;
-}`
+}`,
+ rules: [{ property: 'background-color', value: 'blue' }]
})
})
diff --git a/test/keyframes.ts b/test/keyframes.ts
new file mode 100644
index 0000000..36b5a4e
--- /dev/null
+++ b/test/keyframes.ts
@@ -0,0 +1,76 @@
+import test from 'ava'
+import flattenObject from '../src/utils/flatten-object'
+import { KeyframesTimeline } from '../src/types/keyframes'
+import keyframes from '../src/keyframes'
+import hash from '@emotion/hash'
+import * as stylesheet from '../src/styles'
+import * as cache from '../src/cache'
+
+test.beforeEach(() => {
+ cache.clear()
+ stylesheet.clear()
+})
+
+test.serial('returns name with hashed timeline value', t => {
+ const timeline: KeyframesTimeline = {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-120%)'
+ },
+ to: {
+ transform: 'translateY(0)'
+ }
+ }
+ const expectedHash = hash(flattenObject(timeline))
+
+ const name = keyframes('openAnimation', timeline)
+
+ t.is(name, `openAnimation_${expectedHash}`)
+})
+
+test.serial('should check cache before inserting styles when called multiple times', t => {
+ const timeline: KeyframesTimeline = {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-120%)'
+ },
+ to: {
+ transform: 'translateY(0)'
+ }
+ }
+
+ keyframes('openAnimation', timeline)
+ keyframes('openAnimation', timeline)
+
+ const styles = stylesheet.getAll()
+ const matches = styles.match(/@keyframes/g) || []
+ t.is(matches.length, 1)
+})
+
+test.serial('should insert keyframes styles', t => {
+ const timeline: KeyframesTimeline = {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-120%)'
+ },
+ to: {
+ transform: 'translateY(0)'
+ }
+ }
+
+ keyframes('openAnimation', timeline)
+
+ const styles = stylesheet.getAll()
+ t.is(
+ styles,
+ `@keyframes openAnimation_65p985 {
+ from {
+ opacity: 0;
+ transform: translateY(-120%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}`
+ )
+})
diff --git a/test/snapshots/box.tsx.md b/test/snapshots/box.tsx.md
index 8602dbd..96b665b 100644
--- a/test/snapshots/box.tsx.md
+++ b/test/snapshots/box.tsx.md
@@ -9,13 +9,42 @@ Generated by [AVA](https://ava.li).
> DOM
> CSS
- `␊
+ `@keyframes openAnimation_65p985 {␊
+ from {␊
+ opacity: 0;␊
+ transform: translateY(-120%);␊
+ }␊
+ to {␊
+ transform: translateY(0);␊
+ }␊
+ }␊
+ .ub-a-nm_openAnimation_65p985 {␊
+ animation-name: openAnimation_65p985;␊
+ }␊
+ .ub-a-dur_2-5s {␊
+ animation-duration: 2.5s;␊
+ }␊
+ .ub-a-itr-ct_infinite {␊
+ animation-iteration-count: infinite;␊
+ }␊
+ .ub-a-tmng-fn_lvnx00 {␊
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.175);␊
+ }␊
+ .ub-a-dir_both {␊
+ animation-direction: both;␊
+ }␊
+ .ub-a-ply-ste_running {␊
+ animation-play-state: running;␊
+ }␊
+ .ub-a-dly_0s {␊
+ animation-delay: 0s;␊
+ }␊
.ub-algn-cnt_center {␊
align-content: center;␊
}␊
@@ -137,6 +166,9 @@ Generated by [AVA](https://ava.li).
.ub-col-gap_3px {␊
column-gap: 3px;␊
}␊
+ .ub-cnt_1qde24a {␊
+ content: "";␊
+ }␊
.ub-crsr_pointer {␊
cursor: pointer;␊
}␊
@@ -432,6 +464,33 @@ Generated by [AVA](https://ava.li).
> CSS
`␊
+ .ub-a_inherit {␊
+ animation: inherit;␊
+ }␊
+ .ub-a-dly_inherit {␊
+ animation-delay: inherit;␊
+ }␊
+ .ub-a-dir_inherit {␊
+ animation-direction: inherit;␊
+ }␊
+ .ub-a-dur_inherit {␊
+ animation-duration: inherit;␊
+ }␊
+ .ub-a-fill-md_inherit {␊
+ animation-fill-mode: inherit;␊
+ }␊
+ .ub-a-itr-ct_inherit {␊
+ animation-iteration-count: inherit;␊
+ }␊
+ .ub-a-nm_inherit {␊
+ animation-name: inherit;␊
+ }␊
+ .ub-a-ply-ste_inherit {␊
+ animation-play-state: inherit;␊
+ }␊
+ .ub-a-tmng-fn_inherit {␊
+ animation-timing-function: inherit;␊
+ }␊
.ub-bg_inherit {␊
background: inherit;␊
}␊
@@ -681,6 +740,9 @@ Generated by [AVA](https://ava.li).
.ub-clr_inherit {␊
clear: inherit;␊
}␊
+ .ub-cnt_inherit {␊
+ content: inherit;␊
+ }␊
.ub-dspl_inherit {␊
display: inherit;␊
}␊
@@ -837,6 +899,33 @@ Generated by [AVA](https://ava.li).
> CSS
`␊
+ .ub-a_initial {␊
+ animation: initial;␊
+ }␊
+ .ub-a-dly_initial {␊
+ animation-delay: initial;␊
+ }␊
+ .ub-a-dir_initial {␊
+ animation-direction: initial;␊
+ }␊
+ .ub-a-dur_initial {␊
+ animation-duration: initial;␊
+ }␊
+ .ub-a-fill-md_initial {␊
+ animation-fill-mode: initial;␊
+ }␊
+ .ub-a-itr-ct_initial {␊
+ animation-iteration-count: initial;␊
+ }␊
+ .ub-a-nm_initial {␊
+ animation-name: initial;␊
+ }␊
+ .ub-a-ply-ste_initial {␊
+ animation-play-state: initial;␊
+ }␊
+ .ub-a-tmng-fn_initial {␊
+ animation-timing-function: initial;␊
+ }␊
.ub-bg_initial {␊
background: initial;␊
}␊
@@ -1086,6 +1175,9 @@ Generated by [AVA](https://ava.li).
.ub-clr_initial {␊
clear: initial;␊
}␊
+ .ub-cnt_initial {␊
+ content: initial;␊
+ }␊
.ub-dspl_initial {␊
display: initial;␊
}␊
diff --git a/test/snapshots/box.tsx.snap b/test/snapshots/box.tsx.snap
index 09a9c4d..b47c556 100644
Binary files a/test/snapshots/box.tsx.snap and b/test/snapshots/box.tsx.snap differ
diff --git a/test/utils/flatten-object.ts b/test/utils/flatten-object.ts
new file mode 100644
index 0000000..09591f3
--- /dev/null
+++ b/test/utils/flatten-object.ts
@@ -0,0 +1,37 @@
+import test from 'ava'
+import flattenObject from '../../src/utils/flatten-object'
+
+test.serial('flattens basic object', t => {
+ const result = flattenObject({ width: 10, height: '20' })
+
+ t.is(result, 'width:number:10;height:string:20')
+})
+
+test.serial('handles null values', t => {
+ const result = flattenObject({ width: null })
+
+ t.is(result, 'width:object:null')
+})
+
+test.serial('handles undefined values', t => {
+ const result = flattenObject({ width: undefined })
+
+ t.is(result, 'width:undefined:undefined')
+})
+
+test.serial('handles arrays', t => {
+ const result = flattenObject({ fizz: [1, '2', { foo: 'bar' }] })
+
+ t.is(result, 'fizz:array:[0:number:1,1:string:2,2:object:foo:string:bar]')
+})
+
+test.serial('flattens nested objects', t => {
+ const result = flattenObject({
+ baz: 'buzz',
+ foo: {
+ bar: 123
+ }
+ })
+
+ t.is(result, 'baz:string:buzz;foo:object:bar:number:123')
+})
diff --git a/test/utils/is-production.ts b/test/utils/is-production.ts
new file mode 100644
index 0000000..e707cd8
--- /dev/null
+++ b/test/utils/is-production.ts
@@ -0,0 +1,38 @@
+import test from 'ava'
+import isProduction from '../../src/utils/is-production'
+
+test.beforeEach(() => {
+ process.env.NODE_ENV = 'development'
+})
+
+test.serial('should return false when NODE_ENV is undefined', t => {
+ process.env.NODE_ENV = undefined
+
+ const result = isProduction()
+
+ t.is(result, false)
+})
+
+test.serial('should return false when NODE_ENV is null', t => {
+ ;(process.env.NODE_ENV as any) = null
+
+ const result = isProduction()
+
+ t.is(result, false)
+})
+
+test.serial("should return false when NODE_ENV is not 'production'", t => {
+ process.env.NODE_ENV = 'development'
+
+ const result = isProduction()
+
+ t.is(result, false)
+})
+
+test.serial("should return true when NODE_ENV is 'production'", t => {
+ process.env.NODE_ENV = 'production'
+
+ const result = isProduction()
+
+ t.is(result, true)
+})
diff --git a/tools/all-properties-component.tsx b/tools/all-properties-component.tsx
index fc0185a..fa5fa4d 100644
--- a/tools/all-properties-component.tsx
+++ b/tools/all-properties-component.tsx
@@ -1,5 +1,15 @@
import React from 'react'
-import Box from '../src'
+import Box, { keyframes } from '../src'
+
+const openAnimation = keyframes('openAnimation', {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-120%)'
+ },
+ to: {
+ transform: 'translateY(0)'
+ }
+})
// Built as a regular function instead of a component to reduce impact on the benchmark
export default () => {
@@ -7,6 +17,13 @@ export default () => {
{
clearfix
color="blue"
columnGap={3}
+ content={`""`}
cursor="pointer"
display="flex"
flex={1}
diff --git a/tools/story.tsx b/tools/story.tsx
index b9ed054..18c038e 100644
--- a/tools/story.tsx
+++ b/tools/story.tsx
@@ -1,5 +1,5 @@
import React, { CSSProperties } from 'react'
-import { default as Box, configureSafeHref } from '../src'
+import { default as Box, configureSafeHref, keyframes } from '../src'
import { storiesOf } from '@storybook/react'
import allPropertiesComponent from './all-properties-component'
import { BoxProps } from '../src/types/box-types'
@@ -273,3 +273,52 @@ storiesOf('Box', module)
const style: CSSProperties = { backgroundColor: 'red', width: 200 }
return {JSON.stringify(style, undefined, 4)}
})
+ .add('keyframes', () => {
+ const translateTo0 = {
+ transform: 'translate3d(0,0,0)'
+ }
+ const translateNeg30 = {
+ transform: 'translate3d(0, -30px, 0)'
+ }
+
+ const translateNeg15 = {
+ transform: 'translate3d(0, -15px, 0)'
+ }
+
+ const translateNeg4 = {
+ transform: 'translate3d(0,-4px,0)'
+ }
+
+ // Based on https://emotion.sh/docs/keyframes
+ const bounce = keyframes('bounce', {
+ from: translateTo0,
+ 20: translateTo0,
+ 40: translateNeg30,
+ 43: translateNeg30,
+ 53: translateTo0,
+ 70: translateNeg15,
+ 80: translateTo0,
+ 90: translateNeg4,
+ to: translateTo0
+ })
+
+ return (
+
+ Single prop
+ some bouncing text!
+ Separate props
+
+ some bouncing text!
+
+
+ )
+ })