Skip to content

Commit

Permalink
Fix use of :where(.btn) when matching !btn (#10601)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace committed Feb 16, 2023
1 parent 7f81849 commit 903eacb
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 40 deletions.
30 changes: 20 additions & 10 deletions src/lib/generateRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import selectorParser from 'postcss-selector-parser'
import parseObjectStyles from '../util/parseObjectStyles'
import isPlainObject from '../util/isPlainObject'
import prefixSelector from '../util/prefixSelector'
import { updateAllClasses, filterSelectorsForClass, getMatchingTypes } from '../util/pluginUtils'
import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
import log from '../util/log'
import * as sharedState from './sharedState'
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
import {
formatVariantSelector,
finalizeSelector,
eliminateIrrelevantSelectors,
} from '../util/formatVariantSelector'
import { asClass } from '../util/nameClass'
import { normalize } from '../util/dataTypes'
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
Expand Down Expand Up @@ -111,22 +115,28 @@ function applyImportant(matches, classCandidate) {
if (matches.length === 0) {
return matches
}

let result = []

for (let [meta, rule] of matches) {
let container = postcss.root({ nodes: [rule.clone()] })

container.walkRules((r) => {
r.selector = updateAllClasses(
filterSelectorsForClass(r.selector, classCandidate),
(className) => {
if (className === classCandidate) {
return `!${className}`
}
return className
}
let ast = selectorParser().astSync(r.selector)

// Remove extraneous selectors that do not include the base candidate
ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate))

// Update all instances of the base candidate to include the important marker
updateAllClasses(ast, (className) =>
className === classCandidate ? `!${className}` : className
)

r.selector = ast.toString()

r.walkDecls((d) => (d.important = true))
})

result.push([{ ...meta, important: true }, container.nodes[0]])
}

Expand Down
2 changes: 1 addition & 1 deletion src/util/formatVariantSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function resortSelector(sel) {
* @param {Selector} ast
* @param {string} base
*/
function eliminateIrrelevantSelectors(sel, base) {
export function eliminateIrrelevantSelectors(sel, base) {
let hasClassesMatchingCandidate = false

sel.walk((child) => {
Expand Down
39 changes: 10 additions & 29 deletions src/util/pluginUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import selectorParser from 'postcss-selector-parser'
import escapeCommas from './escapeCommas'
import { withAlphaValue } from './withAlphaVariable'
import {
Expand All @@ -21,37 +20,19 @@ import negateValue from './negateValue'
import { backgroundSize } from './validateFormalSyntax'
import { flagEnabled } from '../featureFlags.js'

/**
* @param {import('postcss-selector-parser').Container} selectors
* @param {(className: string) => string} updateClass
* @returns {string}
*/
export function updateAllClasses(selectors, updateClass) {
let parser = selectorParser((selectors) => {
selectors.walkClasses((sel) => {
let updatedClass = updateClass(sel.value)
sel.value = updatedClass
if (sel.raws && sel.raws.value) {
sel.raws.value = escapeCommas(sel.raws.value)
}
})
})

let result = parser.processSync(selectors)
selectors.walkClasses((sel) => {
sel.value = updateClass(sel.value)

return result
}

export function filterSelectorsForClass(selectors, classCandidate) {
let parser = selectorParser((selectors) => {
selectors.each((sel) => {
const containsClass = sel.nodes.some(
(node) => node.type === 'class' && node.value === classCandidate
)
if (!containsClass) {
sel.remove()
}
})
if (sel.raws && sel.raws.value) {
sel.raws.value = escapeCommas(sel.raws.value)
}
})

let result = parser.processSync(selectors)

return result
}

function resolveArbitraryValue(modifier, validate) {
Expand Down
42 changes: 42 additions & 0 deletions tests/important-modifier.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,46 @@ crosscheck(() => {
`)
})
})

test('the important modifier works on utilities using :where()', () => {
let config = {
content: [
{
raw: html` <div class="btn hover:btn !btn hover:focus:disabled:!btn"></div> `,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents }) {
addComponents({
':where(.btn)': {
backgroundColor: '#00f',
},
})
},
],
}

let input = css`
@tailwind components;
@tailwind utilities;
`

return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:where(.\!btn) {
background-color: #00f !important;
}
:where(.btn) {
background-color: #00f;
}
:where(.hover\:btn:hover) {
background-color: #00f;
}
:where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) {
background-color: #00f !important;
}
`)
})
})
})

0 comments on commit 903eacb

Please sign in to comment.