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