Skip to content

Commit 40cb82c

Browse files
Sort classes using position of first matching rule (#11504)
* Refactor * Sort based on first occurence of a candidate This primarily affects components and utilities which contain multiple matched classes * Simplify * Update changelog * Update
1 parent 615c157 commit 40cb82c

File tree

3 files changed

+44
-1
lines changed

3 files changed

+44
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Fallback to RegEx based parser when using custom transformers or extractors ([#11335](https://github.com/tailwindlabs/tailwindcss/pull/11335))
1919
- Move unknown pseudo-elements outside of `:is` by default ([#11345](https://github.com/tailwindlabs/tailwindcss/pull/11345))
2020
- Escape animation names when prefixes contain special characters ([#11470](https://github.com/tailwindlabs/tailwindcss/pull/11470))
21+
- Sort classes using position of first matching rule ([#11504](https://github.com/tailwindlabs/tailwindcss/pull/11504))
2122

2223
### Added
2324

src/lib/setupContextUtils.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,11 @@ function registerPlugins(plugins, context) {
942942
let idx = BigInt(parasiteUtilities.length)
943943

944944
for (const [, rule] of rules) {
945-
sortedClassNames.set(rule.raws.tailwind.candidate, idx++)
945+
let candidate = rule.raws.tailwind.candidate
946+
947+
// When multiple rules match a candidate
948+
// always take the position of the first one
949+
sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++)
946950
}
947951

948952
return classes.map((className) => {

tests/getSortOrder.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,41 @@ it('sorts classes deterministically across multiple class lists', () => {
140140
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
141141
}
142142
})
143+
144+
it('sorts based on first occurence of a candidate / rule', () => {
145+
let classes = [
146+
['foo-1 foo', 'foo foo-1'],
147+
['bar', 'bar'],
148+
['foo-1 foo', 'foo foo-1'],
149+
]
150+
151+
let config = {
152+
theme: {},
153+
plugins: [
154+
function ({ addComponents }) {
155+
addComponents({
156+
'.foo': { display: 'block' },
157+
'.foo-1': { display: 'block' },
158+
'.bar': { display: 'block' },
159+
160+
// This rule matches both the candidate `foo` and `bar`
161+
// But when sorting `foo` — we've already got a
162+
// position for `foo` so we should use it
163+
'.bar .foo': { display: 'block' },
164+
})
165+
},
166+
],
167+
}
168+
169+
// Same context, different class lists
170+
let context = createContext(resolveConfig(config))
171+
for (const [input, output] of classes) {
172+
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
173+
}
174+
175+
// Different context, different class lists
176+
for (const [input, output] of classes) {
177+
context = createContext(resolveConfig(config))
178+
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
179+
}
180+
})

0 commit comments

Comments
 (0)