Skip to content

Commit

Permalink
[utils] Add component propType (#13816)
Browse files Browse the repository at this point in the history
* [utils] Add component propType

* use the componentPropType everywhere
  • Loading branch information
eps1lon authored and oliviertassinari committed Dec 10, 2018
1 parent 57ae964 commit 5be50c8
Show file tree
Hide file tree
Showing 76 changed files with 253 additions and 72 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module.exports = [
name: 'The size of the @material-ui/core/Popper component',
webpack: true,
path: 'packages/material-ui/build/Popper/index.js',
limit: '10.0 KB',
limit: '10.6 KB',
},
{
name: 'The main docs bundle',
Expand Down
4 changes: 4 additions & 0 deletions docs/src/modules/utils/generateMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ function generatePropType(type) {
return generatePropType(chained);
}

if (type.raw === 'componentProp') {
return 'Component';
}

return type.raw;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui-lab/src/Slider/Slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import classNames from 'classnames';
import withStyles from '@material-ui/core/styles/withStyles';
import ButtonBase from '@material-ui/core/ButtonBase';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { componentPropType } from '@material-ui/utils';
import clamp from '../utils/clamp';

export const styles = theme => {
Expand Down Expand Up @@ -557,7 +558,7 @@ Slider.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the slider will be disabled.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"react-dom": "^16.3.0"
},
"dependencies": {
"@babel/runtime": "7.1.2"
"@babel/runtime": "7.1.2",
"react-is": "^16.6.3"
},
"devDependencies": {},
"sideEffects": false,
Expand Down
48 changes: 48 additions & 0 deletions packages/material-ui-utils/src/componentPropType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as ReactIs from 'react-is';

/**
* A factory that returns a propTypes validator that only accepts values that
* are also accepted by React.createElement
* e.g. "div", functional, class components, forwardRef etc.
*
* @param {boolean} isRequired If `true` returns a validator
* that will throw if nullish values are passed
*/
function createComponentProp(isRequired) {
/* istanbul ignore if */
if (process.env.NODE_ENV === 'production') {
return () => null;
}

return function componentPropType(props, key, componentName, location, propFullName) {
const prop = props[key];
const propName = propFullName || key;
let message;

if (prop == null) {
if (isRequired) {
message =
`The ${location} \`${propName}\` is marked as required in \`${componentName}\`, ` +
`but its value is \`${typeof prop}\`.`;
}
} else if (!ReactIs.isValidElementType(prop)) {
const preciseType = typeof prop;
message =
`Invalid ${location} \`${propName}\` of type \`${preciseType}\` ` +
`supplied to \`${componentName}\`, expected a component.`;
}

if (message != null) {
// change error message slightly on every check to prevent caching when testing
// which would not trigger console errors on subsequent fails
return new Error(`${message}${process.env.NODE_ENV === 'test' ? Date.now() : ''}`);
}

return null;
};
}

const componentPropType = createComponentProp(false);
componentPropType.isRequired = createComponentProp(true);

export default componentPropType;
92 changes: 92 additions & 0 deletions packages/material-ui-utils/src/componentPropType.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { assert } from 'chai';
import PropTypes from 'prop-types';
import React from 'react';
import consoleErrorMock from 'test/utils/consoleErrorMock';
import componentPropType from './componentPropType';

describe('componentPropType', () => {
function testPropType(value, validator, expectedError) {
const propName = 'children';
const componentName = 'ComponentName';
const location = 'prop';

PropTypes.checkPropTypes(
{
[propName]: validator,
},
{
[propName]: value,
},
location,
componentName,
);

if (expectedError != null) {
assert.strictEqual(consoleErrorMock.callCount(), 1);
assert.include(consoleErrorMock.args()[0][0], expectedError);
} else {
assert.strictEqual(consoleErrorMock.callCount(), 0);
}
}

beforeEach(() => {
consoleErrorMock.spy();
});

afterEach(() => {
consoleErrorMock.reset();
});

it('describe .isRequired', () => {
it('rejectes null', () => {
testPropType(
undefined,
componentPropType.isRequired,
'The prop `children` is marked as required in `ComponentName`, ' +
'but its value is `undefined`.',
);
});

it('rejects undefined', () => {
testPropType(
null,
componentPropType.isRequired,
'The prop `children` is marked as required in `ComponentName`, but its value is `object`.',
);
});
});

it('supports optional props', () => {
testPropType(undefined, componentPropType, null);
testPropType(null, componentPropType, null);
});

it('accepts strings, class and functional components', () => {
// eslint-disable-next-line react/prefer-stateless-function
class ClassComponent extends React.Component {
render() {
return null;
}
}

testPropType(ClassComponent, componentPropType, null);
testPropType(() => null, componentPropType, null);
testPropType('will accept any string though', componentPropType, null);
});

it('rejects other types with their type hint', () => {
testPropType(
1,
componentPropType,
'Invalid prop `children` of type `number` supplied to `ComponentName`, expected a component',
);
});

it('rejects objects', () => {
testPropType(
{},
componentPropType,
'Invalid prop `children` of type `object` supplied to `ComponentName`, expected a component',
);
});
});
1 change: 1 addition & 0 deletions packages/material-ui-utils/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as componentPropType } from './componentPropType';
export { default as exactProp } from './exactProp';
export { default as getDisplayName } from './getDisplayName';
export { default as ponyfillGlobal } from './ponyfillGlobal';
3 changes: 2 additions & 1 deletion packages/material-ui/src/Avatar/Avatar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';

export const styles = theme => ({
Expand Down Expand Up @@ -121,7 +122,7 @@ Avatar.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* Attributes applied to the `img` element if the component
* is used to display an image.
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Badge/Badge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { capitalize } from '../utils/helpers';

Expand Down Expand Up @@ -116,7 +117,7 @@ Badge.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the badge will be invisible.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { fade } from '../styles/colorManipulator';
import ButtonBase from '../ButtonBase';
Expand Down Expand Up @@ -307,7 +308,7 @@ Button.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the button will be disabled.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/ButtonBase/ButtonBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import keycode from 'keycode';
import { componentPropType } from '@material-ui/utils';
import ownerWindow from '../utils/ownerWindow';
import withStyles from '../styles/withStyles';
import NoSsr from '../NoSsr';
Expand Down Expand Up @@ -350,7 +351,7 @@ ButtonBase.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the base button will be disabled.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/CardContent/CardContent.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';

export const styles = {
Expand Down Expand Up @@ -33,7 +34,7 @@ CardContent.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
};

CardContent.defaultProps = {
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/CardHeader/CardHeader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import Typography from '../Typography';

Expand Down Expand Up @@ -115,7 +116,7 @@ CardHeader.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the children won't be wrapped by a Typography component.
* This can be useful to render an alternative Typography variant by wrapping
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/CardMedia/CardMedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import warning from 'warning';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';

export const styles = {
Expand Down Expand Up @@ -62,7 +63,7 @@ CardMedia.propTypes = {
* Component for rendering image.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* Image to be displayed as a background image.
* Either `image` or `src` prop must be specified.
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Chip/Chip.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import keycode from 'keycode';
import warning from 'warning';
import { componentPropType } from '@material-ui/utils';
import CancelIcon from '../internal/svg-icons/Cancel';
import withStyles from '../styles/withStyles';
import { emphasize, fade } from '../styles/colorManipulator';
Expand Down Expand Up @@ -426,7 +427,7 @@ Chip.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* Override the default delete icon element. Shown only if `onDelete` is set.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Collapse/Collapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Transition from 'react-transition-group/Transition';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { duration } from '../styles/transitions';
import { getTransitionProps } from '../transitions/utils';
Expand Down Expand Up @@ -203,7 +204,7 @@ Collapse.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the component will transition in.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Divider/Divider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import { fade } from '../styles/colorManipulator';
import chainPropTypes from '../utils/chainPropTypes';
Expand Down Expand Up @@ -83,7 +84,7 @@ Divider.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the divider will be indented.
* __WARNING__: `inset` is deprecated.
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/Fab/Fab.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import withStyles from '../styles/withStyles';
import ButtonBase from '../ButtonBase';
import { capitalize } from '../utils/helpers';
Expand Down Expand Up @@ -166,7 +167,7 @@ Fab.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the button will be disabled.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/FormControl/FormControl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { componentPropType } from '@material-ui/utils';
import { isFilled, isAdornedStart } from '../InputBase/utils';
import withStyles from '../styles/withStyles';
import { capitalize } from '../utils/helpers';
Expand Down Expand Up @@ -173,7 +174,7 @@ FormControl.propTypes = {
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]),
component: componentPropType,
/**
* If `true`, the label, input and helper text should be displayed in a disabled state.
*/
Expand Down
Loading

0 comments on commit 5be50c8

Please sign in to comment.