Skip to content

Commit 240a0ad

Browse files
committed
Sort arbitrary properties alphabetically across multiple class lists (#12911)
* Sort arbitrary properties alphabetically across multiple files * Update test
1 parent 9e62bf2 commit 240a0ad

File tree

4 files changed

+107
-15
lines changed

4 files changed

+107
-15
lines changed

src/lib/generateRules.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ function extractArbitraryProperty(classCandidate, context) {
512512
return null
513513
}
514514

515-
let sort = context.offsets.arbitraryProperty()
515+
let sort = context.offsets.arbitraryProperty(classCandidate)
516516

517517
return [
518518
[

src/lib/offsets.js

+61-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import { remapBitfield } from './remap-bitfield.js'
2323
* @property {bigint} arbitrary 0n if false, 1n if true
2424
* @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
2525
* @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
26-
* @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing.
26+
* @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing.
27+
* @property {bigint} propertyOffset Offset for the arbitrary property. Only valid after sorting.
28+
* @property {string} property Name/Value of the arbitrary property.
2729
* @property {VariantOption[]} options Some information on how we can sort arbitrary variants
2830
*/
2931

@@ -88,17 +90,21 @@ export class Offsets {
8890
variants: 0n,
8991
parallelIndex: 0n,
9092
index: this.offsets[layer]++,
93+
propertyOffset: 0n,
94+
property: '',
9195
options: [],
9296
}
9397
}
9498

9599
/**
100+
* @param {string} name
96101
* @returns {RuleOffset}
97102
*/
98-
arbitraryProperty() {
103+
arbitraryProperty(name) {
99104
return {
100105
...this.create('utilities'),
101106
arbitrary: 1n,
107+
property: name,
102108
}
103109
}
104110

@@ -262,6 +268,11 @@ export class Offsets {
262268
return a.arbitrary - b.arbitrary
263269
}
264270

271+
// Always sort arbitrary properties alphabetically
272+
if (a.propertyOffset !== b.propertyOffset) {
273+
return a.propertyOffset - b.propertyOffset
274+
}
275+
265276
// Sort utilities, components, etc… in the order they were registered
266277
return a.index - b.index
267278
}
@@ -320,14 +331,62 @@ export class Offsets {
320331
})
321332
}
322333

334+
/**
335+
* @template T
336+
* @param {[RuleOffset, T][]} list
337+
* @returns {[RuleOffset, T][]}
338+
*/
339+
sortArbitraryProperties(list) {
340+
// Collect all known arbitrary properties
341+
let known = new Set()
342+
343+
for (let [offset] of list) {
344+
if (offset.arbitrary === 1n) {
345+
known.add(offset.property)
346+
}
347+
}
348+
349+
// No arbitrary properties? Nothing to do.
350+
if (known.size === 0) {
351+
return list
352+
}
353+
354+
// Sort the properties alphabetically
355+
let properties = Array.from(known).sort()
356+
357+
// Create a map from the property name to its offset
358+
let offsets = new Map()
359+
360+
let offset = 1n
361+
for (let property of properties) {
362+
offsets.set(property, offset++)
363+
}
364+
365+
// Apply the sorted offsets to the list
366+
return list.map((item) => {
367+
let [offset, rule] = item
368+
369+
offset = {
370+
...offset,
371+
propertyOffset: offsets.get(offset.property) ?? 0n,
372+
}
373+
374+
return [offset, rule]
375+
})
376+
}
377+
323378
/**
324379
* @template T
325380
* @param {[RuleOffset, T][]} list
326381
* @returns {[RuleOffset, T][]}
327382
*/
328383
sort(list) {
384+
// Sort arbitrary variants so they're in alphabetical order
329385
list = this.remapArbitraryVariantOffsets(list)
330386

387+
// Sort arbitrary properties so they're in alphabetical order
388+
list = this.sortArbitraryProperties(list)
389+
331390
return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
332391
}
333392
}

tests/getSortOrder.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,26 @@ it('Sorting is unchanged when multiple candidates share the same rule / object',
220220
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
221221
}
222222
})
223+
224+
it('sorts arbitrary values across one or more class lists consistently', () => {
225+
let classes = [
226+
['[--fg:#fff]', '[--fg:#fff]'],
227+
['[--bg:#111] [--bg_hover:#000] [--fg:#fff]', '[--bg:#111] [--bg_hover:#000] [--fg:#fff]'],
228+
]
229+
230+
let config = {
231+
theme: {},
232+
}
233+
234+
// Same context, different class lists
235+
let context = createContext(resolveConfig(config))
236+
for (const [input, output] of classes) {
237+
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
238+
}
239+
240+
// Different context, different class lists
241+
for (const [input, output] of classes) {
242+
context = createContext(resolveConfig(config))
243+
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
244+
}
245+
})

tests/variants.test.js

+22-12
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,40 @@ crosscheck(({ stable, oxide }) => {
3636
content: [
3737
{
3838
raw: html`
39-
<div class="hover:file:bg-pink-600"></div>
40-
<div class="file:hover:bg-pink-600"></div>
39+
<div class="hover:file:[--value:1]"></div>
40+
<div class="file:hover:[--value:2]"></div>
4141
`,
4242
},
4343
],
4444
}
4545

4646
return run('@tailwind utilities', config).then((result) => {
4747
stable.expect(result.css).toMatchFormattedCss(css`
48-
.file\:hover\:bg-pink-600:hover::file-selector-button {
49-
--tw-bg-opacity: 1;
50-
background-color: rgb(219 39 119 / var(--tw-bg-opacity));
48+
.hover\:file\:\[--value\:1\]::-webkit-file-upload-button:hover {
49+
--value: 1;
5150
}
52-
.hover\:file\:bg-pink-600::file-selector-button:hover {
53-
--tw-bg-opacity: 1;
54-
background-color: rgb(219 39 119 / var(--tw-bg-opacity));
51+
.hover\:file\:\[--value\:1\]::file-selector-button:hover {
52+
--value: 1;
53+
}
54+
.file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button {
55+
--value: 2;
56+
}
57+
.file\:hover\:\[--value\:2\]:hover::file-selector-button {
58+
--value: 2;
5559
}
5660
`)
5761
oxide.expect(result.css).toMatchFormattedCss(css`
58-
.file\:hover\:bg-pink-600:hover::file-selector-button {
59-
background-color: #db2777;
62+
.hover\:file\:\[--value\:1\]::-webkit-file-upload-button:hover {
63+
--value: 1;
64+
}
65+
.hover\:file\:\[--value\:1\]::file-selector-button:hover {
66+
--value: 1;
67+
}
68+
.file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button {
69+
--value: 2;
6070
}
61-
.hover\:file\:bg-pink-600::file-selector-button:hover {
62-
background-color: #db2777;
71+
.file\:hover\:\[--value\:2\]:hover::file-selector-button {
72+
--value: 2;
6373
}
6474
`)
6575
})

0 commit comments

Comments
 (0)