react-input-mask-format 2.1.0 → 2.3.0

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
@@ -10,7 +10,16 @@ Made with attention to UX.
10
10
 
11
11
  A maintained fork of [react-input-mask](https://github.com/sanniassin/react-input-mask).
12
12
 
13
- ## What's new in 2.1
13
+ ## What's new in 2.3
14
+
15
+ - `react-input-mask-format/time` — HH:MM (24h) masked input with clamping, see [Time](#time--react-input-mask-formattime)
16
+
17
+ ### 2.2
18
+
19
+ - `useMask` hook — mask your own `<input>` with no wrapper component, see [useMask hook](#usemask-hook)
20
+ - `react-input-mask-format/number` — numeric & currency masking, a lightweight `react-number-format` alternative, see [Numeric & currency](#numeric--currency--react-input-mask-formatnumber)
21
+
22
+ ### 2.1
14
23
 
15
24
  - `transform` prop (`uppercase` / `lowercase` / custom) — see [transform](#transform)
16
25
  - Custom tokens via RegExp `formatChars` + shipped `extendedFormatChars` (`A`, `Я`, `#`)
@@ -161,6 +170,135 @@ isValidKzIban("KZ86 125K ZT50 0410 0100"); // ISO 7064 MOD-97
161
170
  luhn("4242 4242 4242 4242"); // card Luhn
162
171
  ```
163
172
 
173
+ ## `useMask` hook
174
+
175
+ Mask your own `<input>` with no wrapper component. `useMask` returns a ref
176
+ callback:
177
+
178
+ ```tsx
179
+ import { useMask } from "react-input-mask-format";
180
+
181
+ function Phone() {
182
+ const ref = useMask({ mask: "+7 (999) 999-99-99", maskPlaceholder: "_" });
183
+ return <input ref={ref} name="phone" />;
184
+ }
185
+ ```
186
+
187
+ `useMask` is **uncontrolled**: attach it to an input you don't drive with a
188
+ React `value` prop, and read the masked value through your own `onChange` (it
189
+ fires with the masked value) or on form submit. For a controlled component,
190
+ use `<InputMask>`.
191
+
192
+ Options: `mask`, `maskPlaceholder`, `alwaysShowMask`, `formatChars`,
193
+ `transform`, `beforeMaskedStateChange`.
194
+
195
+ ## Numeric & currency — `react-input-mask-format/number`
196
+
197
+ A separate, tree-shakeable entry for formatting numbers, currency, and
198
+ percentages. Prop names match `react-number-format`, so migration is mostly a
199
+ find-and-replace of the import.
200
+
201
+ ```tsx
202
+ import { NumberFormat } from "react-input-mask-format/number";
203
+
204
+ function Amount() {
205
+ const [value, setValue] = React.useState<number>();
206
+ return (
207
+ <NumberFormat
208
+ value={value ?? ""}
209
+ onValueChange={({ floatValue }) => setValue(floatValue)}
210
+ thousandSeparator="," decimalScale={2} fixedDecimalScale prefix="$ "
211
+ allowNegative
212
+ />
213
+ );
214
+ }
215
+ ```
216
+
217
+ `onValueChange` receives `{ value, formattedValue, floatValue }`:
218
+
219
+ ```txt
220
+ typing "1234.5" → value "1234.5" formattedValue "$ 1,234.50" floatValue 1234.5
221
+ ```
222
+
223
+ Options: `thousandSeparator` (`true` → `,`, or a custom string),
224
+ `decimalSeparator` (default `.`), `decimalScale` (truncates, no rounding),
225
+ `fixedDecimalScale`, `prefix`, `suffix`, `allowNegative` (default `true`),
226
+ `allowLeadingZeros`, and `isAllowed(values) => boolean` (reject an edit, e.g.
227
+ for min/max).
228
+
229
+ There is also a ref hook and the pure helpers:
230
+
231
+ ```tsx
232
+ import { useNumberFormat, formatNumber, parseNumber } from "react-input-mask-format/number";
233
+
234
+ const ref = useNumberFormat({ thousandSeparator: " ", decimalScale: 2, prefix: "$ " });
235
+ // <input ref={ref} />
236
+
237
+ formatNumber(1234.5, { thousandSeparator: ",", decimalScale: 2, fixedDecimalScale: true }); // "1,234.50"
238
+ ```
239
+
240
+ ### Migrating from react-number-format
241
+
242
+ `NumberFormat`'s props (`thousandSeparator`, `decimalSeparator`, `decimalScale`,
243
+ `fixedDecimalScale`, `prefix`, `suffix`, `allowNegative`, `allowLeadingZeros`,
244
+ `isAllowed`, `onValueChange`) mirror `react-number-format`'s `NumericFormat`.
245
+ The main differences: `decimalScale` **truncates** rather than rounds while
246
+ typing, and this package ships a single `NumberFormat` (no separate
247
+ `PatternFormat` — use the mask engine / `<InputMask>` for pattern masks).
248
+
249
+ ## Time — `react-input-mask-format/time`
250
+
251
+ A separate, tree-shakeable entry for formatting times in 24-hour format (HH:MM).
252
+
253
+ ```tsx
254
+ import { TimeFormat, useTimeFormat, formatTime } from "react-input-mask-format/time";
255
+
256
+ // controlled component
257
+ <TimeFormat value={time} onValueChange={(v) => setTime(v.value)} />
258
+
259
+ // bring-your-own input (e.g. a design-system Input)
260
+ const ref = useTimeFormat({ onValueChange: (v) => setTime(v.value) });
261
+ <input ref={ref} />
262
+
263
+ formatTime("2999"); // "23:59"
264
+ ```
265
+
266
+ `onValueChange` receives `{ value, formattedValue, hours, minutes }`:
267
+
268
+ ```txt
269
+ typing "1234" → value "12:34" formattedValue "12:34" hours 12 minutes 34
270
+ typing "12" → value "" formattedValue "12:" hours 12 minutes undefined
271
+ ```
272
+
273
+ `value` remains empty until both hours and minutes are complete. The canonical
274
+ `value` always uses `:` as the separator, regardless of the display `separator`
275
+ option.
276
+
277
+ **Clamping**: When hours are complete (two digits), they're clamped to ≤23.
278
+ When minutes are complete, they're clamped to ≤59. A single digit is never
279
+ clamped.
280
+
281
+ Because an incomplete time has an empty canonical `value`, a controlled parent
282
+ that resets `value` to `""` won't visibly clear a partially-typed field; to
283
+ force-clear it, remount the input with a changed React `key`.
284
+
285
+ Options: `separator` (default `:`).
286
+
287
+ There is also a ref hook and the pure helpers:
288
+
289
+ ```tsx
290
+ import { useTimeFormat, formatTime, parseTime } from "react-input-mask-format/time";
291
+
292
+ const ref = useTimeFormat({ separator: ":" });
293
+ // <input ref={ref} />
294
+
295
+ formatTime("2999", { separator: ":" }); // "23:59"
296
+ parseTime("14:30"); // { value: "14:30", formattedValue: "14:30", hours: 14, minutes: 30 }
297
+ ```
298
+
299
+ **Reserved for a later minor release**: seconds (HH:MM:SS) and 12-hour format
300
+ with AM/PM.
301
+
164
302
  ### `maskPlaceholder`
165
303
 
166
304
  ```jsx
@@ -0,0 +1,42 @@
1
+ // src/utils/input.ts
2
+ function setInputSelection(input, start, end) {
3
+ if (end === void 0) {
4
+ end = start;
5
+ }
6
+ input.setSelectionRange(start, end);
7
+ }
8
+ function getInputSelection(input) {
9
+ const start = input.selectionStart;
10
+ const end = input.selectionEnd;
11
+ return {
12
+ start,
13
+ end,
14
+ length: (end != null ? end : 0) - (start != null ? start : 0)
15
+ };
16
+ }
17
+ function isInputFocused(input) {
18
+ const inputDocument = input.ownerDocument;
19
+ return inputDocument.hasFocus() && inputDocument.activeElement === input;
20
+ }
21
+
22
+ // src/set-native-value.ts
23
+ function setNativeValue(input, value) {
24
+ const descriptor = Object.getOwnPropertyDescriptor(
25
+ HTMLInputElement.prototype,
26
+ "value"
27
+ );
28
+ const setter = descriptor && descriptor.set;
29
+ if (setter) {
30
+ setter.call(input, value);
31
+ } else {
32
+ input.value = value;
33
+ }
34
+ }
35
+
36
+ export {
37
+ setInputSelection,
38
+ getInputSelection,
39
+ isInputFocused,
40
+ setNativeValue
41
+ };
42
+ //# sourceMappingURL=chunk-CEZXZ7IJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/input.ts","../src/set-native-value.ts"],"sourcesContent":["import type { Selection } from \"../types\";\n\nexport function setInputSelection(\n input: HTMLInputElement,\n start: number,\n end?: number\n): void {\n if (end === undefined) {\n end = start;\n }\n input.setSelectionRange(start, end);\n}\n\nexport function getInputSelection(input: HTMLInputElement): Required<Selection> {\n const start = input.selectionStart;\n const end = input.selectionEnd;\n\n return {\n start,\n end,\n length: (end ?? 0) - (start ?? 0)\n };\n}\n\nexport function isInputFocused(input: HTMLInputElement): boolean {\n const inputDocument = input.ownerDocument;\n return inputDocument.hasFocus() && inputDocument.activeElement === input;\n}\n","// Write to input.value through the native prototype setter so React's\n// internal value tracker stays stale. The user's keystroke `input` event then\n// bubbles into React's root listener, which sees masked value != stale tracker\n// and fires the consumer's onChange with the masked value. Using the tracked\n// (React-wrapped) setter instead would suppress that onChange.\nexport function setNativeValue(input: HTMLInputElement, value: string): void {\n const descriptor = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n \"value\"\n );\n const setter = descriptor && descriptor.set;\n if (setter) {\n setter.call(input, value);\n } else {\n input.value = value;\n }\n}\n"],"mappings":";AAEO,SAAS,kBACd,OACA,OACA,KACM;AACN,MAAI,QAAQ,QAAW;AACrB,UAAM;AAAA,EACR;AACA,QAAM,kBAAkB,OAAO,GAAG;AACpC;AAEO,SAAS,kBAAkB,OAA8C;AAC9E,QAAM,QAAQ,MAAM;AACpB,QAAM,MAAM,MAAM;AAElB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,oBAAO,MAAM,wBAAS;AAAA,EACjC;AACF;AAEO,SAAS,eAAe,OAAkC;AAC/D,QAAM,gBAAgB,MAAM;AAC5B,SAAO,cAAc,SAAS,KAAK,cAAc,kBAAkB;AACrE;;;ACtBO,SAAS,eAAe,OAAyB,OAAqB;AAC3E,QAAM,aAAa,OAAO;AAAA,IACxB,iBAAiB;AAAA,IACjB;AAAA,EACF;AACA,QAAM,SAAS,cAAc,WAAW;AACxC,MAAI,QAAQ;AACV,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B,OAAO;AACL,UAAM,QAAQ;AAAA,EAChB;AACF;","names":[]}
package/dist/index.cjs CHANGED
@@ -32,10 +32,11 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  default: () => src_default,
34
34
  defaultFormatChars: () => defaultFormatChars,
35
- extendedFormatChars: () => extendedFormatChars
35
+ extendedFormatChars: () => extendedFormatChars,
36
+ useMask: () => useMask
36
37
  });
37
38
  module.exports = __toCommonJS(src_exports);
38
- var import_react2 = __toESM(require("react"), 1);
39
+ var import_react3 = __toESM(require("react"), 1);
39
40
 
40
41
  // src/hooks.ts
41
42
  var import_react = require("react");
@@ -672,8 +673,212 @@ function resolveTransform(transform) {
672
673
  return void 0;
673
674
  }
674
675
 
676
+ // src/use-mask.ts
677
+ var import_react2 = require("react");
678
+
679
+ // src/set-native-value.ts
680
+ function setNativeValue(input, value) {
681
+ const descriptor = Object.getOwnPropertyDescriptor(
682
+ HTMLInputElement.prototype,
683
+ "value"
684
+ );
685
+ const setter = descriptor && descriptor.set;
686
+ if (setter) {
687
+ setter.call(input, value);
688
+ } else {
689
+ input.value = value;
690
+ }
691
+ }
692
+
693
+ // src/bind-mask.ts
694
+ var IS_JSDOM = typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
695
+ function createMaskController(maskUtils, options) {
696
+ let input = null;
697
+ let lastValue = "";
698
+ let lastSelection = { start: null, end: null };
699
+ function getInputState() {
700
+ return { value: input.value, selection: getInputSelection(input) };
701
+ }
702
+ function getLastInputState() {
703
+ return { value: lastValue, selection: lastSelection };
704
+ }
705
+ function setInputState({ value, selection }) {
706
+ setNativeValue(input, value);
707
+ lastValue = value;
708
+ if (input && isInputFocused(input) && selection.start !== null && selection.end !== null) {
709
+ setInputSelection(input, selection.start, selection.end);
710
+ lastSelection = getInputSelection(input);
711
+ } else {
712
+ lastSelection = selection;
713
+ }
714
+ if (IS_JSDOM) {
715
+ const resyncValue = value;
716
+ const resyncSelection = selection;
717
+ queueMicrotask(() => {
718
+ if (!input || input.value !== resyncValue) return;
719
+ input.value = resyncValue;
720
+ if (isInputFocused(input) && resyncSelection.start !== null && resyncSelection.end !== null) {
721
+ setInputSelection(input, resyncSelection.start, resyncSelection.end);
722
+ }
723
+ });
724
+ }
725
+ }
726
+ function handleInput() {
727
+ const currentState = getInputState();
728
+ const previousState = getLastInputState();
729
+ let nextState = maskUtils.processChange(currentState, previousState);
730
+ if (options.beforeMaskedStateChange) {
731
+ nextState = options.beforeMaskedStateChange({ currentState, previousState, nextState });
732
+ }
733
+ setInputState(nextState);
734
+ }
735
+ function handleFocus() {
736
+ const currentValue = getInputState().value;
737
+ if (!maskUtils.isValueFilled(currentValue)) {
738
+ const newValue = maskUtils.formatValue(currentValue);
739
+ const newSelection = maskUtils.getDefaultSelectionForValue(newValue);
740
+ let nextState = { value: newValue, selection: newSelection };
741
+ if (options.beforeMaskedStateChange) {
742
+ nextState = options.beforeMaskedStateChange({ currentState: getInputState(), nextState });
743
+ }
744
+ setInputState(nextState);
745
+ defer(() => {
746
+ if (input && isInputFocused(input)) {
747
+ setInputSelection(input, lastSelection.start, lastSelection.end);
748
+ }
749
+ });
750
+ }
751
+ }
752
+ function handleBlur() {
753
+ const lastValueOnBlur = getLastInputState().value;
754
+ if (!options.alwaysShowMask && maskUtils.isValueEmpty(lastValueOnBlur)) {
755
+ let nextState = { value: "", selection: { start: null, end: null } };
756
+ if (options.beforeMaskedStateChange) {
757
+ nextState = options.beforeMaskedStateChange({ currentState: getInputState(), nextState });
758
+ }
759
+ setInputState(nextState);
760
+ }
761
+ }
762
+ function handleMouseDown(event) {
763
+ if (!input) return;
764
+ const { value } = getInputState();
765
+ const inputDocument = getElementDocument(input);
766
+ if (!isInputFocused(input) && !maskUtils.isValueFilled(value)) {
767
+ const mouseDownX = event.clientX;
768
+ const mouseDownY = event.clientY;
769
+ const mouseDownTime = (/* @__PURE__ */ new Date()).getTime();
770
+ const mouseUpHandler = (mouseUpEvent) => {
771
+ inputDocument.removeEventListener("mouseup", mouseUpHandler);
772
+ if (!input || !isInputFocused(input)) return;
773
+ const deltaX = Math.abs(mouseUpEvent.clientX - mouseDownX);
774
+ const deltaY = Math.abs(mouseUpEvent.clientY - mouseDownY);
775
+ const axisDelta = Math.max(deltaX, deltaY);
776
+ const timeDelta = (/* @__PURE__ */ new Date()).getTime() - mouseDownTime;
777
+ if (axisDelta <= 10 && timeDelta <= 200 || axisDelta <= 5 && timeDelta <= 300) {
778
+ const newSelection = maskUtils.getDefaultSelectionForValue(lastValue);
779
+ if (newSelection.start !== null && newSelection.end !== null) {
780
+ setInputSelection(input, newSelection.start, newSelection.end);
781
+ lastSelection = getInputSelection(input);
782
+ }
783
+ }
784
+ };
785
+ inputDocument.addEventListener("mouseup", mouseUpHandler);
786
+ }
787
+ }
788
+ function bind(el) {
789
+ input = el;
790
+ lastValue = el.value;
791
+ lastSelection = getInputSelection(el);
792
+ const formatted = maskUtils.formatValue(el.value);
793
+ if (!maskUtils.isValueEmpty(formatted) || options.alwaysShowMask) {
794
+ setNativeValue(el, formatted);
795
+ lastValue = formatted;
796
+ }
797
+ el.addEventListener("input", handleInput);
798
+ el.addEventListener("focus", handleFocus);
799
+ el.addEventListener("blur", handleBlur);
800
+ el.addEventListener("mousedown", handleMouseDown);
801
+ }
802
+ function unbind() {
803
+ if (input) {
804
+ input.removeEventListener("input", handleInput);
805
+ input.removeEventListener("focus", handleFocus);
806
+ input.removeEventListener("blur", handleBlur);
807
+ input.removeEventListener("mousedown", handleMouseDown);
808
+ }
809
+ input = null;
810
+ }
811
+ function update(nextMaskUtils, nextOptions) {
812
+ maskUtils = nextMaskUtils;
813
+ options = nextOptions;
814
+ }
815
+ return { bind, unbind, update };
816
+ }
817
+
818
+ // src/use-mask.ts
819
+ function useMask(options) {
820
+ const {
821
+ mask,
822
+ maskPlaceholder: maskPlaceholderProp,
823
+ alwaysShowMask = false,
824
+ formatChars,
825
+ transform,
826
+ beforeMaskedStateChange: beforeMaskedStateChangeProp,
827
+ beforeMaskedValueChange
828
+ } = options;
829
+ if (beforeMaskedValueChange !== void 0) {
830
+ warnDeprecatedOnce(
831
+ "beforeMaskedValueChange",
832
+ "beforeMaskedValueChange is deprecated, use beforeMaskedStateChange."
833
+ );
834
+ }
835
+ const resolvedPlaceholder = resolveMaskPlaceholder(maskPlaceholderProp, void 0);
836
+ const maskPlaceholder = resolvedPlaceholder === void 0 ? "_" : resolvedPlaceholder;
837
+ const { formatChars: normalizedFormatChars, hasLegacyString } = normalizeFormatChars(formatChars);
838
+ if (hasLegacyString) {
839
+ warnDeprecatedOnce(
840
+ "formatChars",
841
+ "string values in formatChars are deprecated, pass RegExp values instead."
842
+ );
843
+ }
844
+ const maskUtils = new MaskUtils({
845
+ mask: mask != null ? mask : null,
846
+ maskPlaceholder,
847
+ formatChars: normalizedFormatChars,
848
+ transform: resolveTransform(transform)
849
+ });
850
+ let beforeMaskedStateChange = beforeMaskedStateChangeProp;
851
+ if (!beforeMaskedStateChange && beforeMaskedValueChange) {
852
+ const v2MaskOptions = {
853
+ mask: mask != null ? mask : void 0,
854
+ maskChar: maskPlaceholder,
855
+ alwaysShowMask,
856
+ formatChars: formatChars != null ? formatChars : { "9": "[0-9]", a: "[A-Za-z]", "*": "[A-Za-z0-9]" },
857
+ permanents: maskUtils.maskOptions.permanents
858
+ };
859
+ beforeMaskedStateChange = createBeforeMaskedStateChangeAdapter(beforeMaskedValueChange, v2MaskOptions);
860
+ }
861
+ const controllerOptions = { alwaysShowMask, beforeMaskedStateChange };
862
+ const controllerRef = (0, import_react2.useRef)(null);
863
+ (0, import_react2.useEffect)(() => {
864
+ var _a;
865
+ (_a = controllerRef.current) == null ? void 0 : _a.update(maskUtils, controllerOptions);
866
+ });
867
+ return (0, import_react2.useCallback)((el) => {
868
+ var _a;
869
+ if (el) {
870
+ const controller = createMaskController(maskUtils, controllerOptions);
871
+ controller.bind(el);
872
+ controllerRef.current = controller;
873
+ } else {
874
+ (_a = controllerRef.current) == null ? void 0 : _a.unbind();
875
+ controllerRef.current = null;
876
+ }
877
+ }, []);
878
+ }
879
+
675
880
  // src/index.tsx
676
- var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forwardedRef) {
881
+ var InputMask = (0, import_react3.forwardRef)(function InputMask2(props, forwardedRef) {
677
882
  const {
678
883
  alwaysShowMask = false,
679
884
  children,
@@ -856,7 +1061,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
856
1061
  renderValue = newValue;
857
1062
  }
858
1063
  const lastSelection = getLastInputState().selection;
859
- (0, import_react2.useLayoutEffect)(() => {
1064
+ (0, import_react3.useLayoutEffect)(() => {
860
1065
  if (isMasked && isControlled) {
861
1066
  setInputState({
862
1067
  ...getLastInputState(),
@@ -864,7 +1069,7 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
864
1069
  });
865
1070
  }
866
1071
  });
867
- (0, import_react2.useLayoutEffect)(() => {
1072
+ (0, import_react3.useLayoutEffect)(() => {
868
1073
  if (!isMasked) {
869
1074
  return;
870
1075
  }
@@ -921,15 +1126,16 @@ var InputMask = (0, import_react2.forwardRef)(function InputMask2(props, forward
921
1126
  };
922
1127
  if (children) {
923
1128
  validateChildren(props, children);
924
- return import_react2.default.cloneElement(children, inputProps);
1129
+ return import_react3.default.cloneElement(children, inputProps);
925
1130
  }
926
- return /* @__PURE__ */ import_react2.default.createElement("input", { ...inputProps });
1131
+ return /* @__PURE__ */ import_react3.default.createElement("input", { ...inputProps });
927
1132
  });
928
1133
  InputMask.displayName = "InputMask";
929
1134
  var src_default = InputMask;
930
1135
  // Annotate the CommonJS export names for ESM import in node:
931
1136
  0 && (module.exports = {
932
1137
  defaultFormatChars,
933
- extendedFormatChars
1138
+ extendedFormatChars,
1139
+ useMask
934
1140
  });
935
1141
  //# sourceMappingURL=index.cjs.map