Skip to content

Commit

Permalink
Implement prop type validation of children components (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
bedrich-schindler committed Sep 23, 2021
1 parent 32ed51b commit d68fb0d
Show file tree
Hide file tree
Showing 23 changed files with 287 additions and 42 deletions.
6 changes: 2 additions & 4 deletions src/lib/components/layout/CTA/CTA.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './CTA.scss';

Expand Down Expand Up @@ -52,10 +53,7 @@ CTA.propTypes = {
* * `CTACenter`
* * `CTAEnd`
*/
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,
children: elementOfType(['CTAStart', 'CTACenter', 'CTAEnd']).isRequired,
};

export const CTAWithContext = withProviderContext(CTA, 'CTA');
Expand Down
11 changes: 10 additions & 1 deletion src/lib/components/layout/FormLayout/FormLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import flattenChildren from 'react-keyed-flatten-children';
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './FormLayout.scss';

Expand Down Expand Up @@ -97,7 +98,15 @@ FormLayout.propTypes = {
* * `TextField`
* * `Toggle`
*/
children: PropTypes.element,
children: elementOfType([
'CheckboxField',
'FormLayoutCustomField',
'Radio',
'SelectField',
'TextArea',
'TextField',
'Toggle',
]),
/**
* Layout that is forced on children form fields.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/layout/List/List.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './List.scss';

Expand Down Expand Up @@ -63,7 +64,7 @@ List.propTypes = {
/**
* Individual ListItems.
*/
children: PropTypes.node,
children: elementOfType(['ListItem']),
};

export const ListWithContext = withProviderContext(List, 'List');
Expand Down
5 changes: 3 additions & 2 deletions src/lib/components/layout/List/__tests__/List.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
within,
} from '@testing-library/react';
import { List } from '../List';
import { ListItem } from '../ListItem';

const defaultProps = {
children: <div>content</div>,
children: <ListItem>content</ListItem>,
};

