@cmsgov/design-system-core
Advanced tools
Comparing version 1.25.0 to 1.26.0
@@ -20,2 +20,4 @@ 'use strict'; | ||
var _reactLifecyclesCompat = require('react-lifecycles-compat'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -88,3 +90,3 @@ | ||
/** | ||
* Remove all non-digits | ||
* Remove everything that isn't a digit or asterisk | ||
* @param {String} value | ||
@@ -98,2 +100,11 @@ * @returns {String} | ||
/** | ||
* Remove all non-digits | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
function toDigits(value) { | ||
return value.replace(/[^\d]/g, ''); | ||
} | ||
/** | ||
* Convert string into a number (positive or negative float or integer) | ||
@@ -105,10 +116,46 @@ * @param {String} value | ||
if (typeof value !== 'string') return value; | ||
if (!value.match(/\d/)) return undefined; | ||
// 0 = number, 1 = decimals | ||
var sign = value.charAt(0) === '-' ? -1 : 1; | ||
var parts = value.split('.'); | ||
var digitsRegex = /^-|\d/g; // include a check for a beginning "-" for negative numbers | ||
var a = parts[0].match(digitsRegex).join(''); | ||
var b = parts.length >= 2 && parts[1].match(digitsRegex).join(''); | ||
// This assumes if the user adds a "." it should be a float. If we want it to | ||
// evaluate as an integer if there are no digits beyond the decimal, then we | ||
// can change it. | ||
var hasDecimal = parts[1] !== undefined; | ||
if (hasDecimal) { | ||
var a = toDigits(parts[0]); | ||
var b = toDigits(parts[1]); | ||
return sign * parseFloat(a + '.' + b); | ||
} else { | ||
return sign * parseInt(toDigits(parts[0])); | ||
} | ||
} | ||
return b ? parseFloat(a + '.' + b) : parseInt(a); | ||
/** | ||
* Returns the value with additional masking characters | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
function maskValue() { | ||
var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
var mask = arguments[1]; | ||
if (value && typeof value === 'string') { | ||
value = value.trim(); | ||
if (mask === 'currency') { | ||
// Format number with commas. If the number includes a decimal, | ||
// ensure it includes two decimal points | ||
var number = toNumber(value); | ||
if (number === undefined) { | ||
value = ''; | ||
} else { | ||
value = stringWithFixedDigits(number.toLocaleString('en-US')); | ||
} | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); | ||
} | ||
} | ||
return value; | ||
} | ||
@@ -136,12 +183,34 @@ | ||
var Mask = exports.Mask = function (_React$PureComponent) { | ||
_inherits(Mask, _React$PureComponent); | ||
var _Mask = function (_React$PureComponent) { | ||
_inherits(_Mask, _React$PureComponent); | ||
function Mask(props) { | ||
_classCallCheck(this, Mask); | ||
_createClass(_Mask, null, [{ | ||
key: 'getDerivedStateFromProps', | ||
value: function getDerivedStateFromProps(props, state) { | ||
var fieldProps = _react2.default.Children.only(props.children).props; | ||
var isControlled = fieldProps.value !== undefined; | ||
if (isControlled) { | ||
var mask = props.mask; | ||
var _this = _possibleConstructorReturn(this, (Mask.__proto__ || Object.getPrototypeOf(Mask)).call(this, props)); | ||
if (unmask(fieldProps.value, mask) !== unmask(state.value, mask)) { | ||
return { | ||
value: maskValue(fieldProps.value || '', mask) | ||
}; | ||
} | ||
} | ||
return null; | ||
} | ||
}]); | ||
function _Mask(props) { | ||
_classCallCheck(this, _Mask); | ||
var _this = _possibleConstructorReturn(this, (_Mask.__proto__ || Object.getPrototypeOf(_Mask)).call(this, props)); | ||
var field = _this.field(); | ||
var initialValue = field.props.value || field.props.defaultValue; | ||
// console.log('initial value', initialValue, maskValue(initialValue, props.mask), props.mask) | ||
_this.state = { | ||
value: _this.maskedValue(_this.initialValue()) | ||
value: maskValue(initialValue, props.mask) | ||
}; | ||
@@ -151,3 +220,3 @@ return _this; | ||
_createClass(Mask, [{ | ||
_createClass(_Mask, [{ | ||
key: 'componentDidUpdate', | ||
@@ -174,30 +243,2 @@ value: function componentDidUpdate() { | ||
/** | ||
* Returns the value with additional masking characters | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
}, { | ||
key: 'maskedValue', | ||
value: function maskedValue() { | ||
var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; | ||
if (value && typeof value === 'string') { | ||
var mask = this.props.mask; | ||
value = value.trim(); | ||
if (mask === 'currency') { | ||
// Format number with commas. If the number includes a decimal, | ||
// ensure it includes two decimal points | ||
value = stringWithFixedDigits(toNumber(value).toLocaleString('en-US')); | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); | ||
} | ||
} | ||
return value; | ||
} | ||
/** | ||
* To avoid a jarring experience for screen readers, we only | ||
@@ -213,3 +254,3 @@ * add/remove characters after the field has been blurred, | ||
value: function handleBlur(evt, field) { | ||
var value = this.maskedValue(evt.target.value); | ||
var value = maskValue(evt.target.value, this.props.mask); | ||
@@ -256,8 +297,2 @@ // We only debounce the onBlur when we know for sure that | ||
}, { | ||
key: 'initialValue', | ||
value: function initialValue() { | ||
var field = this.field(); | ||
return field.props.value || field.props.defaultValue; | ||
} | ||
}, { | ||
key: 'render', | ||
@@ -282,6 +317,6 @@ value: function render() { | ||
return Mask; | ||
return _Mask; | ||
}(_react2.default.PureComponent); | ||
Mask.propTypes = { | ||
_Mask.propTypes = { | ||
/** Pass the input as the child */ | ||
@@ -305,3 +340,8 @@ children: _propTypes2.default.node.isRequired, | ||
// Preserve only digits, decimal point, or negative symbol | ||
value = value.match(/^-|[\d.]/g).join(''); | ||
var matches = value.match(/^-|[\d.]/g); | ||
if (matches) { | ||
value = matches.join(''); | ||
} else { | ||
value = ''; | ||
} | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
@@ -317,2 +357,5 @@ // Remove the deliminators and revert to single ungrouped string | ||
var Mask = (0, _reactLifecyclesCompat.polyfill)(_Mask); | ||
exports.Mask = Mask; | ||
exports.default = Mask; |
{ | ||
"name": "@cmsgov/design-system-core", | ||
"version": "1.25.0", | ||
"version": "1.26.0", | ||
"publishConfig": { | ||
@@ -12,3 +12,3 @@ "access": "public" | ||
"dependencies": { | ||
"@cmsgov/design-system-support": "^1.25.0", | ||
"@cmsgov/design-system-support": "^1.26.0", | ||
"classnames": "^2.2.5", | ||
@@ -19,3 +19,4 @@ "core-js": "^2.5.3", | ||
"lodash.uniqueid": "^4.0.1", | ||
"react-aria-modal": "^2.11.1" | ||
"react-aria-modal": "^2.11.1", | ||
"react-lifecycles-compat": "^3.0.4" | ||
}, | ||
@@ -22,0 +23,0 @@ "peerDependencies": { |
@@ -19,3 +19,3 @@ import TextField, { unmaskValue } from './TextField'; | ||
onBlur={evt => handleBlur(evt, 'currency')} | ||
value="2500" | ||
defaultValue="2500" | ||
/> | ||
@@ -29,3 +29,3 @@ | ||
type="tel" | ||
value="1234567890" | ||
defaultValue="1234567890" | ||
/> | ||
@@ -38,3 +38,3 @@ | ||
onBlur={evt => handleBlur(evt, 'ssn')} | ||
value="123456789" | ||
defaultValue="123456789" | ||
/> | ||
@@ -47,3 +47,3 @@ | ||
onBlur={evt => handleBlur(evt, 'zip')} | ||
value="123456789" | ||
defaultValue="123456789" | ||
/> | ||
@@ -50,0 +50,0 @@ </div> |
@@ -11,2 +11,3 @@ /* | ||
import React from 'react'; | ||
import { polyfill } from 'react-lifecycles-compat'; | ||
@@ -64,3 +65,3 @@ // Deliminate chunks of integers | ||
/** | ||
* Remove all non-digits | ||
* Remove everything that isn't a digit or asterisk | ||
* @param {String} value | ||
@@ -74,2 +75,11 @@ * @returns {String} | ||
/** | ||
* Remove all non-digits | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
function toDigits(value) { | ||
return value.replace(/[^\d]/g, ''); | ||
} | ||
/** | ||
* Convert string into a number (positive or negative float or integer) | ||
@@ -81,10 +91,43 @@ * @param {String} value | ||
if (typeof value !== 'string') return value; | ||
if (!value.match(/\d/)) return undefined; | ||
// 0 = number, 1 = decimals | ||
const sign = value.charAt(0) === '-' ? -1 : 1; | ||
const parts = value.split('.'); | ||
const digitsRegex = /^-|\d/g; // include a check for a beginning "-" for negative numbers | ||
const a = parts[0].match(digitsRegex).join(''); | ||
const b = parts.length >= 2 && parts[1].match(digitsRegex).join(''); | ||
// This assumes if the user adds a "." it should be a float. If we want it to | ||
// evaluate as an integer if there are no digits beyond the decimal, then we | ||
// can change it. | ||
const hasDecimal = parts[1] !== undefined; | ||
if (hasDecimal) { | ||
const a = toDigits(parts[0]); | ||
const b = toDigits(parts[1]); | ||
return sign * parseFloat(`${a}.${b}`); | ||
} else { | ||
return sign * parseInt(toDigits(parts[0])); | ||
} | ||
} | ||
return b ? parseFloat(`${a}.${b}`) : parseInt(a); | ||
/** | ||
* Returns the value with additional masking characters | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
function maskValue(value = '', mask) { | ||
if (value && typeof value === 'string') { | ||
value = value.trim(); | ||
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 { | ||
value = stringWithFixedDigits(number.toLocaleString('en-US')); | ||
} | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); | ||
} | ||
} | ||
return value; | ||
} | ||
@@ -111,8 +154,26 @@ | ||
*/ | ||
export class Mask extends React.PureComponent { | ||
class _Mask extends React.PureComponent { | ||
static getDerivedStateFromProps(props, state) { | ||
const fieldProps = React.Children.only(props.children).props; | ||
const isControlled = fieldProps.value !== undefined; | ||
if (isControlled) { | ||
const { mask } = props; | ||
if (unmask(fieldProps.value, mask) !== unmask(state.value, mask)) { | ||
return { | ||
value: maskValue(fieldProps.value || '', mask) | ||
}; | ||
} | ||
} | ||
return null; | ||
} | ||
constructor(props) { | ||
super(props); | ||
const field = this.field(); | ||
const initialValue = field.props.value || field.props.defaultValue; | ||
// console.log('initial value', initialValue, maskValue(initialValue, props.mask), props.mask) | ||
this.state = { | ||
value: this.maskedValue(this.initialValue()) | ||
value: maskValue(initialValue, props.mask) | ||
}; | ||
@@ -138,24 +199,2 @@ } | ||
/** | ||
* Returns the value with additional masking characters | ||
* @param {String} value | ||
* @returns {String} | ||
*/ | ||
maskedValue(value = '') { | ||
if (value && typeof value === 'string') { | ||
const { mask } = this.props; | ||
value = value.trim(); | ||
if (mask === 'currency') { | ||
// Format number with commas. If the number includes a decimal, | ||
// ensure it includes two decimal points | ||
value = stringWithFixedDigits(toNumber(value).toLocaleString('en-US')); | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); | ||
} | ||
} | ||
return value; | ||
} | ||
/** | ||
* To avoid a jarring experience for screen readers, we only | ||
@@ -168,3 +207,3 @@ * add/remove characters after the field has been blurred, | ||
handleBlur(evt, field) { | ||
const value = this.maskedValue(evt.target.value); | ||
const value = maskValue(evt.target.value, this.props.mask); | ||
@@ -209,7 +248,2 @@ // We only debounce the onBlur when we know for sure that | ||
initialValue() { | ||
const field = this.field(); | ||
return field.props.value || field.props.defaultValue; | ||
} | ||
render() { | ||
@@ -227,3 +261,3 @@ const field = this.field(); | ||
Mask.propTypes = { | ||
_Mask.propTypes = { | ||
/** Pass the input as the child */ | ||
@@ -247,3 +281,8 @@ children: PropTypes.node.isRequired, | ||
// Preserve only digits, decimal point, or negative symbol | ||
value = value.match(/^-|[\d.]/g).join(''); | ||
const matches = value.match(/^-|[\d.]/g); | ||
if (matches) { | ||
value = matches.join(''); | ||
} else { | ||
value = ''; | ||
} | ||
} else if (Object.keys(deliminatedMaskRegex).includes(mask)) { | ||
@@ -259,2 +298,5 @@ // Remove the deliminators and revert to single ungrouped string | ||
const Mask = polyfill(_Mask); | ||
export { Mask }; | ||
export default Mask; |
@@ -93,2 +93,38 @@ import Mask, { unmask } from './Mask'; | ||
describe('Controlled component behavior', () => { | ||
it('will not cause masking until blur when value prop still matches unmasked input', () => { | ||
const { wrapper } = render({ mask: 'currency' }, { value: '1000' }, true); | ||
const input = () => wrapper.find('input'); | ||
expect(input().prop('value')).toBe('1,000'); | ||
// Simulate user typing input and the component calling onChange, and that | ||
// cascading back down to a new prop for the input. | ||
input() | ||
.props() | ||
.onChange({ target: { value: '1,0000' } }); | ||
wrapper.setProps({ | ||
children: <input name="foo" type="text" value="10000" /> | ||
}); | ||
expect(input().prop('value')).toBe('1,0000'); | ||
input().simulate('blur', { | ||
target: { value: '1,0000' }, | ||
persist: jest.fn() | ||
}); | ||
expect(input().prop('value')).toBe('10,000'); | ||
}); | ||
it('will change the value of the input when value prop changes (beyond unmasked/masked differences)', () => { | ||
const { wrapper } = render({ mask: 'currency' }, { value: '1000' }, true); | ||
const input = () => wrapper.find('input'); | ||
expect(input().prop('value')).toBe('1,000'); | ||
// Make sure we can change the value | ||
wrapper.setProps({ | ||
children: <input name="foo" type="text" value="2000" /> | ||
}); | ||
expect(input().prop('value')).toBe('2,000'); | ||
}); | ||
}); | ||
describe('Currency', () => { | ||
@@ -302,2 +338,22 @@ it('accepts already masked value', () => { | ||
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 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'); | ||
}); | ||
it('removes mask from currency value', () => { | ||
@@ -304,0 +360,0 @@ const name = 'currency'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1529462
251
11462
11
+ Addedreact-lifecycles-compat@3.0.4(transitive)