Skip to content

Commit 8333d46

Browse files
committed
Add variant prefix to last class in a selector, not the first
1 parent 62994fc commit 8333d46

File tree

5 files changed

+272
-10
lines changed

5 files changed

+272
-10
lines changed

__tests__/responsiveAtRule.test.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import postcss from 'postcss'
2+
import plugin from '../src/lib/substituteResponsiveAtRules'
3+
import config from '../defaultConfig.stub.js'
4+
5+
function run(input, opts = () => config) {
6+
return postcss([plugin(opts)]).process(input, { from: undefined })
7+
}
8+
9+
test('it can generate responsive variants', () => {
10+
const input = `
11+
@responsive {
12+
.banana { color: yellow; }
13+
.chocolate { color: brown; }
14+
}
15+
`
16+
17+
const output = `
18+
.banana { color: yellow; }
19+
.chocolate { color: brown; }
20+
@media (min-width: 500px) {
21+
.sm\\:banana { color: yellow; }
22+
.sm\\:chocolate { color: brown; }
23+
}
24+
@media (min-width: 750px) {
25+
.md\\:banana { color: yellow; }
26+
.md\\:chocolate { color: brown; }
27+
}
28+
@media (min-width: 1000px) {
29+
.lg\\:banana { color: yellow; }
30+
.lg\\:chocolate { color: brown; }
31+
}
32+
`
33+
34+
return run(input, () => ({
35+
screens: {
36+
sm: '500px',
37+
md: '750px',
38+
lg: '1000px',
39+
},
40+
options: {
41+
separator: ':',
42+
},
43+
})).then(result => {
44+
expect(result.css).toMatchCss(output)
45+
expect(result.warnings().length).toBe(0)
46+
})
47+
})
48+
49+
test('it can generate responsive variants with a custom separator', () => {
50+
const input = `
51+
@responsive {
52+
.banana { color: yellow; }
53+
.chocolate { color: brown; }
54+
}
55+
`
56+
57+
const output = `
58+
.banana { color: yellow; }
59+
.chocolate { color: brown; }
60+
@media (min-width: 500px) {
61+
.sm__banana { color: yellow; }
62+
.sm__chocolate { color: brown; }
63+
}
64+
@media (min-width: 750px) {
65+
.md__banana { color: yellow; }
66+
.md__chocolate { color: brown; }
67+
}
68+
@media (min-width: 1000px) {
69+
.lg__banana { color: yellow; }
70+
.lg__chocolate { color: brown; }
71+
}
72+
`
73+
74+
return run(input, () => ({
75+
screens: {
76+
sm: '500px',
77+
md: '750px',
78+
lg: '1000px',
79+
},
80+
options: {
81+
separator: '__',
82+
},
83+
})).then(result => {
84+
expect(result.css).toMatchCss(output)
85+
expect(result.warnings().length).toBe(0)
86+
})
87+
})
88+
89+
test('responsive variants are grouped', () => {
90+
const input = `
91+
@responsive {
92+
.banana { color: yellow; }
93+
}
94+
95+
.apple { color: red; }
96+
97+
@responsive {
98+
.chocolate { color: brown; }
99+
}
100+
`
101+
102+
const output = `
103+
.banana { color: yellow; }
104+
.apple { color: red; }
105+
.chocolate { color: brown; }
106+
@media (min-width: 500px) {
107+
.sm\\:banana { color: yellow; }
108+
.sm\\:chocolate { color: brown; }
109+
}
110+
@media (min-width: 750px) {
111+
.md\\:banana { color: yellow; }
112+
.md\\:chocolate { color: brown; }
113+
}
114+
@media (min-width: 1000px) {
115+
.lg\\:banana { color: yellow; }
116+
.lg\\:chocolate { color: brown; }
117+
}
118+
`
119+
120+
return run(input, () => ({
121+
screens: {
122+
sm: '500px',
123+
md: '750px',
124+
lg: '1000px',
125+
},
126+
options: {
127+
separator: ':',
128+
},
129+
})).then(result => {
130+
expect(result.css).toMatchCss(output)
131+
expect(result.warnings().length).toBe(0)
132+
})
133+
})
134+
135+
test('screen prefix is only applied to the last class in a selector', () => {
136+
const input = `
137+
@responsive {
138+
.banana li * .sandwich #foo > div { color: yellow; }
139+
}
140+
`
141+
142+
const output = `
143+
.banana li * .sandwich #foo > div { color: yellow; }
144+
@media (min-width: 500px) {
145+
.banana li * .sm\\:sandwich #foo > div { color: yellow; }
146+
}
147+
@media (min-width: 750px) {
148+
.banana li * .md\\:sandwich #foo > div { color: yellow; }
149+
}
150+
@media (min-width: 1000px) {
151+
.banana li * .lg\\:sandwich #foo > div { color: yellow; }
152+
}
153+
`
154+
155+
return run(input, () => ({
156+
screens: {
157+
sm: '500px',
158+
md: '750px',
159+
lg: '1000px',
160+
},
161+
options: {
162+
separator: ':',
163+
},
164+
})).then(result => {
165+
expect(result.css).toMatchCss(output)
166+
expect(result.warnings().length).toBe(0)
167+
})
168+
})
169+
170+
test('responsive variants are generated for all selectors in a rule', () => {
171+
const input = `
172+
@responsive {
173+
.foo, .bar { color: yellow; }
174+
}
175+
`
176+
177+
const output = `
178+
.foo, .bar { color: yellow; }
179+
@media (min-width: 500px) {
180+
.sm\\:foo, .sm\\:bar { color: yellow; }
181+
}
182+
@media (min-width: 750px) {
183+
.md\\:foo, .md\\:bar { color: yellow; }
184+
}
185+
@media (min-width: 1000px) {
186+
.lg\\:foo, .lg\\:bar { color: yellow; }
187+
}
188+
`
189+
190+
return run(input, () => ({
191+
screens: {
192+
sm: '500px',
193+
md: '750px',
194+
lg: '1000px',
195+
},
196+
options: {
197+
separator: ':',
198+
},
199+
})).then(result => {
200+
expect(result.css).toMatchCss(output)
201+
expect(result.warnings().length).toBe(0)
202+
})
203+
})
204+
205+
test('selectors with no classes cannot be made responsive', () => {
206+
const input = `
207+
@responsive {
208+
div { color: yellow; }
209+
}
210+
`
211+
expect.assertions(1)
212+
return run(input, () => ({
213+
screens: {
214+
sm: '500px',
215+
md: '750px',
216+
lg: '1000px',
217+
},
218+
options: {
219+
separator: ':',
220+
},
221+
})).catch(e => {
222+
expect(e).toMatchObject({ name: 'CssSyntaxError' })
223+
})
224+
})
225+
226+
test('all selectors in a rule must contain classes', () => {
227+
const input = `
228+
@responsive {
229+
.foo, div { color: yellow; }
230+
}
231+
`
232+
expect.assertions(1)
233+
return run(input, () => ({
234+
screens: {
235+
sm: '500px',
236+
md: '750px',
237+
lg: '1000px',
238+
},
239+
options: {
240+
separator: ':',
241+
},
242+
})).catch(e => {
243+
expect(e).toMatchObject({ name: 'CssSyntaxError' })
244+
})
245+
})

