diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index c414dc636fb7..cdb5294769f6 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -277,27 +277,9 @@ function splitWithSeparator(input, separator) { return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g')) } -// A list of variants that are forced to the end. This is useful for variants -// that have pseudo elements which can't really be combined with other variant -// if they are in the incorrect order. -// -// 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. -let forcedVariantOrder = ['before', 'after'] - function* resolveMatches(candidate, context) { let separator = context.tailwindConfig.separator let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse() - - // Sort the variants if we used a forced variant. - // Note: this will not sort the others, it would only sort the forced variants. - if (variants.some((variant) => forcedVariantOrder.includes(variant))) { - variants.sort((a, z) => forcedVariantOrder.indexOf(a) - forcedVariantOrder.indexOf(z)) - } - let important = false if (classCandidate.startsWith('!')) { diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index d24471cc2eb8..06d0f489b0d7 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -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 diff --git a/tests/format-variant-selector.test.js b/tests/format-variant-selector.test.js index a32ae502501e..94e86ddfcab7 100644 --- a/tests/format-variant-selector.test.js +++ b/tests/format-variant-selector.test.js @@ -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')) + }) +}) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index e92bee89bb4a..5d58ea347c74 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -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; } `)