@wix/design-system
Version:
@wix/design-system
160 lines • 7.35 kB
JavaScript
import React, { useEffect, useCallback } from 'react';
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, disabled, tooltipPlacement, ...props }) => {
useEffect(() => {
if (typeof tooltipPlacement !== 'undefined') {
deprecationLog('<NumberInput/> - prop "tooltipPlacement" is deprecated and will be removed in next major release, please use "statusMessageTooltipProps" instead.');
}
}, [tooltipPlacement]);
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 isControlled = givenValue !== undefined && givenValue !== null && onChange;
const hasPredefinedSteps = Array.isArray(step);
const getCurrentValue = () => {
const currentValue = isControlled ? givenValue : state.localValue;
return Number(currentValue);
};
const getStepperDisabledState = () => {
if (disabled) {
return [true, true];
}
const currentValue = getCurrentValue();
if (hasPredefinedSteps) {
const sortedSteps = [...step].sort((a, b) => a - b);
const minStep = sortedSteps[0];
const maxStep = sortedSteps[sortedSteps.length - 1];
const effectiveMin = min !== undefined ? Math.max(min, minStep) : minStep;
const effectiveMax = max !== undefined ? Math.min(max, maxStep) : maxStep;
return [currentValue >= effectiveMax, currentValue <= effectiveMin];
}
return [
max !== undefined && currentValue >= max,
min !== undefined && currentValue <= min,
];
};
const [isUpDisabled, isDownDisabled] = getStepperDisabledState();
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 increment = () => {
const currentValue = getCurrentValue();
const stringValue = String(currentValue || inputDOM?.value || '').replace(',', '.');
const numberValue = parseFloat(stringValue) || 0;
const updatedValue = hasPredefinedSteps
? 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 currentValue = getCurrentValue();
const stringValue = String(currentValue || inputDOM?.value || '').replace(',', '.');
const numberValue = parseFloat(stringValue) || 0;
const updatedValue = hasPredefinedSteps
? 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, tooltipPlacement: tooltipPlacement, disabled: disabled, 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: suffix || !hideStepper ? (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() })))) : undefined }));
};
NumberInput.displayName = 'NumberInput';
export default NumberInput;
//# sourceMappingURL=NumberInput.js.map