diff --git a/src/docs/customize/global-props.md b/src/docs/customize/global-props.md index 0185350b..71f14731 100644 --- a/src/docs/customize/global-props.md +++ b/src/docs/customize/global-props.md @@ -83,3 +83,53 @@ React.createElement(() => { ); }); ``` + +## Nesting + +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`. + +```docoff-react-preview +React.createElement(() => { + const [variant, setVariant] = React.useState('filled'); + return ( + + + + Grid item + Grid item + Grid item + Grid item + Grid item + Grid item + + + + ); +}); +``` 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..70b75163 --- /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: 3, + }, + }; + const expectedObj = { + props: { + children: null, + className: 'class1 class2', + parent: 'parent', + style: { + backgroundColor: 'black', + color: 'white', + }, + }, + state: { + items: [3, 4, 5], + itemsSize: 3, + }, + }; + + expect(mergeDeep(obj1, obj2)).toEqual(expectedObj); + }); +}); diff --git a/src/utils/mergeDeep.js b/src/utils/mergeDeep.js new file mode 100644 index 00000000..b5b8ab5b --- /dev/null +++ b/src/utils/mergeDeep.js @@ -0,0 +1,28 @@ +const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj); + +/** + * 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 previousVal = prev[key]; + const currentVal = obj[key]; + + if (isObject(previousVal) && isObject(currentVal)) { + newObject[key] = mergeDeep(previousVal, currentVal); + } else { + newObject[key] = currentVal; + } + }); + + return newObject; +}, {});