react-intl-number-input 0.5.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,76 +1,59 @@
1
1
  # react-intl-number-input
2
- A React component for masked and formatted number input.
2
+
3
+ A React component for masked and formatted number input with `Intl.NumberFormat` locale support.
3
4
 
4
5
  [Demo](https://nandorip.github.io/react-intl-number-input/example/dist/)
5
6
 
6
7
  ## Requirements
7
8
 
8
- - Node.js 18+ (recommended: latest LTS)
9
- - React / ReactDOM: `>=16.8.0 <20`
9
+ - React: `>=16.8.0 <20`
10
10
 
11
11
  ## Install
12
12
 
13
- ```
13
+ ```bash
14
14
  npm install react-intl-number-input
15
15
  ```
16
16
 
17
- ## Development
18
-
19
- ```
20
- npm install
21
- npm start
22
- ```
23
-
24
- Open `http://localhost:3001`.
25
-
26
- ## Scripts
27
-
28
- - `npm start`: runs webpack dev server
29
- - `npm run build`: builds the demo bundle in `example/dist`
30
- - `npm run transpile`: transpiles library source from `src` to `dist`
31
- - `npm test`: runs unit tests with Jest
32
- - `npm run publish-demo`: builds and deploys demo to GitHub Pages
33
-
34
17
  ## Usage
35
18
 
36
19
  ```javascript
37
- import React from 'react'
20
+ import React, { useState } from 'react';
38
21
  import IntlNumberInput from 'react-intl-number-input';
39
-
40
- class App extends React.Component {
41
- constructor(props) {
42
- super(props);
43
-
44
- this.state = {
45
- value: 0,
46
- maskedValue: 0,
47
- }
48
-
49
- this.handleChange = this.handleChange.bind(this);
50
- }
51
-
52
- handleChange(event, value, maskedValue) {
53
- event.preventDefault();
54
- this.setState({ value, maskedValue });
55
- }
56
-
57
- render() {
58
- return (
59
- <div>
60
- <p>
61
- <IntlNumberInput onChange={this.handleChange} />
62
- </p>
63
- <p>
64
- value: {this.state.value}
65
- </p>
66
- <p>
67
- maskedValue: {this.state.maskedValue}
68
- </p>
69
- </div>
70
- );
71
- }
22
+
23
+ function App() {
24
+ const [value, setValue] = useState(0);
25
+ const [maskedValue, setMaskedValue] = useState('0.00');
26
+
27
+ const handleChange = (event, nextValue, nextMaskedValue) => {
28
+ setValue(nextValue);
29
+ setMaskedValue(nextMaskedValue);
30
+ };
31
+
32
+ return (
33
+ <div>
34
+ <IntlNumberInput onChange={handleChange} />
35
+ <p>value: {value}</p>
36
+ <p>maskedValue: {maskedValue}</p>
37
+ </div>
38
+ );
72
39
  }
40
+ ```
73
41
 
42
+ ### TypeScript
43
+
44
+ ```typescript
45
+ import IntlNumberInput, {
46
+ IntlNumberInputProps,
47
+ ControlsRenderProps,
48
+ } from 'react-intl-number-input';
49
+
50
+ const props: IntlNumberInputProps = {
51
+ locale: 'pt-BR',
52
+ precision: 2,
53
+ onChange: (event, value, maskedValue) => {
54
+ console.log(value, maskedValue);
55
+ },
56
+ };
74
57
  ```
75
58
 
76
59
  React 18+:
@@ -78,7 +61,7 @@ React 18+:
78
61
  ```javascript
79
62
  import { createRoot } from 'react-dom/client';
80
63
 
81
- const root = createRoot(document.getElementById("root"));
64
+ const root = createRoot(document.getElementById('root'));
82
65
  root.render(<App />);
83
66
  ```
84
67
 
@@ -87,47 +70,120 @@ React 16/17:
87
70
  ```javascript
88
71
  import ReactDOM from 'react-dom';
89
72
 
90
- ReactDOM.render(<App />, document.getElementById("root"));
73
+ ReactDOM.render(<App />, document.getElementById('root'));
74
+ ```
75
+
76
+ ## Properties
77
+
78
+ | Name | Type | Default | Description |
79
+ | --- | --- | :---: | --- |
80
+ | value | `number` \| `string` | `0` | Controlled numeric value |
81
+ | locale | `string` | `'en-US'` | BCP 47 language tag ([Intl locales](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation)) |
82
+ | prefix | `string` | `''` | Prefix shown in the masked value |
83
+ | suffix | `string` | `''` | Suffix shown in the masked value |
84
+ | precision | `number` | `2` | Fraction digits (clamped to `0`–`20`) |
85
+ | onChange | `function` | — | `(event, value, maskedValue) => void` |
86
+ | onBlur | `function` | — | `(event, value, maskedValue) => void` |
87
+ | disabled | `boolean` | `false` | Disables the input |
88
+ | autoFocus | `boolean` | — | Native input autofocus |
89
+ | minValue | `number` | — | Minimum allowed value |
90
+ | maxValue | `number` | — | Maximum allowed value |
91
+ | showStepButtons | `boolean` | `false` | Renders built-in +/- step buttons (ignored when `renderControls` is set) |
92
+ | renderControls | `function` | — | `(props: ControlsRenderProps) => ReactNode` — custom stepper UI |
93
+ | step | `number` | `1` | Step increment in display units (e.g. `1` with `precision={2}` adds `0.01`) |
94
+ | inputMode | `'numeric'` \| `'decimal'` | auto | Overrides mobile keyboard mode |
95
+ | className | `string` | — | Input CSS class |
96
+ | style | `object` | — | Input inline styles |
97
+ | id | `string` | — | Input id |
98
+ | name | `string` | — | Input name |
99
+ | placeholder | `string` | — | Input placeholder |
100
+ | readOnly | `boolean` | — | Read-only input |
101
+ | required | `boolean` | — | Required input |
102
+ | tabIndex | `number` | — | Input tab index |
103
+
104
+ The component also accepts standard `<input>` attributes such as `ref`, `onFocus`, `onKeyDown`, `aria-*`, `data-*`, and `autoComplete`. Custom `onChange` and `onBlur` callbacks receive the clamped numeric value and the formatted masked string.
105
+
106
+ ## Examples
107
+
108
+ ```javascript
109
+ // maskedValue: 1,234,567.89
110
+ <IntlNumberInput />
111
+ ```
112
+
113
+ ```javascript
114
+ // maskedValue: 12,345.6789
115
+ <IntlNumberInput precision={4} />
116
+ ```
117
+
118
+ ```javascript
119
+ // maskedValue: $1,234,567.89
120
+ <IntlNumberInput prefix="$" />
121
+ ```
122
+
123
+ ```javascript
124
+ // maskedValue: 1,234%
125
+ <IntlNumberInput suffix="%" precision={0} />
126
+ ```
127
+
128
+ ```javascript
129
+ // maskedValue: 12.50% — type digits only; decimal is implied by precision
130
+ <IntlNumberInput suffix="%" precision={2} />
131
+ ```
132
+
133
+ ```javascript
134
+ // maskedValue: R$ 1.234.567,89
135
+ <IntlNumberInput locale="pt-BR" prefix="R$ " precision={2} />
136
+ ```
137
+
138
+ ```javascript
139
+ // With min/max and step buttons
140
+ <IntlNumberInput
141
+ value={50}
142
+ minValue={0}
143
+ maxValue={100}
144
+ step={5}
145
+ precision={0}
146
+ showStepButtons
147
+ onChange={(event, value) => console.log(value)}
148
+ />
149
+ ```
150
+
151
+ ```javascript
152
+ // Custom controls (layout is up to you — wrap in a parent div as needed)
153
+ <div className="amount-field">
154
+ <IntlNumberInput
155
+ value={12.34}
156
+ precision={2}
157
+ step={1}
158
+ minValue={0}
159
+ maxValue={100}
160
+ onChange={(event, value) => console.log(value)}
161
+ renderControls={({ increment, decrement, setValue, value, formattedValue, min, max, disabled }) => (
162
+ <div className="amount-controls">
163
+ <button type="button" onClick={() => decrement()} disabled={disabled}>-</button>
164
+ <span>{formattedValue}</span>
165
+ <button type="button" onClick={() => increment()} disabled={disabled}>+</button>
166
+ <button type="button" onClick={() => setValue(max ?? value)} disabled={disabled}>
167
+ Max
168
+ </button>
169
+ </div>
170
+ )}
171
+ />
172
+ </div>
91
173
  ```
92
-
93
- ## Properties
94
-
95
- | Name | Type | Default Value | Description |
96
- | --- | --- | :---: | --- |
97
- | value | number or string | 0 | Initial numeric value |
98
- | locale | string | 'en-US' | BCP 47 language tag. Defines a language code and a country or region code.<br>[Intl Locale Identification](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation) *e.g.* 'de-DE', 'pt-BR' and 'ja-JP' |
99
- | prefix | string | '' | String used as prefix for the masked value |
100
- | suffix | string | '' | String used as suffix for the masked value |
101
- | precision | number | 2 | Number of fraction digits to use |
102
- | onChange | function | n/a | `(event, value, maskedValue) => {}`<br>Callback function to handle value changes |
103
-
104
- ## Examples
105
-
106
- ```javascript
107
- // maskedValue: 1,234,567.89
108
- <IntlNumberInput />
109
- ```
110
-
111
- ```javascript
112
- // maskedValue: 12,345.6789
113
- <IntlNumberInput precision={4} />
114
- ```
115
-
116
- ```javascript
117
- // maskedValue: $1,234,567.89
118
- <IntlNumberInput prefix="$" />
119
- ```
120
-
121
- ```javascript
122
- // maskedValue: 1,234%
123
- <IntlNumberInput suffix="%" precision={0} />
124
- ```
125
-
126
- ```javascript
127
- // maskedValue: R$ 1.234.567,89
128
- <IntlNumberInput
129
- locale="pt-BR"
130
- prefix="R$ "
131
- precision={2}
132
- />
133
- ```
174
+
175
+ `renderControls` replaces `showStepButtons` when both are provided. The component renders the `<input>` and your controls as siblings (no wrapper), so you control layout in the parent.
176
+
177
+ ## How input works
178
+
179
+ Users type digits; the component applies locale formatting and optional prefix/suffix. For `precision={2}`, typing `1234` becomes `12.34`. Negative values are supported when `-` is present in the input.
180
+
181
+ ## Contributing
182
+
183
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and publishing instructions.
184
+
185
+ Local development requires Node.js `^22.18.0` or `>=24.11.0` (Babel 8). The published package has no Node.js version constraint for consumers.
186
+
187
+ ## Changelog
188
+
189
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,31 @@
1
- import * as React from 'react';
2
-
3
- export interface IntlNumberInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
- locale?: string;
5
- prefix?: string;
6
- suffix?: string;
7
- precision?: number;
8
- value?: number | string;
9
- onChange?: (event: React.ChangeEvent<HTMLInputElement>, value: number, maskedValue: string) => void;
1
+ import React from 'react';
2
+ export interface ControlsRenderProps {
3
+ increment: (multiplier?: number) => void;
4
+ decrement: (multiplier?: number) => void;
5
+ setValue: (value: number) => void;
6
+ value: number;
7
+ min?: number;
8
+ max?: number;
9
+ disabled: boolean;
10
+ step: number;
11
+ precision: number;
12
+ formattedValue: string;
10
13
  }
11
-
12
- declare const IntlNumberInput: React.ForwardRefExoticComponent<IntlNumberInputProps & React.RefAttributes<HTMLInputElement>>;
13
-
14
- export default IntlNumberInput;
14
+ type IntlNumberInputOwnProps = {
15
+ locale?: string;
16
+ prefix?: string;
17
+ suffix?: string;
18
+ precision?: number;
19
+ value?: number | string;
20
+ showStepButtons?: boolean;
21
+ step?: number;
22
+ minValue?: number;
23
+ maxValue?: number;
24
+ inputMode?: 'numeric' | 'decimal';
25
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>, value: number, maskedValue: string) => void;
26
+ onBlur?: (event: React.FocusEvent<HTMLInputElement>, value: number, maskedValue: string) => void;
27
+ renderControls?: (props: ControlsRenderProps) => React.ReactNode;
28
+ };
29
+ export type IntlNumberInputProps = IntlNumberInputOwnProps & Omit<React.ComponentPropsWithoutRef<'input'>, keyof IntlNumberInputOwnProps | 'value' | 'type' | 'defaultValue' | 'children'>;
30
+ declare const _default: React.NamedExoticComponent<IntlNumberInputOwnProps & Omit<Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "children" | "defaultValue" | "type" | keyof IntlNumberInputOwnProps> & React.RefAttributes<HTMLInputElement>>;
31
+ export default _default;
package/dist/index.js CHANGED
@@ -1,171 +1,253 @@
1
1
  "use strict";
