Support sort function in matchVariant (#9423)

* support `sort` function in `matchVariant`

This will ensure that we can sort arbitrary variant values (and
hardcoded values) to ensure the order.

* update changelog
This commit is contained in:
Robin Malfait 2022-09-26 19:08:12 +02:00 committed by GitHub
parent 19b86e62e0
commit 26cab53c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 471 additions and 5 deletions

View File

@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don't emit generated utilities with invalid uses of theme functions ([#9319](https://github.com/tailwindlabs/tailwindcss/pull/9319))
- Revert change that only listened for stdin close on TTYs ([#9331](https://github.com/tailwindlabs/tailwindcss/pull/9331))
- Ignore unset values (like `null` or `undefined`) when resolving the classList for intellisense ([#9385](https://github.com/tailwindlabs/tailwindcss/pull/9385))
- Support `sort` function in `matchVariant` ([#9423](https://github.com/tailwindlabs/tailwindcss/pull/9423))
## [3.1.8] - 2022-08-05

View File

@ -294,7 +294,11 @@ function applyVariant(variant, matches, context) {
let withOffset = [
{
...meta,
sort: context.offsets.applyVariantOffset(meta.sort, variantSort),
sort: context.offsets.applyVariantOffset(
meta.sort,
variantSort,
Object.assign({ value: args }, context.variantOptions.get(variant))
),
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
isArbitraryVariant: isArbitraryValue(variant),
},

View File

@ -6,6 +6,13 @@ import bigSign from '../util/bigSign'
* @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer
*/
/**
* @typedef {object} VariantOption
* @property {number} id An unique identifier to identify `matchVariant`
* @property {function | undefined} sort The sort function
* @property {string} value The value we want to compare
*/
/**
* @typedef {object} RuleOffset
* @property {Layer} layer The layer that this rule belongs to
@ -14,6 +21,7 @@ import bigSign from '../util/bigSign'
* @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
* @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
* @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing.
* @property {VariantOption[]} options Some information on how we can sort arbitrary variants
*/
export class Offsets {
@ -77,6 +85,7 @@ export class Offsets {
variants: 0n,
parallelIndex: 0n,
index: this.offsets[layer]++,
options: [],
}
}
@ -112,14 +121,16 @@ export class Offsets {
/**
* @param {RuleOffset} rule
* @param {RuleOffset} variant
* @param {VariantOption} options
* @returns {RuleOffset}
*/
applyVariantOffset(rule, variant) {
applyVariantOffset(rule, variant, options) {
return {
...rule,
layer: 'variants',
parentLayer: rule.layer === 'variants' ? rule.parentLayer : rule.layer,
variants: rule.variants | variant.variants,
options: options.sort ? [].concat(options, rule.options) : rule.options,
// TODO: Technically this is wrong. We should be handling parallel index on a per variant basis.
// We'll take the max of all the parallel indexes for now.
@ -151,7 +162,7 @@ export class Offsets {
* @param {(name: string) => number} getLength
*/
recordVariants(variants, getLength) {
for (const variant of variants) {
for (let variant of variants) {
this.recordVariant(variant, getLength(variant))
}
}
@ -193,6 +204,16 @@ export class Offsets {
return this.layerPositions[a.layer] - this.layerPositions[b.layer]
}
// Sort based on the sorting function
for (let aOptions of a.options) {
for (let bOptions of b.options) {
if (aOptions.id !== bOptions.id) continue
if (!aOptions.sort || !bOptions.sort) continue
let result = aOptions.sort(aOptions.value, bOptions.value)
if (result !== 0) return result
}
}
// Sort variants in the order they were registered
if (a.variants !== b.variants) {
return a.variants - b.variants

View File

@ -496,19 +496,23 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
insertInto(variantList, variantName, options)
variantMap.set(variantName, variantFunctions)
context.variantOptions.set(variantName, options)
},
}
if (flagEnabled(tailwindConfig, 'matchVariant')) {
let variantIdentifier = 0
api.matchVariant = function (variant, variantFn, options) {
let id = ++variantIdentifier // A unique identifier that "groups" these variables together.
for (let [key, value] of Object.entries(options?.values ?? {})) {
api.addVariant(`${variant}-${key}`, variantFn({ value }))
api.addVariant(`${variant}-${key}`, variantFn({ value }), { ...options, value, id })
}
api.addVariant(
variant,
Object.assign(({ args }) => variantFn({ value: args }), { [MATCH_VARIANT]: true }),
options
{ ...options, id }
)
}
}
@ -919,6 +923,7 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
changedContent: changedContent,
variantMap: new Map(),
stylesheetCache: null,
variantOptions: new Map(),
markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate),
markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node),

View File

@ -234,3 +234,438 @@ test('matchVariant can return an array of format strings from the function', ()
`)
})
})
it('should be possible to sort variants', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="min-[500px]:underline min-[700px]:italic"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 500px) {
.min-\[500px\]\:underline {
text-decoration-line: underline;
}
}
@media (min-width: 700px) {
.min-\[700px\]\:italic {
font-style: italic;
}
}
`)
})
})
it('should be possible to compare arbitrary variants and hardcoded variants', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="min-[700px]:italic min-example:italic min-[500px]:italic"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
values: {
example: '600px',
},
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 500px) {
.min-\[500px\]\:italic {
font-style: italic;
}
}
@media (min-width: 600px) {
.min-example\:italic {
font-style: italic;
}
}
@media (min-width: 700px) {
.min-\[700px\]\:italic {
font-style: italic;
}
}
`)
})
})
it('should be possible to sort stacked arbitrary variants correctly', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<!-- 4 -->
<div class="min-[150px]:max-[400px]:underline"></div>
<!-- 2 -->
<div class="min-[100px]:max-[350px]:underline"></div>
<!-- 1 -->
<div class="min-[100px]:max-[300px]:underline"></div>
<!-- 3 -->
<div class="min-[100px]:max-[400px]:underline"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
sort(a, z) {
return parseInt(z) - parseInt(a)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 100px) {
@media (max-width: 400px) {
.min-\[100px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 350px) {
.min-\[100px\]\:max-\[350px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 300px) {
.min-\[100px\]\:max-\[300px\]\:underline {
text-decoration-line: underline;
}
}
}
@media (min-width: 150px) {
@media (max-width: 400px) {
.min-\[150px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
}
`)
})
})
it('should maintain sort from other variants, if sort functions of arbitrary variants return 0', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="min-[100px]:max-[200px]:focus:underline"></div>
<div class="min-[100px]:max-[200px]:hover:underline"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
sort(a, z) {
return parseInt(z) - parseInt(a)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 100px) {
@media (max-width: 200px) {
.min-\[100px\]\:max-\[200px\]\:hover\:underline:hover {
text-decoration-line: underline;
}
.min-\[100px\]\:max-\[200px\]\:focus\:underline:focus {
text-decoration-line: underline;
}
}
}
`)
})
})
it('should sort arbitrary variants left to right (1)', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="min-[200px]:max-[400px]:underline"></div>
<div class="min-[200px]:max-[300px]:underline"></div>
<div class="min-[100px]:max-[400px]:underline"></div>
<div class="min-[100px]:max-[300px]:underline"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
sort(a, z) {
return parseInt(z) - parseInt(a)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 100px) {
@media (max-width: 400px) {
.min-\[100px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 300px) {
.min-\[100px\]\:max-\[300px\]\:underline {
text-decoration-line: underline;
}
}
}
@media (min-width: 200px) {
@media (max-width: 400px) {
.min-\[200px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 300px) {
.min-\[200px\]\:max-\[300px\]\:underline {
text-decoration-line: underline;
}
}
}
`)
})
})
it('should sort arbitrary variants left to right (2)', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="max-[400px]:min-[200px]:underline"></div>
<div class="max-[300px]:min-[200px]:underline"></div>
<div class="max-[400px]:min-[100px]:underline"></div>
<div class="max-[300px]:min-[100px]:underline"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a) - parseInt(z)
},
})
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
sort(a, z) {
return parseInt(z) - parseInt(a)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (max-width: 400px) {
@media (min-width: 100px) {
.max-\[400px\]\:min-\[100px\]\:underline {
text-decoration-line: underline;
}
}
@media (min-width: 200px) {
.max-\[400px\]\:min-\[200px\]\:underline {
text-decoration-line: underline;
}
}
}
@media (max-width: 300px) {
@media (min-width: 100px) {
.max-\[300px\]\:min-\[100px\]\:underline {
text-decoration-line: underline;
}
}
@media (min-width: 200px) {
.max-\[300px\]\:min-\[200px\]\:underline {
text-decoration-line: underline;
}
}
}
`)
})
})
it('should guarantee that we are not passing values from other variants to the wrong function', () => {
let config = {
experimental: { matchVariant: true },
content: [
{
raw: html`
<div>
<div class="min-[200px]:max-[400px]:underline"></div>
<div class="min-[200px]:max-[300px]:underline"></div>
<div class="min-[100px]:max-[400px]:underline"></div>
<div class="min-[100px]:max-[300px]:underline"></div>
</div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
({ matchVariant }) => {
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
sort(a, z) {
let lookup = ['100px', '200px']
if (lookup.indexOf(a) === -1 || lookup.indexOf(z) === -1) {
throw new Error('We are seeing values that should not be there!')
}
return lookup.indexOf(a) - lookup.indexOf(z)
},
})
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
sort(a, z) {
let lookup = ['300px', '400px']
if (lookup.indexOf(a) === -1 || lookup.indexOf(z) === -1) {
throw new Error('We are seeing values that should not be there!')
}
return lookup.indexOf(z) - lookup.indexOf(a)
},
})
},
],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 100px) {
@media (max-width: 400px) {
.min-\[100px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 300px) {
.min-\[100px\]\:max-\[300px\]\:underline {
text-decoration-line: underline;
}
}
}
@media (min-width: 200px) {
@media (max-width: 400px) {
.min-\[200px\]\:max-\[400px\]\:underline {
text-decoration-line: underline;
}
}
@media (max-width: 300px) {
.min-\[200px\]\:max-\[300px\]\:underline {
text-decoration-line: underline;
}
}
}
`)
})
})