Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prefix animation names #2621

Merged
merged 2 commits into from Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
228 changes: 228 additions & 0 deletions __tests__/parseAnimationValue.test.js
@@ -0,0 +1,228 @@
import parseAnimationValue from '../src/util/parseAnimationValue'
import { produce } from './util/produce'

describe('Tailwind Defaults', () => {
it.each([
[
'spin 1s linear infinite',
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
],
[
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
],
[
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
],
['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parseAnimationValue(input)).toEqual(expected)
})
})

describe('MDN Examples', () => {
it.each([
[
'3s ease-in 1s 2 reverse both paused slidein',
{
delay: '1s',
direction: 'reverse',
duration: '3s',
fillMode: 'both',
iterationCount: '2',
name: 'slidein',
playState: 'paused',
timingFunction: 'ease-in',
},
],
[
'slidein 3s linear 1s',
{ delay: '1s', duration: '3s', name: 'slidein', timingFunction: 'linear' },
],
['slidein 3s', { duration: '3s', name: 'slidein' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parseAnimationValue(input)).toEqual(expected)
})
})

describe('duration & delay', () => {
it.each([
// Positive seconds (integer)
['spin 1s 1s linear', { duration: '1s', delay: '1s' }],
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
['spin 1s 2s linear', { duration: '1s', delay: '2s' }],

// Negative seconds (integer)
['spin -1s -1s linear', { duration: '-1s', delay: '-1s' }],
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
['spin -1s -2s linear', { duration: '-1s', delay: '-2s' }],

// Positive seconds (float)
['spin 1.321s 1.321s linear', { duration: '1.321s', delay: '1.321s' }],
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
['spin 1.321s 2.321s linear', { duration: '1.321s', delay: '2.321s' }],

// Negative seconds (float)
['spin -1.321s -1.321s linear', { duration: '-1.321s', delay: '-1.321s' }],
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
['spin -1.321s -2.321s linear', { duration: '-1.321s', delay: '-2.321s' }],

// Positive milliseconds (integer)
['spin 100ms 100ms linear', { duration: '100ms', delay: '100ms' }],
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
['spin 100ms 200ms linear', { duration: '100ms', delay: '200ms' }],

// Negative milliseconds (integer)
['spin -100ms -100ms linear', { duration: '-100ms', delay: '-100ms' }],
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
['spin -100ms -200ms linear', { duration: '-100ms', delay: '-200ms' }],

// Positive milliseconds (float)
['spin 100.321ms 100.321ms linear', { duration: '100.321ms', delay: '100.321ms' }],
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
['spin 100.321ms 200.321ms linear', { duration: '100.321ms', delay: '200.321ms' }],

// Negative milliseconds (float)
['spin -100.321ms -100.321ms linear', { duration: '-100.321ms', delay: '-100.321ms' }],
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
['spin -100.321ms -200.321ms linear', { duration: '-100.321ms', delay: '-200.321ms' }],
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
const parsed = parseAnimationValue(input)
expect(parsed.duration).toEqual(duration)
expect(parsed.delay).toEqual(delay)
})
})

describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],

// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
}
)
})

describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
['100 spin 200s 100s linear', '100'],
['spin 200 200s 100s linear', '200'],
['spin 200s 300 100s linear', '300'],
['spin 200s 100s 400 linear', '400'],
['spin 200s 100s linear 500', '500'],

// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
}
)
})

describe('multiple animations', () => {
it('should be possible to parse multiple applications at once', () => {
const input = [
'spin 1s linear infinite',
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
].join(',')

const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(3)
expect(parsed).toEqual([
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
])
})
})

describe('randomized crazy big examples', () => {
function reOrder(input, offset = 0) {
return [...input.slice(offset), ...input.slice(0, offset)]
}

it.each(
produce((choose) => {
const direction = choose('normal', 'reverse', 'alternate', 'alternate-reverse')
const playState = choose('running', 'paused')
const fillMode = choose('none', 'forwards', 'backwards', 'both')
const iterationCount = choose('infinite', '1', '100')
const timingFunction = choose(
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'cubic-bezier(0, 0, 0.2, 1)',
'steps(4, end)'
)
const name = choose('animation-name-a', 'animation-name-b')
const inputArgs = [direction, playState, fillMode, iterationCount, timingFunction, name]
const orderOffset = choose(...Array(inputArgs.length).keys())

return [
// Input
reOrder(inputArgs, orderOffset).join(' '),

// Output
{
direction,
playState,
fillMode,
iterationCount,
timingFunction,
name,
},
]
})
)('should be possible to parse "%s"', (input, output) => {
expect(parseAnimationValue(input)).toEqual(output)
})
})
92 changes: 92 additions & 0 deletions __tests__/plugins/animation.test.js
@@ -0,0 +1,92 @@
import postcss from 'postcss'
import processPlugins from '../../src/util/processPlugins'
import plugin from '../../src/plugins/animation'

