From ff02420a6932310fa3d90eeca4bae477dfa4758c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 10:43:40 -0400 Subject: [PATCH 1/7] Pull pseudo elements outside of `:is` and `:has` when using `@apply` --- src/lib/expandApplyAtRules.js | 12 ++++++ src/util/formatVariantSelector.js | 53 +++++++++++++++++++---- tests/apply.test.js | 70 +++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index cdcd0b5688c9..836e9ba86c22 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -4,6 +4,7 @@ import parser from 'postcss-selector-parser' import { resolveMatches } from './generateRules' import escapeClassName from '../util/escapeClassName' import { applyImportantSelector } from '../util/applyImportantSelector' +import { collectPseudoElements, sortSelector } from '../util/formatVariantSelector.js' /** @typedef {Map} ApplyCache */ @@ -562,6 +563,17 @@ function processApply(root, context, localCache) { rule.walkDecls((d) => { d.important = meta.important || important }) + + // Move pseudo elements to the end of the selector (if necessary) + let selector = parser().astSync(rule.selector) + selector.each((sel) => { + let [pseudoElements] = collectPseudoElements(sel, true, [':is', ':has']) + if (pseudoElements.length > 0) { + sel.nodes.push(...pseudoElements.sort(sortSelector)) + } + }) + + rule.selector = selector.toString() }) } diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index da5e2c78dfbc..062437061f9d 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -246,7 +246,7 @@ export function finalizeSelector(current, formats, { context, candidate, base }) // Move pseudo elements to the end of the selector (if necessary) selector.each((sel) => { - let pseudoElements = collectPseudoElements(sel) + let [pseudoElements] = collectPseudoElements(sel) if (pseudoElements.length > 0) { sel.nodes.push(pseudoElements.sort(sortSelector)) } @@ -339,6 +339,17 @@ let pseudoElementExceptions = [ '::-webkit-resizer', ] +export function containsNode(selector, values) { + let found = false + selector.walk((node) => { + if (values.includes(node.value)) { + found = true + return false + } + }) + return found +} + /** * This will make sure to move pseudo's to the correct spot (the end for * pseudo elements) because otherwise the selector will never work @@ -351,23 +362,43 @@ let pseudoElementExceptions = [ * `::before:hover` doesn't work, which means that we can make it work for you by flipping the order. * * @param {Selector} selector + * @param {boolean} force + * @param {string[]|null} safelist **/ -function collectPseudoElements(selector) { +export function collectPseudoElements(selector, force = false, safelist = null) { /** @type {Node[]} */ let nodes = [] + let seenPseudoElement = null + + if (safelist !== null && !containsNode(selector, safelist)) { + return [[], seenPseudoElement] + } - for (let node of selector.nodes) { - if (isPseudoElement(node)) { + for (let node of [...selector.nodes]) { + if (isPseudoElement(node, force)) { nodes.push(node) selector.removeChild(node) + seenPseudoElement = node.value + } else if (seenPseudoElement !== null) { + if (pseudoElementExceptions.includes(seenPseudoElement) && isPseudoClass(node, force)) { + nodes.push(node) + selector.removeChild(node) + } else { + seenPseudoElement = null + } } if (node?.nodes) { - nodes.push(...collectPseudoElements(node)) + let [collected, seenPseudoElementInSelector] = collectPseudoElements(node, force) + if (seenPseudoElementInSelector) { + seenPseudoElement = seenPseudoElementInSelector + } + + nodes.push(...collected) } } - return nodes + return [nodes, seenPseudoElement] } // This will make sure to move pseudo's to the correct spot (the end for @@ -380,7 +411,7 @@ function collectPseudoElements(selector) { // // `::before:hover` doesn't work, which means that we can make it work // for you by flipping the order. -function sortSelector(a, z) { +export function sortSelector(a, z) { // Both nodes are non-pseudo's so we can safely ignore them and keep // them in the same order. if (a.type !== 'pseudo' && z.type !== 'pseudo') { @@ -404,9 +435,13 @@ function sortSelector(a, z) { return isPseudoElement(a) - isPseudoElement(z) } -function isPseudoElement(node) { +function isPseudoElement(node, force = false) { if (node.type !== 'pseudo') return false - if (pseudoElementExceptions.includes(node.value)) return false + if (pseudoElementExceptions.includes(node.value) && !force) return false return node.value.startsWith('::') || pseudoElementsBC.includes(node.value) } + +function isPseudoClass(node, force) { + return node.type === 'pseudo' && !isPseudoElement(node, force) +} diff --git a/tests/apply.test.js b/tests/apply.test.js index f7fab7d70938..0ab9ec17567f 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -2357,4 +2357,74 @@ crosscheck(({ stable, oxide }) => { `) }) }) + + it('pseudo elements inside apply are moved outside of :is() or :has()', () => { + let config = { + darkMode: 'class', + content: [ + { + raw: html`
`, + }, + ], + } + + let input = css` + .foo::before { + @apply dark:bg-black/100; + } + + .bar::before { + @apply rtl:dark:bg-black/100; + } + + .baz::before { + @apply rtl:dark:hover:bg-black/100; + } + + .qux::file-selector-button { + @apply rtl:dark:hover:bg-black/100; + } + + .steve::before { + @apply rtl:hover:dark:bg-black/100; + } + + .bob::file-selector-button { + @apply rtl:hover:dark:bg-black/100; + } + + .foo::before { + @apply [:has([dir="rtl"]_&)]:hover:bg-black/100; + } + + .bar::file-selector-button { + @apply [:has([dir="rtl"]_&)]:hover:bg-black/100; + } + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + :is(.dark .foo)::before, + :is([dir='rtl'] :is(.dark .bar))::before, + :is([dir='rtl'] :is(.dark .baz:hover))::before { + background-color: #000; + } + :is([dir='rtl'] :is(.dark .qux))::file-selector-button:hover { + background-color: #000; + } + :is([dir='rtl'] :is(.dark .steve):hover):before { + background-color: #000; + } + :is([dir='rtl'] :is(.dark .bob))::file-selector-button:hover { + background-color: #000; + } + :has([dir='rtl'] .foo:hover):before { + background-color: #000; + } + :has([dir='rtl'] .bar)::file-selector-button:hover { + background-color: #000; + } + `) + }) + }) }) From 3dbfe4acbcb836ebc81b180e9f9cc2f2c3491295 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 10:53:55 -0400 Subject: [PATCH 2/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270fc089d6e8..1fc7165535cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Try resolving `config.default` before `config` to ensure the config file is resolved correctly ([#10898](https://github.com/tailwindlabs/tailwindcss/pull/10898)) +- Pull pseudo elements outside of `:is` and `:has` when using `@apply` ([#10903](https://github.com/tailwindlabs/tailwindcss/pull/10903)) ## [3.3.0] - 2023-03-27 From 8589ab89a2a9e57518d14cbbceec6a7b0a0f33e7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 11:39:14 -0400 Subject: [PATCH 3/7] Refactor --- src/lib/expandApplyAtRules.js | 2 +- src/util/formatVariantSelector.js | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 836e9ba86c22..c6d53d80e29f 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -567,7 +567,7 @@ function processApply(root, context, localCache) { // Move pseudo elements to the end of the selector (if necessary) let selector = parser().astSync(rule.selector) selector.each((sel) => { - let [pseudoElements] = collectPseudoElements(sel, true, [':is', ':has']) + let [pseudoElements] = collectPseudoElements(sel) if (pseudoElements.length > 0) { sel.nodes.push(...pseudoElements.sort(sortSelector)) } diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index 062437061f9d..dd6584db1fcc 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -248,7 +248,7 @@ export function finalizeSelector(current, formats, { context, candidate, base }) selector.each((sel) => { let [pseudoElements] = collectPseudoElements(sel) if (pseudoElements.length > 0) { - sel.nodes.push(pseudoElements.sort(sortSelector)) + sel.nodes.push(...pseudoElements.sort(sortSelector)) } }) @@ -365,15 +365,11 @@ export function containsNode(selector, values) { * @param {boolean} force * @param {string[]|null} safelist **/ -export function collectPseudoElements(selector, force = false, safelist = null) { +export function collectPseudoElements(selector, force = false) { /** @type {Node[]} */ let nodes = [] let seenPseudoElement = null - if (safelist !== null && !containsNode(selector, safelist)) { - return [[], seenPseudoElement] - } - for (let node of [...selector.nodes]) { if (isPseudoElement(node, force)) { nodes.push(node) @@ -389,7 +385,14 @@ export function collectPseudoElements(selector, force = false, safelist = null) } if (node?.nodes) { - let [collected, seenPseudoElementInSelector] = collectPseudoElements(node, force) + let hasPseudoElementRestrictions = + node.type === 'pseudo' && (node.value === ':is' || node.value === ':has') + + let [collected, seenPseudoElementInSelector] = collectPseudoElements( + node, + force || hasPseudoElementRestrictions + ) + if (seenPseudoElementInSelector) { seenPseudoElement = seenPseudoElementInSelector } From 7377102e140fa7e606b5031ff9cf69d559c67ca1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 11:39:28 -0400 Subject: [PATCH 4/7] Update important selector handling for :is and :has --- src/util/applyImportantSelector.js | 34 ++++++++++++++++++++---------- tests/important-selector.test.js | 7 ++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/util/applyImportantSelector.js b/src/util/applyImportantSelector.js index 69de63325c79..dbaf136cce20 100644 --- a/src/util/applyImportantSelector.js +++ b/src/util/applyImportantSelector.js @@ -1,19 +1,31 @@ import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' +import parser from 'postcss-selector-parser' +import { collectPseudoElements, sortSelector } from './formatVariantSelector.js' export function applyImportantSelector(selector, important) { - let matches = /^(.*?)(:before|:after|::[\w-]+)(\)*)$/g.exec(selector) - if (!matches) return `${important} ${wrapWithIs(selector)}` + let sel = parser().astSync(selector) - let [, before, pseudo, brackets] = matches - return `${important} ${wrapWithIs(before + brackets)}${pseudo}` -} + sel.each((sel) => { + // Wrap with :is if it's not already wrapped + let isWrapped = + sel.nodes[0].type === 'pseudo' && + sel.nodes[0].value === ':is' && + sel.nodes.every((node) => node.type !== 'combinator') -function wrapWithIs(selector) { - let parts = splitAtTopLevelOnly(selector, ' ') + if (!isWrapped) { + sel.nodes = [ + parser.pseudo({ + value: ':is', + nodes: [sel.clone()], + }), + ] + } - if (parts.length === 1 && parts[0].startsWith(':is(') && parts[0].endsWith(')')) { - return selector - } + let [pseudoElements] = collectPseudoElements(sel) + if (pseudoElements.length > 0) { + sel.nodes.push(...pseudoElements.sort(sortSelector)) + } + }) - return `:is(${selector})` + return `${important} ${sel.toString()}` } diff --git a/tests/important-selector.test.js b/tests/important-selector.test.js index 5c6515e43086..22edafb94dd1 100644 --- a/tests/important-selector.test.js +++ b/tests/important-selector.test.js @@ -21,6 +21,7 @@ crosscheck(({ stable, oxide }) => {
+
`, }, ], @@ -155,6 +156,12 @@ crosscheck(({ stable, oxide }) => { text-align: right; } } + #app + :is( + [dir='rtl'] :is(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100) + )::file-selector-button:hover { + background-color: #000; + } `) }) }) From a9d0b188ed81ac2a3c77b54c0e524bc02ee11a38 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 11:42:55 -0400 Subject: [PATCH 5/7] fixup --- src/util/formatVariantSelector.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index dd6584db1fcc..f7916c4eb116 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -339,17 +339,6 @@ let pseudoElementExceptions = [ '::-webkit-resizer', ] -export function containsNode(selector, values) { - let found = false - selector.walk((node) => { - if (values.includes(node.value)) { - found = true - return false - } - }) - return found -} - /** * This will make sure to move pseudo's to the correct spot (the end for * pseudo elements) because otherwise the selector will never work From 0d5f2df0ba39e953470bc3ab29eb448cb976d79b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Mar 2023 11:44:46 -0400 Subject: [PATCH 6/7] fixup --- src/util/formatVariantSelector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index f7916c4eb116..8a1a3f50c1f8 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -352,7 +352,6 @@ let pseudoElementExceptions = [ * * @param {Selector} selector * @param {boolean} force - * @param {string[]|null} safelist **/ export function collectPseudoElements(selector, force = false) { /** @type {Node[]} */ From 7d4668dbaeccc4de6bd5ad0b2fe914ae5913e4ba Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 29 Mar 2023 21:13:41 +0200 Subject: [PATCH 7/7] trigger CI