diff --git a/src/docs/customize/global-props.md b/src/docs/customize/global-props.md index 0185350b..395b57bc 100644 --- a/src/docs/customize/global-props.md +++ b/src/docs/customize/global-props.md @@ -83,3 +83,13 @@ React.createElement(() => { ); }); ``` + +## Nested Usage + +Global props can be nested. This is useful e.g. when you want to configure +props across whole application and then override some of them in a specific +part of the application. + +When nested `RUIProvider` is used, the props are merged deeply together. This +means that you can extend specific object with new props or override existing +ones. If you need to remove some prop, you can set it to `undefined`. diff --git a/src/provider/RUIProvider.jsx b/src/provider/RUIProvider.jsx index 6a7b522f..e6067cc4 100644 --- a/src/provider/RUIProvider.jsx +++ b/src/provider/RUIProvider.jsx @@ -1,8 +1,10 @@ import PropTypes from 'prop-types'; import React, { + useContext, useMemo, } from 'react'; import defaultTranslations from '../translations/en'; +import { mergeDeep } from '../utils/mergeDeep'; import RUIContext from './RUIContext'; const RUIProvider = ({ @@ -10,10 +12,11 @@ const RUIProvider = ({ globalProps, translations, }) => { + const context = useContext(RUIContext); const childProps = useMemo(() => ({ - globalProps, - translations, - }), [globalProps, translations]); + globalProps: mergeDeep(context?.globalProps || {}, globalProps), + translations: mergeDeep(context?.translations || {}, translations), + }), [context, globalProps, translations]); return ( { @@ -36,4 +37,77 @@ describe('rendering', () => { assert(dom.container.firstChild); }); + + it('renders with nested providers', () => { + const dom = render(( + + + + +
+ Content text +
+
+
+
+
+ )); + + // Assert alignContent + expect(dom.container.firstChild.style.cssText.includes('--rui-local-align-content')).toBeFalsy(); + + // Assert autoFlow + expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-lg: column')).toBeTruthy(); + expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-md: column')).toBeTruthy(); + expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-sm')).toBeFalsy(); + expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-xs: row dense')).toBeTruthy(); + + // Assert justifyContent + expect(dom.container.firstChild.style.cssText.includes('--rui-local-justify-content-xs: center;')).toBeTruthy(); + + // Assert justifyItems + expect(dom.container.firstChild.style.cssText.includes('--rui-local-justify-items')).toBeFalsy(); + + // Assert tag + expect(dom.container.firstChild.tagName).toEqual('SECTION'); + }); }); + diff --git a/src/utils/__tests__/mergeDeep.js b/src/utils/__tests__/mergeDeep.js new file mode 100644 index 00000000..5341658e --- /dev/null +++ b/src/utils/__tests__/mergeDeep.js @@ -0,0 +1,80 @@ +import { mergeDeep } from '../mergeDeep'; + +describe('mergeDeep', () => { + it('adds new attributes', () => { + const obj1 = {}; + const obj2 = { + props: { + className: 'class', + style: { + color: 'white', + }, + }, + state: { + items: [1, 2], + itemsSize: 2, + }, + }; + const expectedObj = { + props: { + className: 'class', + style: { + color: 'white', + }, + }, + state: { + items: [1, 2], + itemsSize: 2, + }, + }; + + expect(mergeDeep(obj1, obj2)).toEqual(expectedObj); + }); + + it('merges with existing attributes', () => { + const obj1 = { + props: { + children: ['child1', 'child2'], + className: 'class', + parent: 'parent', + style: { + color: 'white', + }, + }, + state: { + items: [1, 2], + itemsSize: 2, + }, + }; + const obj2 = { + props: { + children: null, + className: 'class1 class2', + style: { + backgroundColor: 'black', + }, + }, + state: { + items: [3, 4, 5], + itemsSize: 5, + }, + }; + const expectedObj = { + props: { + children: null, + className: 'class1 class2', + parent: 'parent', + style: { + backgroundColor: 'black', + color: 'white', + }, + }, + state: { + items: [1, 2, 3, 4, 5], + itemsSize: 5, + }, + }; + + expect(mergeDeep(obj1, obj2)).toEqual(expectedObj); + }); +}); diff --git a/src/utils/mergeDeep.js b/src/utils/mergeDeep.js new file mode 100644 index 00000000..d83a0f8c --- /dev/null +++ b/src/utils/mergeDeep.js @@ -0,0 +1,30 @@ +const isObject = (obj) => obj && typeof obj === 'object'; + +/** + * Performs a deep merge of objects and returns new object. + * + * @param {...object} objects + * @returns {object} + */ +export const mergeDeep = (...objects) => objects.reduce((prev, obj) => { + if (obj == null) { + return prev; + } + + const newObject = { ...prev }; + + Object.keys(obj).forEach((key) => { + const pVal = prev[key]; + const oVal = obj[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + newObject[key] = pVal.concat(...oVal); + } else if (isObject(pVal) && isObject(oVal)) { + newObject[key] = mergeDeep(pVal, oVal); + } else { + newObject[key] = oVal; + } + }); + + return newObject; +}, {});