@wix/design-system
Version:
@wix/design-system
158 lines • 6.75 kB
JavaScript
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import Input from '../Input';
import { defaultValueToNullIfEmpty, isInRange, validateValue, normalizeValues, getClosestValue, } from './utils';
import { dataHooks } from './constants';
import deprecationLog from '../utils/deprecationLog';
const NumberInput = ({ suffix, defaultValue, strict = false, max, min, hideStepper = false, value: givenValue, inputRef, step = 1, status, invalidMessage, statusMessage, onChange, onInvalid, onKeyDown, type, ...props }) => {
const initialState = {
hasError: false,
localValue: '',
};
const [state, setState] = React.useReducer((currentState, newState) => ({
...currentState,
...newState,
}), initialState);
const [inputDOM, setInputDOM] = React.useState(null);
const shouldShowError = state.hasError && invalidMessage;
const [isUpDisabled, isDownDisabled] = [
max === Number(state.localValue),
min === Number(state.localValue),
];
const isControlled = givenValue !== undefined && givenValue !== null && onChange;
const setValueAndValidate = ({ value, shouldCallOnChangeCallback = true, }) => {
const { numberValue, stringValue } = normalizeValues(value);
const { hasError, validationType } = validateValue({
value: stringValue,
minValue: min,
maxValue: max,
});
setState({
hasError,
localValue: stringValue,
});
const isControlledValidNumber = isControlled && !Number.isNaN(numberValue);
if (hasError ? isControlledValidNumber : shouldCallOnChangeCallback) {
onChange?.(numberValue, stringValue);
}
if (hasError) {
onInvalid?.(stringValue, {
validationType,
value: stringValue,
});
}
};
useEffect(() => {
if (strict) {
deprecationLog('<NumberInput/> - prop strict is deprecated and not needed anymore. By using min and max it will enforce strict both for the ticker and input automatically.');
}
}, [strict]);
useEffect(() => {
const newLocalValue = defaultValueToNullIfEmpty(givenValue, defaultValue);
setValueAndValidate({
value: newLocalValue,
shouldCallOnChangeCallback: false,
});
// TODO: fix ESLint error
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [givenValue, defaultValue]);
const isClosestMode = Array.isArray(step);
const increment = () => {
const numberValue = parseFloat(String(state.localValue) || String(inputDOM?.value)) || 0;
const updatedValue = isClosestMode
? getClosestValue(numberValue, step, 'up')
: Number((numberValue + step).toPrecision(12));
if (isInRange({ value: updatedValue, minValue: min, maxValue: max })) {
setValueAndValidate({ value: updatedValue });
}
else if (min !== undefined && updatedValue <= min) {
setValueAndValidate({ value: min });
}
inputDOM?.focus();
};
const decrement = () => {
const numberValue = parseFloat(String(state.localValue) || String(inputDOM?.value)) || 0;
const updatedValue = isClosestMode
? getClosestValue(numberValue, step, 'down')
: Number((numberValue - step).toPrecision(12));
if (isInRange({ value: updatedValue, minValue: min, maxValue: max })) {
setValueAndValidate({ value: updatedValue });
}
else if (max !== undefined && updatedValue >= max) {
setValueAndValidate({ value: max });
}
inputDOM?.focus();
};
const getInputRef = useCallback((inputElement) => {
setInputDOM(inputElement);
if (typeof inputRef === 'function') {
inputElement && inputRef(inputElement);
}
else if (inputRef && 'current' in inputRef) {
inputRef.current = inputElement;
}
}, [inputRef]);
const getStatusMessage = () => {
if (shouldShowError) {
return invalidMessage;
}
return statusMessage;
};
const onInputValueChange = (event) => {
const { value } = event.target;
// matches numbers, i.e. 123 or -123
if (/^-?\d*[.,]?\d*$/.test(value)) {
setValueAndValidate({ value });
}
else {
setState({ localValue: state.localValue });
}
};
const incrementOrDecrementValue = (e) => {
if (e.key === 'ArrowUp') {
increment();
e.preventDefault();
}
if (e.key === 'ArrowDown') {
decrement();
e.preventDefault();
}
onKeyDown?.(e);
};
const shouldRenderGivenValue = isControlled &&
// to catch values like 0. or 1. and so on and so on
givenValue !== Number(state.localValue) &&
// to allow entering a negative number
state.localValue !== '-';
return (React.createElement(Input, { ...props, max: max, min: min, type: type || 'text', value: shouldRenderGivenValue ? givenValue : state.localValue, onChange: onInputValueChange, inputRef: getInputRef, status: shouldShowError ? 'error' : status, statusMessage: getStatusMessage(), onKeyDown: incrementOrDecrementValue, ariaRoledescription: "spin button", inputmode: "numeric", suffix: React.createElement(Input.Group, null,
suffix,
!hideStepper && (React.createElement(Input.Ticker, { onUp: increment, onDown: decrement, dataHook: dataHooks.numberInputTicker, upDisabled: isUpDisabled, downDisabled: isDownDisabled, onMouseDown: e => e.preventDefault() }))) }));
};
NumberInput.displayName = 'NumberInput';
NumberInput.propTypes = {
dataHook: PropTypes.string,
className: PropTypes.string,
id: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
disabled: PropTypes.bool,
status: PropTypes.oneOf(['error', 'warning', 'loading']),
statusMessage: PropTypes.node,
name: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onFocus: PropTypes.func,
placeholder: PropTypes.string,
prefix: PropTypes.node,
size: PropTypes.oneOf(['tiny', 'small', 'medium', 'large']),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
strict: PropTypes.bool,
hideStepper: PropTypes.bool,
};
export default NumberInput;
//# sourceMappingURL=NumberInput.js.map