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

fix(number-input): adjust invalid logic and add proptype check for value #5217

Merged
merged 11 commits into from
Feb 4, 2020
15 changes: 15 additions & 0 deletions packages/react/src/components/NumberInput/NumberInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,27 @@ describe('NumberInput', () => {
.find('NumberInput')
.instance()
.setState({ value: '' });

wrapper.update();
wrapper.setProps({ allowEmpty: true });
const invalidText = wrapper.find(`.${prefix}--form-requirement`);
expect(invalidText.length).toEqual(0);
});

it('should allow updating the value with empty string and not be invalid', () => {
// Enzyme doesn't seem to allow setState() in a forwardRef-wrapped class component
wrapper
.find('NumberInput')
.instance()
.setState({ value: 50 });

wrapper.update();
wrapper.setProps({ value: '', allowEmpty: true });
wrapper.update();
const invalidText = wrapper.find(`.${prefix}--form-requirement`);
expect(invalidText.length).toEqual(0);
});

it('should change the value upon change in props', () => {
wrapper.setProps({ value: 1 });
// Enzyme doesn't seem to allow setState() in a forwardRef-wrapped class component
Expand Down
88 changes: 56 additions & 32 deletions packages/react/src/components/NumberInput/NumberInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,6 @@ const capMax = (max, value) =>
: Math.min(max, value);

class NumberInput extends Component {
constructor(props) {
super(props);
this.isControlled = props.value !== undefined;
if (useControlledStateWithValue && this.isControlled) {
// Skips the logic of setting initial state if this component is controlled
return;
}
let value = useControlledStateWithValue ? props.defaultValue : props.value;
value = value === undefined ? 0 : value;
if (props.min || props.min === 0) {
value = Math.max(props.min, value);
}
this.state = { value };
}

static propTypes = {
/**
* Specify an optional className to be applied to the wrapper node
Expand Down Expand Up @@ -113,11 +98,11 @@ class NumberInput extends Component {
/**
* Optional starting value for uncontrolled state
*/
defaultValue: PropTypes.number,
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/**
* Specify the value of the input
*/
value: PropTypes.number,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/**
* Specify if the component should be read-only
*/
Expand Down Expand Up @@ -171,14 +156,16 @@ class NumberInput extends Component {
translateWithId: id => defaultTranslations[id],
};

/**
* The DOM node refernce to the `<input>`.
* @type {HTMLInputElement}
*/
_inputRef = null;

static getDerivedStateFromProps({ min, max, value = 0 }, state) {
const { prevValue } = state;

if (useControlledStateWithValue && value === '' && prevValue !== '') {
return {
value: '',
prevValue: '',
};
}

// If `useControlledStateWithValue` feature flag is on, do nothing here.
// Otherwise, do prop -> state sync with "value capping".
return useControlledStateWithValue || prevValue === value
Expand All @@ -189,6 +176,27 @@ class NumberInput extends Component {
};
}

/**
* The DOM node refernce to the `<input>`.
* @type {HTMLInputElement}
*/
_inputRef = null;

constructor(props) {
super(props);
this.isControlled = props.value !== undefined;
if (useControlledStateWithValue && this.isControlled) {
// Skips the logic of setting initial state if this component is controlled
return;
}
let value = useControlledStateWithValue ? props.defaultValue : props.value;
value = value === undefined ? 0 : value;
if (props.min || props.min === 0) {
value = Math.max(props.min, value);
}
this.state = { value };
}

handleChange = evt => {
const { disabled, onChange } = this.props;
if (!disabled) {
Expand Down Expand Up @@ -310,12 +318,28 @@ class NumberInput extends Component {
const inputWrapperProps = {};
let errorId = null;
let error = null;
if (
invalid ||
(!allowEmpty && this.state.value === '') ||
this.state.value > max ||
this.state.value < min
) {

let isInputInvalid;

// If the user supplied `invalid` through props, we'll defer to the passed in value
if (invalid) {
isInputInvalid = true;
} else {
// Otherwise, if we don't allow an empty value then we check to see
// if the value is empty, or if it is out of range
if (!allowEmpty && this.state.value === '') {
isInputInvalid = true;
} else {
if (
this.state.value !== '' &&
(this.state.value > max || this.state.value < min)
) {
isInputInvalid = true;
}
}
}

if (isInputInvalid) {
inputWrapperProps['data-invalid'] = true;
errorId = `${id}-error-id`;
error = (
Expand Down Expand Up @@ -391,16 +415,16 @@ class NumberInput extends Component {
{helper}
<div className={`${prefix}--number__input-wrapper`}>
<input
data-invalid={invalid || null}
aria-invalid={invalid || null}
data-invalid={isInputInvalid}
aria-invalid={isInputInvalid}
aria-describedby={errorId}
type="number"
pattern="[0-9]*"
{...other}
{...props}
ref={mergeRefs(ref, this._handleInputRef)}
/>
{invalid && (
{isInputInvalid && (
<WarningFilled16 className={`${prefix}--number__invalid`} />
)}
<div className={`${prefix}--number__controls`}>
Expand Down