mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Experimental support for variant grouping (#8405)
* WIP * use correct separator * run all tests * Fix regex * add a few more tests * name the experimental feature flag `variantGrouping` * update changelog * rename test file `variant-grouping` Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
816a0f26c9
commit
68ff4ba500
@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add arbitrary variants ([#8299](https://github.com/tailwindlabs/tailwindcss/pull/8299))
|
||||
- Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310))
|
||||
- Add `prefers-contrast` media query variants ([#8410](https://github.com/tailwindlabs/tailwindcss/pull/8410))
|
||||
- Experimental support for variant grouping ([#8405](https://github.com/tailwindlabs/tailwindcss/pull/8405))
|
||||
|
||||
## [3.0.24] - 2022-04-12
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ let defaults = {
|
||||
|
||||
let featureFlags = {
|
||||
future: ['hoverOnlyWhenSupported'],
|
||||
experimental: ['optimizeUniversalDefaults'],
|
||||
experimental: ['optimizeUniversalDefaults', 'variantGrouping'],
|
||||
}
|
||||
|
||||
export function flagEnabled(config, flag) {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
import * as regex from './regex'
|
||||
|
||||
export function defaultExtractor(context) {
|
||||
@ -20,6 +21,7 @@ export function defaultExtractor(context) {
|
||||
|
||||
function* buildRegExps(context) {
|
||||
let separator = context.tailwindConfig.separator
|
||||
let variantGroupingEnabled = flagEnabled(context.tailwindConfig, 'variantGrouping')
|
||||
|
||||
yield regex.pattern([
|
||||
// Variants
|
||||
@ -43,7 +45,7 @@ function* buildRegExps(context) {
|
||||
// Utilities
|
||||
regex.pattern([
|
||||
// Utility Name / Group Name
|
||||
/-?(?:\w+)/,
|
||||
variantGroupingEnabled ? /-?(?:[\w,()]+)/ : /-?(?:\w+)/,
|
||||
|
||||
// Normal/Arbitrary values
|
||||
regex.optional(
|
||||
|
||||
@ -12,6 +12,7 @@ import { normalize } from '../util/dataTypes'
|
||||
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
|
||||
import isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
|
||||
let classNameParser = selectorParser((selectors) => {
|
||||
return selectors.first.filter(({ type }) => type === 'class').pop().value
|
||||
@ -444,7 +445,7 @@ function* recordCandidates(matches, classCandidate) {
|
||||
}
|
||||
}
|
||||
|
||||
function* resolveMatches(candidate, context) {
|
||||
function* resolveMatches(candidate, context, original = candidate) {
|
||||
let separator = context.tailwindConfig.separator
|
||||
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
|
||||
let important = false
|
||||
@ -454,6 +455,15 @@ function* resolveMatches(candidate, context) {
|
||||
classCandidate = classCandidate.slice(1)
|
||||
}
|
||||
|
||||
if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
|
||||
if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
|
||||
let base = variants.slice().reverse().join(separator)
|
||||
for (let part of classCandidate.slice(1, -1).split(/\,(?![^(]*\))/g)) {
|
||||
yield* resolveMatches(base + separator + part, context, original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Reintroduce this in ways that doesn't break on false positives
|
||||
// function sortAgainst(toSort, against) {
|
||||
// return toSort.slice().sort((a, z) => {
|
||||
@ -585,7 +595,11 @@ function* resolveMatches(candidate, context) {
|
||||
|
||||
rule.selector = finalizeSelector(finalFormat, {
|
||||
selector: rule.selector,
|
||||
candidate,
|
||||
candidate: original,
|
||||
base: candidate
|
||||
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
||||
.pop(),
|
||||
|
||||
context,
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,21 +29,27 @@ export function formatVariantSelector(current, ...others) {
|
||||
return current
|
||||
}
|
||||
|
||||
export function finalizeSelector(format, { selector, candidate, context }) {
|
||||
export function finalizeSelector(
|
||||
format,
|
||||
{
|
||||
selector,
|
||||
candidate,
|
||||
context,
|
||||
|
||||
// Split by the separator, but ignore the separator inside square brackets:
|
||||
//
|
||||
// E.g.: dark:lg:hover:[paint-order:markers]
|
||||
// ┬ ┬ ┬ ┬
|
||||
// │ │ │ ╰── We will not split here
|
||||
// ╰──┴─────┴─────────────── We will split here
|
||||
//
|
||||
base = candidate
|
||||
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
||||
.pop(),
|
||||
}
|
||||
) {
|
||||
let ast = selectorParser().astSync(selector)
|
||||
|
||||
let separator = context?.tailwindConfig?.separator ?? ':'
|
||||
|
||||
// Split by the separator, but ignore the separator inside square brackets:
|
||||
//
|
||||
// E.g.: dark:lg:hover:[paint-order:markers]
|
||||
// ┬ ┬ ┬ ┬
|
||||
// │ │ │ ╰── We will not split here
|
||||
// ╰──┴─────┴─────────────── We will split here
|
||||
//
|
||||
let splitter = new RegExp(`\\${separator}(?![^[]*\\])`)
|
||||
let base = candidate.split(splitter).pop()
|
||||
|
||||
if (context?.tailwindConfig?.prefix) {
|
||||
format = prefixSelector(context.tailwindConfig.prefix, format)
|
||||
}
|
||||
|
||||
198
tests/variant-grouping.test.js
Normal file
198
tests/variant-grouping.test.js
Normal file
@ -0,0 +1,198 @@
|
||||
import { run, html, css } from './util/run'
|
||||
|
||||
// TODO: Remove this once we enable this by default
|
||||
it('should not generate nested selectors if the feature flag is not enabled', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="md:(underline,italic)"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to group variants', () => {
|
||||
let config = {
|
||||
experimental: 'all',
|
||||
content: [{ raw: html`<div class="md:(underline,italic)"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 768px) {
|
||||
.md\:\(underline\2c italic\) {
|
||||
font-style: italic;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to group multiple variants', () => {
|
||||
let config = {
|
||||
experimental: 'all',
|
||||
content: [{ raw: html`<div class="md:dark:(underline,italic)"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 768px) {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.md\:dark\:\(underline\2c italic\) {
|
||||
font-style: italic;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to group nested grouped variants', () => {
|
||||
let config = {
|
||||
experimental: 'all',
|
||||
content: [{ raw: html`<div class="md:(underline,italic,hover:(uppercase,font-bold))"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 768px) {
|
||||
.md\:\(underline\2c italic\2c hover\:\(uppercase\2c font-bold\)\) {
|
||||
font-style: italic;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.md\:\(underline\2c italic\2c hover\:\(uppercase\2c font-bold\)\):hover {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to use nested multiple grouped variants', () => {
|
||||
let config = {
|
||||
experimental: 'all',
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="md:(text-black,dark:(text-white,hover:focus:text-gray-100))"></div>`,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
@media (min-width: 768px) {
|
||||
.md\:\(text-black\2c dark\:\(text-white\2c hover\:focus\:text-gray-100\)\) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(0 0 0 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.md\:\(text-black\2c dark\:\(text-white\2c hover\:focus\:text-gray-100\)\) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
.md\:\(text-black\2c dark\:\(text-white\2c hover\:focus\:text-gray-100\)\):focus:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should group with variants defined in external plugins', () => {
|
||||
let config = {
|
||||
experimental: 'all',
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
<div class="ui-active:(bg-black,text-white) ui-selected:(bg-indigo-500,underline)"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ addVariant }) => {
|
||||
addVariant('ui-active', ['&[data-ui-state~="active"]', '[data-ui-state~="active"] &'])
|
||||
addVariant('ui-selected', ['&[data-ui-state~="selected"]', '[data-ui-state~="selected"] &'])
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.ui-active\:\(bg-black\2c text-white\)[data-ui-state~='active'] {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
[data-ui-state~='active'] .ui-active\:\(bg-black\2c text-white\) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.ui-selected\:\(bg-indigo-500\2c underline\)[data-ui-state~='selected'] {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
[data-ui-state~='selected'] .ui-selected\:\(bg-indigo-500\2c underline\) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user