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 in utility selectors using :where() and :has() #9309

Merged
merged 3 commits into from Sep 12, 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 @@ -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
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
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;
}
}
`)
})
})