Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow returning parallel variants from addVariant or matchVariant callback functions #8455

Merged
merged 9 commits into from May 31, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
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
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
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
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
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
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;
}
`)
})
})