From b424a4584beac48f528c514ff15ea70f8b40dca4 Mon Sep 17 00:00:00 2001 From: s-yadav Date: Sun, 11 Nov 2018 22:08:29 +0530 Subject: [PATCH] - Maintain caret position if suffix/prefix is updated while typing #249. - Refactor code to have one place to update state and caret position. - Add spec for maintaining caret pos when . is used instead of decimal separator --- dist/react-number-format.es.js | 135 +++-- dist/react-number-format.js | 135 +++-- dist/react-number-format.min.js | 4 +- lib/number_format.js | 129 ++-- lib/utils.js | 6 + package.json | 2 +- src/number_format.js | 108 ++-- src/utils.js | 5 + test/library/input_numeric_format.spec.js | 10 +- test/library/keypress_and_caret.spec.js | 693 +++++++++++----------- test/test_util.js | 10 +- 11 files changed, 681 insertions(+), 556 deletions(-) diff --git a/dist/react-number-format.es.js b/dist/react-number-format.es.js index d9ae3398..b22e25ea 100644 --- a/dist/react-number-format.es.js +++ b/dist/react-number-format.es.js @@ -1,5 +1,5 @@ /** - * react-number-format - 4.0.3 + * react-number-format - 4.0.4 * Author : Sudhanshu Yadav * Copyright (c) 2016, 2018 to Sudhanshu Yadav, released under the MIT license. * https://github.com/s-yadav/react-number-format @@ -496,6 +496,10 @@ function findChangedIndex(prevValue, newValue) { function clamp(num, min, max) { return Math.min(Math.max(num, min), max); } +function getCurrentCaretPosition(el) { + /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ + return Math.max(el.selectionStart, el.selectionEnd); +} var propTypes$1 = { thousandSeparator: propTypes.oneOfType([propTypes.string, propTypes.oneOf([true])]), @@ -590,8 +594,7 @@ function (_React$Component) { value: function updateValueIfRequired(prevProps) { var props = this.props, state = this.state, - inFocus = this.inFocus; - var onValueChange = props.onValueChange; + focusedElm = this.focusedElm; var stateValue = state.value, _state$numAsString = state.numAsString, lastNumStr = _state$numAsString === void 0 ? '' : _state$numAsString; @@ -608,12 +611,12 @@ function (_React$Component) { if ( //while typing set state only when float value changes (!isNaN(floatValue) || !isNaN(lastFloatValue)) && floatValue !== lastFloatValue || //can also set state when float value is same and the format props changes lastValueWithNewFormat !== stateValue || //set state always when not in focus and formatted value is changed - inFocus === false && formattedValue !== stateValue) { - this.setState({ - value: formattedValue, - numAsString: numAsString + focusedElm === null && formattedValue !== stateValue) { + this.updateValue({ + formattedValue: formattedValue, + numAsString: numAsString, + input: focusedElm }); - onValueChange(this.getValueObject(formattedValue, numAsString)); } } } @@ -1169,6 +1172,54 @@ function (_React$Component) { return value; } + /** Update value and caret position */ + + }, { + key: "updateValue", + value: function updateValue(params) { + var _this2 = this; + + var onUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; + var formattedValue = params.formattedValue, + input = params.input; + var numAsString = params.numAsString, + caretPos = params.caretPos; + var onValueChange = this.props.onValueChange; + var lastValue = this.state.value; //set caret position, and value imperatively when element is provided + + if (input) { + //calculate caret position if not defined + if (!caretPos) { + var inputValue = params.inputValue || input.value; + var currentCaretPosition = getCurrentCaretPosition(input); //get the caret position + + caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); + } //set the value imperatively, this is required for IE fix + + + input.value = formattedValue; //set caret position + + this.setPatchedCaretPosition(input, caretPos, formattedValue); + } //calculate numeric string if not passed + + + if (numAsString === undefined) { + numAsString = this.removeFormatting(formattedValue); + } //update state if value is changed + + + if (formattedValue !== lastValue) { + this.setState({ + value: formattedValue, + numAsString: numAsString + }, function () { + onValueChange(_this2.getValueObject(formattedValue, numAsString)); + onUpdate(); + }); + } else { + onUpdate(); + } + } }, { key: "onChange", value: function onChange(e) { @@ -1179,9 +1230,7 @@ function (_React$Component) { props = this.props; var isAllowed = props.isAllowed; var lastValue = state.value || ''; - /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ - - var currentCaretPosition = Math.max(el.selectionStart, el.selectionEnd); + var currentCaretPosition = getCurrentCaretPosition(el); inputValue = this.correctInputValue(currentCaretPosition, lastValue, inputValue); var formattedValue = this.formatInput(inputValue) || ''; var numAsString = this.removeFormatting(formattedValue); @@ -1189,39 +1238,27 @@ function (_React$Component) { if (!isAllowed(valueObj)) { formattedValue = lastValue; - } //set the value imperatively, this is required for IE fix - - - el.value = formattedValue; //get the caret position - - var caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); //set caret position - - this.setPatchedCaretPosition(el, caretPos, formattedValue); //change the state + } - if (formattedValue !== lastValue) { - this.setState({ - value: formattedValue, - numAsString: numAsString - }, function () { - props.onValueChange(valueObj); - props.onChange(e); - }); - } else { + this.updateValue({ + formattedValue: formattedValue, + numAsString: numAsString, + inputValue: inputValue, + input: el + }, function () { props.onChange(e); - } + }); } }, { key: "onBlur", value: function onBlur(e) { - var _this2 = this; - var props = this.props, state = this.state; var format = props.format, onBlur = props.onBlur; var numAsString = state.numAsString; var lastValue = state.value; - this.inFocus = false; + this.focusedElm = null; if (!format) { numAsString = fixLeadingZero(numAsString); @@ -1230,13 +1267,10 @@ function (_React$Component) { if (formattedValue !== lastValue) { // the event needs to be persisted because its properties can be accessed in an asynchronous way e.persist(); - this.setState({ - value: formattedValue, + this.updateValue({ + formattedValue: formattedValue, numAsString: numAsString }, function () { - var valueObj = _this2.getValueObject(formattedValue, numAsString); - - props.onValueChange(valueObj); onBlur(e); }); return; @@ -1248,8 +1282,6 @@ function (_React$Component) { }, { key: "onKeyDown", value: function onKeyDown(e) { - var _this3 = this; - var el = e.target; var key = e.key; var selectionStart = el.selectionStart, @@ -1307,18 +1339,13 @@ function (_React$Component) { we will not have any information of keyPress */ if (selectionStart <= leftBound + 1 && value[0] === '-' && typeof format === 'undefined') { - var newValue = value.substring(1); - var numAsString = this.removeFormatting(newValue); - var valueObj = this.getValueObject(newValue, numAsString); //persist event before performing async task + var newValue = value.substring(1); //persist event before performing async task e.persist(); - this.setState({ - value: newValue, - numAsString: numAsString - }, function () { - _this3.setPatchedCaretPosition(el, newCaretPosition, newValue); - - onValueChange(valueObj); + this.updateValue({ + formattedValue: newValue, + caretPos: newCaretPosition, + input: el }); } else if (!negativeRegex.test(value[expectedCaretPosition])) { while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound) { @@ -1372,12 +1399,12 @@ function (_React$Component) { }, { key: "onFocus", value: function onFocus(e) { - var _this4 = this; + var _this3 = this; // Workaround Chrome and Safari bug https://bugs.chromium.org/p/chromium/issues/detail?id=779328 // (onFocus event target selectionStart is always 0 before setTimeout) e.persist(); - this.inFocus = true; + this.focusedElm = e.target; setTimeout(function () { var el = e.target; var selectionStart = el.selectionStart, @@ -1385,14 +1412,14 @@ function (_React$Component) { _el$value3 = el.value, value = _el$value3 === void 0 ? '' : _el$value3; - var caretPosition = _this4.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) + var caretPosition = _this3.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) if (caretPosition !== selectionStart && !(selectionStart === 0 && selectionEnd === value.length)) { - _this4.setPatchedCaretPosition(el, caretPosition, value); + _this3.setPatchedCaretPosition(el, caretPosition, value); } - _this4.props.onFocus(e); + _this3.props.onFocus(e); }, 0); } }, { diff --git a/dist/react-number-format.js b/dist/react-number-format.js index 20d5711e..29dc20bb 100644 --- a/dist/react-number-format.js +++ b/dist/react-number-format.js @@ -1,5 +1,5 @@ /** - * react-number-format - 4.0.3 + * react-number-format - 4.0.4 * Author : Sudhanshu Yadav * Copyright (c) 2016, 2018 to Sudhanshu Yadav, released under the MIT license. * https://github.com/s-yadav/react-number-format @@ -502,6 +502,10 @@ function clamp(num, min, max) { return Math.min(Math.max(num, min), max); } + function getCurrentCaretPosition(el) { + /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ + return Math.max(el.selectionStart, el.selectionEnd); + } var propTypes$1 = { thousandSeparator: propTypes.oneOfType([propTypes.string, propTypes.oneOf([true])]), @@ -596,8 +600,7 @@ value: function updateValueIfRequired(prevProps) { var props = this.props, state = this.state, - inFocus = this.inFocus; - var onValueChange = props.onValueChange; + focusedElm = this.focusedElm; var stateValue = state.value, _state$numAsString = state.numAsString, lastNumStr = _state$numAsString === void 0 ? '' : _state$numAsString; @@ -614,12 +617,12 @@ if ( //while typing set state only when float value changes (!isNaN(floatValue) || !isNaN(lastFloatValue)) && floatValue !== lastFloatValue || //can also set state when float value is same and the format props changes lastValueWithNewFormat !== stateValue || //set state always when not in focus and formatted value is changed - inFocus === false && formattedValue !== stateValue) { - this.setState({ - value: formattedValue, - numAsString: numAsString + focusedElm === null && formattedValue !== stateValue) { + this.updateValue({ + formattedValue: formattedValue, + numAsString: numAsString, + input: focusedElm }); - onValueChange(this.getValueObject(formattedValue, numAsString)); } } } @@ -1175,6 +1178,54 @@ return value; } + /** Update value and caret position */ + + }, { + key: "updateValue", + value: function updateValue(params) { + var _this2 = this; + + var onUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; + var formattedValue = params.formattedValue, + input = params.input; + var numAsString = params.numAsString, + caretPos = params.caretPos; + var onValueChange = this.props.onValueChange; + var lastValue = this.state.value; //set caret position, and value imperatively when element is provided + + if (input) { + //calculate caret position if not defined + if (!caretPos) { + var inputValue = params.inputValue || input.value; + var currentCaretPosition = getCurrentCaretPosition(input); //get the caret position + + caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); + } //set the value imperatively, this is required for IE fix + + + input.value = formattedValue; //set caret position + + this.setPatchedCaretPosition(input, caretPos, formattedValue); + } //calculate numeric string if not passed + + + if (numAsString === undefined) { + numAsString = this.removeFormatting(formattedValue); + } //update state if value is changed + + + if (formattedValue !== lastValue) { + this.setState({ + value: formattedValue, + numAsString: numAsString + }, function () { + onValueChange(_this2.getValueObject(formattedValue, numAsString)); + onUpdate(); + }); + } else { + onUpdate(); + } + } }, { key: "onChange", value: function onChange(e) { @@ -1185,9 +1236,7 @@ props = this.props; var isAllowed = props.isAllowed; var lastValue = state.value || ''; - /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ - - var currentCaretPosition = Math.max(el.selectionStart, el.selectionEnd); + var currentCaretPosition = getCurrentCaretPosition(el); inputValue = this.correctInputValue(currentCaretPosition, lastValue, inputValue); var formattedValue = this.formatInput(inputValue) || ''; var numAsString = this.removeFormatting(formattedValue); @@ -1195,39 +1244,27 @@ if (!isAllowed(valueObj)) { formattedValue = lastValue; - } //set the value imperatively, this is required for IE fix - - - el.value = formattedValue; //get the caret position - - var caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); //set caret position - - this.setPatchedCaretPosition(el, caretPos, formattedValue); //change the state + } - if (formattedValue !== lastValue) { - this.setState({ - value: formattedValue, - numAsString: numAsString - }, function () { - props.onValueChange(valueObj); - props.onChange(e); - }); - } else { + this.updateValue({ + formattedValue: formattedValue, + numAsString: numAsString, + inputValue: inputValue, + input: el + }, function () { props.onChange(e); - } + }); } }, { key: "onBlur", value: function onBlur(e) { - var _this2 = this; - var props = this.props, state = this.state; var format = props.format, onBlur = props.onBlur; var numAsString = state.numAsString; var lastValue = state.value; - this.inFocus = false; + this.focusedElm = null; if (!format) { numAsString = fixLeadingZero(numAsString); @@ -1236,13 +1273,10 @@ if (formattedValue !== lastValue) { // the event needs to be persisted because its properties can be accessed in an asynchronous way e.persist(); - this.setState({ - value: formattedValue, + this.updateValue({ + formattedValue: formattedValue, numAsString: numAsString }, function () { - var valueObj = _this2.getValueObject(formattedValue, numAsString); - - props.onValueChange(valueObj); onBlur(e); }); return; @@ -1254,8 +1288,6 @@ }, { key: "onKeyDown", value: function onKeyDown(e) { - var _this3 = this; - var el = e.target; var key = e.key; var selectionStart = el.selectionStart, @@ -1313,18 +1345,13 @@ we will not have any information of keyPress */ if (selectionStart <= leftBound + 1 && value[0] === '-' && typeof format === 'undefined') { - var newValue = value.substring(1); - var numAsString = this.removeFormatting(newValue); - var valueObj = this.getValueObject(newValue, numAsString); //persist event before performing async task + var newValue = value.substring(1); //persist event before performing async task e.persist(); - this.setState({ - value: newValue, - numAsString: numAsString - }, function () { - _this3.setPatchedCaretPosition(el, newCaretPosition, newValue); - - onValueChange(valueObj); + this.updateValue({ + formattedValue: newValue, + caretPos: newCaretPosition, + input: el }); } else if (!negativeRegex.test(value[expectedCaretPosition])) { while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound) { @@ -1378,12 +1405,12 @@ }, { key: "onFocus", value: function onFocus(e) { - var _this4 = this; + var _this3 = this; // Workaround Chrome and Safari bug https://bugs.chromium.org/p/chromium/issues/detail?id=779328 // (onFocus event target selectionStart is always 0 before setTimeout) e.persist(); - this.inFocus = true; + this.focusedElm = e.target; setTimeout(function () { var el = e.target; var selectionStart = el.selectionStart, @@ -1391,14 +1418,14 @@ _el$value3 = el.value, value = _el$value3 === void 0 ? '' : _el$value3; - var caretPosition = _this4.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) + var caretPosition = _this3.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) if (caretPosition !== selectionStart && !(selectionStart === 0 && selectionEnd === value.length)) { - _this4.setPatchedCaretPosition(el, caretPosition, value); + _this3.setPatchedCaretPosition(el, caretPosition, value); } - _this4.props.onFocus(e); + _this3.props.onFocus(e); }, 0); } }, { diff --git a/dist/react-number-format.min.js b/dist/react-number-format.min.js index 0e881141..effb8f31 100644 --- a/dist/react-number-format.min.js +++ b/dist/react-number-format.min.js @@ -1,8 +1,8 @@ /** - * react-number-format - 4.0.3 + * react-number-format - 4.0.4 * Author : Sudhanshu Yadav * Copyright (c) 2016, 2018 to Sudhanshu Yadav, released under the MIT license. * https://github.com/s-yadav/react-number-format */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("react")):"function"==typeof define&&define.amd?define(["react"],e):t.NumberFormat=e(t.React)}(this,function(h){"use strict";function o(t,e){for(var r=0;rr?(Number(t[0])+Number(e)).toString()+t.substring(1,t.length):e+t},u[0]),c=y(u[1]||"",Math.min(e,i.length),r),f=n?".":"";return"".concat(s?"-":"").concat(l).concat(f).concat(c)}(u,n,o)),l?this.formatNumString(u):this.formatInput(u))}},{key:"formatNegation",value:function(){var t=0=e.length-a.length||i&&s&&e[t]===u))}},{key:"checkIfFormatGotDeleted",value:function(t,e,r){for(var n=t;ne.length||!r.length||g===m||0===f&&p===e.length||f===d&&p===v)return r;if(this.checkIfFormatGotDeleted(g,m,e)&&(r=e),!o){var y=this.removeFormatting(r),S=P(y,a),b=S.beforeDecimal,x=S.afterDecimal,O=S.addNegation,w=tr?(Number(t[0])+Number(e)).toString()+t.substring(1,t.length):e+t},u[0]),c=y(u[1]||"",Math.min(e,i.length),r),f=n?".":"";return"".concat(s?"-":"").concat(l).concat(f).concat(c)}(u,n,a)),l?this.formatNumString(u):this.formatInput(u))}},{key:"formatNegation",value:function(){var t=0=e.length-o.length||i&&s&&e[t]===u))}},{key:"checkIfFormatGotDeleted",value:function(t,e,r){for(var n=t;ne.length||!r.length||g===m||0===f&&p===e.length||f===d&&p===v)return r;if(this.checkIfFormatGotDeleted(g,m,e)&&(r=e),!a){var y=this.removeFormatting(r),S=P(y,o),b=S.beforeDecimal,x=S.afterDecimal,O=S.addNegation,w=t 1 && arguments[1] !== undefined ? arguments[1] : _utils.noop; + var formattedValue = params.formattedValue, + input = params.input; + var numAsString = params.numAsString, + caretPos = params.caretPos; + var onValueChange = this.props.onValueChange; + var lastValue = this.state.value; //set caret position, and value imperatively when element is provided + + if (input) { + //calculate caret position if not defined + if (!caretPos) { + var inputValue = params.inputValue || input.value; + var currentCaretPosition = (0, _utils.getCurrentCaretPosition)(input); //get the caret position + + caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); + } //set the value imperatively, this is required for IE fix + + + input.value = formattedValue; //set caret position + + this.setPatchedCaretPosition(input, caretPos, formattedValue); + } //calculate numeric string if not passed + + + if (numAsString === undefined) { + numAsString = this.removeFormatting(formattedValue); + } //update state if value is changed + + + if (formattedValue !== lastValue) { + this.setState({ + value: formattedValue, + numAsString: numAsString + }, function () { + onValueChange(_this2.getValueObject(formattedValue, numAsString)); + onUpdate(); + }); + } else { + onUpdate(); + } + } }, { key: "onChange", value: function onChange(e) { @@ -715,9 +762,7 @@ function (_React$Component) { props = this.props; var isAllowed = props.isAllowed; var lastValue = state.value || ''; - /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ - - var currentCaretPosition = Math.max(el.selectionStart, el.selectionEnd); + var currentCaretPosition = (0, _utils.getCurrentCaretPosition)(el); inputValue = this.correctInputValue(currentCaretPosition, lastValue, inputValue); var formattedValue = this.formatInput(inputValue) || ''; var numAsString = this.removeFormatting(formattedValue); @@ -725,39 +770,27 @@ function (_React$Component) { if (!isAllowed(valueObj)) { formattedValue = lastValue; - } //set the value imperatively, this is required for IE fix - - - el.value = formattedValue; //get the caret position - - var caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); //set caret position - - this.setPatchedCaretPosition(el, caretPos, formattedValue); //change the state + } - if (formattedValue !== lastValue) { - this.setState({ - value: formattedValue, - numAsString: numAsString - }, function () { - props.onValueChange(valueObj); - props.onChange(e); - }); - } else { + this.updateValue({ + formattedValue: formattedValue, + numAsString: numAsString, + inputValue: inputValue, + input: el + }, function () { props.onChange(e); - } + }); } }, { key: "onBlur", value: function onBlur(e) { - var _this2 = this; - var props = this.props, state = this.state; var format = props.format, onBlur = props.onBlur; var numAsString = state.numAsString; var lastValue = state.value; - this.inFocus = false; + this.focusedElm = null; if (!format) { numAsString = (0, _utils.fixLeadingZero)(numAsString); @@ -766,13 +799,10 @@ function (_React$Component) { if (formattedValue !== lastValue) { // the event needs to be persisted because its properties can be accessed in an asynchronous way e.persist(); - this.setState({ - value: formattedValue, + this.updateValue({ + formattedValue: formattedValue, numAsString: numAsString }, function () { - var valueObj = _this2.getValueObject(formattedValue, numAsString); - - props.onValueChange(valueObj); onBlur(e); }); return; @@ -784,8 +814,6 @@ function (_React$Component) { }, { key: "onKeyDown", value: function onKeyDown(e) { - var _this3 = this; - var el = e.target; var key = e.key; var selectionStart = el.selectionStart, @@ -843,18 +871,13 @@ function (_React$Component) { we will not have any information of keyPress */ if (selectionStart <= leftBound + 1 && value[0] === '-' && typeof format === 'undefined') { - var newValue = value.substring(1); - var numAsString = this.removeFormatting(newValue); - var valueObj = this.getValueObject(newValue, numAsString); //persist event before performing async task + var newValue = value.substring(1); //persist event before performing async task e.persist(); - this.setState({ - value: newValue, - numAsString: numAsString - }, function () { - _this3.setPatchedCaretPosition(el, newCaretPosition, newValue); - - onValueChange(valueObj); + this.updateValue({ + formattedValue: newValue, + caretPos: newCaretPosition, + input: el }); } else if (!negativeRegex.test(value[expectedCaretPosition])) { while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound) { @@ -908,12 +931,12 @@ function (_React$Component) { }, { key: "onFocus", value: function onFocus(e) { - var _this4 = this; + var _this3 = this; // Workaround Chrome and Safari bug https://bugs.chromium.org/p/chromium/issues/detail?id=779328 // (onFocus event target selectionStart is always 0 before setTimeout) e.persist(); - this.inFocus = true; + this.focusedElm = e.target; setTimeout(function () { var el = e.target; var selectionStart = el.selectionStart, @@ -921,14 +944,14 @@ function (_React$Component) { _el$value3 = el.value, value = _el$value3 === void 0 ? '' : _el$value3; - var caretPosition = _this4.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) + var caretPosition = _this3.correctCaretPosition(value, selectionStart); //setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) if (caretPosition !== selectionStart && !(selectionStart === 0 && selectionEnd === value.length)) { - _this4.setPatchedCaretPosition(el, caretPosition, value); + _this3.setPatchedCaretPosition(el, caretPosition, value); } - _this4.props.onFocus(e); + _this3.props.onFocus(e); }, 0); } }, { diff --git a/lib/utils.js b/lib/utils.js index a37a3aa0..1914b959 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -16,6 +16,7 @@ exports.omit = omit; exports.setCaretPosition = setCaretPosition; exports.findChangedIndex = findChangedIndex; exports.clamp = clamp; +exports.getCurrentCaretPosition = getCurrentCaretPosition; // basic noop function function noop() {} @@ -188,4 +189,9 @@ function findChangedIndex(prevValue, newValue) { function clamp(num, min, max) { return Math.min(Math.max(num, min), max); +} + +function getCurrentCaretPosition(el) { + /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ + return Math.max(el.selectionStart, el.selectionEnd); } \ No newline at end of file diff --git a/package.json b/package.json index f2adce8d..4ecb3571 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-number-format", "description": "React component to format number in an input or as a text.", - "version": "4.0.3", + "version": "4.0.4", "main": "lib/number_format.js", "author": "Sudhanshu Yadav", "license": "MIT", diff --git a/src/number_format.js b/src/number_format.js index 208690ff..14b8cfe0 100644 --- a/src/number_format.js +++ b/src/number_format.js @@ -16,6 +16,7 @@ import { findChangedIndex, clamp, getThousandsGroupRegex, + getCurrentCaretPosition, } from './utils'; @@ -89,7 +90,7 @@ class NumberFormat extends React.Component { onMouseUp: Function onFocus: Function onBlur: Function - inFocus: boolean + focusedElm: HTMLElement selectionBeforeInput: { selectionStart: number, selectionEnd: number @@ -127,8 +128,7 @@ class NumberFormat extends React.Component { } updateValueIfRequired(prevProps: Object) { - const {props, state, inFocus} = this; - const {onValueChange} = props; + const {props, state, focusedElm} = this; const {value: stateValue, numAsString: lastNumStr = ''} = state; if(prevProps !== props) { @@ -149,14 +149,9 @@ class NumberFormat extends React.Component { //can also set state when float value is same and the format props changes lastValueWithNewFormat !== stateValue || //set state always when not in focus and formatted value is changed - (inFocus === false && formattedValue !== stateValue) + (focusedElm === null && formattedValue !== stateValue) ) { - this.setState({ - value : formattedValue, - numAsString, - }); - - onValueChange(this.getValueObject(formattedValue, numAsString)); + this.updateValue({ formattedValue, numAsString, input: focusedElm }); } } } @@ -650,6 +645,57 @@ class NumberFormat extends React.Component { return value; } + /** Update value and caret position */ + updateValue(params: { + formattedValue: string, + numAsString: string, + inputValue: string, + input: HTMLInputElement, + caretPos: number, + }, + onUpdate?: Function = noop + ) { + const {formattedValue, input} = params; + let {numAsString, caretPos} = params; + const {onValueChange} = this.props; + const {value: lastValue} = this.state; + + //set caret position, and value imperatively when element is provided + if (input) { + + //calculate caret position if not defined + if (!caretPos) { + const inputValue = params.inputValue || input.value; + + const currentCaretPosition = getCurrentCaretPosition(input); + + //get the caret position + caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); + } + + //set the value imperatively, this is required for IE fix + input.value = formattedValue; + + //set caret position + this.setPatchedCaretPosition(input, caretPos, formattedValue); + } + + //calculate numeric string if not passed + if (numAsString === undefined) { + numAsString = this.removeFormatting(formattedValue); + } + + //update state if value is changed + if (formattedValue !== lastValue) { + this.setState({value : formattedValue, numAsString}, () => { + onValueChange(this.getValueObject(formattedValue, numAsString)); + onUpdate(); + }); + } else { + onUpdate(); + } + } + onChange(e: SyntheticInputEvent) { e.persist(); const el = e.target; @@ -658,8 +704,7 @@ class NumberFormat extends React.Component { const {isAllowed} = props; const lastValue = state.value || ''; - /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ - const currentCaretPosition = Math.max(el.selectionStart, el.selectionEnd); + const currentCaretPosition = getCurrentCaretPosition(el); inputValue = this.correctInputValue(currentCaretPosition, lastValue, inputValue); @@ -671,25 +716,11 @@ class NumberFormat extends React.Component { if (!isAllowed(valueObj)) { formattedValue = lastValue; } - - //set the value imperatively, this is required for IE fix - el.value = formattedValue; - - //get the caret position - const caretPos = this.getCaretPosition(inputValue, formattedValue, currentCaretPosition); - - //set caret position - this.setPatchedCaretPosition(el, caretPos, formattedValue); - - //change the state - if (formattedValue !== lastValue) { - this.setState({value : formattedValue, numAsString}, () => { - props.onValueChange(valueObj); - props.onChange(e); - }); - } else { + + this.updateValue({ formattedValue, numAsString, inputValue, input: el }, () => { props.onChange(e); - } + }); + } onBlur(e: SyntheticInputEvent) { @@ -698,7 +729,7 @@ class NumberFormat extends React.Component { let {numAsString} = state; const lastValue = state.value; - this.inFocus = false; + this.focusedElm = null; if (!format) { numAsString = fixLeadingZero(numAsString); @@ -708,9 +739,7 @@ class NumberFormat extends React.Component { if (formattedValue !== lastValue) { // the event needs to be persisted because its properties can be accessed in an asynchronous way e.persist(); - this.setState({value : formattedValue, numAsString}, () => { - const valueObj = this.getValueObject(formattedValue, numAsString); - props.onValueChange(valueObj); + this.updateValue({ formattedValue, numAsString }, () => { onBlur(e); }); return; @@ -767,15 +796,10 @@ class NumberFormat extends React.Component { */ if (selectionStart <= leftBound + 1 && value[0] === '-' && typeof format === 'undefined') { const newValue = value.substring(1); - const numAsString = this.removeFormatting(newValue); - const valueObj = this.getValueObject(newValue, numAsString); - //persist event before performing async task e.persist(); - this.setState({value: newValue, numAsString}, () => { - this.setPatchedCaretPosition(el, newCaretPosition, newValue); - onValueChange(valueObj); - }); + + this.updateValue({formattedValue: newValue, caretPos: newCaretPosition, input: el}); } else if (!negativeRegex.test(value[expectedCaretPosition])) { while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound){ newCaretPosition--; } newCaretPosition = this.correctCaretPosition(value, newCaretPosition, 'left'); @@ -824,7 +848,7 @@ class NumberFormat extends React.Component { // (onFocus event target selectionStart is always 0 before setTimeout) e.persist(); - this.inFocus = true; + this.focusedElm = e.target; setTimeout(() => { const el = e.target; diff --git a/src/utils.js b/src/utils.js index 47a3046f..a410e430 100644 --- a/src/utils.js +++ b/src/utils.js @@ -155,3 +155,8 @@ export function findChangedIndex(prevValue: string, newValue: string) { export function clamp(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max); } + +export function getCurrentCaretPosition(el: HTMLInputElement ) { + /*Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug*/ + return Math.max(el.selectionStart, el.selectionEnd); +} diff --git a/test/library/input_numeric_format.spec.js b/test/library/input_numeric_format.spec.js index 07081b6f..690a6ecb 100644 --- a/test/library/input_numeric_format.spec.js +++ b/test/library/input_numeric_format.spec.js @@ -492,9 +492,16 @@ describe('Test NumberFormat as input with numeric format options', () => { }); it('should allow typing . as decimal separator when some other decimal separator is given', () => { + let caretPos; + const setSelectionRange = (pos) => { + caretPos = pos; + } + const wrapper = shallow(); - simulateKeyInput(wrapper.find('input'), '.', 5, 5); + simulateKeyInput(wrapper.find('input'), '.', 5, 5, setSelectionRange); expect(wrapper.state().value).toEqual('2.344,56 Sq. ft'); + //check if caret position is maintained + expect(caretPos).toEqual(6); //it should allow typing actual separator simulateKeyInput(wrapper.find('input'), ',', 3, 3); @@ -513,7 +520,6 @@ describe('Test NumberFormat as input with numeric format options', () => { simulateKeyInput(wrapper.find('input'), '-', 2); expect(wrapper.state().value).toEqual('-21-'); }); - describe('Test thousand group style', () => { diff --git a/test/library/keypress_and_caret.spec.js b/test/library/keypress_and_caret.spec.js index 84be94f8..ef4f2fb0 100644 --- a/test/library/keypress_and_caret.spec.js +++ b/test/library/keypress_and_caret.spec.js @@ -1,104 +1,12 @@ import React from 'react'; import NumberFormat from '../../src/number_format'; +import ReactDOM from 'react-dom'; -import {simulateKeyInput, simulateMousUpEvent, simulateFocusEvent, shallow, persist} from '../test_util'; +import {simulateKeyInput, simulateMousUpEvent, simulateFocusEvent, shallow, mount, persist} from '../test_util'; import {cardExpiry} from '../../custom_formatters/card_expiry'; -describe('Test character insertion', () => { - it('should add any number properly when input is empty without format prop passed', () => { - const wrapper = shallow(); +describe('Test keypress and caret position changes', () => { - simulateKeyInput(wrapper.find('input'), '1', 0); - - expect(wrapper.state().value).toEqual('$1'); - - wrapper.setProps({value: ''}); - wrapper.update(); - - simulateKeyInput(wrapper.find('input'), '2456789', 0); - - expect(wrapper.state().value).toEqual('$2,456,789'); - }); - - it('should add any number properly when input is empty with format prop passed', () => { - //case 1: Enter first number - const wrapper = shallow(); - simulateKeyInput(wrapper.find('input'), '1', 0); - expect(wrapper.state().value).toEqual('1___ ____ ____ ____'); - - //case 2: if nun numeric character got added - wrapper.setProps({value: ''}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), 'b', 0); - expect(wrapper.state().value).toEqual(''); - - //case 3: Enter first multiple number - wrapper.setProps({value: undefined}); - wrapper.setProps({value: ''}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '2456789', 0); - expect(wrapper.state().value).toEqual('2456 789_ ____ ____'); - - //case 4: When alpha numeric character got added - wrapper.setProps({value: undefined}); - wrapper.setProps({value: ''}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '245sf6789', 0); - expect(wrapper.state().value).toEqual('2456 789_ ____ ____'); - - //case 5: Similiar to case 4 but a formatted value got added - wrapper.setProps({value: undefined}); - wrapper.setProps({value: ''}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '1234 56', 0); - expect(wrapper.state().value).toEqual('1234 56__ ____ ____'); - - //case 6: If format has numbers - wrapper.setProps({value: undefined}); - wrapper.setProps({value: '', format: '+1 (###) ### # ##'}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '123456', 0); - expect(wrapper.state().value).toEqual('+1 (123) 456 _ __'); - - //case 7: If format has numbers and and formatted value is inserted - wrapper.setProps({value: undefined}); - wrapper.setProps({value: ''}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '+1 (965) 432 1 19', 0); - expect(wrapper.state().value).toEqual('+1 (965) 432 1 19'); - }); - - it('should handle addition of characters at a cursor position', () => { - const wrapper = shallow(); - let caretPos; - const setSelectionRange = (pos) => { - caretPos = pos; - } - - simulateKeyInput(wrapper.find('input'), '8', 2, 2, setSelectionRange); - expect(wrapper.state().value).toEqual('$182,345'); - expect(caretPos).toEqual(3); - - simulateKeyInput(wrapper.find('input'), '67', 3, 3, setSelectionRange); - expect(wrapper.state().value).toEqual('$18,672,345'); - expect(caretPos).toEqual(6); - - wrapper.setProps({format: '### ### ###', value: '123 456 789'}); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), '8', 3, 3, setSelectionRange); - expect(wrapper.state().value).toEqual('123 845 678'); - expect(caretPos).toEqual(5); - - - simulateKeyInput(wrapper.find('input'), '999', 4, 4, setSelectionRange); - expect(wrapper.state().value).toEqual('123 999 845'); - expect(caretPos).toEqual(7); - }); - -}) - -describe('Test delete/backspace with format pattern', () => { - const wrapper = shallow(); let caretPos; const setSelectionRange = (pos) => { caretPos = pos; @@ -109,268 +17,365 @@ describe('Test delete/backspace with format pattern', () => { persist.calls.reset(); }) - it('caret position should not change if its on starting of input area', () => { - simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 456 7 89 US'); - expect(caretPos).toEqual(4); - }); - - it('caret position should not change if its on end of input area', () => { - simulateKeyInput(wrapper.find('input'), 'Delete', 17, 17, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 456 7 89 US'); - expect(caretPos).toEqual(17); - }); - - it('should only remove numbers only from input area in other case it should change the caret position', () => { - simulateKeyInput(wrapper.find('input'), 'Backspace', 10, 10, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); - expect(caretPos).toEqual(9); - - simulateKeyInput(wrapper.find('input'), 'Backspace', 9, 9, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); - expect(caretPos).toEqual(7); - - simulateKeyInput(wrapper.find('input'), 'Delete', 7, 7, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); - expect(caretPos).toEqual(9); - - simulateKeyInput(wrapper.find('input'), 'Delete', 9, 9, setSelectionRange); - expect(wrapper.state().value).toEqual('+1 (123) 678 9 US'); - expect(caretPos).toEqual(9); - }); -}) - -describe('Test delete/backspace with numeric format', () => { - const wrapper = shallow(); - let caretPos; - const setSelectionRange = (pos) => { - caretPos = pos; - } - - beforeEach(() => { - caretPos = 0; - persist.calls.reset(); - }) + it('should maintain caret position if suffix/prefix is updated while typing #249', () => { + class TestComp extends React.Component { + constructor() { + super(); + this.state = { + prefix: '$', + value: '123', + } + } + render() { + const {value, prefix} = this.state; + return ( + { + this.setState({value, prefix: value.length > 3 ? '$$' : '$'}); + }}/> + ) + } + } - it('should not remove prefix', () => { - simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); + const wrapper = mount(); + simulateFocusEvent(wrapper.find('input'), 0, 0, setSelectionRange); + simulateKeyInput(wrapper.find('input'), '4', 2, 2, setSelectionRange); + expect(ReactDOM.findDOMNode(wrapper.instance()).value).toEqual('$$1423'); expect(caretPos).toEqual(4); - }); - - it('should not remove suffix', () => { - simulateKeyInput(wrapper.find('input'), 'Delete', 13, 13, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); - expect(caretPos).toEqual(13); - }); - - it('should only remove number, in other case it should change caret position', () => { - simulateKeyInput(wrapper.find('input'), 'Backspace', 7, 7, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); - expect(caretPos).toEqual(6); - - simulateKeyInput(wrapper.find('input'), 'Delete', 6, 6, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); - expect(caretPos).toEqual(7); - - simulateKeyInput(wrapper.find('input'), 'Backspace', 8, 8, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 1,245.50 /sq.feet'); - expect(caretPos).toEqual(7); - - simulateKeyInput(wrapper.find('input'), 'Delete', 7, 7, setSelectionRange); - expect(wrapper.state().value).toEqual('Rs. 125.50 /sq.feet'); - expect(caretPos).toEqual(6); - }); - - it('should maintain correct caret positon while removing the last character and suffix is not defined. Issue #105', () => { - wrapper.setProps({ - suffix: '', - prefix: '$', - value: '$2,342,343' - }); - wrapper.update(); - simulateKeyInput(wrapper.find('input'), 'Backspace', 10, 10, setSelectionRange); - expect(wrapper.state().value).toEqual('$234,234'); - expect(caretPos).toEqual(8); - }); - - it('should maintain correct caret position while removing the second last character and suffix is not defined, Issue #116', () => { - wrapper.setProps({ - suffix: '', - prefix: '', - value: '1,000' - }); - - wrapper.update(); simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); - expect(wrapper.state().value).toEqual('100'); + expect(ReactDOM.findDOMNode(wrapper.instance()).value).toEqual('$123'); expect(caretPos).toEqual(2); }); - it('should allow removing negation(-), even if its before suffix', () => { - const spy = jasmine.createSpy(); - wrapper.setProps({ - suffix: '', - prefix: '$', - value: '-$1,000', - onValueChange: spy + describe('Test character insertion', () => { + it('should add any number properly when input is empty without format prop passed', () => { + const wrapper = shallow(); + + simulateKeyInput(wrapper.find('input'), '1', 0); + + expect(wrapper.state().value).toEqual('$1'); + + wrapper.setProps({value: ''}); + wrapper.update(); + + simulateKeyInput(wrapper.find('input'), '2456789', 0); + + expect(wrapper.state().value).toEqual('$2,456,789'); }); - wrapper.update(); - - simulateKeyInput(wrapper.find('input'), 'Backspace', 2, 2, setSelectionRange); - expect(wrapper.state().value).toEqual('$1,000'); - expect(caretPos).toEqual(1); - expect(spy).toHaveBeenCalled(); - expect(persist).toHaveBeenCalledWith('keydown'); - }); -}) - -describe('Test arrow keys', () => { - let caretPos; - const setSelectionRange = (pos) => { - caretPos = pos; - } - - beforeEach(() => { - caretPos = 0; - }) - - it('should keep caret position between the prefix and suffix', () => { - const wrapper = shallow(); - simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 4, 4, setSelectionRange); - expect(caretPos).toEqual(4); - - simulateKeyInput(wrapper.find('input'), 'ArrowRight', 13, 13, setSelectionRange); - expect(caretPos).toEqual(13); + + it('should add any number properly when input is empty with format prop passed', () => { + //case 1: Enter first number + const wrapper = shallow(); + simulateKeyInput(wrapper.find('input'), '1', 0); + expect(wrapper.state().value).toEqual('1___ ____ ____ ____'); + + //case 2: if nun numeric character got added + wrapper.setProps({value: ''}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), 'b', 0); + expect(wrapper.state().value).toEqual(''); + + //case 3: Enter first multiple number + wrapper.setProps({value: undefined}); + wrapper.setProps({value: ''}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '2456789', 0); + expect(wrapper.state().value).toEqual('2456 789_ ____ ____'); + + //case 4: When alpha numeric character got added + wrapper.setProps({value: undefined}); + wrapper.setProps({value: ''}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '245sf6789', 0); + expect(wrapper.state().value).toEqual('2456 789_ ____ ____'); + + //case 5: Similiar to case 4 but a formatted value got added + wrapper.setProps({value: undefined}); + wrapper.setProps({value: ''}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '1234 56', 0); + expect(wrapper.state().value).toEqual('1234 56__ ____ ____'); + + //case 6: If format has numbers + wrapper.setProps({value: undefined}); + wrapper.setProps({value: '', format: '+1 (###) ### # ##'}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '123456', 0); + expect(wrapper.state().value).toEqual('+1 (123) 456 _ __'); + + //case 7: If format has numbers and and formatted value is inserted + wrapper.setProps({value: undefined}); + wrapper.setProps({value: ''}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '+1 (965) 432 1 19', 0); + expect(wrapper.state().value).toEqual('+1 (965) 432 1 19'); + }); + + it('should handle addition of characters at a cursor position', () => { + const wrapper = shallow(); + + simulateKeyInput(wrapper.find('input'), '8', 2, 2, setSelectionRange); + expect(wrapper.state().value).toEqual('$182,345'); + expect(caretPos).toEqual(3); + + simulateKeyInput(wrapper.find('input'), '67', 3, 3, setSelectionRange); + expect(wrapper.state().value).toEqual('$18,672,345'); + expect(caretPos).toEqual(6); + + wrapper.setProps({format: '### ### ###', value: '123 456 789'}); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), '8', 3, 3, setSelectionRange); + expect(wrapper.state().value).toEqual('123 845 678'); + expect(caretPos).toEqual(5); + + + simulateKeyInput(wrapper.find('input'), '999', 4, 4, setSelectionRange); + expect(wrapper.state().value).toEqual('123 999 845'); + expect(caretPos).toEqual(7); + }); + }) - - it('should keep caret position within typable area', () => { - const wrapper = shallow(); - simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 4, 4, setSelectionRange); - expect(caretPos).toEqual(4); - - simulateKeyInput(wrapper.find('input'), 'ArrowRight', 17, 17, setSelectionRange); - expect(caretPos).toEqual(17); - - simulateKeyInput(wrapper.find('input'), 'ArrowRight', 7, 7, setSelectionRange); - expect(caretPos).toEqual(9); - - simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 9, 9, setSelectionRange); - expect(caretPos).toEqual(7); - - caretPos = undefined; - simulateKeyInput(wrapper.find('input'), 'ArrowRight', 12, 12, setSelectionRange); - expect(caretPos).toEqual(13); - - caretPos = undefined; - simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 13, 13, setSelectionRange); - expect(caretPos).toEqual(12); - }); - - it('should not move caret positon from left most to right most if left key pressed. #154', () => { - const wrapper = shallow(); - caretPos = undefined; - simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 0, 0, setSelectionRange); - expect(caretPos).toEqual(0); - }); -}) - -describe('Test click / focus on input', () => { - let caretPos; - const setSelectionRange = (pos) => { - caretPos = pos; - } - - beforeEach(() => { - caretPos = 0; - persist.calls.reset(); + + describe('Test delete/backspace with format pattern', () => { + const wrapper = shallow(); + + it('caret position should not change if its on starting of input area', () => { + simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 456 7 89 US'); + expect(caretPos).toEqual(4); + }); + + it('caret position should not change if its on end of input area', () => { + simulateKeyInput(wrapper.find('input'), 'Delete', 17, 17, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 456 7 89 US'); + expect(caretPos).toEqual(17); + }); + + it('should only remove numbers only from input area in other case it should change the caret position', () => { + simulateKeyInput(wrapper.find('input'), 'Backspace', 10, 10, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); + expect(caretPos).toEqual(9); + + simulateKeyInput(wrapper.find('input'), 'Backspace', 9, 9, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); + expect(caretPos).toEqual(7); + + simulateKeyInput(wrapper.find('input'), 'Delete', 7, 7, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 567 8 9 US'); + expect(caretPos).toEqual(9); + + simulateKeyInput(wrapper.find('input'), 'Delete', 9, 9, setSelectionRange); + expect(wrapper.state().value).toEqual('+1 (123) 678 9 US'); + expect(caretPos).toEqual(9); + }); }) - - it('should always keep caret on typable area when we click on the input', () => { - const wrapper = shallow(); - - simulateMousUpEvent(wrapper.find('input'), 0, setSelectionRange); - expect(caretPos).toEqual(4); - - simulateMousUpEvent(wrapper.find('input'), 8, setSelectionRange); - expect([7, 9]).toContain(caretPos); - - simulateMousUpEvent(wrapper.find('input'), 19, setSelectionRange); - expect(caretPos).toEqual(17); + + describe('Test delete/backspace with numeric format', () => { + const wrapper = shallow(); + + it('should not remove prefix', () => { + simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); + expect(caretPos).toEqual(4); + }); + + it('should not remove suffix', () => { + simulateKeyInput(wrapper.find('input'), 'Delete', 13, 13, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); + expect(caretPos).toEqual(13); + }); + + it('should only remove number, in other case it should change caret position', () => { + simulateKeyInput(wrapper.find('input'), 'Backspace', 7, 7, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); + expect(caretPos).toEqual(6); + + simulateKeyInput(wrapper.find('input'), 'Delete', 6, 6, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 12,345.50 /sq.feet'); + expect(caretPos).toEqual(7); + + simulateKeyInput(wrapper.find('input'), 'Backspace', 8, 8, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 1,245.50 /sq.feet'); + expect(caretPos).toEqual(7); + + simulateKeyInput(wrapper.find('input'), 'Delete', 7, 7, setSelectionRange); + expect(wrapper.state().value).toEqual('Rs. 125.50 /sq.feet'); + expect(caretPos).toEqual(6); + }); + + it('should maintain correct caret positon while removing the last character and suffix is not defined. Issue #105', () => { + wrapper.setProps({ + suffix: '', + prefix: '$', + value: '$2,342,343' + }); + wrapper.update(); + simulateKeyInput(wrapper.find('input'), 'Backspace', 10, 10, setSelectionRange); + expect(wrapper.state().value).toEqual('$234,234'); + expect(caretPos).toEqual(8); + }); + + it('should maintain correct caret position while removing the second last character and suffix is not defined, Issue #116', () => { + wrapper.setProps({ + suffix: '', + prefix: '', + value: '1,000' + }); + + wrapper.update(); + + simulateKeyInput(wrapper.find('input'), 'Backspace', 4, 4, setSelectionRange); + expect(wrapper.state().value).toEqual('100'); + expect(caretPos).toEqual(2); + }); + + it('should allow removing negation(-), even if its before suffix', () => { + const spy = jasmine.createSpy(); + wrapper.setProps({ + suffix: '', + prefix: '$', + value: '-$1,000', + onValueChange: spy + }); + wrapper.update(); + + simulateKeyInput(wrapper.find('input'), 'Backspace', 2, 2, setSelectionRange); + expect(wrapper.state().value).toEqual('$1,000'); + expect(caretPos).toEqual(1); + expect(spy).toHaveBeenCalled(); + expect(persist).toHaveBeenCalledWith('keydown'); + }); }) - - it('should limit the caret position to the next position of the typed number', () => { - const wrapper = shallow(); - - simulateKeyInput(wrapper.find('input'), '1', 0); - expect(wrapper.state().value).toEqual('1 / / '); - - simulateMousUpEvent(wrapper.find('input'), 4, setSelectionRange); - expect(caretPos).toEqual(1); - - wrapper.setProps({ - mask: ['D', 'D', 'M', 'M', 'Y', 'Y', 'Y', 'Y'] + + describe('Test arrow keys', () => { + + it('should keep caret position between the prefix and suffix', () => { + const wrapper = shallow(); + simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 4, 4, setSelectionRange); + expect(caretPos).toEqual(4); + + simulateKeyInput(wrapper.find('input'), 'ArrowRight', 13, 13, setSelectionRange); + expect(caretPos).toEqual(13); }) - wrapper.update(); - - expect(wrapper.state().value).toEqual('1D/MM/YYYY'); - simulateMousUpEvent(wrapper.find('input'), 4, setSelectionRange); - expect(caretPos).toEqual(1); - }) - - it('should always keep caret position between suffix and prefix', () => { - const wrapper = shallow(); - - simulateMousUpEvent(wrapper.find('input'), 0, setSelectionRange); - expect(caretPos).toEqual(4); - - simulateMousUpEvent(wrapper.find('input'), 17, setSelectionRange); - expect(caretPos).toEqual(13); - }) - - it('should correct wrong caret position on focus', () => { - jasmine.clock().install() - const wrapper = shallow(); - - simulateFocusEvent(wrapper.find('input'), 0, 0, setSelectionRange); - jasmine.clock().tick(1) - expect(caretPos).toEqual(4) - jasmine.clock().uninstall() - }); - - it('should correct wrong caret positon on focus when allowEmptyFormatting is set', () => { - jasmine.clock().install() - const format = '+1 (###) ### # ## US'; - const wrapper = shallow(); - - simulateFocusEvent(wrapper.find('input'), 1, 1, setSelectionRange); - jasmine.clock().tick(1) - expect(caretPos).toEqual(4) - jasmine.clock().uninstall() + + it('should keep caret position within typable area', () => { + const wrapper = shallow(); + simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 4, 4, setSelectionRange); + expect(caretPos).toEqual(4); + + simulateKeyInput(wrapper.find('input'), 'ArrowRight', 17, 17, setSelectionRange); + expect(caretPos).toEqual(17); + + simulateKeyInput(wrapper.find('input'), 'ArrowRight', 7, 7, setSelectionRange); + expect(caretPos).toEqual(9); + + simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 9, 9, setSelectionRange); + expect(caretPos).toEqual(7); + + caretPos = undefined; + simulateKeyInput(wrapper.find('input'), 'ArrowRight', 12, 12, setSelectionRange); + expect(caretPos).toEqual(13); + + caretPos = undefined; + simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 13, 13, setSelectionRange); + expect(caretPos).toEqual(12); + }); + + it('should not move caret positon from left most to right most if left key pressed. #154', () => { + const wrapper = shallow(); + caretPos = undefined; + simulateKeyInput(wrapper.find('input'), 'ArrowLeft', 0, 0, setSelectionRange); + expect(caretPos).toEqual(0); + }); }) - - it('should not reset correct caret position on focus', () => { - jasmine.clock().install() - const wrapper = shallow(); - - // Note: init caretPos to `6`. Focus to `6`. In case of bug, selectionStart is `0` and the caret will move to `4`. - // otherwise (correct behaviour) the value will not change, and stay `6` - caretPos = 6 - simulateFocusEvent(wrapper.find('input'), 6, 6, setSelectionRange); - jasmine.clock().tick(1) - expect(caretPos).toEqual(6) - jasmine.clock().uninstall() + + describe('Test click / focus on input', () => { + + it('should always keep caret on typable area when we click on the input', () => { + const wrapper = shallow(); + + simulateMousUpEvent(wrapper.find('input'), 0, setSelectionRange); + expect(caretPos).toEqual(4); + + simulateMousUpEvent(wrapper.find('input'), 8, setSelectionRange); + expect([7, 9]).toContain(caretPos); + + simulateMousUpEvent(wrapper.find('input'), 19, setSelectionRange); + expect(caretPos).toEqual(17); + }) + + it('should limit the caret position to the next position of the typed number', () => { + const wrapper = shallow(); + + simulateKeyInput(wrapper.find('input'), '1', 0); + expect(wrapper.state().value).toEqual('1 / / '); + + simulateMousUpEvent(wrapper.find('input'), 4, setSelectionRange); + expect(caretPos).toEqual(1); + + wrapper.setProps({ + mask: ['D', 'D', 'M', 'M', 'Y', 'Y', 'Y', 'Y'] + }) + wrapper.update(); + + expect(wrapper.state().value).toEqual('1D/MM/YYYY'); + simulateMousUpEvent(wrapper.find('input'), 4, setSelectionRange); + expect(caretPos).toEqual(1); + }) + + it('should always keep caret position between suffix and prefix', () => { + const wrapper = shallow(); + + simulateMousUpEvent(wrapper.find('input'), 0, setSelectionRange); + expect(caretPos).toEqual(4); + + simulateMousUpEvent(wrapper.find('input'), 17, setSelectionRange); + expect(caretPos).toEqual(13); + }) + + it('should correct wrong caret position on focus', () => { + jasmine.clock().install() + const wrapper = shallow(); + + simulateFocusEvent(wrapper.find('input'), 0, 0, setSelectionRange); + jasmine.clock().tick(1) + expect(caretPos).toEqual(4) + jasmine.clock().uninstall() + }); + + it('should correct wrong caret positon on focus when allowEmptyFormatting is set', () => { + jasmine.clock().install() + const wrapper = shallow(); + + simulateFocusEvent(wrapper.find('input'), 1, 1, setSelectionRange); + jasmine.clock().tick(1) + expect(caretPos).toEqual(4) + jasmine.clock().uninstall() + }) + + it('should not reset correct caret position on focus', () => { + jasmine.clock().install() + const wrapper = shallow(); + + // Note: init caretPos to `6`. Focus to `6`. In case of bug, selectionStart is `0` and the caret will move to `4`. + // otherwise (correct behaviour) the value will not change, and stay `6` + caretPos = 6 + simulateFocusEvent(wrapper.find('input'), 6, 6, setSelectionRange); + jasmine.clock().tick(1) + expect(caretPos).toEqual(6) + jasmine.clock().uninstall() + }); + + it('should not reset caret position on focus when full value is selected', () => { + jasmine.clock().install(); + const value = "Rs. 12,345.50 /sq.feet"; + const wrapper = shallow(); + + simulateFocusEvent(wrapper.find('input'), 0, value.length, setSelectionRange); + jasmine.clock().tick(1); + expect(caretPos).toEqual(0); + }); }); +}) - it('should not reset caret position on focus when full value is selected', () => { - jasmine.clock().install(); - const value = "Rs. 12,345.50 /sq.feet"; - const wrapper = shallow(); - - simulateFocusEvent(wrapper.find('input'), 0, value.length, setSelectionRange); - jasmine.clock().tick(1); - expect(caretPos).toEqual(0); - }); -}); diff --git a/test/test_util.js b/test/test_util.js index 7bedb208..1d0d4c58 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -7,6 +7,9 @@ const noop = function(){}; export const persist = jasmine.createSpy(); +//keep input element singleton +const target = document.createElement('input'); + export function getCustomEvent(value, selectionStart, selectionEnd) { let event = new Event('custom'); const el = document.createElement('input'); @@ -20,13 +23,12 @@ export function getCustomEvent(value, selectionStart, selectionEnd) { function getEvent(eventProps, targetProps) { let event = new Event('custom'); - const el = document.createElement('input'); Object.keys(targetProps).forEach((key) => { - el[key] = targetProps[key]; - }) + target[key] = targetProps[key]; + }); - event = {...event, ...eventProps, target: el}; + event = {...event, ...eventProps, target}; return event; }