import { Add, Remove } from '@mui/icons-material' import clsx from 'clsx' import { useRef } from 'react' import { FieldValues, Path } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { HugeDecimal } from '@dao-dao/math' import { NumericInputProps } from '@dao-dao/types' import { toAccessibleImageUrl } from '@dao-dao/utils' import { IconButton } from '../icon_buttons' /** * This input is designed for numeric values and takes advantage of the * HugeDecimal class to handle large decimal numbers gracefully. * * By default, it uses human-readable strings for value storage. You can instead * use a number by setting the `numericValue` prop, but this should only be used * when not needing to store potentially large numbers. * * There is no need to provide `value` when providing `fieldName` and `register` * via react-hook-form. * * To show plus/minus buttons, make sure to provide `setValue` and either * `fieldName`+`getValues` or `value`. When using a react-hook-form form, * `setValue` and `getValues` can be retrieved easily from `useForm` or * `useFormContext`. When not using a react-hook-form form, the `setValue` * function can easily be mocked, and its first argument (`fieldName`) can be * ignored. */ export const NumericInput = < FV extends FieldValues, FieldName extends Path, >({ fieldName, register, error, validation, hidePlusMinus, numericValue, value, getValues, setValue, disabled, sizing, className, containerClassName, required, ghost, unit, unitIconUrl, textClassName, unitClassName, unitIconClassName, unitContainerClassName, plusMinusButtonSize = 'sm', ...props }: NumericInputProps) => { const { t } = useTranslation() const validate = validation?.reduce( (a, v) => ({ ...a, [v.toString()]: v }), {} ) const latestValueSet = useRef('') const strValue = typeof value === 'string' ? value : '' // If we set the ending in a decimal point (and any trailing zeroes), and the // current value is the same but without the decimal point/zeroes, show the // decimal point. We don't want to clear the decimal point/zeroes in case this // input is controlled and the parent component transforms the value manually // into a number and back (which would clear the decimal point). This would // prevent people from entering decimal values. const displayValue = latestValueSet.current.startsWith(strValue) && // Either the latest input typed adds a decimal point and trailing zeroes // (e.g. 5 => 5.0), or the current value has a decimal point and the latest // input added trailing zeroes after the decimal point (e.g. 5.1 => 5.10). (latestValueSet.current.slice(strValue.length).match(/^\.[0\s]*$/) || (strValue.includes('.') && latestValueSet.current.slice(strValue.length).match(/^[0\s]+$/))) ? latestValueSet.current : value return (
{!hidePlusMinus && !disabled && setValue && (
{/* Minus button */} setValue( fieldName ?? '', HugeDecimal.min( HugeDecimal.max( ...(props.min !== undefined ? [props.min] : []), // Subtract 1 whole number and truncate. HugeDecimal.from( (fieldName && getValues ? getValues(fieldName) : value) || 0 ) .minus(1) .trunc() ), ...(props.max !== undefined ? [props.max] : []) ).toString(), { shouldValidate: true, } ) } size={ // The larger button size for this input corresponds to the // default icon button size. plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize } variant="ghost" /> setValue( fieldName ?? '', HugeDecimal.min( HugeDecimal.max( ...(props.min !== undefined ? [props.min] : []), // Add 1 whole number and truncate. HugeDecimal.from( (fieldName && getValues ? getValues(fieldName) : value) || 0 ) .plus(1) .trunc() ), ...(props.max !== undefined ? [props.max] : []) ).toString(), { shouldValidate: true, } ) } size={ // The larger button size for this input corresponds to the // default icon button size. plusMinusButtonSize === 'lg' ? 'default' : plusMinusButtonSize } variant="ghost" />
)} { const value = (target as HTMLInputElement).value latestValueSet.current = value setValue( fieldName ?? '', numericValue ? // Treat empty strings as NaN when manually setting value. value.trim() === '' ? NaN : Number(value) : value ) }) } onWheel={(e) => { // Disallow mouse wheel on number inputs that change value. if (e.currentTarget === document.activeElement) { e.currentTarget.blur() } e.preventDefault() }} type="number" value={displayValue} {...props} {...(fieldName && register?.(fieldName, { required: required && t('info.required'), validate, valueAsNumber: numericValue, }))} /> {(unit || unitIconUrl) && (
{unitIconUrl && (
)}

{unit}

)}
) }