describe('rendering', () => {
Expand All @@ -28,7 +29,7 @@ describe('rendering', () => {
(rootElement) => expect(rootElement).not.toHaveClass('isAutoWidth'),
],
[
{ children: <div>content text</div> },
{ children: <ListItem>content text</ListItem> },
(rootElement) => expect(within(rootElement).getByText('content text')),
],
[
Expand Down
7 changes: 2 additions & 5 deletions src/lib/components/layout/Media/Media.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './Media.scss';

Expand All @@ -21,10 +21,7 @@ Media.propTypes = {
* * `MediaBody`
* * `MediaObject`
*/
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,
children: elementOfType(['MediaBody', 'MediaObject']).isRequired,
};

export const MediaWithContext = withProviderContext(Media, 'Media');
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/layout/Toolbar/Toolbar.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './Toolbar.scss';

Expand Down Expand Up @@ -74,7 +75,7 @@ Toolbar.propTypes = {
/**
* Individual ToolbarItems and ToolbarGroups.
*/
children: PropTypes.node.isRequired,
children: elementOfType(['ToolbarGroup', 'ToolbarItem']).isRequired,
/**
* If `true`, spacing of all toolbar items in the toolbar will be reduced.
*/
Expand Down
5 changes: 3 additions & 2 deletions src/lib/components/layout/Toolbar/__tests__/Toolbar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { alignPropTest } from '../../../../../../tests/propTests/alignPropTest';
import { densePropTest } from '../../../../../../tests/propTests/densePropTest';
import { noWrapPropTest } from '../../../../../../tests/propTests/noWrapPropTest';
import { Toolbar } from '../Toolbar';
import { ToolbarItem } from '../ToolbarItem';

const defaultProps = {
children: <div>other content text</div>,
children: <ToolbarItem>other content text</ToolbarItem>,
};

describe('rendering', () => {
it.each([
...alignPropTest,
[
{ children: <div>other content text</div> },
{ children: <ToolbarItem>other content text</ToolbarItem> },
(rootElement) => expect(within(rootElement).getByText('other content text')),
],
...densePropTest,
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/ui/Button/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ Button.propTypes = {
type: PropTypes.oneOf(['button', 'submit']),
};

export const ButtonWithContext = withForwardedRef(withProviderContext(Button, 'Button'));
export const ButtonWithContext = withForwardedRef(
withProviderContext(Button, 'Button'),
'Button',
);

export default ButtonWithContext;
3 changes: 2 additions & 1 deletion src/lib/components/ui/ButtonGroup/ButtonGroup.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './ButtonGroup.scss';

Expand Down Expand Up @@ -50,7 +51,7 @@ ButtonGroup.propTypes = {
/**
* Buttons to be grouped.
*/
children: PropTypes.arrayOf(PropTypes.element).isRequired,
children: elementOfType(['Button']).isRequired,
/**
* If `true`, all buttons inside the group will be disabled.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ describe('rendering', () => {
{ children: [<Button label="label text" />] },
(rootElement) => expect(within(rootElement).getByText('label text')),
],
[
{ children: [] },
(rootElement) => expect(rootElement).toBeInTheDocument(),
],
[
{ disabled: true },
(rootElement) => expect(within(rootElement).getByRole('button')).toBeDisabled(),
Expand Down
6 changes: 2 additions & 4 deletions src/lib/components/ui/Card/Card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import getRootColorClassName from '../../../helpers/getRootColorClassName';
import { withProviderContext } from '../../../provider';
import elementOfType from '../../../propTypes/elementOfType';
import styles from './Card.scss';

export const Card = ({
Expand Down Expand Up @@ -41,10 +42,7 @@ Card.propTypes = {
* * `CardFooter`
* * `ScrollView`
*/
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,
children: elementOfType(['CardBody', 'CardFooter', 'ScrollView']).isRequired,
/**
* [Color variant](/foundation/colors#component-colors) to clarify importance and meaning of the card.
*/
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/ui/CheckboxField/CheckboxField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ CheckboxField.propTypes = {
]),
};

export const CheckboxFieldWithContext = withForwardedRef(withProviderContext(CheckboxField, 'CheckboxField'));
export const CheckboxFieldWithContext = withForwardedRef(
withProviderContext(CheckboxField, 'CheckboxField'),
'CheckboxField',
);

export default CheckboxFieldWithContext;
5 changes: 4 additions & 1 deletion src/lib/components/ui/FileInputField/FileInputField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ FileInputField.propTypes = {
validationText: PropTypes.node,
};

export const FileInputFieldWithContext = withForwardedRef(withProviderContext(FileInputField, 'FileInputField'));
export const FileInputFieldWithContext = withForwardedRef(
withProviderContext(FileInputField, 'FileInputField'),
'FileInputField',
);

export default FileInputFieldWithContext;
3 changes: 2 additions & 1 deletion src/lib/components/ui/Modal/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../../layout/Toolbar';
import Button from '../Button';
import ScrollView from '../ScrollView';
import elementOfType from '../../../propTypes/elementOfType';
import styles from './Modal.scss';

export class Modal extends React.Component {
Expand Down Expand Up @@ -290,7 +291,7 @@ Modal.propTypes = {
* If provided only the modal body will be scrollable.
* If set to `null` the entire modal including header and footer will be scrollable.
*/
scrollView: PropTypes.element,
scrollView: elementOfType(['ScrollView']),
/**
* Size of the modal.
*/
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/ui/SelectField/SelectField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ SelectField.propTypes = {
variant: PropTypes.oneOf(['filled', 'outline']),
};

export const SelectFieldWithContext = withForwardedRef(withProviderContext(SelectField, 'SelectField'));
export const SelectFieldWithContext = withForwardedRef(
withProviderContext(SelectField, 'SelectField'),
'SelectField',
);

export default SelectFieldWithContext;
6 changes: 2 additions & 4 deletions src/lib/components/ui/Tabs/Tabs.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import elementOfType from '../../../propTypes/elementOfType';
import { withProviderContext } from '../../../provider';
import styles from './Tabs.scss';

Expand All @@ -26,10 +27,7 @@ Tabs.propTypes = {
/**
* Nested `TabsItem` elements.
*/
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]),
children: elementOfType(['TabsItem']),
/**
* ID of the root HTML element. It also serves as base for nested element:
* * `<ID>__list`
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/ui/TextArea/TextArea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ TextArea.propTypes = {
variant: PropTypes.oneOf(['filled', 'outline']),
};

export const TextAreaWithContext = withForwardedRef(withProviderContext(TextArea, 'TextArea'));
export const TextAreaWithContext = withForwardedRef(
withProviderContext(TextArea, 'TextArea'),
'TextArea',
);

export default TextAreaWithContext;
5 changes: 4 additions & 1 deletion src/lib/components/ui/TextField/TextField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ TextField.propTypes = {
variant: PropTypes.oneOf(['filled', 'outline']),
};

export const TextFieldWithContext = withForwardedRef(withProviderContext(TextField, 'TextField'));
export const TextFieldWithContext = withForwardedRef(
withProviderContext(TextField, 'TextField'),
'TextField',
);

export default TextFieldWithContext;
5 changes: 4 additions & 1 deletion src/lib/components/ui/Toggle/Toggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ Toggle.propTypes = {
]),
};

export const ToggleWithContext = withForwardedRef(withProviderContext(Toggle, 'Toggle'));
export const ToggleWithContext = withForwardedRef(
withProviderContext(Toggle, 'Toggle'),
'Toggle',
);

export default ToggleWithContext;
11 changes: 6 additions & 5 deletions src/lib/components/ui/withForwardedRef.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';

export default (WrappedComponent) => {
const withForwardedRef = (props, ref) => (
export default (WrappedComponent, componentName) => {
const withForwardedRef = React.forwardRef((props, ref) => (
<WrappedComponent {...props} forwardedRef={ref} />
);
));

withForwardedRef.displayName = `withForwardedRef(${WrappedComponent.name || WrappedComponent.name})`;
// Preserve component name while wrapping former component with higher-order component
withForwardedRef.displayName = componentName;

return React.forwardRef(withForwardedRef);
return withForwardedRef;
};
Loading

0 comments on commit d68fb0d

Please sign in to comment.