wix-style-react
Version:
179 lines (149 loc) • 4.27 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import Input from '../Input';
class NumberInput extends React.PureComponent {
static displayName = 'NumberInput';
constructor(props) {
super(props);
const { value } = props;
this.state = {
value: this._defaultValueNullIfEmpty(value),
};
}
UNSAFE_componentWillReceiveProps({ value }) {
this.setState({
value: this._defaultValueNullIfEmpty(value),
});
}
_isInvalidNumber(value) {
return ['.', '-', undefined, ''].includes(value);
}
_defaultValueNullIfEmpty(value = this.props.defaultValue) {
return value == null || value === '' ? null : Number(value);
}
_defaultValueToNullIfInvalidNumber(value) {
return this._isInvalidNumber(value) ? null : Number(value);
}
_getInputValueFromState() {
const { value } = this.state;
const { defaultValue } = this.props;
if (value != null) {
return value;
}
return defaultValue || '';
}
_isInRange(value) {
const { min, max } = this.props;
return !(!isNaN(min) && value < min) && !(!isNaN(max) && value > max);
}
_increment = () => {
this._applyChange((value, step) => value + step);
};
_decrement = () => {
this._applyChange((value, step) => value - step);
};
_applyChange(operator) {
const { value, step } = this.props,
numberValue = parseFloat(value || this.inputDOM.value) || 0,
numberStep = step,
updatedValue = operator(numberValue, numberStep);
if (this._isInRange(updatedValue)) {
this._triggerOnChange(updatedValue);
}
}
_triggerOnChange(value) {
const { onChange } = this.props;
this.setState(
{
value,
},
() =>
onChange && onChange(this._defaultValueToNullIfInvalidNumber(value)),
);
}
_applyStrictValue(value) {
const { min, max } = this.props;
if (this._isInRange(value) || this._isInvalidNumber(value)) {
return value;
}
const dMax = max - value;
const dMin = min - value;
if (!dMax) {
return min;
}
if (!dMin) {
return max;
}
return Math.abs(dMax) < Math.abs(dMin) ? max : min;
}
_inputValueChanged = e => {
const { strict } = this.props;
const value = this._normalizeValue(e.target.value);
if (strict) {
return this._triggerOnChange(this._applyStrictValue(value));
}
return this._triggerOnChange(value);
};
_normalizeValue = value => {
if (typeof value !== 'string' || this._isInvalidNumber(value)) {
return value;
}
// Preserve minus sign when typing '-00'.
return value.startsWith('-')
? value.replace(/^-0+(?!\.|$)/, '-')
: value.replace(/^0+(?!\.|$)/, '');
};
_getInputRef = ref => {
const { inputRef } = this.props;
this.inputDOM = ref;
if (inputRef) {
inputRef(ref);
}
};
render() {
// <Input/> should always be controlled. Therefore, not passing defaultValue to <Input/>.
const { suffix, defaultValue, strict, max, min, hideStepper, ...props } =
this.props;
const value = this._getInputValueFromState();
return (
<Input
{...props}
max={max}
min={min}
type="number"
value={value}
onChange={this._inputValueChanged}
inputRef={this._getInputRef}
suffix={
<Input.Group>
{suffix}
{!hideStepper && (
<Input.Ticker
onUp={this._increment}
onDown={this._decrement}
dataHook="number-input-ticker"
upDisabled={strict && max === value}
downDisabled={strict && min === value}
/>
)}
</Input.Group>
}
/>
);
}
}
NumberInput.propTypes = {
...Input.propTypes,
/** Defines the initial value of an input */
defaultValue: PropTypes.number,
/** Specifies whether typed values that are beyond min/max range should be rounded to the nearest acceptable value */
strict: PropTypes.bool,
/** Specifies whether stepper should be hidden */
hideStepper: PropTypes.bool,
};
NumberInput.defaultProps = {
step: 1,
strict: false,
hideStepper: false,
};
export default NumberInput;