Skip to content

Commit 05fe015

Browse files
authored
Merge pull request #2071 from tailwindlabs/reduced-motion
Add reduce-motion variant
2 parents 4ee53f3 + 6661901 commit 05fe015

File tree

2 files changed

+234
-16
lines changed

2 files changed

+234
-16
lines changed

__tests__/variantsAtRule.test.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,166 @@ test('it can generate focus-visible variants', () => {
177177
})
178178
})
179179

180+
test('it can generate motion-reduced variants', () => {
181+
const input = `
182+
@variants motion-reduced {
183+
.banana { color: yellow; }
184+
.chocolate { color: brown; }
185+
}
186+
`
187+
188+
const output = `
189+
.banana { color: yellow; }
190+
.chocolate { color: brown; }
191+
@media (prefers-reduced-motion: reduce) {
192+
.motion-reduced\\:banana { color: yellow; }
193+
.motion-reduced\\:chocolate { color: brown; }
194+
}
195+
`
196+
197+
return run(input).then(result => {
198+
expect(result.css).toMatchCss(output)
199+
expect(result.warnings().length).toBe(0)
200+
})
201+
})
202+
203+
test('it can generate motion-safe variants', () => {
204+
const input = `
205+
@variants motion-safe {
206+
.banana { color: yellow; }
207+
.chocolate { color: brown; }
208+
}
209+
`
210+
211+
const output = `
212+
.banana { color: yellow; }
213+
.chocolate { color: brown; }
214+
@media (prefers-reduced-motion: no-preference) {
215+
.motion-safe\\:banana { color: yellow; }
216+
.motion-safe\\:chocolate { color: brown; }
217+
}
218+
`
219+
220+
return run(input).then(result => {
221+
expect(result.css).toMatchCss(output)
222+
expect(result.warnings().length).toBe(0)
223+
})
224+
})
225+
226+
test('it can generate motion-safe and motion-reduced variants', () => {
227+
const input = `
228+
@variants motion-safe, motion-reduced {
229+
.banana { color: yellow; }
230+
.chocolate { color: brown; }
231+
}
232+
`
233+
234+
const output = `
235+
.banana { color: yellow; }
236+
.chocolate { color: brown; }
237+
@media (prefers-reduced-motion: no-preference) {
238+
.motion-safe\\:banana { color: yellow; }
239+
.motion-safe\\:chocolate { color: brown; }
240+
}
241+
@media (prefers-reduced-motion: reduce) {
242+
.motion-reduced\\:banana { color: yellow; }
243+
.motion-reduced\\:chocolate { color: brown; }
244+
}
245+
`
246+
247+
return run(input).then(result => {
248+
expect(result.css).toMatchCss(output)
249+
expect(result.warnings().length).toBe(0)
250+
})
251+
})
252+
253+
test('motion-reduced variants stack with basic variants', () => {
254+
const input = `
255+
@variants motion-reduced, hover {
256+
.banana { color: yellow; }
257+
.chocolate { color: brown; }
258+
}
259+
`
260+
261+
const output = `
262+
.banana { color: yellow; }
263+
.chocolate { color: brown; }
264+
.hover\\:banana:hover { color: yellow; }
265+
.hover\\:chocolate:hover { color: brown; }
266+
@media (prefers-reduced-motion: reduce) {
267+
.motion-reduced\\:banana { color: yellow; }
268+
.motion-reduced\\:chocolate { color: brown; }
269+
.motion-reduced\\:hover\\:banana:hover { color: yellow; }
270+
.motion-reduced\\:hover\\:chocolate:hover { color: brown; }
271+
}
272+
`
273+
274+
return run(input).then(result => {
275+
expect(result.css).toMatchCss(output)
276+
expect(result.warnings().length).toBe(0)
277+
})
278+
})
279+
280+
test('motion-safe variants stack with basic variants', () => {
281+
const input = `
282+
@variants motion-safe, hover {
283+
.banana { color: yellow; }
284+
.chocolate { color: brown; }
285+
}
286+
`
287+
288+
const output = `
289+
.banana { color: yellow; }
290+
.chocolate { color: brown; }
291+
.hover\\:banana:hover { color: yellow; }
292+
.hover\\:chocolate:hover { color: brown; }
293+
@media (prefers-reduced-motion: no-preference) {
294+
.motion-safe\\:banana { color: yellow; }
295+
.motion-safe\\:chocolate { color: brown; }
296+
.motion-safe\\:hover\\:banana:hover { color: yellow; }
297+
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
298+
}
299+
`
300+
301+
return run(input).then(result => {
302+
expect(result.css).toMatchCss(output)
303+
expect(result.warnings().length).toBe(0)
304+
})
305+
})
306+
307+
test('motion-safe and motion-reduced variants stack with basic variants', () => {
308+
const input = `
309+
@variants motion-reduced, motion-safe, hover {
310+
.banana { color: yellow; }
311+
.chocolate { color: brown; }
312+
}
313+
`
314+
315+
const output = `
316+
.banana { color: yellow; }
317+
.chocolate { color: brown; }
318+
.hover\\:banana:hover { color: yellow; }
319+
.hover\\:chocolate:hover { color: brown; }
320+
@media (prefers-reduced-motion: reduce) {
321+
.motion-reduced\\:banana { color: yellow; }
322+
.motion-reduced\\:chocolate { color: brown; }
323+
.motion-reduced\\:hover\\:banana:hover { color: yellow; }
324+
.motion-reduced\\:hover\\:chocolate:hover { color: brown; }
325+
}
326+
@media (prefers-reduced-motion: no-preference) {
327+
.motion-safe\\:banana { color: yellow; }
328+
.motion-safe\\:chocolate { color: brown; }
329+
.motion-safe\\:hover\\:banana:hover { color: yellow; }
330+
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
331+
}
332+
`
333+
334+
return run(input).then(result => {
335+
expect(result.css).toMatchCss(output)
336+
expect(result.warnings().length).toBe(0)
337+
})
338+
})
339+
180340
test('it can generate first-child variants', () => {
181341
const input = `
182342
@variants first {

src/lib/substituteVariantsAtRules.js

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ function ensureIncludesDefault(variants) {
2323

2424
const defaultVariantGenerators = config => ({
2525
default: generateVariantFunction(() => {}),
26+
'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => {
27+
const modified = modifySelectors(({ selector }) => {
28+
return selectorParser(selectors => {
29+
selectors.walkClasses(sel => {
30+
sel.value = `motion-safe${separator}${sel.value}`
31+
})
32+
}).processSync(selector)
33+
})
34+
const mediaQuery = postcss.atRule({
35+
name: 'media',
36+
params: '(prefers-reduced-motion: no-preference)',
37+
})
38+
mediaQuery.append(modified)
39+
container.append(mediaQuery)
40+
}),
41+
'motion-reduced': generateVariantFunction(({ container, separator, modifySelectors }) => {
42+
const modified = modifySelectors(({ selector }) => {
43+
return selectorParser(selectors => {
44+
selectors.walkClasses(sel => {
45+
sel.value = `motion-reduced${separator}${sel.value}`
46+
})
47+
}).processSync(selector)
48+
})
49+
const mediaQuery = postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' })
50+
mediaQuery.append(modified)
51+
container.append(mediaQuery)
52+
}),
2653
'group-hover': generateVariantFunction(({ modifySelectors, separator }) => {
2754
return modifySelectors(({ selector }) => {
2855
return selectorParser(selectors => {
@@ -63,32 +90,63 @@ const defaultVariantGenerators = config => ({
6390
even: generatePseudoClassVariant('nth-child(even)', 'even'),
6491
})
6592

93+
function prependStackableVariants(atRule, variants) {
94+
const stackableVariants = ['motion-safe', 'motion-reduced']
95+
96+
if (!_.some(variants, v => stackableVariants.includes(v))) {
97+
return variants
98+
}
99+
100+
if (_.every(variants, v => stackableVariants.includes(v))) {
101+
return variants
102+
}
103+
104+
const variantsParent = postcss.atRule({
105+
name: 'variants',
106+
params: variants.filter(v => stackableVariants.includes(v)).join(', '),
107+
})
108+
atRule.before(variantsParent)
109+
variantsParent.append(atRule)
110+
variants = _.without(variants, ...stackableVariants)
111+
112+
return variants
113+
}
114+
66115
export default function(config, { variantGenerators: pluginVariantGenerators }) {
67116
return function(css) {
68117
const variantGenerators = {
69118
...defaultVariantGenerators(config),
70119
...pluginVariantGenerators,
71120
}
72121

73-
css.walkAtRules('variants', atRule => {
74-
const variants = postcss.list.comma(atRule.params).filter(variant => variant !== '')
122+
let variantsFound = false
75123

76-
if (variants.includes('responsive')) {
77-
const responsiveParent = postcss.atRule({ name: 'responsive' })
78-
atRule.before(responsiveParent)
79-
responsiveParent.append(atRule)
80-
}
124+
do {
125+
variantsFound = false
126+
css.walkAtRules('variants', atRule => {
127+
variantsFound = true
81128

82-
_.forEach(_.without(ensureIncludesDefault(variants), 'responsive'), variant => {
83-
if (!variantGenerators[variant]) {
84-
throw new Error(
85-
`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?`
86-
)
129+
let variants = postcss.list.comma(atRule.params).filter(variant => variant !== '')
130+
131+
if (variants.includes('responsive')) {
132+
const responsiveParent = postcss.atRule({ name: 'responsive' })
133+
atRule.before(responsiveParent)
134+
responsiveParent.append(atRule)
87135
}
88-
variantGenerators[variant](atRule, config)
89-
})
90136

91-
atRule.remove()
92-
})
137+
const remainingVariants = prependStackableVariants(atRule, variants)
138+
139+
_.forEach(_.without(ensureIncludesDefault(remainingVariants), 'responsive'), variant => {
140+
if (!variantGenerators[variant]) {
141+
throw new Error(
142+
`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?`
143+
)
144+
}
145+
variantGenerators[variant](atRule, config)
146+
})
147+
148+
atRule.remove()
149+
})
150+
} while (variantsFound)
93151
}
94152
}

0 commit comments

Comments
 (0)