Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle variants on complex selectors #9262

Merged
merged 2 commits into from Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
91 changes: 89 additions & 2 deletions src/util/formatVariantSelector.js
Expand Up @@ -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,
{
Expand Down Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions tests/variants.test.js
Expand Up @@ -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;
}
`)
})
})