UNPKG

wix-style-react

Version:
179 lines (149 loc) 4.27 kB
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;