From 40cb82ce7513039b019ba18f93e915ac892ef776 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 29 Jun 2023 09:49:53 -0400 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/lib/setupContextUtils.js | 6 +++++- tests/getSortOrder.test.js | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e17969c52074..7ef52f518ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fallback to RegEx based parser when using custom transformers or extractors ([#11335](https://github.com/tailwindlabs/tailwindcss/pull/11335)) - Move unknown pseudo-elements outside of `:is` by default ([#11345](https://github.com/tailwindlabs/tailwindcss/pull/11345)) - Escape animation names when prefixes contain special characters ([#11470](https://github.com/tailwindlabs/tailwindcss/pull/11470)) +- Sort classes using position of first matching rule ([#11504](https://github.com/tailwindlabs/tailwindcss/pull/11504)) ### Added diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 0bd5639833c7..fe2191b7d5fa 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -942,7 +942,11 @@ function registerPlugins(plugins, context) { let idx = BigInt(parasiteUtilities.length) for (const [, rule] of rules) { - sortedClassNames.set(rule.raws.tailwind.candidate, idx++) + let candidate = rule.raws.tailwind.candidate + + // When multiple rules match a candidate + // always take the position of the first one + sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++) } return classes.map((className) => { diff --git a/tests/getSortOrder.test.js b/tests/getSortOrder.test.js index 0863d268d091..10740880f8bb 100644 --- a/tests/getSortOrder.test.js +++ b/tests/getSortOrder.test.js @@ -140,3 +140,41 @@ it('sorts classes deterministically across multiple class lists', () => { expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output) } }) + +it('sorts based on first occurence of a candidate / rule', () => { + let classes = [ + ['foo-1 foo', 'foo foo-1'], + ['bar', 'bar'], + ['foo-1 foo', 'foo foo-1'], + ] + + let config = { + theme: {}, + plugins: [ + function ({ addComponents }) { + addComponents({ + '.foo': { display: 'block' }, + '.foo-1': { display: 'block' }, + '.bar': { display: 'block' }, + + // This rule matches both the candidate `foo` and `bar` + // But when sorting `foo` — we've already got a + // position for `foo` so we should use it + '.bar .foo': { display: 'block' }, + }) + }, + ], + } + + // 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) + } +})