Pull pseudo elements outside of :is and :has when using @apply (#10903)

* Pull pseudo elements outside of `:is` and `:has` when using `@apply`

* Update changelog

* Refactor

* Update important selector handling for :is and :has

* fixup

* fixup

* trigger CI

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Jordan Pittman 2023-03-29 15:37:26 -04:00 committed by GitHub
parent a785c93b54
commit 0ecc4642fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 24 deletions

View File

@ -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

View File

@ -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<string, [any, import('postcss').Rule[]]>} 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)
if (pseudoElements.length > 0) {
sel.nodes.push(...pseudoElements.sort(sortSelector))
}
})
rule.selector = selector.toString()
})
}

View File

@ -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}`
}
function wrapWithIs(selector) {
let parts = splitAtTopLevelOnly(selector, ' ')
if (parts.length === 1 && parts[0].startsWith(':is(') && parts[0].endsWith(')')) {
return selector
}
return `:is(${selector})`
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')
if (!isWrapped) {
sel.nodes = [
parser.pseudo({
value: ':is',
nodes: [sel.clone()],
}),
]
}
let [pseudoElements] = collectPseudoElements(sel)
if (pseudoElements.length > 0) {
sel.nodes.push(...pseudoElements.sort(sortSelector))
}
})
return `${important} ${sel.toString()}`
}

View File

@ -246,9 +246,9 @@ 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))
sel.nodes.push(...pseudoElements.sort(sortSelector))
}
})
@ -351,23 +351,45 @@ 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
**/
function collectPseudoElements(selector) {
export function collectPseudoElements(selector, force = false) {
/** @type {Node[]} */
let nodes = []
let seenPseudoElement = null
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 hasPseudoElementRestrictions =
node.type === 'pseudo' && (node.value === ':is' || node.value === ':has')
let [collected, seenPseudoElementInSelector] = collectPseudoElements(
node,
force || hasPseudoElementRestrictions
)
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 +402,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 +426,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)
}

View File

@ -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` <div class="foo bar baz qux steve bob"></div> `,
},
],
}
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;
}
`)
})
})
})

View File

@ -21,6 +21,7 @@ crosscheck(({ stable, oxide }) => {
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>
<div class="dark:before:underline"></div>
<div class="hover:[&::file-selector-button]:rtl:dark:bg-black/100"></div>
`,
},
],
@ -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;
}
`)
})
})