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

Sort arbitrary properties alphabetically across multiple class lists #12911

Merged
merged 2 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/generateRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ function extractArbitraryProperty(classCandidate, context) {
return null
}

let sort = context.offsets.arbitraryProperty()
let sort = context.offsets.arbitraryProperty(classCandidate)

return [
[
Expand Down
61 changes: 60 additions & 1 deletion src/lib/offsets.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { remapBitfield } from './remap-bitfield.js'
* @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 its given *parent* layer. Monotonically increasing.
* @property {bigint} propertyOffset Offset for the arbitrary property. Only valid after sorting.
* @property {string} property Name/Value of the arbitrary property.
* @property {VariantOption[]} options Some information on how we can sort arbitrary variants
*/

Expand Down Expand Up @@ -88,17 +90,21 @@ export class Offsets {
variants: 0n,
parallelIndex: 0n,
index: this.offsets[layer]++,
propertyOffset: 0n,
property: '',
options: [],
}
}

/**
* @param {string} name
* @returns {RuleOffset}
*/
arbitraryProperty() {
arbitraryProperty(name) {
return {
...this.create('utilities'),
arbitrary: 1n,
property: name,
}
}

Expand Down Expand Up @@ -262,6 +268,11 @@ export class Offsets {
return a.arbitrary - b.arbitrary
}

// Always sort arbitrary properties alphabetically
if (a.propertyOffset !== b.propertyOffset) {
return a.propertyOffset - b.propertyOffset
}

// Sort utilities, components, etc… in the order they were registered
return a.index - b.index
}
Expand Down Expand Up @@ -320,14 +331,62 @@ export class Offsets {
})
}

/**
* @template T
* @param {[RuleOffset, T][]} list
* @returns {[RuleOffset, T][]}
*/
sortArbitraryProperties(list) {
// Collect all known arbitrary properties
let known = new Set()

for (let [offset] of list) {
if (offset.arbitrary === 1n) {
known.add(offset.property)
}
}

// No arbitrary properties? Nothing to do.
if (known.size === 0) {
return list
}

// Sort the properties alphabetically
let properties = Array.from(known).sort()

// Create a map from the property name to its offset
let offsets = new Map()

let offset = 1n
for (let property of properties) {
offsets.set(property, offset++)
}

// Apply the sorted offsets to the list
return list.map((item) => {
let [offset, rule] = item

offset = {
...offset,
propertyOffset: offsets.get(offset.property) ?? 0n,
}

return [offset, rule]
})
}

/**
* @template T
* @param {[RuleOffset, T][]} list
* @returns {[RuleOffset, T][]}
*/
sort(list) {
// Sort arbitrary variants so they're in alphabetical order
list = this.remapArbitraryVariantOffsets(list)

// Sort arbitrary properties so they're in alphabetical order
list = this.sortArbitraryProperties(list)

return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
}
}
Expand Down
23 changes: 23 additions & 0 deletions tests/getSortOrder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,26 @@ it('Sorting is unchanged when multiple candidates share the same rule / object',
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
})

it('sorts arbitrary values across one or more class lists consistently', () => {
let classes = [
['[--fg:#fff]', '[--fg:#fff]'],
['[--bg:#111] [--bg_hover:#000] [--fg:#fff]', '[--bg:#111] [--bg_hover:#000] [--fg:#fff]'],
]

let config = {
theme: {},
}

// Same context, different class lists
let context = createContext(resolveConfig(config))
for (const [input, output] of classes) {
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}

// Different context, different class lists
for (const [input, output] of classes) {
context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
})
12 changes: 6 additions & 6 deletions tests/variants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ test('order matters and produces different behaviour', () => {

return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button {
--value: 2;
}
.file\:hover\:\[--value\:2\]:hover::file-selector-button {
--value: 2;
}
.hover\:file\:\[--value\:1\]::-webkit-file-upload-button:hover {
--value: 1;
}
.hover\:file\:\[--value\:1\]::file-selector-button:hover {
--value: 1;
}
.file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button {
--value: 2;
}
.file\:hover\:\[--value\:2\]:hover::file-selector-button {
--value: 2;
}
`)
})
})
Expand Down