react-intl-number-input 0.7.0 → 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
@@ -42,7 +42,10 @@ function App() {
42
42
  ### TypeScript
43
43
 
44
44
  ```typescript
45
- import IntlNumberInput, { IntlNumberInputProps } from 'react-intl-number-input';
45
+ import IntlNumberInput, {
46
+ IntlNumberInputProps,
47
+ ControlsRenderProps,
48
+ } from 'react-intl-number-input';
46
49
 
47
50
  const props: IntlNumberInputProps = {
48
51
  locale: 'pt-BR',
@@ -85,8 +88,9 @@ ReactDOM.render(<App />, document.getElementById('root'));
85
88
  | autoFocus | `boolean` | — | Native input autofocus |
86
89
  | minValue | `number` | — | Minimum allowed value |
87
90
  | maxValue | `number` | — | Maximum allowed value |
88
- | showStepButtons | `boolean` | `false` | Renders +/- step buttons |
89
- | step | `number` | `1` | Step increment for buttons |
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`) |
90
94
  | inputMode | `'numeric'` \| `'decimal'` | auto | Overrides mobile keyboard mode |
91
95
  | className | `string` | — | Input CSS class |
92
96
  | style | `object` | — | Input inline styles |
@@ -144,6 +148,32 @@ The component also accepts standard `<input>` attributes such as `ref`, `onFocus
144
148
  />
145
149
  ```