src/lib/substituteResponsiveAtRules.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import _ from 'lodash'
22
import postcss from 'postcss'
33
import cloneNodes from '../util/cloneNodes'
44
import buildMediaQuery from '../util/buildMediaQuery'
5-
import buildClassVariant from '../util/buildClassVariant'
5+
import buildSelectorVariant from '../util/buildSelectorVariant'
66

77
export default function(config) {
88
return function(css) {
@@ -28,7 +28,9 @@ export default function(config) {
2828
responsiveRules.map(rule => {
2929
const cloned = rule.clone()
3030
cloned.selectors = _.map(rule.selectors, selector =>
31-
buildClassVariant(selector, screen, separator)
31+
buildSelectorVariant(selector, screen, separator, message => {
32+
throw rule.error(message)
33+
})
3234
)
3335
return cloned
3436
})

src/lib/substituteVariantsAtRules.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import _ from 'lodash'
22
import postcss from 'postcss'
3-
import buildClassVariant from '../util/buildClassVariant'
3+
import buildSelectorVariant from '../util/buildSelectorVariant'
44

55
function buildPseudoClassVariant(selector, pseudoClass, separator) {
6-
return `${buildClassVariant(selector, pseudoClass, separator)}:${pseudoClass}`
6+
return `${buildSelectorVariant(selector, pseudoClass, separator)}:${pseudoClass}`
77
}
88

99
function generatePseudoClassVariant(pseudoClass) {
@@ -23,7 +23,11 @@ const variantGenerators = {
2323
const cloned = container.clone()
2424

2525
cloned.walkRules(rule => {
26-
rule.selector = `.group:hover ${buildClassVariant(rule.selector, 'group-hover', separator)}`
26+
rule.selector = `.group:hover ${buildSelectorVariant(
27+
rule.selector,
28+
'group-hover',
29+
separator
30+
)}`
2731
})
2832

2933
container.before(cloned.nodes)

src/util/buildClassVariant.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/util/buildSelectorVariant.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import escapeClassName from './escapeClassName'
2+
import parser from 'postcss-selector-parser'
3+
import tap from 'lodash/tap'
4+
5+
export default function buildSelectorVariant(selector, variantName, separator, onError = () => {}) {
6+
return parser(selectors => {
7+
tap(selectors.first.filter(({ type }) => type === 'class').pop(), classSelector => {
8+
if (classSelector === undefined) {
9+
onError('Variant cannot be generated because selector contains no classes.')
10+
return
11+
}
12+
13+
classSelector.value = `${variantName}${escapeClassName(separator)}${classSelector.value}`
14+
})
15+
}).processSync(selector)
16+
}

0 commit comments

Comments
 (0)