UNPKG

@massds/mayflower-react

Version:

React versions of Mayflower design system UI components

422 lines (355 loc) 13.9 kB
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /** * InputCurrency module. * @module @massds/mayflower-react/InputCurrency * @requires module:@massds/mayflower-assets/scss/01-atoms/_input--button * @requires module:@massds/mayflower-assets/scss/01-atoms/01-atoms/helper-text * @requires ma__input--button('currency'); */ import React from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import numbro from "numbro"; import languages from "numbro/dist/languages.min"; import is from "is"; import Input from "../Input/index.mjs"; import { countDecimals } from "../Input/utility.mjs"; import Error from "../Input/error.mjs"; import { InputContext } from "../Input/context.mjs"; import { validNumber } from "../Input/validate.mjs"; const Currency = props => { const ref = /*#__PURE__*/React.createRef(); return /*#__PURE__*/React.createElement(InputContext.Consumer, null, context => { const upRef = /*#__PURE__*/React.createRef(); const downRef = /*#__PURE__*/React.createRef(); const inputClasses = classNames({ 'ma__input-currency__control': true, 'js-is-required': props.required, 'ma__input-currency__control--showButtons': props.showButtons }); const toCurrency = (number, decimal) => { if (is.number(number)) { if (props.language) { let i = 0; const langKeys = Object.keys(languages); const langMax = langKeys.length; for (; i < langMax; i += 1) { const langKey = langKeys[i]; const lang = languages[langKey]; numbro.registerLanguage(lang); } numbro.setLanguage(props.language); } const currency = numbro(number); const format = props.format; if (decimal) { format.mantissa = decimal; } return currency.formatCurrency(format); } return number; }; const hasNumberProperty = (obj, property) => Object.prototype.hasOwnProperty.call(obj, property) && is.number(obj[property]); const greaterThanMin = val => !hasNumberProperty(props, 'min') || val >= props.min; const lessThanMax = val => !hasNumberProperty(props, 'max') || val <= props.max; const displayErrorMessage = val => { const min = props.min, max = props.max, required = props.required; if (required && !is.number(val)) { const errorMsg = 'Please enter a value.'; return { showError: true, errorMsg: errorMsg }; } if (is.number(val)) { const _validNumber = validNumber(val, min, max), showError = _validNumber.showError, errorMsg = _validNumber.errorMsg; return { showError: showError, errorMsg: errorMsg }; } return { showError: false, errorMsg: '' }; }; const handleChange = e => { const type = e.type; const stringValue = ref.current.value; const numberValue = stringValue ? Number(numbro.unformat(stringValue)) : 0; // If the stringvalue is empty, set to empty string so the required error // message is rendered. Otherwise pass the number value for the min/max check. const updateError = displayErrorMessage(!is.empty(stringValue) ? numberValue : ''); context.updateState(_extends({ value: stringValue }, updateError), () => { if (is.fn(props.onChange)) { props.onChange(numberValue, props.id, type); } }); }; const handleAdjust = e => { const direction = e.currentTarget === upRef.current ? 'up' : 'down'; const type = e.type; const stringValue = ref.current.value; const numberValue = stringValue ? Number(numbro.unformat(stringValue)) : 0; let newValue; if (direction === 'up') { newValue = Number(numbro(numberValue).add(props.step).format({ mantissa: countDecimals(props.step) })); } else if (direction === 'down') { newValue = Number(numbro(numberValue).subtract(props.step).format({ mantissa: countDecimals(props.step) })); } if (greaterThanMin(newValue) && lessThanMax(newValue)) { const updateError = displayErrorMessage(!is.empty(stringValue) ? newValue : ''); context.updateState(_extends({ value: toCurrency(newValue, countDecimals(props.step)) }, updateError), () => { if (is.fn(props.onChange)) { props.onChange(newValue, props.id, type, direction); } }); } }; const handleKeyDown = e => { const type = e.type, key = e.key; const stringValue = ref.current.value; const numberValue = stringValue ? Number(numbro.unformat(stringValue)) : 0; // default to 0 if defaultValue is NaN if (is.number(numberValue) && !is.empty(stringValue)) { let newValue = numberValue; if (key === 'ArrowDown') { newValue = Number(numbro(numberValue).subtract(props.step).format({ mantissa: countDecimals(props.step) })); if (greaterThanMin(newValue) && lessThanMax(newValue)) { const updateError = displayErrorMessage(!is.empty(stringValue) ? newValue : ''); context.updateState(_extends({ value: toCurrency(newValue, countDecimals(props.step)) }, updateError), () => { if (is.fn(props.onChange)) { props.onChange(newValue, props.id, type, key); } }); } } else if (key === 'ArrowUp') { newValue = Number(numbro(numberValue).add(props.step).format({ mantissa: countDecimals(props.step) })); if (greaterThanMin(newValue) && lessThanMax(newValue)) { const updateError = displayErrorMessage(!is.empty(stringValue) ? newValue : ''); context.updateState(_extends({ value: toCurrency(newValue, countDecimals(props.step)) }, updateError), () => { if (is.fn(props.onChange)) { props.onChange(newValue, props.id, type, key); } }); } } } }; const handleBlur = e => { const type = e.type; const inputEl = ref.current; const value = inputEl && inputEl.value; const numberValue = value && Number(numbro.unformat(value.replace('$', ''))); // isNotNumber returns true if value is null, undefined or NaN vs Number.isNaN only checks if value is NaN /* eslint-disable-next-line no-restricted-globals */ const isNotNumber = isNaN(numberValue); if (isNotNumber) { inputEl.setAttribute('placeholder', props.placeholder); } else { let newValue = numberValue; if (hasNumberProperty(props, 'max') && newValue > props.max) { newValue = props.max; } if (hasNumberProperty(props, 'min') && newValue < props.min) { newValue = props.min; } const updateError = displayErrorMessage(newValue); context.updateState(_extends({ value: toCurrency(newValue, countDecimals(props.step)) }, updateError), () => { if (is.fn(props.onBlur)) { props.onBlur(newValue, { id: props.id, type: type }); } }); } }; const handleFocus = () => { const inputEl = ref.current; if (is.empty(inputEl.value)) { inputEl.removeAttribute('placeholder'); } }; const inputAttr = { className: inputClasses, name: props.name, id: props.id, type: 'text', placeholder: props.placeholder, 'data-type': 'text', maxLength: is.number(props.maxlength) ? Number(props.maxlength) : null, style: !is.empty(props.width) ? { width: props.width + "px" } : null, ref: ref, onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, onKeyDown: handleKeyDown, required: props.required, value: context.getValue(), disabled: props.disabled }; return /*#__PURE__*/React.createElement("div", { className: "ma__input-currency" }, /*#__PURE__*/React.createElement("input", inputAttr), props.showButtons && /*#__PURE__*/React.createElement("div", { className: "ma__input-number__control-buttons" }, /*#__PURE__*/React.createElement("button", { type: "button", "aria-label": "increase value", className: "ma__input-currency__control-plus", onClick: handleAdjust, disabled: props.disabled, tabIndex: -1, ref: upRef }), /*#__PURE__*/React.createElement("button", { type: "button", "aria-label": "decrease value", className: "ma__input-currency__control-minus", onClick: handleAdjust, disabled: props.disabled, tabIndex: -1, ref: downRef }))); }); }; Currency.propTypes = process.env.NODE_ENV !== "production" ? { required: PropTypes.bool, showButtons: PropTypes.bool, language: PropTypes.string, /* eslint-disable-next-line react/forbid-prop-types */ format: PropTypes.object, max: PropTypes.number, min: PropTypes.number, onChange: PropTypes.func, onBlur: PropTypes.func, step: PropTypes.number, placeholder: PropTypes.string, id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, maxlength: PropTypes.number, width: PropTypes.number, disabled: PropTypes.bool } : {}; const InputCurrency = props => { const max = props.max, min = props.min, step = props.step, name = props.name, onChange = props.onChange, onBlur = props.onBlur, placeholder = props.placeholder, width = props.width, maxlength = props.maxlength, format = props.format, language = props.language, showButtons = props.showButtons, inputProps = _objectWithoutPropertiesLoose(props, ["max", "min", "step", "name", "onChange", "onBlur", "placeholder", "width", "maxlength", "format", "language", "showButtons"]); // Input and Currency share the props.required and props.id values. const currencyProps = { max: max, min: min, step: step, name: name, placeholder: placeholder, width: width, maxlength: maxlength, required: props.required, id: props.id, onChange: onChange, onBlur: onBlur, format: format, language: language, disabled: props.disabled, showButtons: showButtons }; if (!is.empty(inputProps.defaultValue)) { const currency = numbro(inputProps.defaultValue); inputProps.defaultValue = currency.formatCurrency(format); } return /*#__PURE__*/React.createElement(Input, inputProps, /*#__PURE__*/React.createElement(Currency, currencyProps), /*#__PURE__*/React.createElement(Error, { id: props.id })); }; InputCurrency.propTypes = process.env.NODE_ENV !== "production" ? { /** Whether the label should be hidden or not */ hiddenLabel: PropTypes.bool, /** The label text for the input field, can be a string or a component */ labelText: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, /** Whether the field is required or not */ required: PropTypes.bool, /** Whether the field is disabled or not */ disabled: PropTypes.bool, /** The unique ID for the input field */ id: PropTypes.string.isRequired, /** The name for the input field */ name: PropTypes.string.isRequired, /** The max acceptable input length */ maxlength: PropTypes.number, /** The pattern to filter input against, e.g. "[0-9]" for numbers only */ pattern: PropTypes.string, /** The number of characters wide to make the input field */ width: PropTypes.number, /** The placeholder text for the input field */ placeholder: PropTypes.string, /** The message to be displayed in the event of an error. */ errorMsg: PropTypes.string, /** Custom change function */ onChange: PropTypes.func, /** Custom onBlur function */ onBlur: PropTypes.func, /** Default input value */ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Max value for the field. */ max: PropTypes.number, /** Min value for the field. */ min: PropTypes.number, /** Using the up/down arrow keys will increment/decrement the input value by this number. */ step: PropTypes.number, /** A language tag that represents what country the currency should display. Comes from IETF BCP 47: https://numbrojs.com/languages.html */ language: PropTypes.string, /** Numbro Formatting options for displaying the currency. See https://numbrojs.com/format.html */ /* eslint-disable-next-line react/forbid-prop-types */ format: PropTypes.object, /** Inline label and input field */ inline: PropTypes.bool, /** Whether to render up/down buttons */ showButtons: PropTypes.bool } : {}; InputCurrency.defaultProps = { hiddenLabel: false, required: false, onChange: null, onBlur: null, language: 'en-US', format: { mantissa: 2, trimMantissa: false, thousandSeparated: true, negative: 'parenthesis' }, step: 0.01, showButtons: true }; export default InputCurrency;