Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion jest/customMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
22 changes: 21 additions & 1 deletion src/lib/generateRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/setupContextUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
38 changes: 38 additions & 0 deletions tests/match-variants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`<div class="test-[a,b,c]:underline"></div>`,
},
],
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;
}
`)
})
})
43 changes: 43 additions & 0 deletions tests/parallel-variants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`<div
class="hover:test:font-black test:font-bold test:font-medium font-normal"
></div>`,
},
],
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;
}
`)
})
})