mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-02-01 17:26:34 +00:00
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:
parent
19b86e62e0
commit
26cab53c15
@ -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
|
||||
|
||||
|
||||
@ -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),
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user