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

[utils] Add a new integer propType #25224

Merged
merged 14 commits into from
Mar 9, 2021
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
2 changes: 1 addition & 1 deletion docs/pages/api-docs/pagination.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"default": "'standard'"
},
"count": { "type": { "name": "number" }, "default": "1" },
"count": { "type": { "name": "custom", "description": "integer" }, "default": "1" },
"defaultPage": { "type": { "name": "number" }, "default": "1" },
"disabled": { "type": { "name": "bool" } },
"getItemAriaLabel": { "type": { "name": "func" } },
Expand Down
7 changes: 7 additions & 0 deletions docs/src/modules/utils/generatePropTypeDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function isRefType(type: PropTypeDescriptor): boolean {
return type.raw === 'refType';
}

function isIntegerType(type: PropTypeDescriptor): boolean {
return type.raw === 'integerPropType';
}

export function isElementAcceptingRefProp(type: PropTypeDescriptor): boolean {
return /^elementAcceptingRef/.test(type.raw);
}
Expand All @@ -73,6 +77,9 @@ export default function generatePropTypeDescription(type: PropTypeDescriptor): s
if (isElementAcceptingRefProp(type)) {
return 'element';
}
if (isIntegerType(type)) {
return 'integer';
}
if (isRefType(type)) {
return 'ref';
}
Expand Down
1 change: 1 addition & 0 deletions packages/material-ui-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export {
} from './scrollLeft';
export { default as usePreviousProps } from './usePreviousProps';
export { default as visuallyHidden } from './visuallyHidden';
export { default as integerPropType } from './integerPropType';
66 changes: 66 additions & 0 deletions packages/material-ui-utils/src/integerPropType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export function getTypeByValue(value) {
const valueType = typeof value;
switch (valueType) {
case 'number':
if (Number.isNaN(value)) {
return 'NaN';
}
if (!Number.isFinite(value)) {
return 'Infinity';
}
if (value !== Math.floor(value)) {
return 'float';
}

return 'number';
case 'object':
if (value === null) {
return 'null';
}

return value.constructor.name;
default:
return valueType;
}
}

// IE 11 support
function ponyfillIsInteger(x) {
// eslint-disable-next-line no-restricted-globals
return typeof x === 'number' && isFinite(x) && Math.floor(x) === x;
}

const isInteger = Number.isInteger || ponyfillIsInteger;

function requiredInteger(props, propName, componentName, location) {
const propValue = props[propName];

if (propValue == null || !isInteger(propValue)) {
const propType = getTypeByValue(propValue);

return new RangeError(
`Invalid ${location} \`${propName}\` of type \`${propType}\` supplied to \`${componentName}\`, expected \`integer\`.`,
);
}

return null;
}

function validator(props, propName, ...rest) {
const propValue = props[propName];

if (propValue === undefined) {
return null;
}

return requiredInteger(props, propName, ...rest);
}

function validatorNoop() {
return null;
}

validator.isRequired = requiredInteger;
validatorNoop.isRequired = validatorNoop;

export default process.env.NODE_ENV === 'production' ? validatorNoop : validator;
125 changes: 125 additions & 0 deletions packages/material-ui-utils/src/integerPropType.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';
import { expect } from 'chai';
import PropTypes from 'prop-types';
import { integerPropType } from '@material-ui/utils';
import { getTypeByValue } from './integerPropType';

describe('integerPropType', () => {
const location = '';
const componentName = 'DummyComponent';

function checkPropType(props, propName, required) {
PropTypes.checkPropTypes(
{
[propName]: required ? integerPropType.isRequired : integerPropType,
},
props,
'',
'DummyComponent',
);
}

function assertPass({ props }, propName, required = false) {
expect(() => {
checkPropType(props, propName, required);
}).not.toErrorDev();
}

function assertFail({ props }, propName, required = false) {
const propType = getTypeByValue(props[propName]);
const errorMessage = `Warning: Failed type: Invalid ${location} \`${propName}\` of type \`${propType}\` supplied to \`${componentName}\`, expected \`integer\`.`;

expect(() => {
checkPropType(props, propName, required);
}).toErrorDev(errorMessage);
}

describe('passes on undefined but failes on null value', () => {
beforeEach(() => {
PropTypes.resetWarningCache();
});

it('passes on undefined', () => {
assertPass(<div a={undefined} />, 'a');
});

it('fails on null', () => {
assertFail(<div a={null} />, 'a');
});
});

it('passes on zero', () => assertPass(<div a={0} />, 'a'));

it('passes on positive numbers', () => {
assertPass(<div a={42} />, 'a');
});

describe('passes with the conversion before passing', () => {
it('passes with conversion - parseInt', () => {
assertPass(<div a={parseInt(1.1, 10)} />, 'a');
});

it('passes with the conversion - Math.floor', () => {
assertPass(<div a={Math.floor(1.1)} />, 'a');
});

it('passes with the boolean conversion', () => {
// eslint-disable-next-line no-bitwise
assertPass(<div a={1.1 | 0} />, 'a');
});
});

it('passes on negative numbers', () => {
assertPass(<div a={-42} />, 'a');
});

describe('fails on non-integers', () => {
beforeEach(() => {
PropTypes.resetWarningCache();
});

it('fails when we pass float number', () => {
assertFail(<div a={1.5} />, 'a');
});

it('fails when have been made computation which results in float number', () => {
assertFail(<div a={(0.1 + 0.2) * 10} />, 'a');
});

it('fails on string', () => {
assertFail(<div a={'a message'} />, 'a');
});

it('fails on boolean', () => {
assertFail(<div a={false} />, 'a');
});

it('fails on array', () => {
assertFail(<div a={[]} />, 'a');
});
});

describe('fails on number edge cases', () => {
it('fails on infinity', () => {
assertFail(<div a={Infinity} />, 'a');
});

it('fails on NaN', () => {
assertFail(<div a={NaN} />, 'a');
});
});

describe('isRequired', () => {
beforeEach(() => {
PropTypes.resetWarningCache();
});

it('passes when not required', () => {
assertPass(<div />, 'a');
});

it('fails when required', () => {
assertFail(<div />, 'a', true);
});
});
});
4 changes: 2 additions & 2 deletions packages/material-ui/src/Pagination/Pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled';
import { deepmerge } from '@material-ui/utils';
import { deepmerge, integerPropType } from '@material-ui/utils';
import useThemeProps from '../styles/useThemeProps';
import paginationClasses, { getPaginationUtilityClass } from './paginationClasses';
import usePagination from '../usePagination';
Expand Down Expand Up @@ -167,7 +167,7 @@ Pagination.propTypes /* remove-proptypes */ = {
* The total number of pages.
* @default 1
*/
count: PropTypes.number,
count: integerPropType,
/**
* The page selected by default when the component is uncontrolled.
* @default 1
Expand Down