From 121606ea81506e26f901320065503044245980dd Mon Sep 17 00:00:00 2001 From: bombillazo Date: Mon, 18 Dec 2023 17:32:23 -0400 Subject: [PATCH 1/7] add diff function to compare inputs, have stricter number inputs --- src/InputNumber.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 4a64d68c..9d0e72d3 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -44,6 +44,20 @@ const getDecimalIfValidate = (value: ValueType) => { return decimal.isInvalidate() ? null : decimal; }; +const inputDiff=(str1: string, str2: string) =>{ + const maxLength = Math.max(str1.length, str2.length); + let diff = ''; + + for (let i = 0; i < maxLength; i++) { + if (str1[i] !== str2[i]) { + if (str1[i]) diff += str1[i]; + if (str2[i]) diff += str2[i]; + } + } + + return diff; +} + export interface InputNumberProps extends Omit< React.InputHTMLAttributes, @@ -382,11 +396,30 @@ const InternalInputNumber = React.forwardRef( const collectInputValue = (inputStr: string) => { recordCursor(); + // checks what are the diffs between the current state and the input value + const diff = inputDiff(inputStr, String(inputValue)); + + // if only one change is present, check if it is a comma + if (diff.length === 1) { + // if it is a comma, do not update the input value + if (diff[0] === ',') { + return; + } + } + + // Only allow input number characters and precision decimals if specified + const precisionRegex = precision ? `{0,${precision}}` : '*'; + const regex = RegExp(`^-?\\d{1,3}(?:,\\d{3})*\.?\\d${precisionRegex}$`,'g'); + if (inputStr!== '' && !regex.test(inputStr)) { + return; + } + // Update inputValue in case input can not parse as number // Refresh ref value immediately since it may used by formatter inputValueRef.current = inputStr; setInternalInputValue(inputStr); + // Parse number if (!compositionRef.current) { const finalValue = mergedParser(inputStr); From 37f1ed2cefece3ffacd5ef72c3389eaacd856624 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Mon, 18 Dec 2023 21:29:27 -0400 Subject: [PATCH 2/7] store prev value and pass to parser/formatter callbacks --- src/InputNumber.tsx | 51 +++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 9d0e72d3..b42264b9 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -44,20 +44,6 @@ const getDecimalIfValidate = (value: ValueType) => { return decimal.isInvalidate() ? null : decimal; }; -const inputDiff=(str1: string, str2: string) =>{ - const maxLength = Math.max(str1.length, str2.length); - let diff = ''; - - for (let i = 0; i < maxLength; i++) { - if (str1[i] !== str2[i]) { - if (str1[i]) diff += str1[i]; - if (str2[i]) diff += str2[i]; - } - } - - return diff; -} - export interface InputNumberProps extends Omit< React.InputHTMLAttributes, @@ -100,9 +86,9 @@ export interface InputNumberProps wheel?: 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; /** Syntactic sugar of `formatter`. Config precision of display. */ precision?: number; /** Syntactic sugar of `formatter`. Config decimal separator of display. */ @@ -185,7 +171,10 @@ const InternalInputNumber = React.forwardRef( } } - // ====================== Parser & Formatter ====================== + + const prevValueRef = React.useRef(''); + + // ====================== Formatter ====================== /** * `precision` is used for formatter & onChange. * It will auto generate by `value` & `step`. @@ -218,7 +207,7 @@ const InternalInputNumber = React.forwardRef( const numStr = String(num); if (parser) { - return parser(numStr); + return parser(numStr, { prevValue: String(prevValueRef.current) }); } let parsedStr = numStr; @@ -237,7 +226,7 @@ const InternalInputNumber = React.forwardRef( 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; @@ -276,7 +265,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) { @@ -396,26 +389,10 @@ const InternalInputNumber = React.forwardRef( const collectInputValue = (inputStr: string) => { recordCursor(); - // checks what are the diffs between the current state and the input value - const diff = inputDiff(inputStr, String(inputValue)); - - // if only one change is present, check if it is a comma - if (diff.length === 1) { - // if it is a comma, do not update the input value - if (diff[0] === ',') { - return; - } - } - - // Only allow input number characters and precision decimals if specified - const precisionRegex = precision ? `{0,${precision}}` : '*'; - const regex = RegExp(`^-?\\d{1,3}(?:,\\d{3})*\.?\\d${precisionRegex}$`,'g'); - if (inputStr!== '' && !regex.test(inputStr)) { - return; - } // 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); From 36506773a3658531cb7a6063361d25a2a5b08715 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Mon, 18 Dec 2023 22:12:51 -0400 Subject: [PATCH 3/7] format value on every change --- src/InputNumber.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index b42264b9..ea6716c1 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -88,7 +88,10 @@ export interface InputNumberProps /** Parse display value to validate number */ 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, prevValue: string }) => string; + formatter?: ( + value: T | undefined, + info: { userTyping: boolean; input: string; prevValue: string }, + ) => string; /** Syntactic sugar of `formatter`. Config precision of display. */ precision?: number; /** Syntactic sugar of `formatter`. Config decimal separator of display. */ @@ -171,8 +174,8 @@ const InternalInputNumber = React.forwardRef( } } - const prevValueRef = React.useRef(''); + const inputValueRef = React.useRef(''); // ====================== Formatter ====================== /** @@ -222,11 +225,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), prevValue: String(prevValueRef.current) }); + return formatter(number, { + userTyping, + input: String(inputValueRef.current), + prevValue: String(prevValueRef.current), + }); } let str = typeof number === 'number' ? num2str(number) : number; @@ -389,14 +395,12 @@ const InternalInputNumber = React.forwardRef( const collectInputValue = (inputStr: string) => { 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 + prevValueRef.current = inputValueRef.current; inputValueRef.current = inputStr; setInternalInputValue(inputStr); - // Parse number if (!compositionRef.current) { const finalValue = mergedParser(inputStr); @@ -488,8 +492,8 @@ const InternalInputNumber = React.forwardRef( if (value !== undefined) { // Reset back with controlled value first setInputValue(decimalValue, false); - } else if (!formatValue.isNaN()) { - // Reset input back since no validate value + } else { + // Format value setInputValue(formatValue, false); } }; @@ -533,7 +537,7 @@ const InternalInputNumber = React.forwardRef( const onWheel = (event) => { if (wheel === false) { return; - }; + } // moving mouse wheel rises wheel event with deltaY < 0 // scroll value grows from top to bottom, as screen Y coordinate onInternalStep(event.deltaY < 0); @@ -546,7 +550,7 @@ const InternalInputNumber = React.forwardRef( // https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev input.addEventListener('wheel', onWheel); return () => input.removeEventListener('wheel', onWheel); - }; + } }, [onInternalStep]); // >>> Focus & Blur From c39b28d5cc12143a939bdf2de3bd4ebbc69f98f7 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Tue, 19 Dec 2023 01:50:58 -0400 Subject: [PATCH 4/7] add validtor prop callback to validate input before updating --- src/InputNumber.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index ea6716c1..7a229024 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -92,6 +92,8 @@ export interface InputNumberProps 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. */ @@ -137,6 +139,7 @@ const InternalInputNumber = React.forwardRef( classNames, stringMode, + validator, parser, formatter, precision, @@ -393,6 +396,12 @@ 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 @@ -492,8 +501,8 @@ const InternalInputNumber = React.forwardRef( if (value !== undefined) { // Reset back with controlled value first setInputValue(decimalValue, false); - } else { - // Format value + } else if (!formatValue.isNaN()) { + // Reset input back since no validate value setInputValue(formatValue, false); } }; From c25fd57756d343ef9a30b75e5b49227b1156ac8d Mon Sep 17 00:00:00 2001 From: bombillazo Date: Mon, 20 May 2024 16:55:39 -0400 Subject: [PATCH 5/7] chore: update file spacing --- src/InputNumber.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 6e5177b4..8307c2d9 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -133,25 +133,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; From 81e33759c9992f8ccb506ee38cc8cab79e794e3f Mon Sep 17 00:00:00 2001 From: bombillazo Date: Sun, 29 Sep 2024 02:06:01 -0400 Subject: [PATCH 6/7] chore: fix prevValue to be empty string if not defined --- src/InputNumber.tsx | 9 ++++----- tests/formatter.test.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 8307c2d9..26b7ed8f 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -206,7 +206,7 @@ const InternalInputNumber = React.forwardRef( const numStr = String(num); if (parser) { - return parser(numStr, { prevValue: String(prevValueRef.current) }); + return parser(numStr, { prevValue: String(prevValueRef.current ?? '') }); } let parsedStr = numStr; @@ -227,7 +227,7 @@ const InternalInputNumber = React.forwardRef( return formatter(number, { userTyping, input: String(inputValueRef.current), - prevValue: String(prevValueRef.current), + prevValue: String(prevValueRef.current ?? ''), }); } @@ -389,10 +389,9 @@ const InternalInputNumber = React.forwardRef( // >>> Collect input value const collectInputValue = (inputStr: string) => { - // validate string - if(validator){ - if(!validator(inputStr)) return; + if (validator) { + if (!validator(inputStr)) return; } recordCursor(); diff --git a/tests/formatter.test.tsx b/tests/formatter.test.tsx index c5419769..a79414c3 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', () => { From fecd638bf6cbfea7d0d9dccb87a9e14c9de29b4e Mon Sep 17 00:00:00 2001 From: bombillazo Date: Sun, 29 Sep 2024 03:47:54 -0400 Subject: [PATCH 7/7] chore: add validator test --- tests/validator.test.tsx | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/validator.test.tsx 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'); + }); +});