From fe15e02fb33f1aec0c82f51a2fa7135ea7534fdf Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Sat, 20 Sep 2025 18:18:01 -0700 Subject: [PATCH] fix(shared): class presence overrides --- .../__tests__/ssrRenderAttrs.spec.ts | 6 ++ .../shared/__tests__/normalizeProp.spec.ts | 37 ++++++++++++ packages/shared/src/normalizeProp.ts | 60 +++++++++++++++---- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 984387bb864..d7b69d4d93a 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -144,6 +144,12 @@ describe('ssr: renderClass', () => { expect(ssrRenderClass(`"> { + expect( + ssrRenderClass(['foo foo', { foo: true }, 'bar', { bar: false }]), + ).toBe('foo foo foo') + }) + test('className', () => { expect( ssrRenderAttrs({ diff --git a/packages/shared/__tests__/normalizeProp.spec.ts b/packages/shared/__tests__/normalizeProp.spec.ts index 00c6b294da1..c1efdc23026 100644 --- a/packages/shared/__tests__/normalizeProp.spec.ts +++ b/packages/shared/__tests__/normalizeProp.spec.ts @@ -76,6 +76,43 @@ describe('normalizeClass', () => { ).toEqual('foo bar baz qux quux') }) + test('handles overrides correctly', () => { + expect( + normalizeClass([ + { foo: true }, + + 'bar', + { bar: false, foo: undefined }, + + ' baz ', + { baz: false }, + + { qux: false }, + ' qux \n baz ', + + { quux: true }, + [' quux quux2 ', [{ ' quux quux3 ': false }]], + + 'quux3', + ]), + ).toEqual('qux baz quux2 quux3') + }) + + test('handles duplicates correctly', () => { + expect( + normalizeClass([ + 'foo foo baz baz baz', + { foo: true }, + + 'bar bar ', + { bar: false }, + + { baz: false }, + 'baz baz', + ]), + ).toEqual('foo foo foo baz baz') + }) + // #6777 test('parse multi-line inline style', () => { expect( diff --git a/packages/shared/src/normalizeProp.ts b/packages/shared/src/normalizeProp.ts index ef598a03ced..dc9ba856dc8 100644 --- a/packages/shared/src/normalizeProp.ts +++ b/packages/shared/src/normalizeProp.ts @@ -61,24 +61,64 @@ export function stringifyStyle( } export function normalizeClass(value: unknown): string { - let res = '' + // do not implicitly deduplicate classes if user duplicates same class + const classes: (string | undefined)[] = [] + // keep track of where in the above array each class name is specified + // to allow e.g. `

` + // to remove `my-class` from the element + const classIndexes = new Map() + + normalizeClassHelper(value, classes, classIndexes) + + let str = '' + for (const klass of classes) { + if (klass) { + str += klass + ' ' + } + } + return str.trim() +} + +function normalizeClassHelper( + value: unknown, + classes: (string | undefined)[], + classIndexes: Map, +): void { if (isString(value)) { - res = value + const splitClasses = value.trim().split(/\s+/) + for (const klass of splitClasses) { + if (!classIndexes.has(klass)) { + classIndexes.set(klass, []) + } + classIndexes.get(klass)!.push(classes.length) + classes.push(klass) + } } else if (isArray(value)) { for (let i = 0; i < value.length; i++) { - const normalized = normalizeClass(value[i]) - if (normalized) { - res += normalized + ' ' - } + normalizeClassHelper(value[i], classes, classIndexes) } } else if (isObject(value)) { - for (const name in value) { - if (value[name]) { - res += name + ' ' + for (const names in value) { + const splitClasses = names.trim().split(/\s+/) + for (const klass of splitClasses) { + if (value[names]) { + if (!classIndexes.has(klass)) { + classIndexes.set(klass, []) + } + classIndexes.get(klass)!.push(classes.length) + classes.push(klass) + } else { + const indexes = classIndexes.get(klass) + if (indexes) { + for (const i of indexes) { + classes[i] = undefined + } + classIndexes.set(klass, []) + } + } } } } - return res.trim() } export function normalizeProps(