diff --git a/src/mergeProps.ts b/src/mergeProps.ts index 95973e2d..c04c4970 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] || ''} ${item[key] || ''}`.trim(); + } 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..8b1d0ac5 100644 --- a/tests/mergeProps.test.ts +++ b/tests/mergeProps.test.ts @@ -49,4 +49,106 @@ 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: '' }); + }); + + 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', () => { + 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, + }, + }); + }); + }); });