Skip to content

Commit

Permalink
Don't unset keys like --font-weight-* when unsetting --font-* (#1…
Browse files Browse the repository at this point in the history
…4906)

This PR fixes an issue where unsetting `--font-*` would unset
`--font-weight-*` and `--font-size-*`.

```css
@theme {
  --font-weight-bold: bold;
  --font-size-sm: 14px;
  --font-sans: sans-serif;
  --font-serif: serif;
}
@theme {
  --font-*: initial;
  --font-body: Inter;
}
```

Up until now this was sort of intended/desired behavior but with recent
changes there are now more overlapping theme keys (`--inset-*` and
`--inset-shadow-*` as well for example), and we don't want to make it
impossible to unset _just_ the default `font-family` values.

This PR also simplifies how we were handling making sure that the
`inset-*` utility ignored `--inset-shadow-*` variables since it's all
really the same problem.

This does mean we need to maintain a list of known theme keys so we know
when there is a conflict between two keys, which is kind of unfortunate
because up until now this was a totally dynamic thing. End users can
still add whatever custom stuff they want under `@theme` but we don't
really know about those namespaces since we're maintaining a static list
so we can't resolve conflicts there. I'm confident there are ways we
could solve this if it actually becomes a problem, so content to push
forward without solving it right now and just deal with it if/when it
actually arises, because it just might not.

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
  • Loading branch information
