From 5dcb9f903520d65013ce3a897e50fd89d09c3014 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 24 Jul 2020 15:26:55 -0400 Subject: [PATCH 1/3] Add reduce-motion variant --- __tests__/variantsAtRule.test.js | 23 +++++++++++++++++++++++ src/lib/substituteVariantsAtRules.js | 12 ++++++++++++ 2 files changed, 35 insertions(+) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 12f836f9a..c208c0b82 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -177,6 +177,29 @@ test('it can generate focus-visible variants', () => { }) }) +test('it can generate reduce-motion variants', () => { + const input = ` + @variants reduce-motion { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + @media (prefers-reduced-motion: reduce) { + .reduce-motion\\:banana { color: yellow; } + .reduce-motion\\:chocolate { 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..7c2941b64 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -23,6 +23,18 @@ function ensureIncludesDefault(variants) { const defaultVariantGenerators = config => ({ default: generateVariantFunction(() => {}), + 'reduce-motion': generateVariantFunction(({ container, separator, modifySelectors }) => { + const modified = modifySelectors(({ selector }) => { + return selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `reduce-motion${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 => { From 9f3927760741e55eda3fb4e06a3877014c576d93 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 27 Jul 2020 14:36:10 -0400 Subject: [PATCH 2/3] Rename reduced-motion to motion-reduced, add motion-safe --- __tests__/variantsAtRule.test.js | 31 ++++++++++++++++++++++++---- src/lib/substituteVariantsAtRules.js | 19 +++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index c208c0b82..caa0d6994 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -177,9 +177,9 @@ test('it can generate focus-visible variants', () => { }) }) -test('it can generate reduce-motion variants', () => { +test('it can generate motion-reduced variants', () => { const input = ` - @variants reduce-motion { + @variants motion-reduced { .banana { color: yellow; } .chocolate { color: brown; } } @@ -189,8 +189,31 @@ test('it can generate reduce-motion variants', () => { .banana { color: yellow; } .chocolate { color: brown; } @media (prefers-reduced-motion: reduce) { - .reduce-motion\\:banana { color: yellow; } - .reduce-motion\\:chocolate { color: brown; } + .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; } } ` diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 7c2941b64..7cf4fa8a3 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -23,11 +23,26 @@ function ensureIncludesDefault(variants) { const defaultVariantGenerators = config => ({ default: generateVariantFunction(() => {}), - 'reduce-motion': generateVariantFunction(({ container, separator, modifySelectors }) => { + 'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => { const modified = modifySelectors(({ selector }) => { return selectorParser(selectors => { selectors.walkClasses(sel => { - sel.value = `reduce-motion${separator}${sel.value}` + 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) }) From 66619011d64d915d0febd095a4edda930b434e05 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 27 Jul 2020 15:23:56 -0400 Subject: [PATCH 3/3] Make motion variants stackable --- __tests__/variantsAtRule.test.js | 114 +++++++++++++++++++++++++++ src/lib/substituteVariantsAtRules.js | 63 +++++++++++---- 2 files changed, 161 insertions(+), 16 deletions(-) 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) } }