mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
a785c93b54
commit
0ecc4642fc
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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()}`
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user