UNPKG

@totalsoft/rocket-ui

Version:

A set of reusable and composable React components built on top of Material UI core for developing fast and friendly web applications interfaces.

253 lines 10.6 kB
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { NumericFormat } from 'react-number-format'; import { TextField as MuiTextField, StepButton, classes } from './TextFieldStyles'; import InputAdornment from '@mui/material/InputAdornment'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '../../buttons/IconButton'; import { is, isNil } from 'ramda'; import i18n from 'i18next'; import useDebouncedCallback from '../../utils/useDebouncedCallback'; const NumberTextField = React.forwardRef(function NumberFormatCustom(props, ref) { const { value, onChange, decimalScale = 2, fixedDecimalScale = true, thousandSeparator = true, decimalSeparator, language = i18n.language, currency, isStepper, minValue, maxValue, ...other } = props; const isAllowed = useCallback(({ formattedValue, floatValue }) => { if (floatValue && floatValue.toString().includes('e')) return false; if (isNil(floatValue)) { return formattedValue === ''; } else { return floatValue <= maxValue && floatValue >= minValue; } }, [maxValue, minValue]); const formatter = new Intl.NumberFormat(language); const thousandSep = thousandSeparator === true ? formatter.format(1111).replace(/1/g, '') : thousandSeparator; const decimalSep = decimalSeparator || formatter.format(1.1).replace(/1/g, ''); const currencyFormatter = currency && new Intl.NumberFormat(language, { style: 'currency', currency }); const currencySymbol = currencyFormatter?.format(1).replace(/[\d,.\s]/g, ''); const valueIsNumericString = is(String, value) && is(Number, Number(value)); const handleValueChange = useCallback((values, sourceInfo) => { onChange(values.floatValue, sourceInfo.event); }, [onChange]); return (React.createElement(NumericFormat, { style: { textAlign: isStepper ? 'center' : 'right' }, value: value, getInputRef: ref, onValueChange: handleValueChange, isAllowed: isAllowed, decimalScale: decimalScale, fixedDecimalScale: fixedDecimalScale, thousandSeparator: thousandSep, decimalSeparator: decimalSep, prefix: currencySymbol, valueIsNumericString: valueIsNumericString, ...other })); }); NumberTextField.propTypes = { value: PropTypes.any, onChange: PropTypes.func.isRequired, decimalScale: PropTypes.number, fixedDecimalScale: PropTypes.bool, thousandSeparator: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), decimalSeparator: PropTypes.string, language: PropTypes.string, currency: PropTypes.string, isStepper: PropTypes.bool, minValue: PropTypes.number, maxValue: PropTypes.number, format: PropTypes.string, allowEmptyFormatting: PropTypes.bool }; const ClearButton = ({ onClearInput, disabled }) => (React.createElement(IconButton, { disabled: disabled, "aria-label": "Clear", size: "tiny", color: "primary", variant: "text", onClick: onClearInput }, React.createElement(CloseIcon, { fontSize: "small" }))); ClearButton.propTypes = { onClearInput: PropTypes.func, disabled: PropTypes.bool }; const AddButton = ({ onAdd }) => (React.createElement(InputAdornment, { position: "end" }, React.createElement(StepButton, { onClick: onAdd }, "+"))); AddButton.propTypes = { onAdd: PropTypes.func }; const SubtractButton = ({ onSubtract }) => (React.createElement(InputAdornment, { position: "start" }, React.createElement(StepButton, { onClick: onSubtract }, "-"))); SubtractButton.propTypes = { onSubtract: PropTypes.func }; /** * Text Fields let users enter and edit text. * At its core, it uses [Material-UI TextField](https://mui.com/material-ui/react-text-field/#basic-textfield). */ const TextField = ({ isNumeric: receivedIsNumeric, inputProps, InputProps, endAdornment, startAdornment, fullWidth, InputLabelProps, value, onChange = () => { }, debounceBy = 0, decimalScale = 2, fixedDecimalScale, thousandSeparator, decimalSeparator, language, currency, disabled, isClearable, isStepper = false, step = 1, minValue = -Infinity, maxValue = Infinity, variant = 'standard', ...rest }) => { const isNumeric = receivedIsNumeric || isStepper; const [liveValue, setLiveValue] = useState(value); useLayoutEffect(() => { setLiveValue(value); }, [value]); const debouncedOnChange = useDebouncedCallback(onChange, debounceBy); const handleClearInput = useCallback(() => { onChange(''); }, [onChange]); const handleSubtract = useCallback(() => { const nextValue = !value ? -step : Number(value) - Number(step); if (nextValue >= minValue) onChange(nextValue); }, [minValue, onChange, step, value]); const handleAdd = useCallback(() => { const nextValue = !value ? Number(step) : Number(value) + Number(step); if (nextValue <= maxValue) onChange(nextValue); }, [maxValue, onChange, step, value]); const internalStartAdornment = useMemo(() => { if (!isStepper && !startAdornment) return null; return (React.createElement(React.Fragment, null, isStepper && (React.createElement(InputAdornment, { position: "start" }, React.createElement(SubtractButton, { onSubtract: handleSubtract }))), startAdornment)); }, [handleSubtract, isStepper, startAdornment]); const internalEndAdornment = useMemo(() => { if (!isStepper && !isClearable && !endAdornment) return null; return (React.createElement(InputAdornment, { position: "end" }, isStepper && React.createElement(AddButton, { onAdd: handleAdd }), isClearable && React.createElement(ClearButton, { onClearInput: handleClearInput, disabled: disabled }), endAdornment)); }, [disabled, endAdornment, handleAdd, handleClearInput, isClearable, isStepper]); const muiInputProps = { className: `${isStepper && !fullWidth ? classes.stepperFixedWidth : ''}`, ...InputProps, startAdornment: internalStartAdornment, endAdornment: internalEndAdornment, style: InputProps?.style }; const numericProps = { decimalScale, fixedDecimalScale, thousandSeparator, decimalSeparator, language, currency, isStepper, minValue, maxValue }; // props applied to the Input element const customMuiInputProps = isNumeric ? { ...muiInputProps, inputComponent: NumberTextField } : muiInputProps; // attributes applied to the input element const customReactInputProps = { ...(isNumeric && numericProps), ...inputProps, className: `${classes.input} ${inputProps?.className ? inputProps.className : ''}` }; const handleChange = useCallback((valueOrEvent, origEvent) => { const value = isNumeric ? valueOrEvent : valueOrEvent?.target?.value; const event = isNumeric ? origEvent : valueOrEvent; setLiveValue(value); debouncedOnChange(value, event); }, [debouncedOnChange, isNumeric]); return (React.createElement(MuiTextField, { onChange: handleChange, value: liveValue, fullWidth: fullWidth, disabled: disabled, variant: variant, ...rest, InputProps: customMuiInputProps, inputProps: customReactInputProps, InputLabelProps: { className: classes.label, ...InputLabelProps } })); }; TextField.propTypes = { /** * @default false * If `true`, the input will accept only numeric values. */ isNumeric: PropTypes.bool, /** * Attributes applied to the input element. * For the numeric input, you can provide properties like thousandSeparator, decimalScale and allowNegative. */ inputProps: PropTypes.object, /** * Other properties you can provide to the Input component. */ InputProps: PropTypes.object, /** * End adornment of component. (Usually an InputAdornment from material-ui) */ endAdornment: PropTypes.node, /** * Start adornment of component. (Usually an InputAdornment from material-ui) */ startAdornment: PropTypes.node, /** * If `true`, the input will take up the full width of its container. */ fullWidth: PropTypes.bool, /** * Props applied to the InputLabel element. */ InputLabelProps: PropTypes.object, /** * The value of the `input` element, required for a controlled component. */ value: PropTypes.any, /** * @default '() => {}' * Callback fired when the value is changed. * @param {unknown} value The target value from the event source of the callback. */ onChange: PropTypes.func, /** * The delay of debouncing. */ debounceBy: PropTypes.number, /** * If defined, it limits to given decimal scale. */ decimalScale: PropTypes.number, /** * If `true`, it add 0s to match given decimalScale. */ fixedDecimalScale: PropTypes.bool, /** * Character that separates thousands from hundreds. */ thousandSeparator: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), /** * Character that separates decimals from integers. */ decimalSeparator: PropTypes.string, /** * The current language, preferably taken from the i18next (i18.language) or another internationalization library. */ language: PropTypes.string, /** * The currency that will be displayed as a pre-fix. */ currency: PropTypes.string, /** * If `true`, the component is disabled */ disabled: PropTypes.bool, /** * If `true`, a clear button is shown. */ isClearable: PropTypes.bool, /** * @default false * If `true`, will display `+` and `-` buttons for increasing/decreasing the value. */ isStepper: PropTypes.bool, /** * @default 1 * Used together with `isStepper` prop; the value by which the current input increases. */ step: PropTypes.number, /** * @default -Infinity * Lower limit for the input. */ minValue: PropTypes.number, /** * @default Infinity * Upper limit for the input. */ maxValue: PropTypes.number, /** * @default 'standard' * The variant to use. */ variant: PropTypes.oneOf(['filled', 'standard', 'outlined']) }; export default TextField; //# sourceMappingURL=TextField.js.map