Don't wrap relative selectors in arbitrary variants with :is(…) (#14203)
Prior to this PR, we weren't accounting for the fact that `:has(…)` supports [relative selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector), which are sort of like partial selectors that can start with a combinator like `>`, `+`, or `~`. Before, a class like `has-[>img]:flex` would generate this: ```css .has-\[\>img\]\:flex:has(*:is(> img)) { display: flex; } ``` This was incorrect because `*:is(> img)` isn't even valid CSS at all, so the rule would do nothing. After this change, we generate this instead: ```css .has-\[\>img\]\:flex:has(> img) { display: flex; } ``` This PR also ensures that relative selectors are recognized as invalid in places where they are not supported, so classes like `group-[>img]:flex` for example will produce nothing now instead of invalid CSS. This is mostly a simple change but it did involve storing some additional information in the variant AST. Fixes #14202. --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
e10b786437
commit
3df38a7458
7 changed files with 147 additions and 13 deletions
|
|
@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Add support for `addBase` plugins using the `@plugin` directive ([#14172](https://github.com/tailwindlabs/tailwindcss/pull/14172))
|
||||
- Add support for the `tailwindcss/plugin` export ([#14173](https://github.com/tailwindlabs/tailwindcss/pull/14173))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't wrap relative selectors in arbitrary variants with `:is(…)` ([#14203](https://github.com/tailwindlabs/tailwindcss/pull/14203))
|
||||
|
||||
## [4.0.0-alpha.19] - 2024-08-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ it('should parse a simple utility with an arbitrary variant', () => {
|
|||
{
|
||||
"compounds": true,
|
||||
"kind": "arbitrary",
|
||||
"relative": false,
|
||||
"selector": "& p",
|
||||
},
|
||||
],
|
||||
|
|
@ -207,6 +208,7 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia
|
|||
"variant": {
|
||||
"compounds": true,
|
||||
"kind": "arbitrary",
|
||||
"relative": false,
|
||||
"selector": "& p",
|
||||
},
|
||||
},
|
||||
|
|
@ -316,6 +318,7 @@ it('should parse a simple utility with an arbitrary media variant', () => {
|
|||
{
|
||||
"compounds": true,
|
||||
"kind": "arbitrary",
|
||||
"relative": false,
|
||||
"selector": "@media(width>=123px)",
|
||||
},
|
||||
],
|
||||
|
|
@ -1102,11 +1105,13 @@ it('should parse arbitrary properties that are important and using stacked arbit
|
|||
{
|
||||
"compounds": true,
|
||||
"kind": "arbitrary",
|
||||
"relative": false,
|
||||
"selector": "& p",
|
||||
},
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "arbitrary",
|
||||
"relative": false,
|
||||
"selector": "@media(width>=123px)",
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ export type Variant =
|
|||
|
||||
// If true, it can be applied as a child of a compound variant
|
||||
compounds: boolean
|
||||
|
||||
// Whether or not the selector is a relative selector
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector
|
||||
relative: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -510,21 +514,23 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
|||
|
||||
let selector = decodeArbitraryValue(variant.slice(1, -1))
|
||||
|
||||
if (selector[0] !== '@') {
|
||||
// Ensure `&` is always present by wrapping the selector in `&:is(…)`
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `[p]:flex`
|
||||
if (!selector.includes('&')) {
|
||||
selector = `&:is(${selector})`
|
||||
}
|
||||
let relative = selector[0] === '>' || selector[0] === '+' || selector[0] === '~'
|
||||
|
||||
// Ensure `&` is always present by wrapping the selector in `&:is(…)`,
|
||||
// unless it's a relative selector like `> img`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `[p]:flex`
|
||||
if (!relative && selector[0] !== '@' && !selector.includes('&')) {
|
||||
selector = `&:is(${selector})`
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'arbitrary',
|
||||
selector,
|
||||
compounds: true,
|
||||
relative,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,8 +134,19 @@ export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem
|
|||
}
|
||||
}
|
||||
|
||||
export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void {
|
||||
export function applyVariant(
|
||||
node: Rule,
|
||||
variant: Variant,
|
||||
variants: Variants,
|
||||
depth: number = 0,
|
||||
): null | void {
|
||||
if (variant.kind === 'arbitrary') {
|
||||
// Relative selectors are not valid as an entire arbitrary variant, only as
|
||||
// an arbitrary variant that is part of another compound variant.
|
||||
//
|
||||
// E.g. `[>img]:flex` is not valid, but `has-[>img]:flex` is
|
||||
if (variant.relative && depth === 0) return null
|
||||
|
||||
node.nodes = [rule(variant.selector, node.nodes)]
|
||||
return
|
||||
}
|
||||
|
|
@ -162,7 +173,7 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants):
|
|||
// affecting the original node.
|
||||
let isolatedNode = rule('@slot', [])
|
||||
|
||||
let result = applyVariant(isolatedNode, variant.variant, variants)
|
||||
let result = applyVariant(isolatedNode, variant.variant, variants, depth + 1)
|
||||
if (result === null) return null
|
||||
|
||||
for (let child of isolatedNode.nodes) {
|
||||
|
|
|
|||
|
|
@ -380,6 +380,10 @@ describe('arbitrary variants', () => {
|
|||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
it('discards arbitrary variants using relative selectors', async () => {
|
||||
expect(await run(['[>img]:flex', '[+img]:flex', '[~img]:flex'])).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('variant stacking', () => {
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ test('group-[...]', async () => {
|
|||
css`
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['group-[@media_foo]:flex'],
|
||||
['group-[@media_foo]:flex', 'group-[>img]:flex'],
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -861,7 +861,7 @@ test('peer-[...]', async () => {
|
|||
css`
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['peer-[@media_foo]:flex'],
|
||||
['peer-[@media_foo]:flex', 'peer-[>img]:flex'],
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -1668,6 +1668,9 @@ test('not', async () => {
|
|||
@tailwind utilities;
|
||||
`,
|
||||
[
|
||||
'not-[>img]:flex',
|
||||
'not-[+img]:flex',
|
||||
'not-[~img]:flex',
|
||||
'not-[:checked]/foo:flex',
|
||||
'not-[@media_print]:flex',
|
||||
'not-custom-at-rule:flex',
|
||||
|
|
@ -1690,18 +1693,37 @@ test('has', async () => {
|
|||
@tailwind utilities;
|
||||
`,
|
||||
[
|
||||
'has-checked:flex',
|
||||
'has-[:checked]:flex',
|
||||
'has-[>img]:flex',
|
||||
'has-[+img]:flex',
|
||||
'has-[~img]:flex',
|
||||
'has-[&>img]:flex',
|
||||
'has-hocus:flex',
|
||||
|
||||
'group-has-[:checked]:flex',
|
||||
'group-has-[:checked]/parent-name:flex',
|
||||
'group-has-checked:flex',
|
||||
'group-has-checked/parent-name:flex',
|
||||
'group-has-[>img]:flex',
|
||||
'group-has-[>img]/parent-name:flex',
|
||||
'group-has-[+img]:flex',
|
||||
'group-has-[~img]:flex',
|
||||
'group-has-[&>img]:flex',
|
||||
'group-has-[&>img]/parent-name:flex',
|
||||
'group-has-hocus:flex',
|
||||
'group-has-hocus/parent-name:flex',
|
||||
|
||||
'peer-has-[:checked]:flex',
|
||||
'peer-has-[:checked]/sibling-name:flex',
|
||||
'peer-has-checked:flex',
|
||||
'peer-has-checked/sibling-name:flex',
|
||||
'peer-has-[>img]:flex',
|
||||
'peer-has-[>img]/sibling-name:flex',
|
||||
'peer-has-[+img]:flex',
|
||||
'peer-has-[~img]:flex',
|
||||
'peer-has-[&>img]:flex',
|
||||
'peer-has-[&>img]/sibling-name:flex',
|
||||
'peer-has-hocus:flex',
|
||||
'peer-has-hocus/sibling-name:flex',
|
||||
],
|
||||
|
|
@ -1711,6 +1733,10 @@ test('has', async () => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-hocus\\:flex:is(:where(.group):has(:hover, :focus) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -1727,10 +1753,38 @@ test('has', async () => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\&\\>img\\]\\:flex:is(:where(.group):has(* > img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\&\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(* > img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\+img\\]\\:flex:is(:where(.group):has( + img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\>img\\]\\:flex:is(:where(.group):has( > img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has( > img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-has-\\[\\~img\\]\\:flex:is(:where(.group):has( ~ img) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-checked\\:flex:is(:where(.peer):has(:checked) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-checked\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-hocus\\:flex:is(:where(.peer):has(:hover, :focus) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -1747,12 +1801,56 @@ test('has', async () => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\&\\>img\\]\\:flex:is(:where(.peer):has(* > img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\&\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(* > img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\+img\\]\\:flex:is(:where(.peer):has( + img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\>img\\]\\:flex:is(:where(.peer):has( > img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has( > img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.peer-has-\\[\\~img\\]\\:flex:is(:where(.peer):has( ~ img) ~ *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-checked\\:flex:has(:checked) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-hocus\\:flex:has(:hover, :focus) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-\\[\\:checked\\]\\:flex:has(:checked) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-\\[\\&\\>img\\]\\:flex:has(* > img) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-\\[\\+img\\]\\:flex:has( + img) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-\\[\\>img\\]\\:flex:has( > img) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.has-\\[\\~img\\]\\:flex:has( ~ img) {
|
||||
display: flex;
|
||||
}"
|
||||
`)
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@ export function createVariants(theme: Theme): Variants {
|
|||
staticVariant('*', [':where(& > *)'], { compounds: false })
|
||||
|
||||
variants.compound('not', (ruleNode, variant) => {
|
||||
if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null
|
||||
|
||||
if (variant.modifier) return null
|
||||
|
||||
let didApply = false
|
||||
|
|
@ -242,6 +244,8 @@ export function createVariants(theme: Theme): Variants {
|
|||
})
|
||||
|
||||
variants.compound('group', (ruleNode, variant) => {
|
||||
if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null
|
||||
|
||||
// Name the group by appending the modifier to `group` class itself if
|
||||
// present.
|
||||
let groupSelector = variant.modifier
|
||||
|
|
@ -301,6 +305,8 @@ export function createVariants(theme: Theme): Variants {
|
|||
})
|
||||
|
||||
variants.compound('peer', (ruleNode, variant) => {
|
||||
if (variant.variant.kind === 'arbitrary' && variant.variant.relative) return null
|
||||
|
||||
// Name the peer by appending the modifier to `peer` class itself if
|
||||
// present.
|
||||
let peerSelector = variant.modifier
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue