From 01f928d6de1ddd3cef66adf642b08179c1ba4760 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 12 Sep 2022 15:08:31 -0400 Subject: [PATCH] Handle variants in utility selectors using `:where()` and `:has()` (#9309) * Replaces classes in utility selectors like :where and :has * Update changelog * wip --- CHANGELOG.md | 1 + src/util/formatVariantSelector.js | 31 ++++++++--- tests/variants.test.js | 85 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 596245edc..a3d2cf2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index 47d9ade7a..ffcc5f037 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -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, { @@ -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.: // diff --git a/tests/variants.test.js b/tests/variants.test.js index 5d9131786..45584fc36 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -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`
` }, + { raw: html`
` }, + { raw: html`
` }, + ], + 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; + } + } + `) + }) +})