diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 12f836f9a..13ecddfc5 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -177,6 +177,166 @@ test('it can generate focus-visible variants', () => { }) }) +test('it can generate motion-reduced variants', () => { + const input = ` + @variants motion-reduced { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .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('it can generate motion-safe variants', () => { + const input = ` + @variants motion-safe { + .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; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +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 d553beba1..692a92d71 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -23,6 +23,33 @@ function ensureIncludesDefault(variants) { const defaultVariantGenerators = config => ({ default: generateVariantFunction(() => {}), + 'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => { + const modified = modifySelectors(({ selector }) => { + return selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `motion-safe${separator}${sel.value}` + }) + }).processSync(selector) + }) + const mediaQuery = postcss.atRule({ + name: 'media', + params: '(prefers-reduced-motion: no-preference)', + }) + mediaQuery.append(modified) + container.append(mediaQuery) + }), + 'motion-reduced': generateVariantFunction(({ container, separator, modifySelectors }) => { + const modified = modifySelectors(({ selector }) => { + return selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `motion-reduced${separator}${sel.value}` + }) + }).processSync(selector) + }) + const mediaQuery = postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' }) + mediaQuery.append(modified) + container.append(mediaQuery) + }), 'group-hover': generateVariantFunction(({ modifySelectors, separator }) => { return modifySelectors(({ selector }) => { return selectorParser(selectors => { @@ -63,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 = { @@ -70,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) } }