Skip to content

Commit 0ecc464

Browse files
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 <[email protected]>
1 parent a785c93 commit 0ecc464

6 files changed

+149
-21
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Try resolving `config.default` before `config` to ensure the config file is resolved correctly ([#10898](https://github.com/tailwindlabs/tailwindcss/pull/10898))
13+
- Pull pseudo elements outside of `:is` and `:has` when using `@apply` ([#10903](https://github.com/tailwindlabs/tailwindcss/pull/10903))
1314

1415
## [3.3.0] - 2023-03-27
1516

src/lib/expandApplyAtRules.js

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import parser from 'postcss-selector-parser'
44
import { resolveMatches } from './generateRules'
55
import escapeClassName from '../util/escapeClassName'
66
import { applyImportantSelector } from '../util/applyImportantSelector'
7+
import { collectPseudoElements, sortSelector } from '../util/formatVariantSelector.js'
78

89
/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
910

@@ -562,6 +563,17 @@ function processApply(root, context, localCache) {
562563
rule.walkDecls((d) => {
563564
d.important = meta.important || important
564565
})
566+
567+
// Move pseudo elements to the end of the selector (if necessary)
568+
let selector = parser().astSync(rule.selector)
569+
selector.each((sel) => {
570+
let [pseudoElements] = collectPseudoElements(sel)
571+
if (pseudoElements.length > 0) {
572+
sel.nodes.push(...pseudoElements.sort(sortSelector))
573+
}
574+
})
575+
576+
rule.selector = selector.toString()
565577
})
566578
}
567579

src/util/applyImportantSelector.js

+23-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
2+
import parser from 'postcss-selector-parser'
3+
import { collectPseudoElements, sortSelector } from './formatVariantSelector.js'
24

35
export function applyImportantSelector(selector, important) {
4-
let matches = /^(.*?)(:before|:after|::[\w-]+)(\)*)$/g.exec(selector)
5-
if (!matches) return `${important} ${wrapWithIs(selector)}`
6+
let sel = parser().astSync(selector)
67

7-
let [, before, pseudo, brackets] = matches
8-
return `${important} ${wrapWithIs(before + brackets)}${pseudo}`
9-
}
8+
sel.each((sel) => {
9+
// Wrap with :is if it's not already wrapped
10+
let isWrapped =
11+
sel.nodes[0].type === 'pseudo' &&
12+
sel.nodes[0].value === ':is' &&
13+
sel.nodes.every((node) => node.type !== 'combinator')
1014

11-
function wrapWithIs(selector) {
12-
let parts = splitAtTopLevelOnly(selector, ' ')
15+
if (!isWrapped) {
16+
sel.nodes = [
17+
parser.pseudo({
18+
value: ':is',
19+
nodes: [sel.clone()],
20+
}),
21+
]
22+
}
1323

14-
if (parts.length === 1 && parts[0].startsWith(':is(') && parts[0].endsWith(')')) {
15-
return selector
16-
}
24+
let [pseudoElements] = collectPseudoElements(sel)
25+
if (pseudoElements.length > 0) {
26+
sel.nodes.push(...pseudoElements.sort(sortSelector))
27+
}
28+
})
1729

18-
return `:is(${selector})`
30+
return `${important} ${sel.toString()}`
1931
}

src/util/formatVariantSelector.js

+36-10
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,9 @@ export function finalizeSelector(current, formats, { context, candidate, base })
246246

247247
// Move pseudo elements to the end of the selector (if necessary)
248248
selector.each((sel) => {
249-
let pseudoElements = collectPseudoElements(sel)
249+
let [pseudoElements] = collectPseudoElements(sel)
250250
if (pseudoElements.length > 0) {
251-
sel.nodes.push(pseudoElements.sort(sortSelector))
251+
sel.nodes.push(...pseudoElements.sort(sortSelector))
252252
}
253253
})
254254

@@ -351,23 +351,45 @@ let pseudoElementExceptions = [
351351
* `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
352352
*
353353
* @param {Selector} selector
354+
* @param {boolean} force
354355
**/
355-
function collectPseudoElements(selector) {
356+
export function collectPseudoElements(selector, force = false) {
356357
/** @type {Node[]} */
357358
let nodes = []
359+
let seenPseudoElement = null
358360

359-
for (let node of selector.nodes) {
360-
if (isPseudoElement(node)) {
361+
for (let node of [...selector.nodes]) {
362+
if (isPseudoElement(node, force)) {
361363
nodes.push(node)
362364
selector.removeChild(node)
365+
seenPseudoElement = node.value
366+
} else if (seenPseudoElement !== null) {
367+
if (pseudoElementExceptions.includes(seenPseudoElement) && isPseudoClass(node, force)) {
368+
nodes.push(node)
369+
selector.removeChild(node)
370+
} else {
371+
seenPseudoElement = null
372+
}
363373
}
364374

365375
if (node?.nodes) {
366-
nodes.push(...collectPseudoElements(node))
376+
let hasPseudoElementRestrictions =
377+
node.type === 'pseudo' && (node.value === ':is' || node.value === ':has')
378+
379+
let [collected, seenPseudoElementInSelector] = collectPseudoElements(
380+
node,
381+
force || hasPseudoElementRestrictions
382+
)
383+
384+
if (seenPseudoElementInSelector) {
385+
seenPseudoElement = seenPseudoElementInSelector
386+
}
387+
388+
nodes.push(...collected)
367389
}
368390
}
369391

370-
return nodes
392+
return [nodes, seenPseudoElement]
371393
}
372394

373395
// This will make sure to move pseudo's to the correct spot (the end for
@@ -380,7 +402,7 @@ function collectPseudoElements(selector) {
380402
//
381403
// `::before:hover` doesn't work, which means that we can make it work
382404
// for you by flipping the order.
383-
function sortSelector(a, z) {
405+
export function sortSelector(a, z) {
384406
// Both nodes are non-pseudo's so we can safely ignore them and keep
385407
// them in the same order.
386408
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
@@ -404,9 +426,13 @@ function sortSelector(a, z) {
404426
return isPseudoElement(a) - isPseudoElement(z)
405427
}
406428

407-
function isPseudoElement(node) {
429+
function isPseudoElement(node, force = false) {
408430
if (node.type !== 'pseudo') return false
409-
if (pseudoElementExceptions.includes(node.value)) return false
431+
if (pseudoElementExceptions.includes(node.value) && !force) return false
410432

411433
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
412434
}
435+
436+
function isPseudoClass(node, force) {
437+
return node.type === 'pseudo' && !isPseudoElement(node, force)
438+
}

tests/apply.test.js

+70
Original file line numberDiff line numberDiff line change
@@ -2357,4 +2357,74 @@ crosscheck(({ stable, oxide }) => {
23572357
`)
23582358
})
23592359
})
2360+
2361+
it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
2362+
let config = {
2363+
darkMode: 'class',
2364+
content: [
2365+
{
2366+
raw: html` <div class="foo bar baz qux steve bob"></div> `,
2367+
},
2368+
],
2369+
}
2370+
2371+
let input = css`
2372+
.foo::before {
2373+
@apply dark:bg-black/100;
2374+
}
2375+
2376+
.bar::before {
2377+
@apply rtl:dark:bg-black/100;
2378+
}
2379+
2380+
.baz::before {
2381+
@apply rtl:dark:hover:bg-black/100;
2382+
}
2383+
2384+
.qux::file-selector-button {
2385+
@apply rtl:dark:hover:bg-black/100;
2386+
}
2387+
2388+
.steve::before {
2389+
@apply rtl:hover:dark:bg-black/100;
2390+
}
2391+
2392+
.bob::file-selector-button {
2393+
@apply rtl:hover:dark:bg-black/100;
2394+
}
2395+
2396+
.foo::before {
2397+
@apply [:has([dir="rtl"]_&)]:hover:bg-black/100;
2398+
}
2399+
2400+
.bar::file-selector-button {
2401+
@apply [:has([dir="rtl"]_&)]:hover:bg-black/100;
2402+
}
2403+
`
2404+
2405+
return run(input, config).then((result) => {
2406+
expect(result.css).toMatchFormattedCss(css`
2407+
:is(.dark .foo)::before,
2408+
:is([dir='rtl'] :is(.dark .bar))::before,
2409+
:is([dir='rtl'] :is(.dark .baz:hover))::before {
2410+
background-color: #000;
2411+
}
2412+
:is([dir='rtl'] :is(.dark .qux))::file-selector-button:hover {
2413+
background-color: #000;
2414+
}
2415+
:is([dir='rtl'] :is(.dark .steve):hover):before {
2416+
background-color: #000;
2417+
}
2418+
:is([dir='rtl'] :is(.dark .bob))::file-selector-button:hover {
2419+
background-color: #000;
2420+
}
2421+
:has([dir='rtl'] .foo:hover):before {
2422+
background-color: #000;
2423+
}
2424+
:has([dir='rtl'] .bar)::file-selector-button:hover {
2425+
background-color: #000;
2426+
}
2427+
`)
2428+
})
2429+
})
23602430
})

tests/important-selector.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ crosscheck(({ stable, oxide }) => {
2121
<div class="group-hover:focus-within:text-left"></div>
2222
<div class="rtl:active:text-center"></div>
2323
<div class="dark:before:underline"></div>
24+
<div class="hover:[&::file-selector-button]:rtl:dark:bg-black/100"></div>
2425
`,
2526
},
2627
],
@@ -155,6 +156,12 @@ crosscheck(({ stable, oxide }) => {
155156
text-align: right;
156157
}
157158
}
159+
#app
160+
:is(
161+
[dir='rtl'] :is(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
162+
)::file-selector-button:hover {
163+
background-color: #000;
164+
}
158165
`)
159166
})
160167
})

0 commit comments

Comments
 (0)