mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Merge pull request #497 from tailwindcss/add-screen-prefix-to-last-class
Add variant prefix to last class in a selector, not the first
This commit is contained in:
commit
490e378f64
245
__tests__/responsiveAtRule.test.js
Normal file
245
__tests__/responsiveAtRule.test.js
Normal file
@ -0,0 +1,245 @@
|
||||
import postcss from 'postcss'
|
||||
import plugin from '../src/lib/substituteResponsiveAtRules'
|
||||
import config from '../defaultConfig.stub.js'
|
||||
|
||||
function run(input, opts = () => config) {
|
||||
return postcss([plugin(opts)]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it can generate responsive variants', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana { color: yellow; }
|
||||
.sm\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:banana { color: yellow; }
|
||||
.md\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana { color: yellow; }
|
||||
.lg\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate responsive variants with a custom separator', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm__banana { color: yellow; }
|
||||
.sm__chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md__banana { color: yellow; }
|
||||
.md__chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg__banana { color: yellow; }
|
||||
.lg__chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: '__',
|
||||
},
|
||||
})).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('responsive variants are grouped', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
}
|
||||
|
||||
.apple { color: red; }
|
||||
|
||||
@responsive {
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.apple { color: red; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana { color: yellow; }
|
||||
.sm\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:banana { color: yellow; }
|
||||
.md\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana { color: yellow; }
|
||||
.lg\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('screen prefix is only applied to the last class in a selector', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana li * .sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana li * .sandwich #foo > div { color: yellow; }
|
||||
@media (min-width: 500px) {
|
||||
.banana li * .sm\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.banana li * .md\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.banana li * .lg\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('responsive variants are generated for all selectors in a rule', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.foo, .bar { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.foo, .bar { color: yellow; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:foo, .sm\\:bar { color: yellow; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:foo, .md\\:bar { color: yellow; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:foo, .lg\\:bar { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('selectors with no classes cannot be made responsive', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
div { color: yellow; }
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('all selectors in a rule must contain classes', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.foo, div { color: yellow; }
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input, () => ({
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
options: {
|
||||
separator: ':',
|
||||
},
|
||||
})).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
@ -2,7 +2,7 @@ import _ from 'lodash'
|
||||
import postcss from 'postcss'
|
||||
import cloneNodes from '../util/cloneNodes'
|
||||
import buildMediaQuery from '../util/buildMediaQuery'
|
||||
import buildClassVariant from '../util/buildClassVariant'
|
||||
import buildSelectorVariant from '../util/buildSelectorVariant'
|
||||
|
||||
export default function(config) {
|
||||
return function(css) {
|
||||
@ -28,7 +28,9 @@ export default function(config) {
|
||||
responsiveRules.map(rule => {
|
||||
const cloned = rule.clone()
|
||||
cloned.selectors = _.map(rule.selectors, selector =>
|
||||
buildClassVariant(selector, screen, separator)
|
||||
buildSelectorVariant(selector, screen, separator, message => {
|
||||
throw rule.error(message)
|
||||
})
|
||||
)
|
||||
return cloned
|
||||
})
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import _ from 'lodash'
|
||||
import postcss from 'postcss'
|
||||
import buildClassVariant from '../util/buildClassVariant'
|
||||
import buildSelectorVariant from '../util/buildSelectorVariant'
|
||||
|
||||
function buildPseudoClassVariant(selector, pseudoClass, separator) {
|
||||
return `${buildClassVariant(selector, pseudoClass, separator)}:${pseudoClass}`
|
||||
return `${buildSelectorVariant(selector, pseudoClass, separator)}:${pseudoClass}`
|
||||
}
|
||||
|
||||
function generatePseudoClassVariant(pseudoClass) {
|
||||
@ -23,7 +23,11 @@ const variantGenerators = {
|
||||
const cloned = container.clone()
|
||||
|
||||
cloned.walkRules(rule => {
|
||||
rule.selector = `.group:hover ${buildClassVariant(rule.selector, 'group-hover', separator)}`
|
||||
rule.selector = `.group:hover ${buildSelectorVariant(
|
||||
rule.selector,
|
||||
'group-hover',
|
||||
separator
|
||||
)}`
|
||||
})
|
||||
|
||||
container.before(cloned.nodes)
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import escapeClassName from './escapeClassName'
|
||||
|
||||
export default function buildClassVariant(className, variantName, separator) {
|
||||
return `.${variantName}${escapeClassName(separator)}${className.slice(1)}`
|
||||
}
|
||||
16
src/util/buildSelectorVariant.js
Normal file
16
src/util/buildSelectorVariant.js
Normal file
@ -0,0 +1,16 @@
|
||||
import escapeClassName from './escapeClassName'
|
||||
import parser from 'postcss-selector-parser'
|
||||
import tap from 'lodash/tap'
|
||||
|
||||
export default function buildSelectorVariant(selector, variantName, separator, onError = () => {}) {
|
||||
return parser(selectors => {
|
||||
tap(selectors.first.filter(({ type }) => type === 'class').pop(), classSelector => {
|
||||
if (classSelector === undefined) {
|
||||
onError('Variant cannot be generated because selector contains no classes.')
|
||||
return
|
||||
}
|
||||
|
||||
classSelector.value = `${variantName}${escapeClassName(separator)}${classSelector.value}`
|
||||
})
|
||||
}).processSync(selector)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user