2
2
 
3
- function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
4
3
  Object.defineProperty(exports, "__esModule", {
5
4
  value: true
6
5
  });
7
- exports["default"] = void 0;
6
+ exports.default = void 0;
8
7
  var _react = _interopRequireWildcard(require("react"));
9
- var _propTypes = _interopRequireDefault(require("prop-types"));
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
11
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
12
- function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
13
- function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
14
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
15
- function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
16
- function _readOnlyError(r) { throw new TypeError('"' + r + '" is read-only'); }
17
- function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
18
- function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
19
- function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
20
- function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
21
- function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
22
- function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
23
- function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
24
- function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
25
- function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
26
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
27
- function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
28
- function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
29
- function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
30
- function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
31
- function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
32
- function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
33
- function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); }
34
- function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); }
35
- var IntlNumberInput = /*#__PURE__*/function (_Component) {
36
- function IntlNumberInput(props) {
37
- var _this;
38
- _classCallCheck(this, IntlNumberInput);
39
- _this = _callSuper(this, IntlNumberInput, [props]);
40
- _this.state = {
41
- maskedValue: '0',
42
- value: 0
43
- };
44
- return _this;
8
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
9
+ function sanitizeString(str) {
10
+ return str.replace(/<[^>]*>/g, '');
11
+ }
12
+ function resolveLocale(locale) {
13
+ try {
14
+ const supported = Intl.NumberFormat.supportedLocalesOf([locale]);
15
+ return supported.length > 0 ? supported[0] : 'en-US';
16
+ } catch {
17
+ return 'en-US';
18
+ }
19
+ }
20
+ function isValidNumber(value) {
21
+ return Number.isFinite(value) && !Number.isNaN(value);
22
+ }
23
+ function clamp(value, min, max) {
24
+ return Math.min(Math.max(value, min), max);
25
+ }
26
+ function clampToBounds(value, min, max) {
27
+ let result = value;
28
+ if (min !== undefined && result < min) {
29
+ result = min;
30
+ }
31
+ if (max !== undefined && result > max) {
32
+ result = max;
45
33
  }
46
- _inherits(IntlNumberInput, _Component);
47
- return _createClass(IntlNumberInput, [{
48
- key: "componentDidMount",
49
- value: function componentDidMount() {
50
- this.setMaskedValue(this.props.value);
34
+ return result;
35
+ }
36
+ const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberInput({
37
+ locale = 'en-US',
38
+ prefix = '',
39
+ suffix = '',
40
+ precision = 2,
41
+ value,
42
+ showStepButtons = false,
43
+ step = 1,
44
+ minValue,
45
+ maxValue,
46
+ inputMode: inputModeProp,
47
+ onChange,
48
+ onBlur,
49
+ disabled = false,
50
+ renderControls,
51
+ ...inputProps
52
+ }, ref) {
53
+ const safePrecision = (0, _react.useMemo)(() => {
54
+ if (typeof precision !== 'number' || Number.isNaN(precision)) return 2;
55
+ return clamp(Math.round(precision), 0, 20);
56
+ }, [precision]);
57
+ const safeStep = (0, _react.useMemo)(() => {
58
+ if (typeof step !== 'number' || Number.isNaN(step) || step <= 0) return 1;
59
+ return step;
60
+ }, [step]);
61
+ const [effectiveMin, effectiveMax] = (0, _react.useMemo)(() => {
62
+ if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
63
+ return [maxValue, minValue];
64
+ }
65
+ return [minValue, maxValue];
66
+ }, [minValue, maxValue]);
67
+ const getPrecisionValue = (0, _react.useCallback)(p => {
68
+ return Math.pow(10, p);
69
+ }, []);
70
+ const getNumberValue = (0, _react.useCallback)(strValue => {
71
+ const inputValue = String(strValue || '');
72
+ const hasNegativeSign = inputValue.includes('-');
73
+ const onlyDigits = inputValue.replace(/[^0-9]/g, '');
74
+ if (!onlyDigits) {
75
+ return hasNegativeSign ? -0 : 0;
76
+ }
77
+ const numberValue = Number(onlyDigits);
78
+ const parsedValue = numberValue / getPrecisionValue(safePrecision);
79
+ return hasNegativeSign ? -parsedValue : parsedValue;
80
+ }, [getPrecisionValue, safePrecision]);
81
+ const numberFormatter = (0, _react.useMemo)(() => {
82
+ const options = {
83
+ style: 'decimal',
84
+ minimumFractionDigits: safePrecision,
85
+ maximumFractionDigits: safePrecision
86
+ };
87
+ const resolvedLocale = resolveLocale(locale);
88
+ return new Intl.NumberFormat(resolvedLocale, options);
89
+ }, [locale, safePrecision]);
90
+ const formatNumber = (0, _react.useCallback)(val => {
91
+ let numberValue;
92
+ if (typeof val === 'string') {
93
+ numberValue = getNumberValue(val);
94
+ } else {
95
+ numberValue = val;
51
96
  }
52
- }, {
53
- key: "componentDidUpdate",
54
- value: function componentDidUpdate(prevProps) {
55
- if (prevProps.value !== this.props.value) {
56
- this.setMaskedValue(this.props.value);
57
- }
97
+ if (!isValidNumber(numberValue)) {
98
+ numberValue = 0;
58
99
  }
59
- }, {
60
- key: "setMaskedValue",
61
- value: function setMaskedValue() {
62
- var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
63
- this.setState({
64
- maskedValue: this.formatNumber(value)
65
- });
100
+ numberValue = clampToBounds(numberValue, effectiveMin, effectiveMax);
101
+ const formattedNumber = numberFormatter.format(numberValue);
102
+ const safePrefix = sanitizeString(prefix);
103
+ const safeSuffix = sanitizeString(suffix);
104
+ return `${safePrefix}${formattedNumber}${safeSuffix}`;
105
+ }, [getNumberValue, numberFormatter, prefix, suffix, effectiveMin, effectiveMax]);
106
+ const getInitialValue = (0, _react.useCallback)(() => {
107
+ const initial = value !== undefined ? value : 0;
108
+ return formatNumber(initial);
109
+ }, [value, formatNumber]);
110
+ const getInitialNumericValue = (0, _react.useCallback)(() => {
111
+ const initial = value !== undefined ? value : 0;
112
+ if (typeof initial === 'string') {
113
+ return getNumberValue(initial);
66
114
  }
67
- }, {
68
- key: "getMaskedValue",
69
- value: function getMaskedValue() {
70
- return this.state.maskedValue;
115
+ return initial;
116
+ }, [value, getNumberValue]);
117
+ const [maskedValue, setMaskedValue] = (0, _react.useState)(getInitialValue);
118
+ const numericValueRef = (0, _react.useRef)(getInitialNumericValue());
119
+ (0, _react.useEffect)(() => {
120
+ if (value === undefined) return;
121
+ const numericVal = typeof value === 'string' ? getNumberValue(value) : value;
122
+ const clamped = clampToBounds(numericVal, effectiveMin, effectiveMax);
123
+ setMaskedValue(formatNumber(clamped));
124
+ numericValueRef.current = clamped;
125
+ }, [value, formatNumber, getNumberValue, effectiveMin, effectiveMax]);
126
+ const handleChange = (0, _react.useCallback)(event => {
127
+ const rawValue = event.target.value;
128
+ const newValue = getNumberValue(rawValue);
129
+ if (!isValidNumber(newValue)) {
130
+ return;
71
131
  }
72
- }, {
73
- key: "getValue",
74
- value: function getValue() {
75
- return this.state.value;
132
+ const clampedValue = clampToBounds(newValue, effectiveMin, effectiveMax);
133
+ const newMaskedValue = formatNumber(clampedValue);
134
+ setMaskedValue(newMaskedValue);
135
+ numericValueRef.current = clampedValue;
136
+ if (onChange) {
137
+ onChange(event, clampedValue, newMaskedValue);
76
138
  }
77
- }, {
78
- key: "getPrecisionValue",
79
- value: function getPrecisionValue(precision) {
80
- return Math.pow(10, precision);
139
+ }, [getNumberValue, formatNumber, effectiveMin, effectiveMax, onChange]);
140
+ const handleBlur = (0, _react.useCallback)(event => {
141
+ const rawValue = event.target.value;
142
+ const currentValue = getNumberValue(rawValue);
143
+ const clampedValue = clampToBounds(currentValue, effectiveMin, effectiveMax);
144
+ const formattedValue = formatNumber(clampedValue);
145
+ setMaskedValue(formattedValue);
146
+ numericValueRef.current = clampedValue;
147
+ if (onBlur) {
148
+ onBlur(event, clampedValue, formattedValue);
81
149
  }
82
- }, {
83
- key: "getNumberValue",
84
- value: function getNumberValue(strValue) {
85
- var numberValue = Number(strValue.replace(/[^0-9]/g, ''));
86
- return numberValue / this.getPrecisionValue(this.props.precision);
150
+ }, [getNumberValue, formatNumber, effectiveMin, effectiveMax, onBlur]);
151
+ const handleStep = (0, _react.useCallback)((delta, multiplier = 1) => {
152
+ if (disabled) return;
153
+ const currentValue = numericValueRef.current;
154
+ const stepValue = safeStep / Math.pow(10, safePrecision) * multiplier;
155
+ const newValue = clampToBounds(currentValue + delta * stepValue, effectiveMin, effectiveMax);
156
+ const newMaskedValue = formatNumber(newValue);
157
+ setMaskedValue(newMaskedValue);
158
+ numericValueRef.current = newValue;
159
+ const syntheticEvent = {
160
+ target: {
161
+ value: newMaskedValue,
162
+ name: inputProps.name || '',
163
+ id: inputProps.id || ''
164
+ },
165
+ preventDefault: () => {}
166
+ };
167
+ if (onChange) {
168
+ onChange(syntheticEvent, newValue, newMaskedValue);
87
169
  }
88
- }, {
89
- key: "formatNumber",
90
- value: function formatNumber() {
91
- var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
92
- var numberValue = value;
93
- if (typeof numberValue === 'string') {
94
- numberValue = this.getNumberValue(numberValue);
95
- }
96
- var formattedNumber = new Intl.NumberFormat(this.props.locale, {
97
- style: 'decimal',
98
- minimumFractionDigits: this.props.precision,
99
- maximumFractionDigits: this.props.precision
100
- }).format(numberValue);
101
- return "".concat(this.props.prefix).concat(formattedNumber).concat(this.props.suffix);
170
+ }, [disabled, safeStep, safePrecision, effectiveMin, effectiveMax, formatNumber, onChange, inputProps.name, inputProps.id]);
171
+ const increment = (0, _react.useCallback)((multiplier = 1) => handleStep(1, multiplier), [handleStep]);
172
+ const decrement = (0, _react.useCallback)((multiplier = 1) => handleStep(-1, multiplier), [handleStep]);
173
+ const setValue = (0, _react.useCallback)(newValue => {
174
+ const clampedValue = clampToBounds(newValue, effectiveMin, effectiveMax);
175
+ const newMaskedValue = formatNumber(clampedValue);
176
+ setMaskedValue(newMaskedValue);
177
+ numericValueRef.current = clampedValue;
178
+ const syntheticEvent = {
179
+ target: {
180
+ value: newMaskedValue,
181
+ name: inputProps.name || '',
182
+ id: inputProps.id || ''
183
+ },
184
+ preventDefault: () => {}
185
+ };
186
+ if (onChange) {
187
+ onChange(syntheticEvent, clampedValue, newMaskedValue);
102
188
  }
103
- }, {
104
- key: "updateValues",
105
- value: function updateValues(event) {
106
- var value = this.getNumberValue(event.target.value);
107
- var maskedValue = this.formatNumber(value);
108
- return [value, maskedValue];
189
+ }, [effectiveMin, effectiveMax, formatNumber, onChange, inputProps.name, inputProps.id]);
190
+ const inputMode = (0, _react.useMemo)(() => {
191
+ if (inputModeProp) {
192
+ return inputModeProp;
109
193
  }
110
- }, {
111
- key: "handleChange",
112
- value: function handleChange(event) {
113
- var _this2 = this;
114
- event.preventDefault();
115
- var _this$updateValues = this.updateValues(event),
116
- _this$updateValues2 = _slicedToArray(_this$updateValues, 2),
117
- value = _this$updateValues2[0],
118
- maskedValue = _this$updateValues2[1];
119
- event.persist();
120
- if (this.props.onChange && maskedValue) {
121
- this.setState({
122
- value: value,
123
- maskedValue: maskedValue
124
- }, function () {
125
- _this2.props.onChange(event, value, maskedValue);
126
- });
127
- }
194
+ return safePrecision > 0 ? 'decimal' : 'numeric';
195
+ }, [inputModeProp, safePrecision]);
196
+ const inputElement = /*#__PURE__*/_react.default.createElement("input", {
197
+ ref: ref,
198
+ type: "text",
199
+ inputMode: inputMode,
200
+ value: maskedValue,
201
+ disabled: disabled,
202
+ onChange: handleChange,
203
+ onBlur: handleBlur,
204
+ ...inputProps
205
+ });
206
+ if (renderControls) {
207
+ const controlsProps = {
208
+ increment,
209
+ decrement,
210
+ setValue,
211
+ value: numericValueRef.current,
212
+ min: effectiveMin,
213
+ max: effectiveMax,
214
+ disabled,
215
+ step: safeStep,
216
+ precision: safePrecision,
217
+ formattedValue: maskedValue
218
+ };
219
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, inputElement, renderControls(controlsProps));
220
+ }
221
+ if (!showStepButtons) {
222
+ return inputElement;
223
+ }
224
+ return /*#__PURE__*/_react.default.createElement("div", {
225
+ style: {
226
+ display: 'inline-flex',
227
+ alignItems: 'center',
228
+ gap: '4px'
128
229
  }
129
- }, {
130
- key: "customProps",
131
- value: function customProps() {
132
- var customProps = _objectSpread({}, this.props);
133
- delete customProps.locale;
134
- delete customProps.prefix;
135
- delete customProps.suffix;
136
- delete customProps.precision;
137
- delete customProps.autoFocus;
138
- delete customProps.value;
139
- delete customProps.onChange;
140
- return customProps;
230
+ }, /*#__PURE__*/_react.default.createElement("button", {
231
+ type: "button",
232
+ onClick: () => handleStep(-1),
233
+ disabled: disabled,
234
+ "aria-label": "Decrease value",
235
+ style: {
236
+ padding: '4px 8px',
237
+ cursor: disabled ? 'not-allowed' : 'pointer',
238
+ opacity: disabled ? 0.5 : 1
141
239
  }
142
- }, {
143
- key: "render",
144
- value: function render() {
145
- var _this3 = this;
146
- return /*#__PURE__*/_react["default"].createElement("input", _extends({
147
- value: this.state.maskedValue,
148
- disabled: this.props.disabled,
149
- onChange: function onChange(e) {
150
- return _this3.handleChange(e);
151
- }
152
- }, this.customProps()));
240
+ }, "-"), inputElement, /*#__PURE__*/_react.default.createElement("button", {
241
+ type: "button",
242
+ onClick: () => handleStep(1),
243
+ disabled: disabled,
244
+ "aria-label": "Increase value",
245
+ style: {
246
+ padding: '4px 8px',
247
+ cursor: disabled ? 'not-allowed' : 'pointer',
248
+ opacity: disabled ? 0.5 : 1
153
249
  }
154
- }]);
155
- }(_react.Component);
156
- IntlNumberInput.propTypes = {
157
- locale: _propTypes["default"].string.isRequired,
158
- prefix: _propTypes["default"].string.isRequired,
159
- suffix: _propTypes["default"].string.isRequired,
160
- precision: _propTypes["default"].number.isRequired,
161
- value: _propTypes["default"].oneOfType([_propTypes["default"].number, _propTypes["default"].string]),
162
- disabled: _propTypes["default"].bool,
163
- onChange: _propTypes["default"].func
164
- };
165
- IntlNumberInput.defaultProps = {
166
- locale: 'en-US',
167
- prefix: '',
168
- suffix: '',
169
- precision: 2
170
- };
171
- var _default = exports["default"] = IntlNumberInput;
250
+ }, "+"));
251
+ });
252
+ IntlNumberInput.displayName = 'IntlNumberInput';
253
+ var _default = exports.default = /*#__PURE__*/(0, _react.memo)(IntlNumberInput);
package/package.json CHANGED
@@ -1,13 +1,34 @@
1
1
  {
2
2
  "name": "react-intl-number-input",
3
- "version": "0.5.1",
3
+ "version": "0.8.1",
4
4
  "description": "A React component for masked and formatted number input.",
5
5
  "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "browserslist": [
15
+ "> 0.5%",
16
+ "last 2 versions",
17
+ "not dead"
18
+ ],
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "sideEffects": false,
6
25
  "scripts": {
7
26
  "test": "jest --runInBand",
27
+ "dev": "webpack serve --mode development",
8
28
  "start": "webpack serve --mode development",
9
- "transpile": "babel src -d dist --copy-files --no-copy-ignored --ignore \"**/*.test.jsx\"",
10
- "prepublishOnly": "npm run transpile",
29
+ "transpile": "babel src -d dist --extensions \".ts,.tsx\" --ignore \"**/*.test.ts\" --ignore \"**/*.test.tsx\"",
30
+ "build:types": "tsc --emitDeclarationOnly",
31
+ "prepublishOnly": "npm test && npm run transpile && npm run build:types",
11
32
  "build": "webpack --mode production",
12
33
  "deploy": "gh-pages -d example/dist",
13
34
  "publish-demo": "npm run build && npm run deploy"
@@ -25,7 +46,8 @@
25
46
  "money",
26
47
  "number",
27
48
  "intl",
28
- "input"
49
+ "input",
50
+ "typescript"
29
51
  ],
30
52
  "author": "Fernando Pires Reherman <nandorip@live.com>",
31
53
  "license": "MIT",
@@ -34,35 +56,32 @@
34
56
  },
35
57
  "homepage": "https://github.com/nandorip/react-intl-number-input#readme",
36
58
  "peerDependencies": {
37
- "react": ">=16.8.0 <20",
38
- "react-dom": ">=16.8.0 <20"
39
- },
40
- "dependencies": {
41
- "prop-types": "^15.8.1"
59
+ "react": ">=16.8.0 <20"
42
60
  },
43
61
  "devDependencies": {
44
- "@babel/cli": "^7.27.2",
45
- "@babel/core": "^7.27.4",
46
- "@babel/plugin-transform-object-rest-spread": "^7.28.4",
47
- "@babel/preset-env": "^7.29.5",
48
- "@babel/preset-react": "^7.27.1",
49
- "@testing-library/jest-dom": "^6.9.1",
50
- "@testing-library/react": "^16.3.0",
51
- "babel-jest": "^30.2.0",
52
- "babel-loader": "^10.0.0",
62
+ "@babel/cli": "^8.0.4",
63
+ "@babel/core": "^8.0.1",
64
+ "@babel/preset-env": "^8.0.2",
65
+ "@babel/preset-react": "^8.0.1",
66
+ "@babel/preset-typescript": "^8.0.1",
67
+ "@testing-library/jest-dom": "^7.0.1",
68
+ "@testing-library/react": "^16.3.2",
69
+ "@types/react": "^19.2.18",
70
+ "babel-jest": "^30.4.1",
71
+ "babel-loader": "^10.1.1",
53
72
  "gh-pages": "^6.3.0",
54
- "html-webpack-plugin": "^5.6.4",
55
- "jest": "^30.2.0",
56
- "jest-environment-jsdom": "^30.2.0",
57
- "react": "^19.2.0",
58
- "react-dom": "^19.2.0",
59
- "webpack": "^5.101.3",
60
- "webpack-cli": "^6.0.1",
61
- "webpack-dev-server": "^5.2.4"
73
+ "html-webpack-plugin": "^5.6.8",
74
+ "jest": "^30.4.2",
75
+ "jest-environment-jsdom": "^30.4.1",
76
+ "react": "^19.2.8",
77
+ "react-dom": "^19.2.8",
78
+ "typescript": "^7.0.2",
79
+ "webpack": "^5.109.2",
80
+ "webpack-cli": "^7.2.2",
81
+ "webpack-dev-server": "^6.0.0"
62
82
  },
63
83
  "overrides": {
64
84
  "fast-uri": "^3.1.1",
65
- "follow-redirects": "^1.16.0",
66
- "uuid": "^11.1.1"
85
+ "js-yaml": "^4.1.1"
67
86
  }
68
87
  }
package/jest.config.cjs DELETED
@@ -1,6 +0,0 @@
1
- module.exports = {
2
- testEnvironment: "jsdom",
3
- setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
4
- moduleFileExtensions: ["js", "jsx"],
5
- testMatch: ["**/*.test.jsx"]
6
- };
package/jest.setup.js DELETED
@@ -1 +0,0 @@
1
- import "@testing-library/jest-dom";