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:
Adam Wathan 2018-06-21 02:27:39 +09:30 committed by GitHub
commit 490e378f64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 272 additions and 10 deletions

View 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' })
})
})

View File

@ -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
})

View File

@ -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)

View File

@ -1,5 +0,0 @@
import escapeClassName from './escapeClassName'
export default function buildClassVariant(className, variantName, separator) {
return `.${variantName}${escapeClassName(separator)}${className.slice(1)}`
}

View 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)
}