Handle variants on complex selectors (#9262)

* Handle variants on complex selector utilities

* Update changelog
This commit is contained in:
Jordan Pittman 2022-09-06 12:58:38 -04:00 committed by GitHub
parent 09f38d2964
commit db50bbbc71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 179 additions and 2 deletions

View File

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
- Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
- Fix `fontFamily` config TypeScript types ([#9214](https://github.com/tailwindlabs/tailwindcss/pull/9214))
- Handle variants on complex selector utilities ([#9262](https://github.com/tailwindlabs/tailwindcss/pull/9262))
## [3.1.8] - 2022-08-05

View File

@ -29,6 +29,58 @@ export function formatVariantSelector(current, ...others) {
return current
}
/**
* Given any node in a selector this gets the "simple" selector it's a part of
* A simple selector is just a list of nodes without any combinators
* Technically :is(), :not(), :has(), etc can have combinators but those are nested
* inside the relevant node and won't be picked up so they're fine to ignore
*
* @param {import('postcss-selector-parser').Node} node
* @returns {import('postcss-selector-parser').Node[]}
**/
function simpleSelectorForNode(node) {
/** @type {import('postcss-selector-parser').Node[]} */
let nodes = []
// Walk backwards until we hit a combinator node (or the start)
while (node.prev() && node.prev().type !== 'combinator') {
node = node.prev()
}
// Now record all non-combinator nodes until we hit one (or the end)
while (node && node.type !== 'combinator') {
nodes.push(node)
node = node.next()
}
return nodes
}
/**
* Resorts the nodes in a selector to ensure they're in the correct order
* Tags go before classes, and pseudo classes go after classes
*
* @param {import('postcss-selector-parser').Selector} sel
* @returns {import('postcss-selector-parser').Selector}
**/
function resortSelector(sel) {
sel.sort((a, b) => {
if (a.type === 'tag' && b.type === 'class') {
return -1
} else if (a.type === 'class' && b.type === 'tag') {
return 1
} else if (a.type === 'class' && b.type === 'pseudo' && b.value !== ':merge') {
return -1
} else if (a.type === 'pseudo' && a.value !== ':merge' && b.type === 'class') {
return 1
}
return sel.index(a) - sel.index(b)
})
return sel
}
export function finalizeSelector(
format,
{
@ -88,12 +140,47 @@ export function finalizeSelector(
}
})
let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
// We can safely replace the escaped base now, since the `base` section is
// now in a normalized escaped value.
ast.walkClasses((node) => {
if (node.value === base) {
node.replaceWith(...formatAst.nodes)
if (node.value !== base) {
return
}
let parent = node.parent
let formatNodes = formatAst.nodes[0].nodes
// Perf optimization: if the parent is a single class we can just replace it and be done
if (parent.nodes.length === 1) {
node.replaceWith(...formatNodes)
return
}
let simpleSelector = simpleSelectorForNode(node)
parent.insertBefore(simpleSelector[0], simpleStart)
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
for (let child of formatNodes) {
parent.insertBefore(simpleSelector[0], child)
}
node.remove()
// Re-sort the simple selector to ensure it's in the correct order
simpleSelector = simpleSelectorForNode(simpleStart)
let firstNode = parent.index(simpleStart)
parent.nodes.splice(
firstNode,
simpleSelector.length,
...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
)
simpleStart.remove()
simpleEnd.remove()
})
// This will make sure to move pseudo's to the correct spot (the end for

View File

@ -855,3 +855,92 @@ test('hoverOnlyWhenSupported adds hover and pointer media features by default',
`)
})
})
test('multi-class utilities handle selector-mutating variants correctly', () => {
let config = {
content: [
{
raw: html`<div
class="hover:foo hover:bar hover:baz group-hover:foo group-hover:bar group-hover:baz peer-checked:foo peer-checked:bar peer-checked:baz"
></div>`,
},
{
raw: html`<div
class="hover:foo1 hover:bar1 hover:baz1 group-hover:foo1 group-hover:bar1 group-hover:baz1 peer-checked:foo1 peer-checked:bar1 peer-checked:baz1"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@layer utilities {
.foo.bar.baz {
color: red;
}
.foo1 .bar1 .baz1 {
color: red;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.hover\:foo.bar.baz:hover {
color: red;
}
.hover\:bar.foo.baz:hover {
color: red;
}
.hover\:baz.foo.bar:hover {
color: red;
}
.hover\:foo1:hover .bar1 .baz1 {
color: red;
}
.foo1 .hover\:bar1:hover .baz1 {
color: red;
}
.foo1 .bar1 .hover\:baz1:hover {
color: red;
}
.group:hover .group-hover\:foo.bar.baz {
color: red;
}
.group:hover .group-hover\:bar.foo.baz {
color: red;
}
.group:hover .group-hover\:baz.foo.bar {
color: red;
}
.group:hover .group-hover\:foo1 .bar1 .baz1 {
color: red;
}
.foo1 .group:hover .group-hover\:bar1 .baz1 {
color: red;
}
.foo1 .bar1 .group:hover .group-hover\:baz1 {
color: red;
}
.peer:checked ~ .peer-checked\:foo.bar.baz {
color: red;
}
.peer:checked ~ .peer-checked\:bar.foo.baz {
color: red;
}
.peer:checked ~ .peer-checked\:baz.foo.bar {
color: red;
}
.peer:checked ~ .peer-checked\:foo1 .bar1 .baz1 {
color: red;
}
.foo1 .peer:checked ~ .peer-checked\:bar1 .baz1 {
color: red;
}
.foo1 .bar1 .peer:checked ~ .peer-checked\:baz1 {
color: red;
}
`)
})
})