Skip to content

Commit 903eacb

Browse files
committed
Fix use of :where(.btn) when matching !btn (#10601)
* Cleanup code This makes it more explicit that we’re parsing a string selector, modifying it, and turning it back into a string * Fix important modifier when :where is involved * Only parse selector list once when handling the important modifier * Fix import * Fix lint errors
1 parent 7f81849 commit 903eacb

File tree

4 files changed

+73
-40
lines changed

4 files changed

+73
-40
lines changed

src/lib/generateRules.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import selectorParser from 'postcss-selector-parser'
33
import parseObjectStyles from '../util/parseObjectStyles'
44
import isPlainObject from '../util/isPlainObject'
55
import prefixSelector from '../util/prefixSelector'
6-
import { updateAllClasses, filterSelectorsForClass, getMatchingTypes } from '../util/pluginUtils'
6+
import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
77
import log from '../util/log'
88
import * as sharedState from './sharedState'
9-
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
9+
import {
10+
formatVariantSelector,
11+
finalizeSelector,
12+
eliminateIrrelevantSelectors,
13+
} from '../util/formatVariantSelector'
1014
import { asClass } from '../util/nameClass'
1115
import { normalize } from '../util/dataTypes'
1216
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
@@ -111,22 +115,28 @@ function applyImportant(matches, classCandidate) {
111115
if (matches.length === 0) {
112116
return matches
113117
}
118+
114119
let result = []
115120

116121
for (let [meta, rule] of matches) {
117122
let container = postcss.root({ nodes: [rule.clone()] })
123+
118124
container.walkRules((r) => {
119-
r.selector = updateAllClasses(
120-
filterSelectorsForClass(r.selector, classCandidate),
121-
(className) => {
122-
if (className === classCandidate) {
123-
return `!${className}`
124-
}
125-
return className
126-
}
125+
let ast = selectorParser().astSync(r.selector)
126+
127+
// Remove extraneous selectors that do not include the base candidate
128+
ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate))
129+
130+
// Update all instances of the base candidate to include the important marker
131+
updateAllClasses(ast, (className) =>
132+
className === classCandidate ? `!${className}` : className
127133
)
134+
135+
r.selector = ast.toString()
136+
128137
r.walkDecls((d) => (d.important = true))
129138
})
139+
130140
result.push([{ ...meta, important: true }, container.nodes[0]])
131141
}
132142

src/util/formatVariantSelector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function resortSelector(sel) {
120120
* @param {Selector} ast
121121
* @param {string} base
122122
*/
123-
function eliminateIrrelevantSelectors(sel, base) {
123+
export function eliminateIrrelevantSelectors(sel, base) {
124124
let hasClassesMatchingCandidate = false
125125

126126
sel.walk((child) => {

src/util/pluginUtils.js

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import selectorParser from 'postcss-selector-parser'
21
import escapeCommas from './escapeCommas'
32
import { withAlphaValue } from './withAlphaVariable'
43
import {
@@ -21,37 +20,19 @@ import negateValue from './negateValue'
2120
import { backgroundSize } from './validateFormalSyntax'
2221
import { flagEnabled } from '../featureFlags.js'
2322

23+
/**
24+
* @param {import('postcss-selector-parser').Container} selectors
25+
* @param {(className: string) => string} updateClass
26+
* @returns {string}
27+
*/
2428
export function updateAllClasses(selectors, updateClass) {
25-
let parser = selectorParser((selectors) => {
26-
selectors.walkClasses((sel) => {
27-
let updatedClass = updateClass(sel.value)
28-
sel.value = updatedClass
29-
if (sel.raws && sel.raws.value) {
30-
sel.raws.value = escapeCommas(sel.raws.value)
31-
}
32-
})
33-
})
34-
35-
let result = parser.processSync(selectors)
29+
selectors.walkClasses((sel) => {
30+
sel.value = updateClass(sel.value)
3631

37-
return result
38-
}
39-
40-
export function filterSelectorsForClass(selectors, classCandidate) {
41-
let parser = selectorParser((selectors) => {
42-
selectors.each((sel) => {
43-
const containsClass = sel.nodes.some(
44-
(node) => node.type === 'class' && node.value === classCandidate
45-
)
46-
if (!containsClass) {
47-
sel.remove()
48-
}
49-
})
32+
if (sel.raws && sel.raws.value) {
33+
sel.raws.value = escapeCommas(sel.raws.value)
34+
}
5035
})
51-
52-
let result = parser.processSync(selectors)
53-
54-
return result
5536
}
5637

5738
function resolveArbitraryValue(modifier, validate) {

tests/important-modifier.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,46 @@ crosscheck(() => {
108108
`)
109109
})
110110
})
111+
112+
test('the important modifier works on utilities using :where()', () => {
113+
let config = {
114+
content: [
115+
{
116+
raw: html` <div class="btn hover:btn !btn hover:focus:disabled:!btn"></div> `,
117+
},
118+
],
119+
corePlugins: { preflight: false },
120+
plugins: [
121+
function ({ addComponents }) {
122+
addComponents({
123+
':where(.btn)': {
124+
backgroundColor: '#00f',
125+
},
126+
})
127+
},
128+
],
129+
}
130+
131+
let input = css`
132+
@tailwind components;
133+
@tailwind utilities;
134+
`
135+
136+
return run(input, config).then((result) => {
137+
expect(result.css).toMatchFormattedCss(css`
138+
:where(.\!btn) {
139+
background-color: #00f !important;
140+
}
141+
:where(.btn) {
142+
background-color: #00f;
143+
}
144+
:where(.hover\:btn:hover) {
145+
background-color: #00f;
146+
}
147+
:where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) {
148+
background-color: #00f !important;
149+
}
150+
`)
151+
})
152+
})
111153
})

0 commit comments

Comments
 (0)