diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index caa0d6994..13ecddfc5 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -223,6 +223,120 @@ test('it can generate motion-safe variants', () => { }) }) +test('it can generate motion-safe and motion-reduced variants', () => { + const input = ` + @variants motion-safe, motion-reduced { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + @media (prefers-reduced-motion: no-preference) { + .motion-safe\\:banana { color: yellow; } + .motion-safe\\:chocolate { color: brown; } + } + @media (prefers-reduced-motion: reduce) { + .motion-reduced\\:banana { color: yellow; } + .motion-reduced\\:chocolate { color: brown; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('motion-reduced variants stack with basic variants', () => { + const input = ` + @variants motion-reduced, hover { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .hover\\:banana:hover { color: yellow; } + .hover\\:chocolate:hover { color: brown; } + @media (prefers-reduced-motion: reduce) { + .motion-reduced\\:banana { color: yellow; } + .motion-reduced\\:chocolate { color: brown; } + .motion-reduced\\:hover\\:banana:hover { color: yellow; } + .motion-reduced\\:hover\\:chocolate:hover { color: brown; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('motion-safe variants stack with basic variants', () => { + const input = ` + @variants motion-safe, hover { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .hover\\:banana:hover { color: yellow; } + .hover\\:chocolate:hover { color: brown; } + @media (prefers-reduced-motion: no-preference) { + .motion-safe\\:banana { color: yellow; } + .motion-safe\\:chocolate { color: brown; } + .motion-safe\\:hover\\:banana:hover { color: yellow; } + .motion-safe\\:hover\\:chocolate:hover { color: brown; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('motion-safe and motion-reduced variants stack with basic variants', () => { + const input = ` + @variants motion-reduced, motion-safe, hover { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .hover\\:banana:hover { color: yellow; } + .hover\\:chocolate:hover { color: brown; } + @media (prefers-reduced-motion: reduce) { + .motion-reduced\\:banana { color: yellow; } + .motion-reduced\\:chocolate { color: brown; } + .motion-reduced\\:hover\\:banana:hover { color: yellow; } + .motion-reduced\\:hover\\:chocolate:hover { color: brown; } + } + @media (prefers-reduced-motion: no-preference) { + .motion-safe\\:banana { color: yellow; } + .motion-safe\\:chocolate { color: brown; } + .motion-safe\\:hover\\:banana:hover { color: yellow; } + .motion-safe\\:hover\\:chocolate:hover { color: brown; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + test('it can generate first-child variants', () => { const input = ` @variants first { diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 7cf4fa8a3..692a92d71 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -90,6 +90,28 @@ const defaultVariantGenerators = config => ({ even: generatePseudoClassVariant('nth-child(even)', 'even'), }) +function prependStackableVariants(atRule, variants) { + const stackableVariants = ['motion-safe', 'motion-reduced'] + + if (!_.some(variants, v => stackableVariants.includes(v))) { + return variants + } + + if (_.every(variants, v => stackableVariants.includes(v))) { + return variants + } + + const variantsParent = postcss.atRule({ + name: 'variants', + params: variants.filter(v => stackableVariants.includes(v)).join(', '), + }) + atRule.before(variantsParent) + variantsParent.append(atRule) + variants = _.without(variants, ...stackableVariants) + + return variants +} + export default function(config, { variantGenerators: pluginVariantGenerators }) { return function(css) { const variantGenerators = { @@ -97,25 +119,34 @@ export default function(config, { variantGenerators: pluginVariantGenerators }) ...pluginVariantGenerators, } - css.walkAtRules('variants', atRule => { - const variants = postcss.list.comma(atRule.params).filter(variant => variant !== '') + let variantsFound = false - if (variants.includes('responsive')) { - const responsiveParent = postcss.atRule({ name: 'responsive' }) - atRule.before(responsiveParent) - responsiveParent.append(atRule) - } + do { + variantsFound = false + css.walkAtRules('variants', atRule => { + variantsFound = true - _.forEach(_.without(ensureIncludesDefault(variants), 'responsive'), variant => { - if (!variantGenerators[variant]) { - throw new Error( - `Your config mentions the "${variant}" variant, but "${variant}" doesn't appear to be a variant. Did you forget or misconfigure a plugin that supplies that variant?` - ) + let variants = postcss.list.comma(atRule.params).filter(variant => variant !== '') + + if (variants.includes('responsive')) { + const responsiveParent = postcss.atRule({ name: 'responsive' }) + atRule.before(responsiveParent) + responsiveParent.append(atRule) } - variantGenerators[variant](atRule, config) - }) - atRule.remove() - }) + const remainingVariants = prependStackableVariants(atRule, variants) + + _.forEach(_.without(ensureIncludesDefault(remainingVariants), 'responsive'), variant => { + if (!variantGenerators[variant]) { + throw new Error( + `Your config mentions the "${variant}" variant, but "${variant}" doesn't appear to be a variant. Did you forget or misconfigure a plugin that supplies that variant?` + ) + } + variantGenerators[variant](atRule, config) + }) + + atRule.remove() + }) + } while (variantsFound) } }