Skip to content

Commit

Permalink
Pull pseudo elements outside of :is and :has when using @apply (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
thecrypticace and RobinMalfait committed Mar 29, 2023
1 parent a785c93 commit 0ecc464
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions src/lib/expandApplyAtRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -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()
})
}

Expand Down
34 changes: 23 additions & 11 deletions src/util/applyImportantSelector.js
Original file line number Diff line number Diff line change
@@ -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()}`
}
46 changes: 36 additions & 10 deletions src/util/formatVariantSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
})

Expand Down Expand Up @@ -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
Expand All @@ -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') {
Expand All @@ -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)
}
70 changes: 70 additions & 0 deletions tests/apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`)
})
})
})
7 changes: 7 additions & 0 deletions tests/important-selector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`,
},
],
Expand Down Expand Up @@ -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;
}
`)
})
})
Expand Down

0 comments on commit 0ecc464

Please sign in to comment.