146
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>
173
+ ```
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
+
147
177
  ## How input works
148
178
 
149
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.
@@ -152,6 +182,8 @@ Users type digits; the component applies locale formatting and optional prefix/s
152
182
 
153
183
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and publishing instructions.
154
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
+
155
187
  ## Changelog
156
188
 
157
189
  See [CHANGELOG.md](CHANGELOG.md) for release history.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,16 @@
1
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;
13
+ }
2
14
  type IntlNumberInputOwnProps = {
3
15
  locale?: string;
4
16
  prefix?: string;
@@ -12,7 +24,8 @@ type IntlNumberInputOwnProps = {
12
24
  inputMode?: 'numeric' | 'decimal';
13
25
  onChange?: (event: React.ChangeEvent<HTMLInputElement>, value: number, maskedValue: string) => void;
14
26
  onBlur?: (event: React.FocusEvent<HTMLInputElement>, value: number, maskedValue: string) => void;
27
+ renderControls?: (props: ControlsRenderProps) => React.ReactNode;
15
28
  };
16
29
  export type IntlNumberInputProps = IntlNumberInputOwnProps & Omit<React.ComponentPropsWithoutRef<'input'>, keyof IntlNumberInputOwnProps | 'value' | 'type' | 'defaultValue' | 'children'>;
17
- declare const _default: React.NamedExoticComponent<IntlNumberInputOwnProps & Omit<Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "type" | "defaultValue" | "children" | keyof IntlNumberInputOwnProps> & React.RefAttributes<HTMLInputElement>>;
30
+ declare const _default: React.NamedExoticComponent<IntlNumberInputOwnProps & Omit<Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "children" | "defaultValue" | "type" | keyof IntlNumberInputOwnProps> & React.RefAttributes<HTMLInputElement>>;
18
31
  export default _default;
package/dist/index.js CHANGED
@@ -5,11 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
- const _excluded = ["locale", "prefix", "suffix", "precision", "value", "showStepButtons", "step", "minValue", "maxValue", "inputMode", "onChange", "onBlur", "disabled"];
9
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); }
10
- 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); }
11
- function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
12
- function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
13
9
  function sanitizeString(str) {
14
10
  return str.replace(/<[^>]*>/g, '');
15
11
  }
@@ -37,23 +33,23 @@ function clampToBounds(value, min, max) {
37
33
  }
38
34
  return result;
39
35
  }
40
- const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberInput(_ref, ref) {
41
- let {
42
- locale = 'en-US',
43
- prefix = '',
44
- suffix = '',
45
- precision = 2,
46
- value,
47
- showStepButtons = false,
48
- step = 1,
49
- minValue,
50
- maxValue,
51
- inputMode: inputModeProp,
52
- onChange,
53
- onBlur,
54
- disabled = false
55
- } = _ref,
56
- inputProps = _objectWithoutProperties(_ref, _excluded);
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) {
57
53
  const safePrecision = (0, _react.useMemo)(() => {
58
54
  if (typeof precision !== 'number' || Number.isNaN(precision)) return 2;
59
55
  return clamp(Math.round(precision), 0, 20);
@@ -111,10 +107,22 @@ const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberI
111
107
  const initial = value !== undefined ? value : 0;
112
108
  return formatNumber(initial);
113
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);
114
+ }
115
+ return initial;
116
+ }, [value, getNumberValue]);
114
117
  const [maskedValue, setMaskedValue] = (0, _react.useState)(getInitialValue);
118
+ const numericValueRef = (0, _react.useRef)(getInitialNumericValue());
115
119
  (0, _react.useEffect)(() => {
116
- setMaskedValue(formatNumber(value ?? 0));
117
- }, [value, formatNumber]);
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]);
118
126
  const handleChange = (0, _react.useCallback)(event => {
119
127
  const rawValue = event.target.value;
120
128
  const newValue = getNumberValue(rawValue);
@@ -124,6 +132,7 @@ const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberI
124
132
  const clampedValue = clampToBounds(newValue, effectiveMin, effectiveMax);
125
133
  const newMaskedValue = formatNumber(clampedValue);
126
134
  setMaskedValue(newMaskedValue);
135
+ numericValueRef.current = clampedValue;
127
136
  if (onChange) {
128
137
  onChange(event, clampedValue, newMaskedValue);
129
138
  }
@@ -134,17 +143,19 @@ const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberI
134
143
  const clampedValue = clampToBounds(currentValue, effectiveMin, effectiveMax);
135
144
  const formattedValue = formatNumber(clampedValue);
136
145
  setMaskedValue(formattedValue);
146
+ numericValueRef.current = clampedValue;
137
147
  if (onBlur) {
138
148
  onBlur(event, clampedValue, formattedValue);
139
149
  }
140
150
  }, [getNumberValue, formatNumber, effectiveMin, effectiveMax, onBlur]);
141
- const handleStep = (0, _react.useCallback)(delta => {
151
+ const handleStep = (0, _react.useCallback)((delta, multiplier = 1) => {
142
152
  if (disabled) return;
143
- const currentValue = getNumberValue(maskedValue);
144
- const stepValue = safeStep * Math.pow(10, safePrecision);
153
+ const currentValue = numericValueRef.current;
154
+ const stepValue = safeStep / Math.pow(10, safePrecision) * multiplier;
145
155
  const newValue = clampToBounds(currentValue + delta * stepValue, effectiveMin, effectiveMax);
146
156
  const newMaskedValue = formatNumber(newValue);
147
157
  setMaskedValue(newMaskedValue);
158
+ numericValueRef.current = newValue;
148
159
  const syntheticEvent = {
149
160
  target: {
150
161
  value: newMaskedValue,
@@ -156,22 +167,57 @@ const IntlNumberInput = /*#__PURE__*/(0, _react.forwardRef)(function IntlNumberI
156
167
  if (onChange) {
157
168
  onChange(syntheticEvent, newValue, newMaskedValue);
158
169
  }
159
- }, [disabled, getNumberValue, maskedValue, safeStep, safePrecision, effectiveMin, effectiveMax, formatNumber, onChange, inputProps.name, inputProps.id]);
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);
188
+ }
189
+ }, [effectiveMin, effectiveMax, formatNumber, onChange, inputProps.name, inputProps.id]);
160
190
  const inputMode = (0, _react.useMemo)(() => {
161
191
  if (inputModeProp) {
162
192
  return inputModeProp;
163
193
  }
164
194
  return safePrecision > 0 ? 'decimal' : 'numeric';
165
195
  }, [inputModeProp, safePrecision]);
166
- const inputElement = /*#__PURE__*/_react.default.createElement("input", _extends({
196
+ const inputElement = /*#__PURE__*/_react.default.createElement("input", {
167
197
  ref: ref,
168
198
  type: "text",
169
199
  inputMode: inputMode,
170
200
  value: maskedValue,
171
201
  disabled: disabled,
172
202
  onChange: handleChange,
173
- onBlur: handleBlur
174
- }, inputProps));
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
+ }
175
221
  if (!showStepButtons) {
176
222
  return inputElement;
177
223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-intl-number-input",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "A React component for masked and formatted number input.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "sideEffects": false,
25
25
  "scripts": {
26
26
  "test": "jest --runInBand",
27
+ "dev": "webpack serve --mode development",
27
28
  "start": "webpack serve --mode development",
28
29
  "transpile": "babel src -d dist --extensions \".ts,.tsx\" --ignore \"**/*.test.ts\" --ignore \"**/*.test.tsx\"",
29
30
  "build:types": "tsc --emitDeclarationOnly",
@@ -58,32 +59,29 @@
58
59
  "react": ">=16.8.0 <20"
59
60
  },
60
61
  "devDependencies": {
61
- "@babel/cli": "^7.29.7",
62
- "@babel/core": "^7.29.7",
63
- "@babel/plugin-transform-object-rest-spread": "^7.29.7",
64
- "@babel/preset-env": "^7.29.7",
65
- "@babel/preset-react": "^7.29.7",
66
- "@babel/preset-typescript": "^7.29.7",
67
- "@testing-library/jest-dom": "^6.9.1",
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
68
  "@testing-library/react": "^16.3.2",
69
- "@types/react": "^19.2.17",
69
+ "@types/react": "^19.2.18",
70
70
  "babel-jest": "^30.4.1",
71
71
  "babel-loader": "^10.1.1",
72
72
  "gh-pages": "^6.3.0",
73
- "html-webpack-plugin": "^5.6.7",
73
+ "html-webpack-plugin": "^5.6.8",
74
74
  "jest": "^30.4.2",
75
75
  "jest-environment-jsdom": "^30.4.1",
76
- "react": "^19.2.7",
77
- "react-dom": "^19.2.7",
78
- "typescript": "^6.0.3",
79
- "webpack": "^5.107.2",
80
- "webpack-cli": "^7.0.3",
81
- "webpack-dev-server": "^5.2.5"
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"
82
82
  },
83
83
  "overrides": {
84
84
  "fast-uri": "^3.1.1",
85
- "follow-redirects": "^1.16.0",
86
- "js-yaml": "^4.1.1",
87
- "uuid": "^11.1.1"
85
+ "js-yaml": "^4.1.1"
88
86
  }
89
87
  }