UNPKG

@wix/design-system

Version:

@wix/design-system

160 lines 7.35 kB
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