Skip to content

Commit 32eac19

Browse files
adamwathanJNavith
andauthored
Allow variant plugins to tell Tailwind they should stack (#2382)
* Fix unwanted stacking behavior on any non-darkModeVariant "dark" variant (#2380) * Add failing tests for non-darkModeVariant "dark" variant stacking behavior * Fix unwanted non-darkModeVariant "dark" variant stacking (by making the failing test pass) * Add unstable_stack option for variants to tell Tailwind they should stack * Update eslint to allow unstable_ variables * Update changelog Co-authored-by: Navith <[email protected]>
1 parent 2d8cf7f commit 32eac19

7 files changed

+143
-85
lines changed

.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"extends": ["eslint-config-postcss", "prettier"],
1010
"plugins": ["prettier"],
1111
"rules": {
12+
"camelcase": ["error", { "allow": ["^unstable_"] }],
1213
"no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }],
1314
"no-warning-comments": 0,
1415
"prettier/prettier": [

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Prevent new `dark` experiment from causing third-party `dark` variants to inherit stacking behavior ([#2382](https://github.com/tailwindlabs/tailwindcss/pull/2382))
1113

1214
## [1.8.9] - 2020-09-13
1315

__tests__/darkMode.test.js

+40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import postcss from 'postcss'
22
import tailwind from '../src/index'
3+
import createPlugin from '../src/util/createPlugin'
34

45
function run(input, config = {}) {
56
return postcss([tailwind({ experimental: { darkModeVariant: true }, ...config })]).process(
@@ -21,6 +22,45 @@ test('dark mode variants cannot be generated without enabling the dark mode expe
2122
return expect(run(input, { experimental: {} })).rejects.toThrow()
2223
})
2324

25+
test('user-defined dark mode variants do not stack when the dark mode experiment is disabled', () => {
26+
const input = `
27+
@variants dark, hover {
28+
.text-red {
29+
color: red;
30+
}
31+
}
32+
`
33+
34+
const expected = `
35+
.text-red {
36+
color: red;
37+
}
38+
.custom-dark .custom-dark\\:text-red {
39+
color: red;
40+
}
41+
.hover\\:text-red:hover {
42+
color: red;
43+
}
44+
`
45+
46+
const userPlugin = createPlugin(function({ addVariant }) {
47+
addVariant('dark', function({ modifySelectors }) {
48+
modifySelectors(function({ className }) {
49+
return `.custom-dark .custom-dark\\:${className}`
50+
})
51+
})
52+
})
53+
54+
expect.assertions(2)
55+
56+
return postcss([tailwind({ experimental: { darkModeVariant: false }, plugins: [userPlugin] })])
57+
.process(input, { from: undefined })
58+
.then(result => {
59+
expect(result.css).toMatchCss(expected)
60+
expect(result.warnings().length).toBe(0)
61+
})
62+
})
63+
2464
test('generating dark mode variants uses the media strategy by default', () => {
2565
const input = `
2666
@variants dark {

src/flagged/darkModeVariantPlugin.js

+31-27
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,42 @@
11
import buildSelectorVariant from '../util/buildSelectorVariant'
22

33
export default function({ addVariant, config, postcss, prefix }) {
4-
addVariant('dark', ({ container, separator, modifySelectors }) => {
5-
if (config('dark') === 'media') {
6-
const modified = modifySelectors(({ selector }) => {
7-
return buildSelectorVariant(selector, 'dark', separator, message => {
8-
throw container.error(message)
4+
addVariant(
5+
'dark',
6+
({ container, separator, modifySelectors }) => {
7+
if (config('dark') === 'media') {
8+
const modified = modifySelectors(({ selector }) => {
9+
return buildSelectorVariant(selector, 'dark', separator, message => {
10+
throw container.error(message)
11+
})
912
})
10-
})
11-
const mediaQuery = postcss.atRule({
12-
name: 'media',
13-
params: '(prefers-color-scheme: dark)',
14-
})
15-
mediaQuery.append(modified)
16-
container.append(mediaQuery)
17-
return container
18-
}
13+
const mediaQuery = postcss.atRule({
14+
name: 'media',
15+
params: '(prefers-color-scheme: dark)',
16+
})
17+
mediaQuery.append(modified)
18+
container.append(mediaQuery)
19+
return container
20+
}
1921

20-
if (config('dark') === 'class') {
21-
const modified = modifySelectors(({ selector }) => {
22-
return buildSelectorVariant(selector, 'dark', separator, message => {
23-
throw container.error(message)
22+
if (config('dark') === 'class') {
23+
const modified = modifySelectors(({ selector }) => {
24+
return buildSelectorVariant(selector, 'dark', separator, message => {
25+
throw container.error(message)
26+
})
2427
})
25-
})
2628

27-
modified.walkRules(rule => {
28-
rule.selectors = rule.selectors.map(selector => {
29-
return `${prefix('.dark')} ${selector}`
29+
modified.walkRules(rule => {
30+
rule.selectors = rule.selectors.map(selector => {
31+
return `${prefix('.dark')} ${selector}`
32+
})
3033
})
31-
})
3234

33-
return modified
34-
}
35+
return modified
36+
}
3537

36-
throw new Error("The `dark` config option must be either 'media' or 'class'.")
37-
})
38+
throw new Error("The `dark` config option must be either 'media' or 'class'.")
39+
},
40+
{ unstable_stack: true }
41+
)
3842
}

src/lib/substituteVariantsAtRules.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,38 @@ function ensureIncludesDefault(variants) {
2424

2525
const defaultVariantGenerators = config => ({
2626
default: generateVariantFunction(() => {}),
27-
'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => {
28-
const modified = modifySelectors(({ selector }) => {
29-
return buildSelectorVariant(selector, 'motion-safe', separator, message => {
30-
throw container.error(message)
27+
'motion-safe': generateVariantFunction(
28+
({ container, separator, modifySelectors }) => {
29+
const modified = modifySelectors(({ selector }) => {
30+
return buildSelectorVariant(selector, 'motion-safe', separator, message => {
31+
throw container.error(message)
32+
})
3133
})
32-
})
33-
const mediaQuery = postcss.atRule({
34-
name: 'media',
35-
params: '(prefers-reduced-motion: no-preference)',
36-
})
37-
mediaQuery.append(modified)
38-
container.append(mediaQuery)
39-
}),
40-
'motion-reduce': generateVariantFunction(({ container, separator, modifySelectors }) => {
41-
const modified = modifySelectors(({ selector }) => {
42-
return buildSelectorVariant(selector, 'motion-reduce', separator, message => {
43-
throw container.error(message)
34+
const mediaQuery = postcss.atRule({
35+
name: 'media',
36+
params: '(prefers-reduced-motion: no-preference)',
4437
})
45-
})
46-
const mediaQuery = postcss.atRule({
47-
name: 'media',
48-
params: '(prefers-reduced-motion: reduce)',
49-
})
50-
mediaQuery.append(modified)
51-
container.append(mediaQuery)
52-
}),
38+
mediaQuery.append(modified)
39+
container.append(mediaQuery)
40+
},
41+
{ unstable_stack: true }
42+
),
43+
'motion-reduce': generateVariantFunction(
44+
({ container, separator, modifySelectors }) => {
45+
const modified = modifySelectors(({ selector }) => {
46+
return buildSelectorVariant(selector, 'motion-reduce', separator, message => {
47+
throw container.error(message)
48+
})
49+
})
50+
const mediaQuery = postcss.atRule({
51+
name: 'media',
52+
params: '(prefers-reduced-motion: reduce)',
53+
})
54+
mediaQuery.append(modified)
55+
container.append(mediaQuery)
56+
},
57+
{ unstable_stack: true }
58+
),
5359
'group-hover': generateVariantFunction(({ modifySelectors, separator }) => {
5460
const parser = selectorParser(selectors => {
5561
selectors.walkClasses(sel => {
@@ -88,9 +94,7 @@ const defaultVariantGenerators = config => ({
8894
even: generatePseudoClassVariant('nth-child(even)', 'even'),
8995
})
9096

91-
function prependStackableVariants(atRule, variants) {
92-
const stackableVariants = ['dark', 'motion-safe', 'motion-reduce']
93-
97+
function prependStackableVariants(atRule, variants, stackableVariants) {
9498
if (!_.some(variants, v => stackableVariants.includes(v))) {
9599
return variants
96100
}
@@ -117,6 +121,10 @@ export default function(config, { variantGenerators: pluginVariantGenerators })
117121
...pluginVariantGenerators,
118122
}
119123

124+
const stackableVariants = Object.entries(variantGenerators)
125+
.filter(([_variant, { options }]) => options.unstable_stack)
126+
.map(([variant]) => variant)
127+
120128
let variantsFound = false
121129

122130
do {
@@ -132,15 +140,15 @@ export default function(config, { variantGenerators: pluginVariantGenerators })
132140
responsiveParent.append(atRule)
133141
}
134142

135-
const remainingVariants = prependStackableVariants(atRule, variants)
143+
const remainingVariants = prependStackableVariants(atRule, variants, stackableVariants)
136144

137145
_.forEach(_.without(ensureIncludesDefault(remainingVariants), 'responsive'), variant => {
138146
if (!variantGenerators[variant]) {
139147
throw new Error(
140148
`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?`
141149
)
142150
}
143-
variantGenerators[variant](atRule, config)
151+
variantGenerators[variant].handler(atRule, config)
144152
})
145153

146154
atRule.remove()

src/util/generateVariantFunction.js

+29-26
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,38 @@ const getClassNameFromSelector = useMemo(
1212
selector => selector
1313
)
1414

15-
export default function generateVariantFunction(generator) {
16-
return (container, config) => {
17-
const cloned = postcss.root({ nodes: container.clone().nodes })
15+
export default function generateVariantFunction(generator, options = {}) {
16+
return {
17+
options,
18+
handler: (container, config) => {
19+
const cloned = postcss.root({ nodes: container.clone().nodes })
1820

19-
container.before(
20-
_.defaultTo(
21-
generator({
22-
container: cloned,
23-
separator: config.separator,
24-
modifySelectors: modifierFunction => {
25-
cloned.each(rule => {
26-
if (rule.type !== 'rule') {
27-
return
28-
}
21+
container.before(
22+
_.defaultTo(
23+
generator({
24+
container: cloned,
25+
separator: config.separator,
26+
modifySelectors: modifierFunction => {
27+
cloned.each(rule => {
28+
if (rule.type !== 'rule') {
29+
return
30+
}
2931

30-
rule.selectors = rule.selectors.map(selector => {
31-
return modifierFunction({
32-
get className() {
33-
return getClassNameFromSelector(selector)
34-
},
35-
selector,
32+
rule.selectors = rule.selectors.map(selector => {
33+
return modifierFunction({
34+
get className() {
35+
return getClassNameFromSelector(selector)
36+
},
37+
selector,
38+
})
3639
})
3740
})
38-
})
39-
return cloned
40-
},
41-
}),
42-
cloned
43-
).nodes
44-
)
41+
return cloned
42+
},
43+
}),
44+
cloned
45+
).nodes
46+
)
47+
},
4548
}
4649
}

src/util/processPlugins.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ export default function(plugins, config) {
126126
addBase: baseStyles => {
127127
pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))
128128
},
129-
addVariant: (name, generator) => {
130-
pluginVariantGenerators[name] = generateVariantFunction(generator)
129+
addVariant: (name, generator, options = {}) => {
130+
pluginVariantGenerators[name] = generateVariantFunction(generator, options)
131131
},
132132
})
133133
})

0 commit comments

Comments
 (0)