Skip to content

Commit ff02420

Browse files
committed
Pull pseudo elements outside of :is and :has when using @apply
1 parent a785c93 commit ff02420

File tree

3 files changed

+126
-9
lines changed

3 files changed

+126
-9
lines changed

src/lib/expandApplyAtRules.js

Lines changed: 12 additions & 0 deletions
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, true, [':is', ':has'])
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/formatVariantSelector.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ 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) {
251251
sel.nodes.push(pseudoElements.sort(sortSelector))
252252
}
@@ -339,6 +339,17 @@ let pseudoElementExceptions = [
339339
'::-webkit-resizer',
340340
]
341341

342+
export function containsNode(selector, values) {
343+
let found = false
344+
selector.walk((node) => {
345+
if (values.includes(node.value)) {
346+
found = true
347+
return false
348+
}
349+
})
350+
return found
351+
}
352+
342353
/**
343354
* This will make sure to move pseudo's to the correct spot (the end for
344355
* pseudo elements) because otherwise the selector will never work
@@ -351,23 +362,43 @@ let pseudoElementExceptions = [
351362
* `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
352363
*
353364
* @param {Selector} selector
365+
* @param {boolean} force
366+
* @param {string[]|null} safelist
354367
**/
355-
function collectPseudoElements(selector) {
368+
export function collectPseudoElements(selector, force = false, safelist = null) {
356369
/** @type {Node[]} */
357370
let nodes = []
371+
let seenPseudoElement = null
372+
373+
if (safelist !== null && !containsNode(selector, safelist)) {
374+
return [[], seenPseudoElement]
375+
}
358376

359-
for (let node of selector.nodes) {
360-
if (isPseudoElement(node)) {
377+
for (let node of [...selector.nodes]) {
378+
if (isPseudoElement(node, force)) {
361379
nodes.push(node)
362380
selector.removeChild(node)
381+
seenPseudoElement = node.value
382+
} else if (seenPseudoElement !== null) {
383+
if (pseudoElementExceptions.includes(seenPseudoElement) && isPseudoClass(node, force)) {
384+
nodes.push(node)
385+
selector.removeChild(node)
386+
} else {
387+
seenPseudoElement = null
388+
}
363389
}
364390

365391
if (node?.nodes) {
366-
nodes.push(...collectPseudoElements(node))
392+
let [collected, seenPseudoElementInSelector] = collectPseudoElements(node, force)
393+
if (seenPseudoElementInSelector) {
394+
seenPseudoElement = seenPseudoElementInSelector
395+
}
396+
397+
nodes.push(...collected)
367398
}
368399
}
369400

370-
return nodes
401+
return [nodes, seenPseudoElement]
371402
}
372403

373404
// This will make sure to move pseudo's to the correct spot (the end for
@@ -380,7 +411,7 @@ function collectPseudoElements(selector) {
380411
//
381412
// `::before:hover` doesn't work, which means that we can make it work
382413
// for you by flipping the order.
383-
function sortSelector(a, z) {
414+
export function sortSelector(a, z) {
384415
// Both nodes are non-pseudo's so we can safely ignore them and keep
385416
// them in the same order.
386417
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
@@ -404,9 +435,13 @@ function sortSelector(a, z) {
404435
return isPseudoElement(a) - isPseudoElement(z)
405436
}
406437

407-
function isPseudoElement(node) {
438+
function isPseudoElement(node, force = false) {
408439
if (node.type !== 'pseudo') return false
409-
if (pseudoElementExceptions.includes(node.value)) return false
440+
if (pseudoElementExceptions.includes(node.value) && !force) return false
410441

411442
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
412443
}
444+
445+
function isPseudoClass(node, force) {
446+
return node.type === 'pseudo' && !isPseudoElement(node, force)
447+
}

tests/apply.test.js

Lines changed: 70 additions & 0 deletions
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
})

0 commit comments

Comments
 (0)