Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle variants on complex selector utilities ([#9262](https://github.com/tailwindlabs/tailwindcss/pull/9262))
- Don't mutate shared config objects ([#9294](https://github.com/tailwindlabs/tailwindcss/pull/9294))
- Fix ordering of parallel variants ([#9282](https://github.com/tailwindlabs/tailwindcss/pull/9282))
- Handle variants in utility selectors using `:where()` and `:has()` ([#9309](https://github.com/tailwindlabs/tailwindcss/pull/9309))

## [3.1.8] - 2022-08-05

Expand Down
31 changes: 24 additions & 7 deletions src/util/formatVariantSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ function resortSelector(sel) {
return sel
}

function eliminateIrrelevantSelectors(sel, base) {
let hasClassesMatchingCandidate = false

sel.walk((child) => {
if (child.type === 'class' && child.value === base) {
hasClassesMatchingCandidate = true
return false // Stop walking
}
})

if (!hasClassesMatchingCandidate) {
sel.remove()
}

// We do NOT recursively eliminate sub selectors that don't have the base class
// as this is NOT a safe operation. For example, if we have:
// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
// We cannot remove the [hidden] from the :not() because it would change the
// meaning of the selector.

// TODO: Can we do this for :matches, :is, and :where?
}

export function finalizeSelector(
format,
{
Expand Down Expand Up @@ -115,13 +138,7 @@ export function finalizeSelector(
// Remove extraneous selectors that do not include the base class/candidate being matched against
// For example if we have a utility defined `.a, .b { color: red}`
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
ast.each((node) => {
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)

if (!hasClassesMatchingCandidate) {
node.remove()
}
})
ast.each((sel) => eliminateIrrelevantSelectors(sel, base))

// Normalize escaped classes, e.g.:
//
Expand Down
85 changes: 85 additions & 0 deletions tests/variants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -944,3 +944,88 @@ test('multi-class utilities handle selector-mutating variants correctly', () =>
`)
})
})

test('class inside pseudo-class function :has', () => {
let config = {
content: [
{ raw: html`<div class="foo hover:foo sm:foo"></div>` },
{ raw: html`<div class="bar hover:bar sm:bar"></div>` },
{ raw: html`<div class="baz hover:baz sm:baz"></div>` },
],
corePlugins: { preflight: false },
}

let input = css`
@tailwind utilities;
@layer utilities {
:where(.foo) {
color: red;
}
:matches(.foo, .bar, .baz) {
color: orange;
}
:is(.foo) {
color: yellow;
}
html:has(.foo) {
color: green;
}
}
`

return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:where(.foo) {
color: red;
}
:matches(.foo, .bar, .baz) {
color: orange;
}
:is(.foo) {
color: yellow;
}
html:has(.foo) {
color: green;
}

:where(.hover\:foo:hover) {
color: red;
}
:matches(.hover\:foo:hover, .bar, .baz) {
color: orange;
}
:matches(.foo, .hover\:bar:hover, .baz) {
color: orange;
}
:matches(.foo, .bar, .hover\:baz:hover) {
color: orange;
}
:is(.hover\:foo:hover) {
color: yellow;
}
html:has(.hover\:foo:hover) {
color: green;
}
@media (min-width: 640px) {
:where(.sm\:foo) {
color: red;
}
:matches(.sm\:foo, .bar, .baz) {
color: orange;
}
:matches(.foo, .sm\:bar, .baz) {
color: orange;
}
:matches(.foo, .bar, .sm\:baz) {
color: orange;
}
:is(.sm\:foo) {
color: yellow;
}
html:has(.sm\:foo) {
color: green;
}
}
`)
})
})