@massds/mayflower-react
Version:
React versions of Mayflower design system UI components
422 lines (355 loc) • 13.9 kB
JavaScript
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;