Skip to content

Commit 6cdbcf9

Browse files
committed
Replaces classes in utility selectors like :where and :has
1 parent cc1be47 commit 6cdbcf9

File tree

2 files changed

+97
-7
lines changed

2 files changed

+97
-7
lines changed

src/util/formatVariantSelector.js

+24-7
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 = sel.some((n) => n.type === 'class' && n.value === base)
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

+73
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,76 @@ 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+
:is(.hover\:foo:hover) {
998+
color: yellow;
999+
}
1000+
html:has(.hover\:foo:hover) {
1001+
color: green;
1002+
}
1003+
@media (min-width: 640px) {
1004+
:where(.sm\:foo) {
1005+
color: red;
1006+
}
1007+
:matches(.sm\:foo, .bar, .baz) {
1008+
color: orange;
1009+
}
1010+
:is(.sm\:foo) {
1011+
color: yellow;
1012+
}
1013+
html:has(.sm\:foo) {
1014+
color: green;
1015+
}
1016+
}
1017+
`)
1018+
})
1019+
})

0 commit comments

Comments
 (0)