diff --git a/src/plugins/animation.js b/src/plugins/animation.js index e028fb1decbf..9d51c80b414a 100644 --- a/src/plugins/animation.js +++ b/src/plugins/animation.js @@ -19,18 +19,23 @@ export default function () { matchUtilities( { animate: (value, { includeRules }) => { - let { name: animationName } = parseAnimationValue(value) + let animations = parseAnimationValue(value) - if (keyframes[animationName] !== undefined) { - includeRules(keyframes[animationName], { respectImportant: false }) - } - - if (animationName === undefined || keyframes[animationName] === undefined) { - return { animation: value } + for (let { name } of animations) { + if (keyframes[name] !== undefined) { + includeRules(keyframes[name], { respectImportant: false }) + } } return { - animation: value.replace(animationName, prefixName(animationName)), + animation: animations + .map(({ name, value }) => { + if (name === undefined || keyframes[name] === undefined) { + return value + } + return value.replace(name, prefixName(name)) + }) + .join(', '), } }, }, diff --git a/src/util/parseAnimationValue.js b/src/util/parseAnimationValue.js index bd34f5898fd7..94c77ff3e44a 100644 --- a/src/util/parseAnimationValue.js +++ b/src/util/parseAnimationValue.js @@ -20,9 +20,10 @@ const DIGIT = /^(\d+)$/ export default function parseAnimationValue(input) { let animations = input.split(COMMA) - let result = animations.map((animation) => { - let result = {} - let parts = animation.trim().split(SPACE) + return animations.map((animation) => { + let value = animation.trim() + let result = { value } + let parts = value.split(SPACE) let seen = new Set() for (let part of parts) { @@ -58,6 +59,4 @@ export default function parseAnimationValue(input) { return result }) - - return animations.length > 1 ? result : result[0] } diff --git a/tests/jit/animations.test.css b/tests/jit/animations.test.css deleted file mode 100644 index 78244c7dd9e5..000000000000 --- a/tests/jit/animations.test.css +++ /dev/null @@ -1,32 +0,0 @@ -@keyframes spin { - to { - transform: rotate(360deg); - } -} -.animate-spin { - animation: spin 1s linear infinite; -} -@keyframes ping { - 75%, - 100% { - transform: scale(2); - opacity: 0; - } -} -.hover\:animate-ping:hover { - animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; -} -@keyframes bounce { - 0%, - 100% { - transform: translateY(-25%); - animation-timing-function: cubic-bezier(0.8, 0, 1, 1); - } - 50% { - transform: none; - animation-timing-function: cubic-bezier(0, 0, 0.2, 1); - } -} -.group:hover .group-hover\:animate-bounce { - animation: bounce 1s infinite; -} diff --git a/tests/jit/animations.test.html b/tests/jit/animations.test.html deleted file mode 100644 index 3d5a919e6a1d..000000000000 --- a/tests/jit/animations.test.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/tests/jit/animations.test.js b/tests/jit/animations.test.js index 7ddedcf49ea7..1b88fdcd6d8d 100644 --- a/tests/jit/animations.test.js +++ b/tests/jit/animations.test.js @@ -1,5 +1,4 @@ import postcss from 'postcss' -import fs from 'fs' import path from 'path' import tailwind from '../../src/jit/index.js' @@ -9,22 +8,199 @@ function run(input, config = {}) { }) } -test('animations', () => { +test('basic', () => { let config = { - darkMode: 'class', mode: 'jit', - purge: [path.resolve(__dirname, './animations.test.html')], - corePlugins: {}, - theme: {}, - plugins: [], + purge: [ + { + raw: ` +
+
+
+ `, + }, + ], } let css = `@tailwind utilities` return run(css, config).then((result) => { - let expectedPath = path.resolve(__dirname, './animations.test.css') - let expected = fs.readFileSync(expectedPath, 'utf8') + expect(result.css).toMatchFormattedCss(` + @keyframes spin { + to { + transform: rotate(360deg); + } + } + .animate-spin { + animation: spin 1s linear infinite; + } + @keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + .hover\\:animate-ping:hover { + animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + } + @keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + .group:hover .group-hover\\:animate-bounce { + animation: bounce 1s infinite; + } + `) + }) +}) + +test('custom', () => { + let config = { + mode: 'jit', + purge: [{ raw: `
` }], + theme: { + extend: { + keyframes: { + one: { to: { transform: 'rotate(360deg)' } }, + }, + animation: { + one: 'one 2s', + }, + }, + }, + } + + let css = `@tailwind utilities` + + return run(css, config).then((result) => { + expect(result.css).toMatchFormattedCss(` + @keyframes one { + to { + transform: rotate(360deg); + } + } + .animate-one { + animation: one 2s; + } + `) + }) +}) + +test('custom prefixed', () => { + let config = { + mode: 'jit', + prefix: 'tw-', + purge: [{ raw: `
` }], + theme: { + extend: { + keyframes: { + one: { to: { transform: 'rotate(360deg)' } }, + }, + animation: { + one: 'one 2s', + }, + }, + }, + } + + let css = `@tailwind utilities` - expect(result.css).toMatchFormattedCss(expected) + return run(css, config).then((result) => { + expect(result.css).toMatchFormattedCss(` + @keyframes tw-one { + to { + transform: rotate(360deg); + } + } + .tw-animate-one { + animation: tw-one 2s; + } + `) + }) +}) + +test('multiple', () => { + let config = { + mode: 'jit', + purge: [{ raw: `
` }], + theme: { + extend: { + animation: { + multiple: 'bounce 2s linear, pulse 3s ease-in', + }, + }, + }, + } + + let css = `@tailwind utilities` + + return run(css, config).then((result) => { + expect(result.css).toMatchFormattedCss(` + @keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + @keyframes pulse { + 50% { + opacity: 0.5; + } + } + .animate-multiple { + animation: bounce 2s linear, pulse 3s ease-in; + } + `) + }) +}) + +test('multiple custom', () => { + let config = { + mode: 'jit', + purge: [{ raw: `
` }], + theme: { + extend: { + keyframes: { + one: { to: { transform: 'rotate(360deg)' } }, + two: { to: { transform: 'scale(1.23)' } }, + }, + animation: { + multiple: 'one 2s, two 3s', + }, + }, + }, + } + + let css = `@tailwind utilities` + + return run(css, config).then((result) => { + expect(result.css).toMatchFormattedCss(` + @keyframes one { + to { + transform: rotate(360deg); + } + } + @keyframes two { + to { + transform: scale(1.23); + } + } + .animate-multiple { + animation: one 2s, two 3s; + } + `) }) }) diff --git a/tests/parseAnimationValue.test.js b/tests/parseAnimationValue.test.js index 38e360e7f07f..8fd211d2fb21 100644 --- a/tests/parseAnimationValue.test.js +++ b/tests/parseAnimationValue.test.js @@ -5,6 +5,7 @@ describe('Tailwind Defaults', () => { [ 'spin 1s linear infinite', { + value: 'spin 1s linear infinite', name: 'spin', duration: '1s', timingFunction: 'linear', @@ -14,15 +15,26 @@ describe('Tailwind Defaults', () => { [ 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', { + value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', name: 'ping', duration: '1s', timingFunction: 'cubic-bezier(0, 0, 0.2, 1)', iterationCount: 'infinite', }, ], - ['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }], + [ + 'bounce 1s infinite', + { + value: 'bounce 1s infinite', + name: 'bounce', + duration: '1s', + iterationCount: 'infinite', + }, + ], ])('should be possible to parse: "%s"', (input, expected) => { - expect(parseAnimationValue(input)).toEqual(expected) + const parsed = parseAnimationValue(input) + expect(parsed).toHaveLength(1) + expect(parsed[0]).toEqual(expected) }) }) @@ -31,6 +43,7 @@ describe('MDN Examples', () => { [ '3s ease-in 1s 2 reverse both paused slidein', { + value: '3s ease-in 1s 2 reverse both paused slidein', delay: '1s', direction: 'reverse', duration: '3s', @@ -44,15 +57,18 @@ describe('MDN Examples', () => { [ 'slidein 3s linear 1s', { + value: 'slidein 3s linear 1s', delay: '1s', duration: '3s', name: 'slidein', timingFunction: 'linear', }, ], - ['slidein 3s', { duration: '3s', name: 'slidein' }], + ['slidein 3s', { value: 'slidein 3s', duration: '3s', name: 'slidein' }], ])('should be possible to parse: "%s"', (input, expected) => { - expect(parseAnimationValue(input)).toEqual(expected) + const parsed = parseAnimationValue(input) + expect(parsed).toHaveLength(1) + expect(parsed[0]).toEqual(expected) }) }) @@ -83,8 +99,9 @@ describe('duration & delay', () => { ['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.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) + expect(parsed).toHaveLength(1) + expect(parsed[0].duration).toEqual(duration) + expect(parsed[0].delay).toEqual(delay) }) }) @@ -106,7 +123,9 @@ describe('iteration count', () => { ])( 'should be possible to parse "%s" with an iteraction count of "%s"', (input, iterationCount) => { - expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount) + const parsed = parseAnimationValue(input) + expect(parsed).toHaveLength(1) + expect(parsed[0].iterationCount).toEqual(iterationCount) } ) }) @@ -123,18 +142,21 @@ describe('multiple animations', () => { expect(parsed).toHaveLength(3) expect(parsed).toEqual([ { + value: 'spin 1s linear infinite', name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite', }, { + value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', name: 'ping', duration: '1s', timingFunction: 'cubic-bezier(0, 0, 0.2, 1)', iterationCount: 'infinite', }, { + value: 'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite', name: 'pulse', duration: '2s', timingFunction: 'cubic-bezier(0.4, 0, 0.6)', @@ -145,20 +167,21 @@ describe('multiple animations', () => { }) it.each` - input | direction | playState | fillMode | iterationCount | timingFunction | duration | delay | name - ${'1s spin 1s infinite'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'spin'} - ${'infinite infinite 1s 1s'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'infinite'} - ${'ease 1s ease 1s'} | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${'ease'} | ${'1s'} | ${'1s'} | ${'ease'} - ${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'paused backwards infinite ease-in 1s 2s name normal'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'backwards infinite ease-in 1s 2s name normal paused'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'infinite ease-in 1s 2s name normal paused backwards'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'ease-in 1s 2s name normal paused backwards infinite'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'1s 2s name normal paused backwards infinite ease-in'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${'2s name normal paused backwards infinite ease-in 1s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'2s'} | ${'1s'} | ${'name'} - ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} - ${' name normal paused backwards infinite ease-in 1s 2s '} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + input | value | direction | playState | fillMode | iterationCount | timingFunction | duration | delay | name + ${'1s spin 1s infinite'} | ${'1s spin 1s infinite'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'spin'} + ${'infinite infinite 1s 1s'} | ${'infinite infinite 1s 1s'} | ${undefined} | ${undefined} | ${undefined} | ${'infinite'} | ${undefined} | ${'1s'} | ${'1s'} | ${'infinite'} + ${'ease 1s ease 1s'} | ${'ease 1s ease 1s'} | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${'ease'} | ${'1s'} | ${'1s'} | ${'ease'} + ${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal paused backwards infinite ease-in 1s 2s name'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'paused backwards infinite ease-in 1s 2s name normal'} | ${'paused backwards infinite ease-in 1s 2s name normal'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'backwards infinite ease-in 1s 2s name normal paused'} | ${'backwards infinite ease-in 1s 2s name normal paused'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'infinite ease-in 1s 2s name normal paused backwards'} | ${'infinite ease-in 1s 2s name normal paused backwards'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'ease-in 1s 2s name normal paused backwards infinite'} | ${'ease-in 1s 2s name normal paused backwards infinite'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'1s 2s name normal paused backwards infinite ease-in'} | ${'1s 2s name normal paused backwards infinite ease-in'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${'2s name normal paused backwards infinite ease-in 1s'} | ${'2s name normal paused backwards infinite ease-in 1s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'2s'} | ${'1s'} | ${'name'} + ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} + ${' name normal paused backwards infinite ease-in 1s 2s '} | ${'name normal paused backwards infinite ease-in 1s 2s'} | ${'normal'} | ${'paused'} | ${'backwards'} | ${'infinite'} | ${'ease-in'} | ${'1s'} | ${'2s'} | ${'name'} `('should parse "$input" correctly', ({ input, ...expected }) => { let parsed = parseAnimationValue(input) - expect(parsed).toEqual(expected) + expect(parsed).toHaveLength(1) + expect(parsed[0]).toEqual(expected) })