mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Enforce the order of pseudo elements (#6018)
* enforce the order of some variants * update changelog * use better algorithm
This commit is contained in:
parent
4e21639903
commit
a3579bcf2f
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'))
|
||||
})
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user