Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce nested RUIProvider (#541) #542

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/docs/customize/global-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<RUIProvider globalProps={{
Grid: {
columns: {
xs: '1fr',
md: '1fr 1fr',
},
justifyItems: 'center',
rows: {
xs: '50px',
md: '100px',
},
},
}}>
<RUIProvider globalProps={{
Grid: {
columns: {
sm: '1fr 1fr 1fr',
},
justifyItems: 'undefined',
rows: undefined,
},
}}>
<Grid>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
</Grid>
</RUIProvider>
</RUIProvider>
);
});
```
9 changes: 6 additions & 3 deletions src/provider/RUIProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
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 = ({
children,
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 (
<RUIContext.Provider
Expand Down
76 changes: 75 additions & 1 deletion src/provider/__tests__/RUIProvider.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
render,
within,
} from '@testing-library/react';
import { Badge } from '../../components/Badge';
import { Alert } from '../../components/Alert';
import { Badge } from '../../components/Badge';
import { Grid } from '../../components/Grid';
import RUIProvider from '../RUIProvider';

describe('rendering', () => {
Expand Down Expand Up @@ -36,4 +37,77 @@ describe('rendering', () => {

assert(dom.container.firstChild);
});

it('renders with nested providers', () => {
const dom = render((
<RUIProvider
globalProps={{
Grid: {
alignContent: {
sm: 'column',
xs: 'row dense',
},
autoFlow: {
sm: 'column',
xs: 'row dense',
},
justifyItems: 'center',
tag: 'main',
},
}}
>
<RUIProvider
globalProps={{
Grid: {
alignContent: undefined,
autoFlow: {
lg: 'column',
sm: undefined,
xs: 'row dense',
},
justifyContent: undefined,
justifyItems: undefined,
tag: 'section',
},
}}
>
<RUIProvider
globalProps={{
Grid: {
autoFlow: {
md: 'column',
},
justifyContent: 'center',
},
}}
>
<Grid>
<div>
Content text
</div>
</Grid>
</RUIProvider>
</RUIProvider>
</RUIProvider>
));

// Assert alignContent
expect(dom.container.firstChild.style.cssText.includes('--rui-local-align-content')).toBeFalsy();
adamkudrna marked this conversation as resolved.
Show resolved Hide resolved

// 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');
});
});

80 changes: 80 additions & 0 deletions src/utils/__tests__/mergeDeep.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
28 changes: 28 additions & 0 deletions src/utils/mergeDeep.js
Original file line number Diff line number Diff line change
@@ -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;
}, {});