Skip to content

Commit 8b737f6

Browse files
adamwathanthecrypticace
authored andcommitted
Add has-* variants for :has(...) pseudo-class (#11318)
* Add `has-*` variants for `:has(...)` pseudo-class * Update changelog * Fix mistake in test --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent c2935e1 commit 8b737f6

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Added
1515

1616
- Add `svh`, `lvh`, and `dvh` values to default `height`/`min-height`/`max-height` theme ([#11317](https://github.com/tailwindlabs/tailwindcss/pull/11317))
17+
- Add `has-*` variants for `:has(...)` pseudo-class ([#11318](https://github.com/tailwindlabs/tailwindcss/pull/11318))
1718

1819
## [3.3.6] - 2023-12-04
1920

src/corePlugins.js

+20
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,26 @@ export let variantPlugins = {
392392
)
393393
},
394394

395+
hasVariants: ({ matchVariant }) => {
396+
matchVariant('has', (value) => `&:has(${normalize(value)})`, { values: {} })
397+
matchVariant(
398+
'group-has',
399+
(value, { modifier }) =>
400+
modifier
401+
? `:merge(.group\\/${modifier}):has(${normalize(value)}) &`
402+
: `:merge(.group):has(${normalize(value)}) &`,
403+
{ values: {} }
404+
)
405+
matchVariant(
406+
'peer-has',
407+
(value, { modifier }) =>
408+
modifier
409+
? `:merge(.peer\\/${modifier}):has(${normalize(value)}) ~ &`
410+
: `:merge(.peer):has(${normalize(value)}) ~ &`,
411+
{ values: {} }
412+
)
413+
},
414+
395415
ariaVariants: ({ matchVariant, theme }) => {
396416
matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} })
397417
matchVariant(

src/lib/setupContextUtils.js

+1
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ function resolvePlugins(context, root) {
758758
let beforeVariants = [
759759
variantPlugins['pseudoElementVariants'],
760760
variantPlugins['pseudoClassVariants'],
761+
variantPlugins['hasVariants'],
761762
variantPlugins['ariaVariants'],
762763
variantPlugins['dataVariants'],
763764
]

tests/arbitrary-variants.test.js

+126
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,132 @@ crosscheck(({ stable, oxide }) => {
845845
})
846846
})
847847

848+
test('has-* variants with arbitrary values', () => {
849+
let config = {
850+
theme: {},
851+
content: [
852+
{
853+
raw: html`
854+
<div>
855+
<figure class="has-[figcaption]:inline-block"></figure>
856+
<div class="has-[.foo]:flex"></div>
857+
<div class="has-[.foo:hover]:block"></div>
858+
<div class="has-[[data-active]]:inline"></div>
859+
<div class="has-[>_.potato]:table"></div>
860+
<div class="has-[+_h2]:grid"></div>
861+
<div class="has-[>_h1_+_h2]:contents"></div>
862+
<div class="has-[h2]:has-[.banana]:hidden"></div>
863+
</div>
864+
`,
865+
},
866+
],
867+
corePlugins: { preflight: false },
868+
}
869+
870+
let input = css`
871+
@tailwind utilities;
872+
`
873+
874+
return run(input, config).then((result) => {
875+
expect(result.css).toMatchFormattedCss(css`
876+
.has-\[\.foo\:hover\]\:block:has(.foo:hover) {
877+
display: block;
878+
}
879+
.has-\[figcaption\]\:inline-block:has(figcaption) {
880+
display: inline-block;
881+
}
882+
.has-\[\[data-active\]\]\:inline:has([data-active]) {
883+
display: inline;
884+
}
885+
.has-\[\.foo\]\:flex:has(.foo) {
886+
display: flex;
887+
}
888+
.has-\[\>_\.potato\]\:table:has(> .potato) {
889+
display: table;
890+
}
891+
.has-\[\+_h2\]\:grid:has(+ h2) {
892+
display: grid;
893+
}
894+
.has-\[\>_h1_\+_h2\]\:contents:has(> h1 + h2) {
895+
display: contents;
896+
}
897+
.has-\[h2\]\:has-\[\.banana\]\:hidden:has(.banana):has(h2) {
898+
display: none;
899+
}
900+
`)
901+
})
902+
})
903+
904+
test('group-has-* variants with arbitrary values', () => {
905+
let config = {
906+
theme: {},
907+
content: [
908+
{
909+
raw: html`
910+
<div class="group">
911+
<div class="group-has-[>_h1_+_.foo]:block"></div>
912+
</div>
913+
<div class="group/two">
914+
<div class="group-has-[>_h1_+_.foo]/two:flex"></div>
915+
</div>
916+
`,
917+
},
918+
],
919+
corePlugins: { preflight: false },
920+
}
921+
922+
let input = css`
923+
@tailwind utilities;
924+
`
925+
926+
return run(input, config).then((result) => {
927+
expect(result.css).toMatchFormattedCss(css`
928+
.group:has(> h1 + .foo) .group-has-\[\>_h1_\+_\.foo\]\:block {
929+
display: block;
930+
}
931+
.group\/two:has(> h1 + .foo) .group-has-\[\>_h1_\+_\.foo\]\/two\:flex {
932+
display: flex;
933+
}
934+
`)
935+
})
936+
})
937+
938+
test('peer-has-* variants with arbitrary values', () => {
939+
let config = {
940+
theme: {},
941+
content: [
942+
{
943+
raw: html`
944+
<div>
945+
<div className="peer"></div>
946+
<div class="peer-has-[>_h1_+_.foo]:block"></div>
947+
</div>
948+
<div>
949+
<div className="peer"></div>
950+
<div class="peer-has-[>_h1_+_.foo]/two:flex"></div>
951+
</div>
952+
`,
953+
},
954+
],
955+
corePlugins: { preflight: false },
956+
}
957+
958+
let input = css`
959+
@tailwind utilities;
960+
`
961+
962+
return run(input, config).then((result) => {
963+
expect(result.css).toMatchFormattedCss(css`
964+
.peer:has(> h1 + .foo) ~ .peer-has-\[\>_h1_\+_\.foo\]\:block {
965+
display: block;
966+
}
967+
.peer\/two:has(> h1 + .foo) ~ .peer-has-\[\>_h1_\+_\.foo\]\/two\:flex {
968+
display: flex;
969+
}
970+
`)
971+
})
972+
})
973+
848974
it('should be possible to use modifiers and arbitrary groups', () => {
849975
let config = {
850976
content: [

0 commit comments

Comments
 (0)