mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Handle utilities with multiple and/or grouped selectors better (#8262)
* Add failing test cases * Flatten finalizeSelector code * Use AST operations to format selector classes With this change we only parse the selector once and operate on the AST until we need to turn it back into a selector. In addition this lets us solve an issue where .replace(…) did the wrong thing because it doesn’t understand that .base and .base-foo are two different classes * Remove extraneous, non-matching selectors from utilities * Update changelog
This commit is contained in:
parent
7c337f24fc
commit
1402be2dd0
@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Support PostCSS config options in config file in CLI ([#8226](https://github.com/tailwindlabs/tailwindcss/pull/8226))
|
||||
- Remove default `[hidden]` style in preflight ([#8248](https://github.com/tailwindlabs/tailwindcss/pull/8248))
|
||||
- Only check selectors containing base apply candidates for circular dependencies ([#8222](https://github.com/tailwindlabs/tailwindcss/pull/8222))
|
||||
- Handle utilities with multiple and/or grouped selectors better ([#8262](https://github.com/tailwindlabs/tailwindcss/pull/8262))
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@ -30,6 +30,8 @@ export function formatVariantSelector(current, ...others) {
|
||||
}
|
||||
|
||||
export function finalizeSelector(format, { selector, candidate, context }) {
|
||||
let ast = selectorParser().astSync(selector)
|
||||
|
||||
let separator = context?.tailwindConfig?.separator ?? ':'
|
||||
|
||||
// Split by the separator, but ignore the separator inside square brackets:
|
||||
@ -48,6 +50,19 @@ export function finalizeSelector(format, { selector, candidate, context }) {
|
||||
|
||||
format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
|
||||
|
||||
let formatAst = selectorParser().astSync(format)
|
||||
|
||||
// Remove extraneous selectors that do not include the base class/candidate being matched against
|
||||
// For example if we have a utility defined `.a, .b { color: red}`
|
||||
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
|
||||
ast.each((node) => {
|
||||
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
|
||||
|
||||
if (!hasClassesMatchingCandidate) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// Normalize escaped classes, e.g.:
|
||||
//
|
||||
// The idea would be to replace the escaped `base` in the selector with the
|
||||
@ -59,65 +74,61 @@ export function finalizeSelector(format, { selector, candidate, context }) {
|
||||
// base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
|
||||
// escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
|
||||
//
|
||||
selector = selectorParser((selectors) => {
|
||||
return selectors.walkClasses((node) => {
|
||||
if (node.raws && node.value.includes(base)) {
|
||||
node.raws.value = escapeClassName(unescape(node.raws.value))
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
}).processSync(selector)
|
||||
ast.walkClasses((node) => {
|
||||
if (node.raws && node.value.includes(base)) {
|
||||
node.raws.value = escapeClassName(unescape(node.raws.value))
|
||||
}
|
||||
})
|
||||
|
||||
// We can safely replace the escaped base now, since the `base` section is
|
||||
// now in a normalized escaped value.
|
||||
selector = selector.replace(`.${escapeClassName(base)}`, format)
|
||||
ast.walkClasses((node) => {
|
||||
if (node.value === base) {
|
||||
node.replaceWith(...formatAst.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Remove unnecessary pseudo selectors that we used as placeholders
|
||||
return selectorParser((selectors) => {
|
||||
return selectors.map((selector) => {
|
||||
selector.walkPseudos((p) => {
|
||||
if (selectorFunctions.has(p.value)) {
|
||||
p.replaceWith(p.nodes)
|
||||
}
|
||||
|
||||
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
|
||||
ast.each((selector) => {
|
||||
selector.walkPseudos((p) => {
|
||||
if (selectorFunctions.has(p.value)) {
|
||||
p.replaceWith(p.nodes)
|
||||
}
|
||||
|
||||
let pseudoElements = collectPseudoElements(selector)
|
||||
if (pseudoElements.length > 0) {
|
||||
selector.nodes.push(pseudoElements.sort(sortSelector))
|
||||
}
|
||||
|
||||
return selector
|
||||
})
|
||||
}).processSync(selector)
|
||||
|
||||
let pseudoElements = collectPseudoElements(selector)
|
||||
if (pseudoElements.length > 0) {
|
||||
selector.nodes.push(pseudoElements.sort(sortSelector))
|
||||
}
|
||||
})
|
||||
|
||||
return ast.toString()
|
||||
}
|
||||
|
||||
// Note: As a rule, double colons (::) should be used instead of a single colon
|
||||
|
||||
@ -603,3 +603,131 @@ it('appends variants to the correct place when using postcss documents', () => {
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('variants support multiple, grouped selectors (html)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="sm:base1 sm:base2"></div>` }],
|
||||
plugins: [],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.base1 .foo,
|
||||
.base1 .bar {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.base2 .bar .base2-foo {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
return expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 640px) {
|
||||
.sm\:base1 .foo,
|
||||
.sm\:base1 .bar {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.sm\:base2 .bar .base2-foo {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('variants support multiple, grouped selectors (apply)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="baz"></div>` }],
|
||||
plugins: [],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.base .foo,
|
||||
.base .bar {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.baz {
|
||||
@apply sm:base;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
return expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 640px) {
|
||||
.baz .foo,
|
||||
.baz .bar {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('variants only picks the used selectors in a group (html)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="sm:b"></div>` }],
|
||||
plugins: [],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.a,
|
||||
.b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
return expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 640px) {
|
||||
.sm\:b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('variants only picks the used selectors in a group (apply)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="baz"></div>` }],
|
||||
plugins: [],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.a,
|
||||
.b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.baz {
|
||||
@apply sm:b;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
return expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 640px) {
|
||||
.baz {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user