From 0acf756fafa820e5f956da56094b6c0efa6baa8b Mon Sep 17 00:00:00 2001 From: Bernard Date: Wed, 3 Jul 2019 11:58:14 -0500 Subject: [PATCH] [WNMGDS-62] Return original value in Mask component when unable to mask (#435) * Return original value if no numerics found in numeric masks * Change maskValue and unmask to return the same value back if masking is not possible * Change unmask to unmaskValue * Update tests --- .../src/components/TextField/Mask.example.jsx | 2 +- .../core/src/components/TextField/Mask.jsx | 70 ++++++++++-------- .../src/components/TextField/Mask.test.jsx | 74 +++++++++---------- .../src/components/TextField/TextField.jsx | 4 +- 4 files changed, 79 insertions(+), 71 deletions(-) diff --git a/packages/core/src/components/TextField/Mask.example.jsx b/packages/core/src/components/TextField/Mask.example.jsx index 15fe2e6fed..c50f07ce2c 100644 --- a/packages/core/src/components/TextField/Mask.example.jsx +++ b/packages/core/src/components/TextField/Mask.example.jsx @@ -5,7 +5,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; function handleBlur(evt, mask) { - console.log('Unmasked value:', unmaskValue(evt.target.value, mask)); + console.log('Unmasked value: ', unmaskValue(evt.target.value, mask)); } class ControlledCurrencyField extends React.PureComponent { diff --git a/packages/core/src/components/TextField/Mask.jsx b/packages/core/src/components/TextField/Mask.jsx index 708f0e0144..c2015396a0 100644 --- a/packages/core/src/components/TextField/Mask.jsx +++ b/packages/core/src/components/TextField/Mask.jsx @@ -25,7 +25,6 @@ const deliminatedMaskRegex = { */ function deliminateRegexGroups(value, rx) { const matches = toDigitsAndAsterisks(value).match(rx); - if (matches && matches.length > 1) { value = matches .slice(1) @@ -84,9 +83,6 @@ function toDigits(value) { * @returns {Number} */ function toNumber(value) { - if (typeof value !== 'string') return value; - if (!value.match(/\d/)) return undefined; - const sign = value.charAt(0) === '-' ? -1 : 1; const parts = value.split('.'); // This assumes if the user adds a "." it should be a float. If we want it to @@ -103,24 +99,39 @@ function toNumber(value) { } /** - * Returns the value with additional masking characters + * Determines if a value is a valid string with numeric digits * @param {String} value - * @returns {String} + * @param {String} mask + * @returns {Boolean} */ -export function maskValue(value = '', mask) { +function isValueMaskable(value, mask) { if (value && typeof value === 'string') { - value = value.trim(); + const hasDigits = value.match(/\d/); + const hasDigitsAsterisks = value.match(/[\d*]/g); + if (hasDigits || (hasDigitsAsterisks && mask === 'ssn')) { + return true; + } + } + return false; +} +/** + * Returns the value with additional masking characters, or the same value back if invalid numeric string + * @param {String} value + * @returns {String} + */ +export function maskValue(value = '', mask) { + if (isValueMaskable(value, mask)) { if (mask === 'currency') { // Format number with commas. If the number includes a decimal, // ensure it includes two decimal points const number = toNumber(value); - if (number === undefined) { - value = ''; - } else { + if (number !== undefined) { value = stringWithFixedDigits(number.toLocaleString('en-US')); } - } else if (Object.keys(deliminatedMaskRegex).includes(mask)) { + } else if (deliminatedMaskRegex[mask]) { + // Use deliminator regex to mask value and remove unwanted characters + // If the regex does not match, return the numeric digits. value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); } } @@ -177,7 +188,10 @@ export class Mask extends React.PureComponent { // given and what we have locally don't match, that means the controlling // component has made its own unrelated change, so we should update our // state and mask this new value. - if (unmask(fieldProps.value, mask) !== unmask(this.state.value, mask)) { + if ( + unmaskValue(fieldProps.value, mask) !== + unmaskValue(this.state.value, mask) + ) { const value = maskValue(fieldProps.value || '', mask); this.setState({ value }); // eslint-disable-line react/no-did-update-set-state } @@ -260,29 +274,23 @@ Mask.propTypes = { }; /** - * Remove mask characters from value + * Remove mask characters from value, or the same value back if invalid numeric string * @param {String} value * @param {String} mask * @returns {String} */ -export function unmask(value, mask) { - if (!value || typeof value !== 'string') return value; - const rawValue = value; - value = value.trim(); - - if (mask === 'currency') { - // Preserve only digits, decimal point, or negative symbol - const matches = value.match(/^-|[\d.]/g); - if (matches) { - value = matches.join(''); - } else { - value = ''; +export function unmaskValue(value, mask) { + if (isValueMaskable(value, mask)) { + if (mask === 'currency') { + // Preserve only digits, decimal point, or negative symbol + const matches = value.match(/^-|[\d.]/g); + if (matches) { + value = matches.join(''); + } + } else if (deliminatedMaskRegex[mask]) { + // Remove the deliminators and revert to single ungrouped string + value = toDigitsAndAsterisks(value); } - } else if (Object.keys(deliminatedMaskRegex).includes(mask)) { - // Remove the deliminators and revert to single ungrouped string - value = toDigitsAndAsterisks(value); - } else { - return rawValue; } return value; diff --git a/packages/core/src/components/TextField/Mask.test.jsx b/packages/core/src/components/TextField/Mask.test.jsx index da65084f97..fc12b27b01 100644 --- a/packages/core/src/components/TextField/Mask.test.jsx +++ b/packages/core/src/components/TextField/Mask.test.jsx @@ -1,4 +1,4 @@ -import Mask, { unmask } from './Mask'; +import Mask, { unmaskValue } from './Mask'; import { mount, shallow } from 'enzyme'; import React from 'react'; @@ -101,7 +101,7 @@ describe('Mask', function() { ) }); @@ -332,72 +332,72 @@ describe('Mask', function() { }); }); -describe('unmask', () => { +describe('unmaskValue', () => { it('returns value when mask is undefined', () => { - expect(unmask(' 1,234 Foo ')).toBe(' 1,234 Foo '); + expect(unmaskValue(' 1,234 Foo ')).toBe(' 1,234 Foo '); }); it('returns value when mask is unknown', () => { - expect(unmask('1,234', 'foo')).toBe('1,234'); + expect(unmaskValue('1,234', 'foo')).toBe('1,234'); }); it('exits when value is undefined or null', () => { - expect(unmask()).toBeUndefined(); - expect(unmask(null)).toBeNull(); + expect(unmaskValue()).toBeUndefined(); + expect(unmaskValue(null)).toBeNull(); }); - it('returns empty string when there are no numeric characters in the value', () => { - expect(unmask('banana', 'currency')).toBe(''); - expect(unmask('banana', 'zip')).toBe(''); - expect(unmask('banana', 'ssn')).toBe(''); - expect(unmask('banana', 'phone')).toBe(''); + it('returns same string back when there are no numeric characters in the value', () => { + expect(unmaskValue('banana', 'currency')).toBe('banana'); + expect(unmaskValue('banana', 'zip')).toBe('banana'); + expect(unmaskValue('banana', 'ssn')).toBe('banana'); + expect(unmaskValue('banana', 'phone')).toBe('banana'); }); it('returns just the numbers when there is other garbage mixed in', () => { - expect(unmask('b4n4n4', 'currency')).toBe('444'); - expect(unmask('b4n4n4', 'zip')).toBe('444'); - expect(unmask('b4n4n4', 'ssn')).toBe('444'); - expect(unmask('b4n4n4', 'phone')).toBe('444'); - - expect(unmask('a1.b2c3', 'currency')).toBe('1.23'); - expect(unmask('1,,00.b', 'currency')).toBe('100.'); - expect(unmask('1-1-1-2-3-4', 'zip')).toBe('111234'); - expect(unmask('4---31', 'ssn')).toBe('431'); - expect(unmask('--2-3444', 'phone')).toBe('23444'); + expect(unmaskValue('b4n4n4', 'currency')).toBe('444'); + expect(unmaskValue('b4n4n4', 'zip')).toBe('444'); + expect(unmaskValue('b4n4n4', 'ssn')).toBe('444'); + expect(unmaskValue('b4n4n4', 'phone')).toBe('444'); + + expect(unmaskValue('a1.b2c3', 'currency')).toBe('1.23'); + expect(unmaskValue('1,,00.b', 'currency')).toBe('100.'); + expect(unmaskValue('1-1-1-2-3-4', 'zip')).toBe('111234'); + expect(unmaskValue('4---31', 'ssn')).toBe('431'); + expect(unmaskValue('--2-3444', 'phone')).toBe('23444'); }); it('removes mask from currency value', () => { const name = 'currency'; - expect(unmask('', name)).toBe(''); - expect(unmask(' 1,234 ', name)).toBe('1234'); // whitespace - expect(unmask('1,234', name)).toBe('1234'); - expect(unmask('1,234.5', name)).toBe('1234.5'); - expect(unmask('1,234,000.50', name)).toBe('1234000.50'); - expect(unmask('-1,234,000.50', name)).toBe('-1234000.50'); + expect(unmaskValue('', name)).toBe(''); + expect(unmaskValue(' 1,234 ', name)).toBe('1234'); // whitespace + expect(unmaskValue('1,234', name)).toBe('1234'); + expect(unmaskValue('1,234.5', name)).toBe('1234.5'); + expect(unmaskValue('1,234,000.50', name)).toBe('1234000.50'); + expect(unmaskValue('-1,234,000.50', name)).toBe('-1234000.50'); }); it('removes mask from zip code', () => { const name = 'zip'; - expect(unmask('', name)).toBe(''); - expect(unmask(' 12345 ', name)).toBe('12345'); - expect(unmask('12345-6789', name)).toBe('123456789'); + expect(unmaskValue('', name)).toBe(''); + expect(unmaskValue(' 12345 ', name)).toBe('12345'); + expect(unmaskValue('12345-6789', name)).toBe('123456789'); }); it('removes mask from ssn value', () => { const name = 'ssn'; - expect(unmask('', name)).toBe(''); - expect(unmask(' 123-45-6789 ', name)).toBe('123456789'); - expect(unmask('123456789', name)).toBe('123456789'); - expect(unmask('***-**-6789', name)).toBe('*****6789'); + expect(unmaskValue('', name)).toBe(''); + expect(unmaskValue(' 123-45-6789 ', name)).toBe('123456789'); + expect(unmaskValue('123456789', name)).toBe('123456789'); + expect(unmaskValue('***-**-6789', name)).toBe('*****6789'); }); it('removes mask from phone number', () => { const name = 'phone'; - expect(unmask('', name)).toBe(''); - expect(unmask(' 123-456-7890 ', name)).toBe('1234567890'); + expect(unmaskValue('', name)).toBe(''); + expect(unmaskValue(' 123-456-7890 ', name)).toBe('1234567890'); }); }); diff --git a/packages/core/src/components/TextField/TextField.jsx b/packages/core/src/components/TextField/TextField.jsx index 62d9b3c8f2..b1b44d8729 100644 --- a/packages/core/src/components/TextField/TextField.jsx +++ b/packages/core/src/components/TextField/TextField.jsx @@ -1,11 +1,11 @@ -import Mask, { unmask } from './Mask'; import FormLabel from '../FormLabel/FormLabel'; +import Mask from './Mask'; import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; import uniqueId from 'lodash.uniqueid'; -export { unmask as unmaskValue }; +export { unmaskValue } from './Mask'; /** * A `TextField` component renders an input field as well as supporting UI