feat: prefix animation names (#2621)
* add animation value parser * prefix animation names
This commit is contained in:
parent
539738d418
commit
e1ef99814e
5 changed files with 410 additions and 3 deletions
228
__tests__/parseAnimationValue.test.js
Normal file
228
__tests__/parseAnimationValue.test.js
Normal file
|
|
@ -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
__tests__/plugins/animation.test.js
Normal file
92
__tests__/plugins/animation.test.js
Normal file
|
|
@ -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
__tests__/util/produce.js
Normal file
42
__tests__/util/produce.js
Normal file
|
|
@ -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++])
|
||||
})
|
||||
}
|
||||
|
|
@ -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'))
|
||||
}
|
||||
|
|
|
|||
35
src/util/parseAnimationValue.js
Normal file
35
src/util/parseAnimationValue.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
|
||||
const PLAY_STATES = new Set(['running', 'paused'])
|
||||
const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both'])
|
||||
const ITERATION_COUNTS = new Set(['infinite'])
|
||||
const TIMINGS = new Set(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'])
|
||||
const TIMING_FNS = ['cubic-bezier', 'steps']
|
||||
|
||||
const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
const SPACE = /\ (?![^(]*\))/g // Similar to the one above, but with spaces instead.
|
||||
const TIME = /^(-?[\d.]+m?s)$/
|
||||
const DIGIT = /^(\d+)$/
|
||||
|
||||
export default function parseAnimationValue(input) {
|
||||
const animations = input.split(COMMA)
|
||||
const result = animations.map((animation) => {
|
||||
const result = {}
|
||||
const parts = animation.split(SPACE)
|
||||
|
||||
for (let part of parts) {
|
||||
if (DIRECTIONS.has(part)) result.direction = part
|
||||
else if (PLAY_STATES.has(part)) result.playState = part
|
||||
else if (FILL_MODES.has(part)) result.fillMode = part
|
||||
else if (ITERATION_COUNTS.has(part)) result.iterationCount = part
|
||||
else if (TIMINGS.has(part)) result.timingFunction = part
|
||||
else if (TIMING_FNS.some((f) => part.startsWith(`${f}(`))) result.timingFunction = part
|
||||
else if (TIME.test(part)) result[result.duration === undefined ? 'duration' : 'delay'] = part
|
||||
else if (DIGIT.test(part)) result.iterationCount = part
|
||||
else result.name = part
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return animations.length > 1 ? result : result[0]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue