diff --git a/src/components/devsupport/components/falsyNode/falsyNode.component.tsx b/src/components/devsupport/components/falsyNode/falsyNode.component.tsx new file mode 100644 index 000000000..09143cf15 --- /dev/null +++ b/src/components/devsupport/components/falsyNode/falsyNode.component.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { PropsService } from '../../services/props/props.service'; +import { StyleType } from '../../../theme'; + +export type RenderComponent = React.ReactElement | React.ReactElement[]; + +export type FalsyNodeProps = Props & { + component?: RenderComponent; + style?: StyleType; +}; + +/** + * Helper component for optional properties that should render cloned component. + * + * Accepts props of a component that is expected to be rendered, + * and `component` which may be React Element only. + * + * If it is a React Element, will call it with props passed to this component. + * + * @property {RenderComponent} component - React jsx component to be rendered. + * + * @example Will render nothing. + * ``` + * + * ``` + * + * @example Will render red title. + * ``` + * const Title = () => ( + * Title} + * /> + * ); + * ``` + */ + +type ChildElement = React.ReactElement; +type ChildrenProp = ChildElement | ChildElement[]; + +export class FalsyNode extends React.Component> { + private renderChildElement = (source: ChildElement, props: any): ChildElement => { + return React.cloneElement(source, { + ...props, + ...source.props, + style: PropsService.mergeObjectsWithArrays([this.props?.style, source.props?.style]), + }); + }; + + private renderComponentChildren = (source: ChildrenProp, props: any): ChildElement[] => { + return React.Children.map(source, child => this.renderChildElement(child, props)); + }; + + public render(): React.ReactElement { + const { component, ...props } = this.props; + + if (!component) { + return null; + } + + return <>{this.renderComponentChildren(component, props)}; + } +} diff --git a/src/components/devsupport/components/falsyNode/falsyNode.spec.tsx b/src/components/devsupport/components/falsyNode/falsyNode.spec.tsx new file mode 100644 index 000000000..e1f693777 --- /dev/null +++ b/src/components/devsupport/components/falsyNode/falsyNode.spec.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Text } from "react-native"; +import { fireEvent, render } from "react-native-testing-library"; +import { FalsyNode } from "./falsyNode.component"; + +it("should render nothing", function () { + const component = render(); + expect(component.toJSON()).toEqual(null); +}); + +it("should render provided React Element", () => { + const component = render( + I love Babel} /> + ); + + const textComponent = component.getByText("I love Babel"); + + expect(textComponent).toBeTruthy(); + expect(textComponent.props.style).toEqual({ + color: "red", + }); +}); + +it("should render provided React Element with overwritten styles", () => { + const renderComponent = I love Babel; + + const component = render( + + ); + + const textComponent = component.getByText("I love Babel"); + + expect(textComponent).toBeTruthy(); + expect(textComponent.props.style).toEqual({ + color: "blue", + backgroundColor: "black", + }); +}); + +it('should keep props passed in FalsyNode component', function () { + const onPress = jest.fn(); + + const component = render( + I love Babel} + />, + ); + + fireEvent.press(component.queryByText('I love Babel')); + expect(onPress).toBeCalledTimes(1); +}); + +it('should override props passed in FalsyNode component', function () { + const onPress = jest.fn(); + const onInnerPress = jest.fn(); + + const component = render( + I love Babel} + />, + ); + + fireEvent.press(component.queryByText('I love Babel')); + expect(onPress).toBeCalledTimes(0); + expect(onInnerPress).toBeCalledTimes(1); +}); diff --git a/src/components/devsupport/index.ts b/src/components/devsupport/index.ts index ccb1338c1..f320cf270 100644 --- a/src/components/devsupport/index.ts +++ b/src/components/devsupport/index.ts @@ -3,6 +3,7 @@ export { RenderProp, } from './components/falsyFC/falsyFC.component'; export { FalsyText } from './components/falsyText/falsyText.component'; +export { FalsyNode } from './components/falsyNode/falsyNode.component'; export { TouchableWithoutFeedback, TouchableWithoutFeedbackProps, diff --git a/src/components/devsupport/services/props/props.service.ts b/src/components/devsupport/services/props/props.service.ts index fbb546d38..a10aabcdd 100644 --- a/src/components/devsupport/services/props/props.service.ts +++ b/src/components/devsupport/services/props/props.service.ts @@ -102,6 +102,8 @@ export interface RestProps { export type AllOfProps = Partial; export type AllWithRestProps = Partial & RestProps; +type ObjectsToMerge = Object[] | [Object[]]; + class NativePropsService { /** * Retrieves all props included in `from` array @@ -159,6 +161,21 @@ class NativePropsService { }; }, {}); } + + /** + * Merge objects & array of objects passed to array parameter. + * + * @param {Partial} objects - array which needs to be merged. + * + * @return {Object} - merged object with merged values inside. + */ + public mergeObjectsWithArrays = (objects: Partial): Object => { + return Object.assign({}, + ...objects.map(currentObject => + Array.isArray(currentObject) + ? Object.assign({}, ...currentObject.map(obj => obj)) + : currentObject)); + }; } export const PropsService = new NativePropsService(); diff --git a/src/components/devsupport/services/props/props.spec.ts b/src/components/devsupport/services/props/props.spec.ts index ed8428765..16c1e6ea6 100644 --- a/src/components/devsupport/services/props/props.spec.ts +++ b/src/components/devsupport/services/props/props.spec.ts @@ -51,4 +51,15 @@ describe('@props: service checks', () => { }); }); + it('should merge object & array of objects in appropriate object', () => { + const stylesArray = [{color: 'blue'}, {backgroundColor: 'black'}]; + const styles = {color: 'red'}; + + const mergedStyles = PropsService.mergeObjectsWithArrays([stylesArray, styles]); + expect(mergedStyles).toEqual({ + color: 'red', + backgroundColor: 'black', + }) + }); + }); diff --git a/src/components/ui/card/card.component.tsx b/src/components/ui/card/card.component.tsx index c90eb04da..92897e7ba 100644 --- a/src/components/ui/card/card.component.tsx +++ b/src/components/ui/card/card.component.tsx @@ -14,6 +14,7 @@ import { import { EvaStatus, FalsyFC, + FalsyNode, RenderProp, TouchableWeb, TouchableWebElement, @@ -33,7 +34,7 @@ type CardStyledProps = Overwrite; export interface CardProps extends TouchableWebProps, CardStyledProps { - children?: React.ReactNode; + children?: React.ReactElement | React.ReactElement[]; header?: RenderProp; footer?: RenderProp; accent?: RenderProp; @@ -150,9 +151,10 @@ export class Card extends React.Component { component={header} /> {header && } - - {children} - + {footer && }