Merge suggestions when using @utility (#18900)

This PR fixes a bug where custom `@utility` implementations with a name
that match an existing utility would override the existing suggestions
even though we generate both utilities.

With this, we want to make sure that both the custom and the built-in
utilities are suggested. We also want to make sure that we don't get
duplicate suggestions.

E.g.:

- `font-` would suggest:
  - 'font-black'
  - 'font-bold'
  - 'font-extrabold'
  - 'font-extralight'
  - 'font-light'
  - 'font-medium'
  - 'font-mono'
  - 'font-normal'
  - 'font-sans'
  - 'font-semibold'
  - 'font-serif'
  - 'font-thin'

But if you introduce this little custom utility:

```css
@theme {
  --custom-font-weights-foo: 123;
}

@utility font-* {
  --my-weight: --value(--custom-font-weights- *);
}
```

- `font-` would suggest:
  - 'font-foo'

With this fix, we would suggest:

- `font-` would suggest:
  - 'font-black'
  - 'font-bold'
  - 'font-extrabold'
  - 'font-extralight'
  - 'font-foo'          // This is now added
  - 'font-light'
  - 'font-medium'
  - 'font-mono'
  - 'font-normal'
  - 'font-sans'
  - 'font-semibold'
  - 'font-serif'
  - 'font-thin'

We also make sure that they are unique, so if you have a custom utility
that happens to match another existing utility (e.g. `font-bold`), you
won't see `font-bold` twice in the suggestions.

```css
@theme {
  --custom-font-weights-bold: bold;
  --custom-font-weights-normal: normal;
  --custom-font-weights-foo: 1234;
}

@utility font-* {
  --my-weight: --value(--custom-font-weights-*);
}
```

- `font-` would suggest:
  - 'font-black'
  - 'font-bold'          // Overlaps with existing utility
  - 'font-extrabold'
  - 'font-extralight'
  - 'font-foo'           // This is now added
  - 'font-light'
  - 'font-medium'
  - 'font-mono'
  - 'font-normal'        // Overlaps with existing utility
  - 'font-sans'
  - 'font-semibold'
  - 'font-serif'
  - 'font-thin'
This commit is contained in:
Robin Malfait 2025-09-08 12:18:30 +02:00 committed by GitHub
parent 77b3cb5318
commit 2f1cbbfed2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 43 additions and 3 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888))
- Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885))
- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900))
## [4.1.13] - 2025-09-03

View File

@ -572,6 +572,39 @@ test('Custom functional @utility', async () => {
expect(classMap.get('example-xs')?.modifiers).toEqual(['normal', 'foo', 'bar'])
})
test('Custom utilities sharing a root with built-in utilities should merge suggestions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@theme {
--font-sans: sans-serif;
}
@theme {
--font-weight-custom: 1234;
--font-weight-bold: bold; /* Overlap with existing utility */
}
@utility font-* {
--my-font-weight: --value(--font-weight- *);
}
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
path: '',
base,
content: '@tailwind utilities;',
}),
})
let classMap = new Map(design.getClassList())
let classNames = Array.from(classMap.keys())
expect(classNames).toContain('font-sans') // Existing font-family utility
expect(classNames).toContain('font-bold') // Existing font-family utility & custom font-weight utility
expect(classNames).toContain('font-custom') // Custom font-weight utility
})
test('Theme keys with underscores are suggested with underscores', async () => {
let input = css`
@import 'tailwindcss/utilities';

View File

@ -55,6 +55,9 @@ export function getClassList(design: DesignSystem): ClassEntry[] {
item.fraction ||= fraction
item.modifiers.push(...group.modifiers)
}
// Deduplicate modifiers
item.modifiers = Array.from(new Set(item.modifiers))
}
}
}

View File

@ -124,9 +124,12 @@ export class Utilities {
}
suggest(name: string, groups: () => SuggestionGroup[]) {
// TODO: We are calling this multiple times on purpose but ideally only ever
// once per utility root.
this.completions.set(name, groups)
let existingGroups = this.completions.get(name)
if (existingGroups) {
this.completions.set(name, () => [...existingGroups?.(), ...groups?.()])
} else {
this.completions.set(name, groups)
}
}
keys(kind: 'static' | 'functional') {