import React, { useState, useCallback, useEffect } from 'react'; import IconButton from '../IconButton'; import Icon from '../Icon'; import './InputNumber.css'; import Label from '../Label'; import getMaxDigits from '../../utils/getMaxDigits'; const arrowHorizontalClassName = 'cursor-pointer text-primary-active active:text-primary-light hover:opacity-70 w-4 flex items-center justify-center'; /** * React Number Input component' * it has two props, value and onChange * value is a number value * onChange is a function that will be called when the number input is changed * it can get changed by clicking on the up and down buttons * or by typing a number in the input */ const sizesClasses = { sm: 'w-[45px] h-[28px]', md: 'w-[58px] h-[28px]', lg: 'w-[206px] h-[35px]', }; const InputNumber: React.FC<{ value: number; onChange: (value: number) => void; minValue?: number; maxValue?: number; step?: number; size?: 'sm' | 'lg' | 'md'; className?: string; labelClassName?: string; label?: string; showAdjustmentArrows?: boolean; arrowsDirection: 'vertical' | 'horizontal'; labelPosition?: 'left' | 'bottom' | 'right' | 'top'; inputClassName?: string; sizeClassName?: string; inputContainerClassName?: string; }> = ({ value, onChange, step = 1, className, size = 'sm', minValue = 0, maxValue = 100, labelClassName = 'text-aqua-pale text-[11px] mx-auto', label, showAdjustmentArrows = true, arrowsDirection = 'vertical', labelPosition = 'left', inputClassName = 'text-white bg-primary-dark text-[14px]', sizeClassName, inputContainerClassName = 'bg-primary-dark border-secondary-light border rounded-[4px]', }) => { const [numberValue, setNumberValue] = useState(value); const [isFocused, setIsFocused] = useState(false); const maxDigits = getMaxDigits(maxValue, step); const inputWidth = Math.max(maxDigits * 10, showAdjustmentArrows ? 20 : 28); const decimalPlaces = Number.isInteger(step) ? 0 : step.toString().split('.')[1].length; const sizeToUse = sizeClassName ? sizeClassName : sizesClasses[size]; useEffect(() => { setNumberValue(value); }, [value]); const handleMinMax = useCallback( (val: number) => Math.min(Math.max(val, minValue), maxValue), [maxValue, minValue] ); const handleChange = (e: React.ChangeEvent) => { const inputValue = e.target.value; // Allow negative sign, empty string, or single decimal point for user flexibility if (inputValue === '-' || inputValue === '' || inputValue === '.') { setNumberValue(inputValue); return; } const number = Number(inputValue); // Filter out invalid inputs like 'NaN' if (!isNaN(number)) { updateValue(number); } }; const updateValue = (val: number) => { const newValue = handleMinMax(val); setNumberValue(newValue); onChange(newValue); }; const handleFocus = () => { setIsFocused(true); }; const handleBlur = () => { setIsFocused(false); setNumberValue(parseFloat(numberValue).toFixed(decimalPlaces)); }; const increment = () => updateValue(parseFloat(numberValue) + step); const decrement = () => updateValue(parseFloat(numberValue) - step); const labelElement = label && (