import { useCallback, useMemo, useRef, useState } from 'react' import { NativeSyntheticEvent, TextInputFocusEventData } from 'react-native/types' import { useInputBase } from '../InputBase/useInputBase' import { NumberIncrementProps } from './types' import { TypeGuards } from '@codeleap/types' import { Field, fields } from '@codeleap/form' /** Beyond this value JS floating-point arithmetic loses integer precision, so increment/decrement results become unreliable. */ export const MAX_VALID_DIGITS = 1000000000000000 // maximum number of digits that the input supports to perform operations export function useNumberIncrement(props: Partial) { const { onFocus, onBlur, field, actionPressAutoFocus, timeoutActionFocus, onChangeMask, forceError, editable, step, parseValue, min, max, value, onValueChange, } = props const [isFocused, setIsFocused] = useState(false) const { fieldHandle, validation, innerInputRef, wrapperRef, inputValue, onInputValueChange, } = useInputBase( field as Field, fields.number as () => Field, { value, onValueChange } ) const incrementDisabled = useMemo(() => { const maxLimit = TypeGuards.isNumber(max) && (Number(inputValue) >= max) return maxLimit }, [inputValue]) const decrementDisabled = useMemo(() => { const minLimit = TypeGuards.isNumber(min) && (Number(inputValue) <= min) return minLimit }, [inputValue]) /** * The increment/decrement buttons briefly set `isFocused` to style the input as active, then * clear it after `timeoutActionFocus` ms. The timeout must be cancelled if the user taps again * or focuses the real text input before it fires, otherwise the focus state flickers off mid-type. */ const actionTimeoutRef = useRef(null) const clearActionTimeoutRef = useCallback(() => { if (actionTimeoutRef.current !== null) { clearTimeout(actionTimeoutRef.current) actionTimeoutRef.current = null } }, [actionTimeoutRef.current]) const handleChange = useCallback((action: 'increment' | 'decrement') => { if (actionPressAutoFocus) setIsFocused(true) clearActionTimeoutRef() if (action === 'increment' && !incrementDisabled) { const newValue = Number(inputValue) + step onInputValueChange(newValue) } else if (action === 'decrement' && !decrementDisabled) { const newValue = Number(inputValue) - step onInputValueChange(newValue) } if (actionPressAutoFocus) { actionTimeoutRef.current = setTimeout(() => { setIsFocused(false) }, timeoutActionFocus) } }, [inputValue, incrementDisabled, decrementDisabled]) const checkValue = useCallback((newValue: number = null, withLimits = true) => { const value = newValue ?? inputValue if (withLimits) { if (TypeGuards.isNumber(max) && (Number(value) >= max)) { return max } else if (TypeGuards.isNumber(min) && (Number(value) <= min) || TypeGuards.isNil(value) || String(value)?.length <= 0) { return min } } if (!value) { return min } if (value >= MAX_VALID_DIGITS) { onInputValueChange(MAX_VALID_DIGITS) return MAX_VALID_DIGITS } return value }, [inputValue]) /** Clamps to min/max on blur so a user who clears the field or types an out-of-range number gets corrected without showing an error mid-type. */ const handleBlur = useCallback((e: NativeSyntheticEvent) => { onInputValueChange(checkValue()) validation?.onInputBlurred?.() setIsFocused(false) onBlur?.(e) }, [validation?.onInputBlurred, onBlur, checkValue]) const handleFocus = useCallback((e: NativeSyntheticEvent) => { clearActionTimeoutRef() if (editable) setIsFocused(true) onFocus?.(e) }, [onFocus]) const handleChangeInput = useCallback((text: string) => { const value = checkValue(parseValue(text), false) onInputValueChange(value) return value }, [checkValue]) const handleMaskChange = useCallback((masked, unmasked) => { handleChangeInput?.(masked) if (onChangeMask) onChangeMask(masked, unmasked) }, [onChangeMask, handleChangeInput]) const hasValue = TypeGuards.isString(inputValue) ? (inputValue as string).length > 0 : !TypeGuards.isNil(inputValue) const hasError = validation?.showError || forceError return { isFocused, handleBlur, handleFocus, handleMaskChange, handleChange, handleChangeInput, fieldHandle, validation, innerInputRef, wrapperRef, hasValue, hasError, incrementDisabled, decrementDisabled, min, max, inputValue, onInputValueChange, } }