adamwathan and adamwathan authored Nov 7, 2024
1 parent 15fc7f4 commit a4f8a36
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 115 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873))
- Rebase `url()` inside imported CSS files when using Vite ([#14877](https://github.com/tailwindlabs/tailwindcss/pull/14877))
- Ensure that CSS transforms from other Vite plugins correctly work in full builds (e.g. `:deep()` in Vue) ([#14871](https://github.com/tailwindlabs/tailwindcss/pull/14871))
- Don't unset keys like `--inset-shadow-*` when unsetting keys like `--inset-*` ([#14906](https://github.com/tailwindlabs/tailwindcss/pull/14906))
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
- _Upgrade (experimental)_: Fix crash during upgrade when content globs escape root of project ([#14896](https://github.com/tailwindlabs/tailwindcss/pull/14896))
Expand Down
173 changes: 173 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,179 @@ describe('Parsing themes values from CSS', () => {
`)
})

test('unsetting `--font-*` does not unset `--font-weight-*` or `--font-size-*`', async () => {
expect(
await compileCss(
css`
@theme {
--font-weight-bold: bold;
--font-size-sm: 14px;
--font-sans: sans-serif;
--font-serif: serif;
}
@theme {
--font-*: initial;
--font-body: Inter;
}
@tailwind utilities;
`,
['font-bold', 'text-sm', 'font-sans', 'font-serif', 'font-body'],
),
).toMatchInlineSnapshot(`
":root {
--font-weight-bold: bold;
--font-size-sm: 14px;
--font-body: Inter;
}
.font-body {
font-family: var(--font-body);
}
.text-sm {
font-size: var(--font-size-sm);
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-font-weight: initial;
}
}
}
@property --tw-font-weight {
syntax: "*";
inherits: false
}"
`)
})

test('unsetting `--inset-*` does not unset `--inset-shadow-*`', async () => {
expect(
await compileCss(
css`
@theme {
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
--inset-lg: 100px;
--inset-sm: 10px;
}
@theme {
--inset-*: initial;
--inset-md: 50px;
}
@tailwind utilities;
`,
['inset-shadow-sm', 'inset-ring-thick', 'inset-lg', 'inset-sm', 'inset-md'],
),
).toMatchInlineSnapshot(`
":root {
--inset-shadow-sm: inset 0 2px 4px #0000000d;
--inset-md: 50px;
}
.inset-md {
inset: var(--inset-md);
}
.inset-shadow-sm {
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, #0000000d);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
}
}
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false
}
@property --tw-ring-color {
syntax: "*";
inherits: false
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}"
`)
})

test('unused keyframes are removed when an animation is unset', async () => {
expect(
await compileCss(
Expand Down
1 change: 0 additions & 1 deletion packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ function loadDesignSystem() {
theme.add('--opacity-background', '0.3')
theme.add('--drop-shadow-sm', '0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--inset-shadow-sm', 'inset 0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--inset-ring-big', '100px')
return buildDesignSystem(theme)
}

Expand Down
64 changes: 32 additions & 32 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ export const enum ThemeOptions {
DEFAULT = 1 << 2,
}

function isIgnoredThemeKey(themeKey: ThemeKey, ignoredThemeKeys: ThemeKey[]) {
return ignoredThemeKeys.some(
// In the future we may want to replace this with just a `Set` of known theme
// keys and let the computer sort out which keys should ignored which other keys
// based on overlapping prefixes.
const ignoredThemeKeyMap = new Map([
['--font', ['--font-weight', '--font-size']],
['--inset', ['--inset-shadow', '--inset-ring']],
])

function isIgnoredThemeKey(themeKey: ThemeKey, namespace: ThemeKey) {
return (ignoredThemeKeyMap.get(namespace) ?? []).some(
(ignoredThemeKey) => themeKey === ignoredThemeKey || themeKey.startsWith(`${ignoredThemeKey}-`),
)
}
Expand Down Expand Up @@ -50,22 +58,22 @@ export class Theme {
}
}

keysInNamespaces(themeKeys: ThemeKey[], ignoredThemeKeys: ThemeKey[] = []): string[] {
keysInNamespaces(themeKeys: ThemeKey[]): string[] {
let keys: string[] = []

for (let prefix of themeKeys) {
let namespace = `${prefix}-`
for (let namespace of themeKeys) {
let prefix = `${namespace}-`

for (let key of this.values.keys()) {
if (!key.startsWith(namespace)) continue
if (!key.startsWith(prefix)) continue

if (key.indexOf('--', 2) !== -1) continue

if (isIgnoredThemeKey(key as ThemeKey, ignoredThemeKeys)) {
if (isIgnoredThemeKey(key as ThemeKey, namespace)) {
continue
}

keys.push(key.slice(namespace.length))
keys.push(key.slice(prefix.length))
}
}

Expand Down Expand Up @@ -106,32 +114,33 @@ export class Theme {
}

clearNamespace(namespace: string, clearOptions: ThemeOptions) {
for (let key of this.values.keys()) {
let ignored = ignoredThemeKeyMap.get(namespace) ?? []

outer: for (let key of this.values.keys()) {
if (key.startsWith(namespace)) {
if (clearOptions !== ThemeOptions.NONE) {
let options = this.getOptions(key)
if ((options & clearOptions) !== clearOptions) {
continue
}
}
for (let ignoredNamespace of ignored) {
if (key.startsWith(ignoredNamespace)) continue outer
}
this.values.delete(key)
}
}
}

#resolveKey(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
for (let key of themeKeys) {
#resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
for (let namespace of themeKeys) {
let themeKey =
candidateValue !== null
? (escape(`${key}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
: key
? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
: namespace

if (!this.values.has(themeKey)) continue
if (isIgnoredThemeKey(themeKey, ignoredThemeKeys)) continue
if (isIgnoredThemeKey(themeKey, namespace)) continue

return themeKey
}
Expand All @@ -147,12 +156,8 @@ export class Theme {
return `var(${this.#prefixKey(themeKey)})`
}

resolve(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand All @@ -165,12 +170,8 @@ export class Theme {
return this.#var(themeKey)
}

resolveValue(
candidateValue: string | null,
themeKeys: ThemeKey[],
ignoredThemeKeys: ThemeKey[] = [],
): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
resolveValue(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand All @@ -181,9 +182,8 @@ export class Theme {
candidateValue: string,
themeKeys: ThemeKey[],
nestedKeys: `--${string}`[] = [],
ignoredThemeKeys: ThemeKey[] = [],
): [string, Record<string, string>] | null {
let themeKey = this.#resolveKey(candidateValue, themeKeys, ignoredThemeKeys)
let themeKey = this.#resolveKey(candidateValue, themeKeys)

if (!themeKey) return null

Expand Down
Loading

0 comments on commit a4f8a36

Please sign in to comment.