From 53a394964b467402e1f86ea146f034e790605441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B9=A4=E4=BB=99?= Date: Thu, 26 Mar 2026 12:58:43 +0800 Subject: [PATCH 1/4] feat(mergeProps): enhance mergeProps to handle className and style merging --- src/mergeProps.ts | 13 ++++++++++++- tests/mergeProps.test.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/mergeProps.ts b/src/mergeProps.ts index 95973e2d..6ae9a87c 100644 --- a/src/mergeProps.ts +++ b/src/mergeProps.ts @@ -1,6 +1,11 @@ /** * Merges multiple props objects into one. Unlike `Object.assign()` or `{ ...a, ...b }`, it skips * properties whose value is explicitly set to `undefined`. + * + * @example + * ```ts + * const { a, b } = mergeProps(defaults, config, props); + * ``` */ function mergeProps(a: A, b: B): B & A; function mergeProps(a: A, b: B, c: C): C & B & A; @@ -11,7 +16,13 @@ function mergeProps(...items: any[]) { if (item) { for (const key of Object.keys(item)) { if (item[key] !== undefined) { - ret[key] = item[key]; + if (key === 'className') { + ret[key] = ret[key] ? `${ret[key]} ${item[key]}` : item[key]; + } else if (key === 'style') { + ret[key] = { ...ret[key], ...item[key] }; + } else { + ret[key] = item[key]; + } } } } diff --git a/tests/mergeProps.test.ts b/tests/mergeProps.test.ts index 53a30e62..f1f508f4 100644 --- a/tests/mergeProps.test.ts +++ b/tests/mergeProps.test.ts @@ -7,6 +7,20 @@ describe('mergeProps', () => { expect(mergeProps(a, b)).toEqual({ foo: 1, bar: 3, baz: 4 }); }); + it('merges className', () => { + const a = { className: 'a' }; + const b = { className: 'b' }; + expect(mergeProps(a, b)).toEqual({ className: 'a b' }); + }); + + it('merges style', () => { + const a = { style: { color: 'red' } }; + const b = { style: { backgroundColor: 'blue' } }; + expect(mergeProps(a, b)).toEqual({ + style: { color: 'red', backgroundColor: 'blue' }, + }); + }); + it('excludes keys with undefined values', () => { const a = { foo: 1, bar: undefined }; const b = { bar: 2 }; From 5e8788b9da9392342fb6e300a9985dd727db28fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B9=A4=E4=BB=99?= Date: Fri, 27 Mar 2026 10:16:22 +0800 Subject: [PATCH 2/4] fix(mergeProps): improve className merging to handle undefined values correctly --- src/mergeProps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mergeProps.ts b/src/mergeProps.ts index 6ae9a87c..c04c4970 100644 --- a/src/mergeProps.ts +++ b/src/mergeProps.ts @@ -17,7 +17,7 @@ function mergeProps(...items: any[]) { for (const key of Object.keys(item)) { if (item[key] !== undefined) { if (key === 'className') { - ret[key] = ret[key] ? `${ret[key]} ${item[key]}` : item[key]; + ret[key] = `${ret[key] || ''} ${item[key] || ''}`.trim(); } else if (key === 'style') { ret[key] = { ...ret[key], ...item[key] }; } else { From 801db8cde61dcce69c2a7a1d550544bc24340cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B9=A4=E4=BB=99?= Date: Fri, 27 Mar 2026 13:14:37 +0800 Subject: [PATCH 3/4] test(mergeProps): expand tests for className and style merging, including handling of undefined and null values --- tests/mergeProps.test.ts | 92 ++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/tests/mergeProps.test.ts b/tests/mergeProps.test.ts index f1f508f4..618ca244 100644 --- a/tests/mergeProps.test.ts +++ b/tests/mergeProps.test.ts @@ -7,20 +7,6 @@ describe('mergeProps', () => { expect(mergeProps(a, b)).toEqual({ foo: 1, bar: 3, baz: 4 }); }); - it('merges className', () => { - const a = { className: 'a' }; - const b = { className: 'b' }; - expect(mergeProps(a, b)).toEqual({ className: 'a b' }); - }); - - it('merges style', () => { - const a = { style: { color: 'red' } }; - const b = { style: { backgroundColor: 'blue' } }; - expect(mergeProps(a, b)).toEqual({ - style: { color: 'red', backgroundColor: 'blue' }, - }); - }); - it('excludes keys with undefined values', () => { const a = { foo: 1, bar: undefined }; const b = { bar: 2 }; @@ -63,4 +49,82 @@ describe('mergeProps', () => { it('handles empty objects', () => { expect(mergeProps({}, { a: 1 }, {})).toEqual({ a: 1 }); }); + + describe('className', () => { + it('merges strings', () => { + const a = { className: 'a' }; + const b = { className: 'b' }; + expect(mergeProps(a, b)).toEqual({ className: 'a b' }); + }); + + it('keeps previous when later is undefined', () => { + expect(mergeProps({ className: 'a' }, { className: undefined })).toEqual({ + className: 'a', + }); + }); + + it('omits key when only undefined', () => { + expect( + (mergeProps as (...items: any[]) => any)({ className: undefined }), + ).toEqual({}); + }); + + it('null merges like empty string', () => { + expect( + mergeProps({ className: 'a' }, { className: null as any }), + ).toEqual({ className: 'a' }); + expect( + mergeProps({ className: null as any }, { className: 'b' }), + ).toEqual({ className: 'b' }); + expect( + (mergeProps as (...items: any[]) => any)({ className: null as any }), + ).toEqual({ className: '' }); + }); + }); + + describe('style', () => { + it('merges objects', () => { + const a = { style: { color: 'red' } }; + const b = { style: { backgroundColor: 'blue' } }; + expect(mergeProps(a, b)).toEqual({ + style: { color: 'red', backgroundColor: 'blue' }, + }); + }); + + it('keeps previous when later is undefined', () => { + expect( + mergeProps({ style: { color: 'red' } }, { style: undefined }), + ).toEqual({ style: { color: 'red' } }); + }); + + it('null source does not wipe previous style', () => { + expect( + mergeProps({ style: { color: 'red' } }, { style: null as any }), + ).toEqual({ style: { color: 'red' } }); + }); + + it('applies when earlier style is undefined or null', () => { + expect( + mergeProps({ style: undefined }, { style: { color: 'red' } }), + ).toEqual({ style: { color: 'red' } }); + expect( + mergeProps({ style: null as any }, { style: { color: 'red' } }), + ).toEqual({ style: { color: 'red' } }); + }); + + it('nested properties may be null or undefined', () => { + expect( + mergeProps( + { style: { color: 'red', margin: undefined as any } }, + { style: { padding: null as any, color: 'blue' } }, + ), + ).toEqual({ + style: { + color: 'blue', + margin: undefined, + padding: null, + }, + }); + }); + }); }); From bdf7adda79c580b91089b744bca900a3d9ee9673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B9=A4=E4=BB=99?= Date: Fri, 27 Mar 2026 13:17:20 +0800 Subject: [PATCH 4/4] test(mergeProps): add tests for handling empty and whitespace-only className values --- tests/mergeProps.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/mergeProps.test.ts b/tests/mergeProps.test.ts index 618ca244..8b1d0ac5 100644 --- a/tests/mergeProps.test.ts +++ b/tests/mergeProps.test.ts @@ -80,6 +80,30 @@ describe('mergeProps', () => { (mergeProps as (...items: any[]) => any)({ className: null as any }), ).toEqual({ className: '' }); }); + + it('empty string className', () => { + expect(mergeProps({ className: 'a' }, { className: '' })).toEqual({ + className: 'a', + }); + expect(mergeProps({ className: '' }, { className: 'b' })).toEqual({ + className: 'b', + }); + expect( + (mergeProps as (...items: any[]) => any)({ className: '' }), + ).toEqual({ className: '' }); + }); + + it('whitespace-only className is trimmed', () => { + expect(mergeProps({ className: 'a' }, { className: ' ' })).toEqual({ + className: 'a', + }); + expect(mergeProps({ className: ' ' }, { className: 'b' })).toEqual({ + className: 'b', + }); + expect( + (mergeProps as (...items: any[]) => any)({ className: ' ' }), + ).toEqual({ className: '' }); + }); }); describe('style', () => {