diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e955e7e5..4b5efc628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 499fc9260..fcbe692ac 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -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)", }, ], diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 6dc97150f..2a8a6430c 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -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, } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 80f923e1a..8478ba5cd 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -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) { diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index e04515229..15dfaa942 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -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', () => { diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 92707cdd1..cfc3d12a5 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -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; }" `) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index cef987c95..28009817a 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -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