Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow variant plugins to tell Tailwind they should stack #2382

Merged
merged 4 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"extends": ["eslint-config-postcss", "prettier"],
"plugins": ["prettier"],
"rules": {
"camelcase": ["error", { "allow": ["^unstable_"] }],
"no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }],
"no-warning-comments": 0,
"prettier/prettier": [
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Prevent new `dark` experiment from causing third-party `dark` variants to inherit stacking behavior ([#2382](https://github.com/tailwindlabs/tailwindcss/pull/2382))

## [1.8.9] - 2020-09-13

Expand Down
40 changes: 40 additions & 0 deletions __tests__/darkMode.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import postcss from 'postcss'
import tailwind from '../src/index'
import createPlugin from '../src/util/createPlugin'

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

test('user-defined dark mode variants do not stack when the dark mode experiment is disabled', () => {
const input = `
@variants dark, hover {
.text-red {
color: red;
}
}
`

const expected = `
.text-red {
color: red;
}
.custom-dark .custom-dark\\:text-red {
color: red;
}
.hover\\:text-red:hover {
color: red;
}
`

const userPlugin = createPlugin(function({ addVariant }) {
addVariant('dark', function({ modifySelectors }) {
modifySelectors(function({ className }) {
return `.custom-dark .custom-dark\\:${className}`
})
})
})

expect.assertions(2)

return postcss([tailwind({ experimental: { darkModeVariant: false }, plugins: [userPlugin] })])
.process(input, { from: undefined })
.then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

test('generating dark mode variants uses the media strategy by default', () => {
const input = `
@variants dark {
Expand Down
58 changes: 31 additions & 27 deletions src/flagged/darkModeVariantPlugin.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import buildSelectorVariant from '../util/buildSelectorVariant'

export default function({ addVariant, config, postcss, prefix }) {
addVariant('dark', ({ container, separator, modifySelectors }) => {
if (config('dark') === 'media') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, message => {
throw container.error(message)
addVariant(
'dark',
({ container, separator, modifySelectors }) => {
if (config('dark') === 'media') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, message => {
throw container.error(message)
})
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-color-scheme: dark)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
return container
}
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-color-scheme: dark)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
return container
}

if (config('dark') === 'class') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, message => {
throw container.error(message)
if (config('dark') === 'class') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, message => {
throw container.error(message)
})
})
})

modified.walkRules(rule => {
rule.selectors = rule.selectors.map(selector => {
return `${prefix('.dark')} ${selector}`
modified.walkRules(rule => {
rule.selectors = rule.selectors.map(selector => {
return `${prefix('.dark')} ${selector}`
})
})
})

return modified
}
return modified
}

throw new Error("The `dark` config option must be either 'media' or 'class'.")
})
throw new Error("The `dark` config option must be either 'media' or 'class'.")
},
{ unstable_stack: true }
)
}
66 changes: 37 additions & 29 deletions src/lib/substituteVariantsAtRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,38 @@ function ensureIncludesDefault(variants) {

const defaultVariantGenerators = config => ({
default: generateVariantFunction(() => {}),
'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-safe', separator, message => {
throw container.error(message)
'motion-safe': generateVariantFunction(
({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-safe', separator, message => {
throw container.error(message)
})
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
}),
'motion-reduce': generateVariantFunction(({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-reduce', separator, message => {
throw container.error(message)
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: reduce)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
}),
mediaQuery.append(modified)
container.append(mediaQuery)
},
{ unstable_stack: true }
),
'motion-reduce': generateVariantFunction(
({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-reduce', separator, message => {
throw container.error(message)
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: reduce)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
},
{ unstable_stack: true }
),
'group-hover': generateVariantFunction(({ modifySelectors, separator }) => {
const parser = selectorParser(selectors => {
selectors.walkClasses(sel => {
Expand Down Expand Up @@ -88,9 +94,7 @@ const defaultVariantGenerators = config => ({
even: generatePseudoClassVariant('nth-child(even)', 'even'),
})

function prependStackableVariants(atRule, variants) {
const stackableVariants = ['dark', 'motion-safe', 'motion-reduce']

function prependStackableVariants(atRule, variants, stackableVariants) {
if (!_.some(variants, v => stackableVariants.includes(v))) {
return variants
}
Expand All @@ -117,6 +121,10 @@ export default function(config, { variantGenerators: pluginVariantGenerators })
...pluginVariantGenerators,
}

const stackableVariants = Object.entries(variantGenerators)
.filter(([_variant, { options }]) => options.unstable_stack)
.map(([variant]) => variant)

let variantsFound = false

do {
Expand All @@ -132,15 +140,15 @@ export default function(config, { variantGenerators: pluginVariantGenerators })
responsiveParent.append(atRule)
}

const remainingVariants = prependStackableVariants(atRule, variants)
const remainingVariants = prependStackableVariants(atRule, variants, stackableVariants)

_.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)
variantGenerators[variant].handler(atRule, config)
})

atRule.remove()
Expand Down
55 changes: 29 additions & 26 deletions src/util/generateVariantFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,38 @@ const getClassNameFromSelector = useMemo(
selector => selector
)

export default function generateVariantFunction(generator) {
return (container, config) => {
const cloned = postcss.root({ nodes: container.clone().nodes })
export default function generateVariantFunction(generator, options = {}) {
return {
options,
handler: (container, config) => {
const cloned = postcss.root({ nodes: container.clone().nodes })

container.before(
_.defaultTo(
generator({
container: cloned,
separator: config.separator,
modifySelectors: modifierFunction => {
cloned.each(rule => {
if (rule.type !== 'rule') {
return
}
container.before(
_.defaultTo(
generator({
container: cloned,
separator: config.separator,
modifySelectors: modifierFunction => {
cloned.each(rule => {
if (rule.type !== 'rule') {
return
}

rule.selectors = rule.selectors.map(selector => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
rule.selectors = rule.selectors.map(selector => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
})
})
})
})
return cloned
},
}),
cloned
).nodes
)
return cloned
},
}),
cloned
).nodes
)
},
}
}
4 changes: 2 additions & 2 deletions src/util/processPlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ export default function(plugins, config) {
addBase: baseStyles => {
pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))
},
addVariant: (name, generator) => {
pluginVariantGenerators[name] = generateVariantFunction(generator)
addVariant: (name, generator, options = {}) => {
pluginVariantGenerators[name] = generateVariantFunction(generator, options)
},
})
})
Expand Down