diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 969e97e2..32f0b3aa 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -87,9 +87,14 @@ export interface InputNumberProps changeOnWheel?: boolean; /** Parse display value to validate number */ - parser?: (displayValue: string | undefined) => T; + parser?: (displayValue: string | undefined, info: { prevValue: string }) => T; /** Transform `value` to display value show in input */ - formatter?: (value: T | undefined, info: { userTyping: boolean; input: string }) => string; + formatter?: ( + value: T | undefined, + info: { userTyping: boolean; input: string; prevValue: string }, + ) => string; + /** Validate an input string before processing */ + validator?: (input: string) => boolean; /** Syntactic sugar of `formatter`. Config precision of display. */ precision?: number; /** Syntactic sugar of `formatter`. Config decimal separator of display. */ @@ -130,24 +135,18 @@ const InternalInputNumber = React.forwardRef( keyboard, changeOnWheel = false, controls = true, - - classNames, stringMode, - + validator, parser, formatter, precision, decimalSeparator, - onChange, onInput, onPressEnter, onStep, - changeOnBlur = true, - domRef, - ...inputProps } = props; @@ -173,7 +172,10 @@ const InternalInputNumber = React.forwardRef( } } - // ====================== Parser & Formatter ====================== + const prevValueRef = React.useRef(''); + const inputValueRef = React.useRef(''); + + // ====================== Formatter ====================== /** * `precision` is used for formatter & onChange. * It will auto generate by `value` & `step`. @@ -206,7 +208,7 @@ const InternalInputNumber = React.forwardRef( const numStr = String(num); if (parser) { - return parser(numStr); + return parser(numStr, { prevValue: String(prevValueRef.current ?? '') }); } let parsedStr = numStr; @@ -221,11 +223,14 @@ const InternalInputNumber = React.forwardRef( ); // >>> Formatter - const inputValueRef = React.useRef(''); const mergedFormatter = React.useCallback( (number: string, userTyping: boolean) => { if (formatter) { - return formatter(number, { userTyping, input: String(inputValueRef.current) }); + return formatter(number, { + userTyping, + input: String(inputValueRef.current), + prevValue: String(prevValueRef.current ?? ''), + }); } let str = typeof number === 'number' ? num2str(number) : number; @@ -264,7 +269,11 @@ const InternalInputNumber = React.forwardRef( } return mergedFormatter(decimalValue.toString(), false); }); - inputValueRef.current = inputValue; + + React.useEffect(() => { + prevValueRef.current = inputValueRef.current; + inputValueRef.current = inputValue; + }, [inputValue]); // Should always be string function setInputValue(newValue: DecimalClass, userTyping: boolean) { @@ -382,10 +391,16 @@ const InternalInputNumber = React.forwardRef( // >>> Collect input value const collectInputValue = (inputStr: string) => { + // validate string + if (validator) { + if (!validator(inputStr)) return; + } + recordCursor(); // Update inputValue in case input can not parse as number // Refresh ref value immediately since it may used by formatter + prevValueRef.current = inputValueRef.current; inputValueRef.current = inputStr; setInternalInputValue(inputStr); diff --git a/tests/formatter.test.tsx b/tests/formatter.test.tsx index 1947de7d..661391c6 100644 --- a/tests/formatter.test.tsx +++ b/tests/formatter.test.tsx @@ -163,7 +163,7 @@ describe('InputNumber.Formatter', () => { fireEvent.change(container.querySelector('input'), { target: { value: '1' } }); expect(formatter).toHaveBeenCalledTimes(1); - expect(formatter).toHaveBeenCalledWith('1', { userTyping: true, input: '1' }); + expect(formatter).toHaveBeenCalledWith('1', { userTyping: true, input: '1', prevValue: '' }); }); describe('dynamic formatter', () => { diff --git a/tests/validator.test.tsx b/tests/validator.test.tsx new file mode 100644 index 00000000..2a61e61a --- /dev/null +++ b/tests/validator.test.tsx @@ -0,0 +1,84 @@ +import KeyCode from 'rc-util/lib/KeyCode'; +import InputNumber from '../src'; +import { fireEvent, render } from './util/wrapper'; + +describe('InputNumber.validator', () => { + it('validator on direct input', () => { + const onChange = jest.fn(); + const { container } = render( + { + return /^[0-9]*$/.test(num); + }} + onChange={onChange} + />, + ); + const input = container.querySelector('input'); + fireEvent.focus(input); + + fireEvent.change(input, { target: { value: 'a' } }); + expect(input.value).toEqual('0'); + fireEvent.change(input, { target: { value: '5' } }); + expect(input.value).toEqual('5'); + expect(onChange).toHaveBeenCalledWith(5); + fireEvent.change(input, { target: { value: '10e' } }); + expect(input.value).toEqual('5'); + fireEvent.change(input, { target: { value: '_' } }); + expect(input.value).toEqual('5'); + fireEvent.change(input, { target: { value: '10' } }); + expect(input.value).toEqual('10'); + expect(onChange).toHaveBeenCalledWith(10); + }); + + it('validator and formatter', () => { + const onChange = jest.fn(); + const { container } = render( + `$ ${num} boeing 737`} + validator={(num) => { + return /^[0-9]*$/.test(num); + }} + onChange={onChange} + />, + ); + const input = container.querySelector('input'); + fireEvent.focus(input); + + expect(input.value).toEqual('$ 1 boeing 737'); + fireEvent.change(input, { target: { value: '5' } }); + expect(input.value).toEqual('$ 5 boeing 737'); + + fireEvent.keyDown(input, { + which: KeyCode.UP, + key: 'ArrowUp', + code: 'ArrowUp', + keyCode: KeyCode.UP, + }); + + expect(input.value).toEqual('$ 6 boeing 737'); + expect(onChange).toHaveBeenLastCalledWith(6); + + fireEvent.change(input, { target: { value: '#' } }); + expect(input.value).toEqual('$ 6 boeing 737'); + + fireEvent.keyDown(input, { + which: KeyCode.DOWN, + key: 'ArrowDown', + code: 'ArrowDown', + keyCode: KeyCode.DOWN, + }); + + expect(input.value).toEqual('$ 5 boeing 737'); + expect(onChange).toHaveBeenLastCalledWith(5); + + fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'), { + which: KeyCode.DOWN, + }); + expect(input.value).toEqual('$ 6 boeing 737'); + expect(onChange).toHaveBeenLastCalledWith(6); + fireEvent.change(input, { target: { value: 'a' } }); + expect(input.value).toEqual('$ 6 boeing 737'); + }); +});