Enforce the order of pseudo elements (#6018)

* enforce the order of some variants

* update changelog

* use better algorithm
This commit is contained in:
Robin Malfait 2021-11-12 16:38:03 +01:00 committed by GitHub
parent 4e21639903
commit a3579bcf2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 4 deletions

View File

@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Nothing yet!
### Fixed
- Enforce the order of some variants (like `before` and `after`) ([#6018](https://github.com/tailwindlabs/tailwindcss/pull/6018))
## [3.0.0-alpha.2] - 2021-11-08
### Changed

View File

@ -74,11 +74,92 @@ export function finalizeSelector(format, { selector, candidate, context }) {
return p
})
// This will make sure to move pseudo's to the correct spot (the end for
// pseudo elements) because otherwise the selector will never work
// anyway.
//
// E.g.:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
function collectPseudoElements(selector) {
let nodes = []
for (let node of selector.nodes) {
if (isPseudoElement(node)) {
nodes.push(node)
selector.removeChild(node)
}
if (node?.nodes) {
nodes.push(...collectPseudoElements(node))
}
}
return nodes
}
let pseudoElements = collectPseudoElements(selector)
if (pseudoElements.length > 0) {
selector.nodes.push(pseudoElements.sort(sortSelector))
}
return selector
})
}).processSync(selector)
}
// Note: As a rule, double colons (::) should be used instead of a single colon
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
// this distinction was not present in older versions of the W3C spec, most
// browsers support both syntaxes for the original pseudo-elements.
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
let pseudoElementExceptions = ['::file-selector-button']
// This will make sure to move pseudo's to the correct spot (the end for
// pseudo elements) because otherwise the selector will never work
// anyway.
//
// E.g.:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// `::before:hover` doesn't work, which means that we can make it work
// for you by flipping the order.
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') {
return 0
}
// If one of them is a combinator, we need to keep it in the same order
// because that means it will start a new "section" in the selector.
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
return 0
}
// One of the items is a pseudo and the other one isn't. Let's move
// the pseudo to the right.
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
return (a.type === 'pseudo') - (z.type === 'pseudo')
}
// Both are pseudo's, move the pseudo elements (except for
// ::file-selector-button) to the right.
return isPseudoElement(a) - isPseudoElement(z)
}
function isPseudoElement(node) {
if (node.type !== 'pseudo') return false
if (pseudoElementExceptions.includes(node.value)) return false
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
}
function resolveFunctionArgument(haystack, needle, arg) {
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
if (startIdx === -1) return null

View File

@ -259,3 +259,26 @@ describe('real examples', () => {
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector(formatVariantSelector('&', before), {
selector: '.a',
candidate: 'a',
})
expect(result).toEqual(after.replace('&', '.a'))
})
})

View File

@ -27,7 +27,7 @@ test('basic parallel variants', async () => {
.test\:font-medium *::test {
font-weight: 500;
}
.hover\:test\:font-black *::test:hover {
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.test\:font-bold::test {
@ -36,7 +36,7 @@ test('basic parallel variants', async () => {
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black::test:hover {
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)

View File

@ -252,12 +252,12 @@ test('with multi-class pseudo-element and pseudo-class variants', async () => {
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
/* --- */
.group:hover .group-hover\:hover\:before\:scale-x-110::before:hover {
.group:hover .group-hover\:hover\:before\:scale-x-110:hover::before {
content: var(--tw-content);
--tw-scale-x: 1.1;
transform: var(--tw-transform);
}
.peer:focus ~ .peer-focus\:focus\:after\:rotate-3::after:focus {
.peer:focus ~ .peer-focus\:focus\:after\:rotate-3:focus::after {
content: var(--tw-content);
--tw-rotate: 3deg;
transform: var(--tw-transform);

View File

@ -323,3 +323,63 @@ test('custom addVariant with nested media & format shorthand', () => {
`)
})
})
test('before and after variants are a bit special, and forced to the end', () => {
let config = {
content: [
{
raw: html`
<div class="before:hover:text-center"></div>
<div class="hover:before:text-center"></div>
`,
},
],
plugins: [],
}
return run('@tailwind components;@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.before\:hover\:text-center:hover::before {
content: var(--tw-content);
text-align: center;
}
.hover\:before\:text-center:hover::before {
content: var(--tw-content);
text-align: center;
}
`)
})
})
test('before and after variants are a bit special, and forced to the end (2)', () => {
let config = {
content: [
{
raw: html`
<div class="before:prose-headings:text-center"></div>
<div class="prose-headings:before:text-center"></div>
`,
},
],
plugins: [
function ({ addVariant }) {
addVariant('prose-headings', ':where(&) :is(h1, h2, h3, h4)')
},
],
}
return run('@tailwind components;@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:where(.before\:prose-headings\:text-center) :is(h1, h2, h3, h4)::before {
content: var(--tw-content);
text-align: center;
}
:where(.prose-headings\:before\:text-center) :is(h1, h2, h3, h4)::before {
content: var(--tw-content);
text-align: center;
}
`)
})
})