function css(nodes) {
return postcss.root({ nodes }).toString()
}

test('defining animation and keyframes', () => {
const config = {
theme: {
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
},
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
ping: { '75%, 100%': { transform: 'scale(2)', opacity: '0' } },
},
},
variants: {
animation: [],
},
}

const { utilities } = processPlugins([plugin()], config)

expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes ping {
75%, 100% { transform: scale(2); opacity: 0; }
}
}
}

@layer utilities {
@variants {
.animate-none { animation: none; }
.animate-spin { animation: spin 1s linear infinite; }
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
}
}
`)
})

test('defining animation and keyframes with prefix', () => {
const config = {
prefix: 'tw-',
theme: {
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
},
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
ping: { '75%, 100%': { transform: 'scale(2)', opacity: '0' } },
},
},
variants: {
animation: [],
},
}

const { utilities } = processPlugins([plugin()], config)

expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes tw-spin {
to { transform: rotate(360deg); }
}
@keyframes tw-ping {
75%, 100% { transform: scale(2); opacity: 0; }
}
}
}

@layer utilities {
@variants {
.tw-animate-none { animation: none; }
.tw-animate-spin { animation: tw-spin 1s linear infinite; }
.tw-animate-ping { animation: tw-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
}
}
`)
})
42 changes: 42 additions & 0 deletions __tests__/util/produce.js
@@ -0,0 +1,42 @@
// Full credit goes to: https://github.com/purplestone/exhaust
// However, it is modified so that it is a bit more modern
export function produce(blueprint) {
let groups = []

// Call the blueprint once so that we can collect all the possible values we
// spit out in the callback function.
blueprint((...args) => {
if (args.length <= 0) throw new Error('Blueprint callback must have at least a single value')
groups.push(args)
})

// Calculate how many combinations there are
let iterations = groups.reduce((total, current) => total * current.length, 1)

// Calculate all the combinations possible
let zippedGroups = []
let currentIteration = iterations
groups.forEach((a) => {
let n = a.length
currentIteration = currentIteration / n
let iS = -1
let aS = []

for (let i = 0; i < iterations; i++) {
if (!(i % currentIteration)) iS++
aS.push(a[iS % n])
}
zippedGroups.push(aS)
})

// Transpose the matrix, so that we can get the correct rows/columns structure
// again.
zippedGroups = zippedGroups[0].map((_, i) => zippedGroups.map((o) => o[i]))

// Call the blueprint again, but now give the inner function a single value
// every time so that we can build up the final result with single values.
return zippedGroups.map((group) => {
let i = 0
return blueprint(() => group[i++])
})
}
16 changes: 13 additions & 3 deletions src/plugins/animation.js
@@ -1,16 +1,26 @@
import _ from 'lodash'
import nameClass from '../util/nameClass'
import parseAnimationValue from '../util/parseAnimationValue'

export default function () {
return function ({ addUtilities, theme, variants }) {
return function ({ addUtilities, theme, variants, prefix }) {
const prefixName = (name) => prefix(`.${name}`).slice(1)
const keyframesConfig = theme('keyframes')
const keyframesStyles = _.mapKeys(keyframesConfig, (_keyframes, name) => `@keyframes ${name}`)
const keyframesStyles = _.mapKeys(
keyframesConfig,
(_keyframes, name) => `@keyframes ${prefixName(name)}`
)

addUtilities(keyframesStyles, { respectImportant: false })

const animationConfig = theme('animation')
const utilities = _.mapValues(
_.mapKeys(animationConfig, (_animation, suffix) => nameClass('animate', suffix)),
(animation) => ({ animation })
(animation) => {
const { name } = parseAnimationValue(animation)
if (name === undefined) return { animation }
return { animation: animation.replace(name, prefixName(name)) }
}
)
addUtilities(utilities, variants('animation'))
}
Expand Down