diff --git a/CHANGELOG.md b/CHANGELOG.md index cb797c11ca34..72537c9141ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create tailwind.config.cjs file in ESM package when running init ([#8363](https://github.com/tailwindlabs/tailwindcss/pull/8363)) - Fix `matchVariants` that use at-rules and placeholders ([#8392](https://github.com/tailwindlabs/tailwindcss/pull/8392)) - Improve types of the `tailwindcss/plugin` ([#8400](https://github.com/tailwindlabs/tailwindcss/pull/8400)) +- Allow returning parallel variants from `addVariant` or `matchVariant` callback functions ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455)) ### Changed diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 3f1ea2d83d50..385faf7bdda5 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -100,7 +100,7 @@ expect.extend({ expect.extend({ // Compare two CSS strings with all whitespace removed // This is probably naive but it's fast and works well enough. - toMatchFormattedCss(received, argument) { + toMatchFormattedCss(received = '', argument = '') { function format(input) { return prettier.format(input.replace(/\n/g, ''), { parser: 'css', diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index d1f216dd4d9b..c5f82d8c391c 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -152,7 +152,7 @@ function applyVariant(variant, matches, context) { } if (context.variantMap.has(variant)) { - let variantFunctionTuples = context.variantMap.get(variant) + let variantFunctionTuples = context.variantMap.get(variant).slice() let result = [] for (let [meta, rule] of matches) { @@ -216,6 +216,26 @@ function applyVariant(variant, matches, context) { args, }) + // It can happen that a list of format strings is returned from within the function. In that + // case, we have to process them as well. We can use the existing `variantSort`. + if (Array.isArray(ruleWithVariant)) { + for (let [idx, variantFunction] of ruleWithVariant.entries()) { + // This is a little bit scary since we are pushing to an array of items that we are + // currently looping over. However, you can also think of it like a processing queue + // where you keep handling jobs until everything is done and each job can queue more + // jobs if needed. + variantFunctionTuples.push([ + // TODO: This could have potential bugs if we shift the sort order from variant A far + // enough into the sort space of variant B. The chances are low, but if this happens + // then this might be the place too look at. One potential solution to this problem is + // reserving additional X places for these 'unknown' variants in between. + variantSort | BigInt(idx << ruleWithVariant.length), + variantFunction, + ]) + } + continue + } + if (typeof ruleWithVariant === 'string') { collectedFormats.push(ruleWithVariant) } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 5f0a3203ede7..004b8f9756cb 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -463,6 +463,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs ) } + if (Array.isArray(result)) { + return result.map((variant) => parseVariant(variant)) + } + // result may be undefined with legacy variants that use APIs like `modifySelectors` return result && parseVariant(result)(api) } diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js index 1f0252ce8178..528a63e9ff94 100644 --- a/tests/match-variants.test.js +++ b/tests/match-variants.test.js @@ -206,3 +206,41 @@ test('matched variant values maintain the sort order they are registered in', () `) }) }) + +test('matchVariant can return an array of format strings from the function', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant({ + test: (selector) => selector.split(',').map((selector) => `&.${selector} > *`), + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .test-\[a\2c b\2c c\]\:underline.a > * { + text-decoration-line: underline; + } + + .test-\[a\2c b\2c c\]\:underline.b > * { + text-decoration-line: underline; + } + + .test-\[a\2c b\2c c\]\:underline.c > * { + text-decoration-line: underline; + } + `) + }) +}) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index 94cb1bc13f90..f1be86832884 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -42,3 +42,46 @@ test('basic parallel variants', async () => { `) }) }) + +test('parallel variants can be generated using a function that returns parallel variants', async () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function test({ addVariant }) { + addVariant('test', () => ['& *::test', '&::test']) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-normal { + font-weight: 400; + } + .test\:font-bold *::test { + font-weight: 700; + } + .test\:font-medium *::test { + font-weight: 500; + } + .test\:font-bold::test { + font-weight: 700; + } + .test\:font-medium::test { + font-weight: 500; + } + .hover\:test\:font-black *:hover::test { + font-weight: 900; + } + .hover\:test\:font-black:hover::test { + font-weight: 900; + } + `) + }) +})