Skip to content

Commit 69d8687

Browse files
authored
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 7248f21 commit 69d8687

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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
- Fix parsing of `theme()` inside `calc()` when there are no spaces around operators ([#11157](https://github.com/tailwindlabs/tailwindcss/pull/11157))
1515
- Ensure `repeating-conic-gradient` is detected as an image ([#11180](https://github.com/tailwindlabs/tailwindcss/pull/11180))
1616
- Remove `autoprefixer` dependency ([#11315](https://github.com/tailwindlabs/tailwindcss/pull/11315))
17+
- Add `has-*` variants for `:has(...)` pseudo-class ([#11318](https://github.com/tailwindlabs/tailwindcss/pull/11318))
1718

1819
### Added
1920

src/corePlugins.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,26 @@ export let variantPlugins = {
386386
)
387387
},
388388

389+
hasVariants: ({ matchVariant }) => {
390+
matchVariant('has', (value) => `&:has(${normalize(value)})`, { values: {} })
391+
matchVariant(
392+
'group-has',
393+
(value, { modifier }) =>
394+
modifier
395+
? `:merge(.group\\/${modifier}):has(${normalize(value)}) &`
396+
: `:merge(.group):has(${normalize(value)}) &`,
397+
{ values: {} }
398+
)
399+
matchVariant(
400+
'peer-has',
401+
(value, { modifier }) =>
402+
modifier
403+
? `:merge(.peer\\/${modifier}):has(${normalize(value)}) ~ &`
404+
: `:merge(.peer):has(${normalize(value)}) ~ &`,
405+
{ values: {} }
406+
)
407+
},
408+
389409
ariaVariants: ({ matchVariant, theme }) => {
390410
matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} })
391411
matchVariant(

src/lib/setupContextUtils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ function resolvePlugins(context, root) {
744744
let beforeVariants = [
745745
variantPlugins['pseudoElementVariants'],
746746
variantPlugins['pseudoClassVariants'],
747+
variantPlugins['hasVariants'],
747748
variantPlugins['ariaVariants'],
748749
variantPlugins['dataVariants'],
749750
]

tests/arbitrary-variants.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,132 @@ it('should support supports', () => {
772772
})
773773
})
774774

775+
test('has-* variants with arbitrary values', () => {
776+
let config = {
777+
theme: {},
778+
content: [
779+
{
780+
raw: html`
781+
<div>
782+
<figure class="has-[figcaption]:inline-block"></figure>
783+
<div class="has-[.foo]:flex"></div>
784+
<div class="has-[.foo:hover]:block"></div>
785+
<div class="has-[[data-active]]:inline"></div>
786+
<div class="has-[>_.potato]:table"></div>
787+
<div class="has-[+_h2]:grid"></div>
788+
<div class="has-[>_h1_+_h2]:contents"></div>
789+
<div class="has-[h2]:has-[.banana]:hidden"></div>
790+
</div>
791+
`,
792+
},
793+
],
794+
corePlugins: { preflight: false },
795+
}
796+
797+
let input = css`
798+
@tailwind utilities;
799+
`
800+
801+
return run(input, config).then((result) => {
802+
expect(result.css).toMatchFormattedCss(css`
803+
.has-\[\.foo\:hover\]\:block:has(.foo:hover) {
804+
display: block;
805+
}
806+
.has-\[figcaption\]\:inline-block:has(figcaption) {
807+
display: inline-block;
808+
}
809+
.has-\[\[data-active\]\]\:inline:has([data-active]) {
810+
display: inline;
811+
}
812+
.has-\[\.foo\]\:flex:has(.foo) {
813+
display: flex;
814+
}
815+
.has-\[\>_\.potato\]\:table:has(> .potato) {
816+
display: table;
817+
}
818+
.has-\[\+_h2\]\:grid:has(+ h2) {
819+
display: grid;
820+
}
821+
.has-\[\>_h1_\+_h2\]\:contents:has(> h1 + h2) {
822+
display: contents;
823+
}
824+
.has-\[h2\]\:has-\[\.banana\]\:hidden:has(.banana):has(h2) {
825+
display: none;
826+
}
827+
`)
828+
})
829+
})
830+
831+
test('group-has-* variants with arbitrary values', () => {
832+
let config = {
833+
theme: {},
834+
content: [
835+
{
836+
raw: html`
837+
<div class="group">
838+
<div class="group-has-[>_h1_+_.foo]:block"></div>
839+
</div>
840+
<div class="group/two">
841+
<div class="group-has-[>_h1_+_.foo]/two:flex"></div>
842+
</div>
843+
`,
844+
},
845+
],
846+
corePlugins: { preflight: false },
847+
}
848+
849+
let input = css`
850+
@tailwind utilities;
851+
`
852+
853+
return run(input, config).then((result) => {
854+
expect(result.css).toMatchFormattedCss(css`
855+
.group:has(> h1 + .foo) .group-has-\[\>_h1_\+_\.foo\]\:block {
856+
display: block;
857+
}
858+
.group\/two:has(> h1 + .foo) .group-has-\[\>_h1_\+_\.foo\]\/two\:flex {
859+
display: flex;
860+
}
861+
`)
862+
})
863+
})
864+
865+
test('peer-has-* variants with arbitrary values', () => {
866+
let config = {
867+
theme: {},
868+
content: [
869+
{
870+
raw: html`
871+
<div>
872+
<div className="peer"></div>
873+
<div class="peer-has-[>_h1_+_.foo]:block"></div>
874+
</div>
875+
<div>
876+
<div className="peer"></div>
877+
<div class="peer-has-[>_h1_+_.foo]/two:flex"></div>
878+
</div>
879+
`,
880+
},
881+
],
882+
corePlugins: { preflight: false },
883+
}
884+
885+
let input = css`
886+
@tailwind utilities;
887+
`
888+
889+
return run(input, config).then((result) => {
890+
expect(result.css).toMatchFormattedCss(css`
891+
.peer:has(> h1 + .foo) ~ .peer-has-\[\>_h1_\+_\.foo\]\:block {
892+
display: block;
893+
}
894+
.peer\/two:has(> h1 + .foo) ~ .peer-has-\[\>_h1_\+_\.foo\]\/two\:flex {
895+
display: flex;
896+
}
897+
`)
898+
})
899+
})
900+
775901
it('should be possible to use modifiers and arbitrary groups', () => {
776902
let config = {
777903
content: [

0 commit comments

Comments
 (0)