mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Handle variants on complex selectors (#9262)
* Handle variants on complex selector utilities * Update changelog
This commit is contained in:
parent
09f38d2964
commit
db50bbbc71
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user