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! + + + ) + })