Skip to content

Commit 01f928d

Browse files
Handle variants in utility selectors using :where() and :has() (#9309)
* Replaces classes in utility selectors like :where and :has * Update changelog * wip
1 parent f92b31b commit 01f928d

File tree

3 files changed

+110
-7
lines changed

3 files changed

+110
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- Handle variants on complex selector utilities ([#9262](https://github.com/tailwindlabs/tailwindcss/pull/9262))
3131
- Don't mutate shared config objects ([#9294](https://github.com/tailwindlabs/tailwindcss/pull/9294))
3232
- Fix ordering of parallel variants ([#9282](https://github.com/tailwindlabs/tailwindcss/pull/9282))
33+
- Handle variants in utility selectors using `:where()` and `:has()` ([#9309](https://github.com/tailwindlabs/tailwindcss/pull/9309))
3334

3435
## [3.1.8] - 2022-08-05
3536

src/util/formatVariantSelector.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,29 @@ function resortSelector(sel) {
8181
return sel
8282
}
8383

84+
function eliminateIrrelevantSelectors(sel, base) {
85+
let hasClassesMatchingCandidate = false
86+
87+
sel.walk((child) => {
88+
if (child.type === 'class' && child.value === base) {
89+
hasClassesMatchingCandidate = true
90+
return false // Stop walking
91+
}
92+
})
93+
94+
if (!hasClassesMatchingCandidate) {
95+
sel.remove()
96+
}
97+
98+
// We do NOT recursively eliminate sub selectors that don't have the base class
99+
// as this is NOT a safe operation. For example, if we have:
100+
// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
101+
// We cannot remove the [hidden] from the :not() because it would change the
102+
// meaning of the selector.
103+
104+
// TODO: Can we do this for :matches, :is, and :where?
105+
}
106+
84107
export function finalizeSelector(
85108
format,
86109
{
@@ -115,13 +138,7 @@ export function finalizeSelector(
115138
// Remove extraneous selectors that do not include the base class/candidate being matched against
116139
// For example if we have a utility defined `.a, .b { color: red}`
117140
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
118-
ast.each((node) => {
119-
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
120-
121-
if (!hasClassesMatchingCandidate) {
122-
node.remove()
123-
}
124-
})
141+
ast.each((sel) => eliminateIrrelevantSelectors(sel, base))
125142

126143
// Normalize escaped classes, e.g.:
127144
//

tests/variants.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,88 @@ test('multi-class utilities handle selector-mutating variants correctly', () =>
944944
`)
945945
})
946946
})
947+
948+
test('class inside pseudo-class function :has', () => {
949+
let config = {
950+
content: [
951+
{ raw: html`<div class="foo hover:foo sm:foo"></div>` },
952+
{ raw: html`<div class="bar hover:bar sm:bar"></div>` },
953+
{ raw: html`<div class="baz hover:baz sm:baz"></div>` },
954+
],
955+
corePlugins: { preflight: false },
956+
}
957+
958+
let input = css`
959+
@tailwind utilities;
960+
@layer utilities {
961+
:where(.foo) {
962+
color: red;
963+
}
964+
:matches(.foo, .bar, .baz) {
965+
color: orange;
966+
}
967+
:is(.foo) {
968+
color: yellow;
969+
}
970+
html:has(.foo) {
971+
color: green;
972+
}
973+
}
974+
`
975+
976+
return run(input, config).then((result) => {
977+
expect(result.css).toMatchFormattedCss(css`
978+
:where(.foo) {
979+
color: red;
980+
}
981+
:matches(.foo, .bar, .baz) {
982+
color: orange;
983+
}
984+
:is(.foo) {
985+
color: yellow;
986+
}
987+
html:has(.foo) {
988+
color: green;
989+
}
990+
991+
:where(.hover\:foo:hover) {
992+
color: red;
993+
}
994+
:matches(.hover\:foo:hover, .bar, .baz) {
995+
color: orange;
996+
}
997+
:matches(.foo, .hover\:bar:hover, .baz) {
998+
color: orange;
999+
}
1000+
:matches(.foo, .bar, .hover\:baz:hover) {
1001+
color: orange;
1002+
}
1003+
:is(.hover\:foo:hover) {
1004+
color: yellow;
1005+
}
1006+
html:has(.hover\:foo:hover) {
1007+
color: green;
1008+
}
1009+
@media (min-width: 640px) {
1010+
:where(.sm\:foo) {
1011+
color: red;
1012+
}
1013+
:matches(.sm\:foo, .bar, .baz) {
1014+
color: orange;
1015+
}
1016+
:matches(.foo, .sm\:bar, .baz) {
1017+
color: orange;
1018+
}
1019+
:matches(.foo, .bar, .sm\:baz) {
1020+
color: orange;
1021+
}
1022+
:is(.sm\:foo) {
1023+
color: yellow;
1024+
}
1025+
html:has(.sm\:foo) {
1026+
color: green;
1027+
}
1028+
}
1029+
`)
1030+
})
1031+
})

0 commit comments

Comments
 (0)