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,
+ },
+ });
+ });
+ });
});