import * as React from 'react' ; import classNames from 'classnames' ; import KeyCode from 'rc-util/lib/KeyCode' ; import { composeRef } from 'rc-util/lib/ref' ; import getMiniDecimal , { DecimalClass , roundDownUnsignedDecimal , roundUpUnsignedDecimal , toFixed , ValueType } from './utils/MiniDecimal' ; import StepHandler from './StepHandler' ; import { getNumberPrecision , num2str , trimNumber , validateNumber } from './utils/numberUtil' ; import useCursor from './hooks/useCursor' ; import useUpdateEffect from './hooks/useUpdateEffect' ; import useFrame from './hooks/useFrame' ; /**
 * We support `stringMode` which need handle correct type when user call in onChange
 * format max or min value
 * 1. if isInvalid return null
 * 2. if precision is undefined, return decimal
 * 3. format with precision
 *    I. if max > 0, round down with precision. Example: max= 3.5, precision=0  afterFormat: 3
 *    II. if max < 0, round up with precision. Example: max= -3.5, precision=0  afterFormat: -4
 *    III. if min > 0, round up with precision. Example: min= 3.5, precision=0  afterFormat: 4
 *    IV. if min < 0, round down with precision. Example: max= -3.5, precision=0  afterFormat: -3
 */ const getDecimalValue = ( stringMode , decimalValue ) => { if ( stringMode || decimalValue . isEmpty ( ) ) { return decimalValue . toString ( ) ; } return decimalValue . toNumber ( ) ; } ; const getDecimalIfValidate = ( value , precision , isMax ? ) => { const decimal = getMiniDecimal ( value ) ; if ( decimal . isInvalidate ( ) ) { return null ; } if ( precision === undefined ) { return decimal ; } const { negative , integerStr , decimalStr , negativeStr } = trimNumber ( decimal . toString ( ) ) ; const unSignedNumberStr = integerStr + '.' + decimalStr ; if ( ( isMax && ! negative ) || ( ! isMax && negative ) ) { return getMiniDecimal ( negativeStr + roundDownUnsignedDecimal ( unSignedNumberStr , precision ) ) ; } else { return getMiniDecimal ( negativeStr + roundUpUnsignedDecimal ( unSignedNumberStr , precision ) ) ; } } ; export interface InputNumberProps < T extends ValueType = ValueType > extends Omit < React . InputHTMLAttributes < HTMLInputElement > , 'value' | 'defaultValue' | 'onInput' | 'onChange' > { /** value will show as string */ stringMode ? : boolean ; defaultValue ? : T ; value ? : T ; prefixCls ? : string ; className ? : string ; style ? : React . CSSProperties ; min ? : T ; max ? : T ; step ? : ValueType ; tabIndex ? : number ; controls ? : boolean ; upHandler ? : React . ReactNode ; downHandler ? : React . ReactNode ; keyboard ? : boolean ; /** Parse display value to validate number */ parser ? : ( displayValue : string | undefined ) => T ; /** Transform `value` to display value show in input */ formatter ? : ( value : T | undefined , info : { userTyping : boolean ; input : string ; } ) => string ; /** Syntactic sugar of `formatter`. Config precision of display. */ precision ? : number ; /** Syntactic sugar of `formatter`. Config decimal separator of display. */ decimalSeparator ? : string ; onInput ? : ( text : string ) => void ; onChange ? : ( value : T ) => void ; onPressEnter ? : React . KeyboardEventHandler < HTMLInputElement > ; onStep ? : ( value : T , info : { offset : ValueType ; type : 'up' | 'down' ; } ) => void ; } const InputNumber = React . forwardRef ( ( props , ref ) => { const { prefixCls = 'rc-input-number' , className , style , min , max , step = 1 , defaultValue , value , disabled , readOnly , upHandler , downHandler , keyboard , controls = true , stringMode , parser , formatter , precision , decimalSeparator , onChange , onInput , onPressEnter , onStep , ... inputProps } = props ; const inputClassName = `${ prefixCls }-input` ; const inputRef = React . useRef < HTMLInputElement > ( null ) ; const [ focus , setFocus ] = React . useState ( false ) ; const userTypingRef = React . useRef ( false ) ; const compositionRef = React . useRef ( false ) ; const [ decimalValue , setDecimalValue ] = React . useState < DecimalClass > ( ( ) => getMiniDecimal ( value ?? defaultValue ) ) ; function setUncontrolledDecimalValue ( newDecimal ) { if ( value === undefined ) { setDecimalValue ( newDecimal ) ; } } param number param userTyping const getPrecision = React . useCallback ( ( numStr , userTyping ) => { if ( userTyping ) { return undefined ; } if ( precision >= 0 ) { return precision ; } return Math . max ( getNumberPrecision ( numStr ) , getNumberPrecision ( step ) ) ; } , [ precision , step ] ) ; const mergedParser = React . useCallback ( ( num ) => { const numStr = String ( num ) ; if ( parser ) { return parser ( numStr ) ; } let parsedStr = numStr ; if ( decimalSeparator ) { parsedStr = parsedStr . replace ( decimalSeparator , '.' ) ; } return parsedStr . replace ( /[^\w.-]+/g , '' ) ; } , [ parser , decimalSeparator ] ) ; const inputValueRef = React . useRef < string | number > ( '' ) ; const mergedFormatter = React . useCallback ( ( number , userTyping ) => { if ( formatter ) { return formatter ( number , { userTyping , input : String ( inputValueRef . current ) } ) ; } let str = typeof number === 'number' ? num2str ( number ) : number ; if ( ! userTyping ) { const mergedPrecision = getPrecision ( str , userTyping ) ; if ( validateNumber ( str ) && ( decimalSeparator || mergedPrecision >= 0 ) ) { const separatorStr = decimalSeparator || '.' ; str = toFixed ( str , separatorStr , mergedPrecision ) ; } } return str ; } , [ formatter , getPrecision , decimalSeparator ] ) ; /**
     * Input text value control
     *
     * User can not update input content directly. It update with follow rules by priority:
     *  1. controlled `value` changed
     *    * [SPECIAL] Typing like `1.` should not immediately convert to `1`
     *  2. User typing with format (not precision)
     *  3. Blur or Enter trigger revalidate
     */ const [ inputValue , setInternalInputValue ] = React . useState < string | number > ( ( ) => { const initValue = defaultValue ?? value ; if ( decimalValue . isInvalidate ( ) && [ 'string' , 'number' ] . includes ( typeof initValue ) ) { return Number . isNaN ( initValue ) ? '' : initValue ; } return mergedFormatter ( decimalValue . toString ( ) , false ) ; } ) ; inputValueRef . current = inputValue ; function setInputValue ( newValue , userTyping ) { setInternalInputValue ( mergedFormatter ( newValue . isInvalidate ( ) ? newValue . toString ( false ) : newValue . toString ( ! userTyping ) , userTyping ) ) ; } const maxDecimal = React . useMemo ( ( ) => getDecimalIfValidate ( max , precision , true ) , [ max , precision ] ) ; const minDecimal = React . useMemo ( ( ) => getDecimalIfValidate ( min , precision , false ) , [ min , precision ] ) ; const upDisabled = React . useMemo ( ( ) => { if ( ! maxDecimal || ! decimalValue || decimalValue . isInvalidate ( ) ) { return false ; } return maxDecimal . lessEquals ( decimalValue ) ; } , [ maxDecimal , decimalValue ] ) ; const downDisabled = React . useMemo ( ( ) => { if ( ! minDecimal || ! decimalValue || decimalValue . isInvalidate ( ) ) { return false ; } return decimalValue . lessEquals ( minDecimal ) ; } , [ minDecimal , decimalValue ] ) ; const [ recordCursor , restoreCursor ] = useCursor ( inputRef . current , focus ) ; /**
     * Find target value closet within range.
     * e.g. [11, 28]:
     *    3  => 11
     *    23 => 23
     *    99 => 28
     */ const getRangeValue = ( target ) => { if ( maxDecimal && ! target . lessEquals ( maxDecimal ) ) { return maxDecimal ; } if ( minDecimal && ! minDecimal . lessEquals ( target ) ) { return minDecimal ; } return null ; } ; /**
     * Check value is in [min, max] range
     */ const isInRange = ( target ) => ! getRangeValue ( target ) ; /**
     * Trigger `onChange` if value validated and not equals of origin.
     * Return the value that re-align in range.
     */ const triggerValueUpdate = ( newValue , userTyping ) : DecimalClass => { let updateValue = newValue ; let isRangeValidate = isInRange ( updateValue ) || updateValue . isEmpty ( ) ; if ( ! updateValue . isEmpty ( ) && ! userTyping ) { updateValue = getRangeValue ( updateValue ) || updateValue ; isRangeValidate = true ; } if ( ! readOnly && ! disabled && isRangeValidate ) { const numStr = updateValue . toString ( ) ; const mergedPrecision = getPrecision ( numStr , userTyping ) ; if ( mergedPrecision >= 0 ) { updateValue = getMiniDecimal ( toFixed ( numStr , '.' , mergedPrecision ) ) ; } if ( ! updateValue . equals ( decimalValue ) ) { setUncontrolledDecimalValue ( updateValue ) ; onChange ?. ( updateValue . isEmpty ( ) ? null : getDecimalValue ( stringMode , updateValue ) ) ; if ( value === undefined ) { setInputValue ( updateValue , userTyping ) ; } } return updateValue ; } return decimalValue ; } ; const onNextPromise = useFrame ( ) ; const collectInputValue = ( inputStr ) => { recordCursor ( ) ; setInternalInputValue ( inputStr ) ; if ( ! compositionRef . current ) { const finalValue = mergedParser ( inputStr ) ; const finalDecimal = getMiniDecimal ( finalValue ) ; if ( ! finalDecimal . isNaN ( ) ) { triggerValueUpdate ( finalDecimal , true ) ; } } onInput ?. ( inputStr ) ; onNextPromise ( ( ) => { let nextInputStr = inputStr ; if ( ! parser ) { nextInputStr = inputStr . replace ( /。/g , '.' ) ; } if ( nextInputStr !== inputStr ) { collectInputValue ( nextInputStr ) ; } } ) ; } ; const onCompositionStart = ( ) => { compositionRef . current = true ; } ; const onCompositionEnd = ( ) => { compositionRef . current = false ; collectInputValue ( inputRef . current . value ) ; } ; const onInternalInput = ( e ) => { collectInputValue ( e . target . value ) ; } ; const onInternalStep = ( up ) => { if ( ( up && upDisabled ) || ( ! up && downDisabled ) ) { return ; } userTypingRef . current = false ; let stepDecimal = getMiniDecimal ( step ) ; if ( ! up ) { stepDecimal = stepDecimal . negate ( ) ; } const target = ( decimalValue || getMiniDecimal ( 0 ) ) . add ( stepDecimal . toString ( ) ) ; const updatedValue = triggerValueUpdate ( target , false ) ; onStep ?. ( getDecimalValue ( stringMode , updatedValue ) , { offset : step , type : up ? 'up' : 'down' , } ) ; inputRef . current ?. focus ( ) ; } ; /**
     * Flush current input content to trigger value change & re-formatter input if needed
     */ const flushInputValue = ( userTyping ) => { const parsedValue = getMiniDecimal ( mergedParser ( inputValue ) ) ; let formatValue = parsedValue ; if ( ! parsedValue . isNaN ( ) ) { formatValue = triggerValueUpdate ( parsedValue , userTyping ) ; } else { formatValue = decimalValue ; } if ( value !== undefined ) { setInputValue ( decimalValue , false ) ; } else if ( ! formatValue . isNaN ( ) ) { setInputValue ( formatValue , false ) ; } } ; const onKeyDown = ( event ) => { const { which } = event ; userTypingRef . current = true ; if ( which === KeyCode . ENTER ) { if ( ! compositionRef . current ) { userTypingRef . current = false ; } flushInputValue ( false ) ; onPressEnter ?. ( event ) ; } if ( keyboard === false ) { return ; } if ( ! compositionRef . current && [ KeyCode . UP , KeyCode . DOWN ] . includes ( which ) ) { onInternalStep ( KeyCode . UP === which ) ; event . preventDefault ( ) ; } } ; const onKeyUp = ( ) => { userTypingRef . current = false ; } ; const onBlur = ( ) => { flushInputValue ( false ) ; setFocus ( false ) ; userTypingRef . current = false ; } ; useUpdateEffect ( ( ) => { if ( ! decimalValue . isInvalidate ( ) ) { setInputValue ( decimalValue , false ) ; } } , [ precision ] ) ; useUpdateEffect ( ( ) => { const newValue = getMiniDecimal ( value ) ; setDecimalValue ( newValue ) ; const currentParsedValue = getMiniDecimal ( mergedParser ( inputValue ) ) ; if ( ! newValue . equals ( currentParsedValue ) || ! userTypingRef . current || formatter ) { setInputValue ( newValue , userTypingRef . current ) ; } } , [ value ] ) ; useUpdateEffect ( ( ) => { if ( formatter ) { restoreCursor ( ) ; } } , [ inputValue ] ) ; return ( < div className = { classNames ( prefixCls , className , { [ `${ prefixCls }-focused` ] : focus , [ `${ prefixCls }-disabled` ] : disabled , [ `${ prefixCls }-readonly` ] : readOnly , [ `${ prefixCls }-not-a-number` ] : decimalValue . isNaN ( ) , [ `${ prefixCls }-out-of-range` ] : ! decimalValue . isInvalidate ( ) && ! isInRange ( decimalValue ) , } ) } style = { style } onFocus = { ( ) => { setFocus ( true ) ; } } onBlur = { onBlur } onKeyDown = { onKeyDown } onKeyUp = { onKeyUp } onCompositionStart = { onCompositionStart } onCompositionEnd = { onCompositionEnd } > { controls && ( < StepHandler prefixCls = { prefixCls } upNode = { upHandler } downNode = { downHandler } upDisabled = { upDisabled } downDisabled = { downDisabled } onStep = { onInternalStep } / > ) } < div className = { `${ inputClassName }-wrap` } > < input autoComplete = "off" role = "spinbutton" aria - valuemin = { min as any } aria - valuemax = { max as any } aria - valuenow = { decimalValue . isInvalidate ( ) ? null : ( decimalValue . toString ( ) as any ) } step = { step } { ... inputProps } ref = { composeRef ( inputRef , ref ) } className = { inputClassName } value = { inputValue } onChange = { onInternalInput } disabled = { disabled } readOnly = { readOnly } / > < /div> < /div>); } ) as ( < T extends ValueType = ValueType > ( props : React . PropsWithChildren < InputNumberProps < T > > & { ref ? : React . Ref < HTMLInputElement > ; } ) => React . ReactElement ) & { displayName ? : string ; } ; InputNumber . displayName = 'InputNumber' ; export default InputNumber ;