react-input-mask-format 2.2.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,11 @@ 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.2
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
14
18
 
15
19
  - `useMask` hook — mask your own `<input>` with no wrapper component, see [useMask hook](#usemask-hook)
16
20
  - `react-input-mask-format/number` — numeric & currency masking, a lightweight `react-number-format` alternative, see [Numeric & currency](#numeric--currency--react-input-mask-formatnumber)
@@ -242,6 +246,59 @@ The main differences: `decimalScale` **truncates** rather than rounds while
242
246
  typing, and this package ships a single `NumberFormat` (no separate
243
247
  `PatternFormat` — use the mask engine / `<InputMask>` for pattern masks).
244
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
+
245
302
  ### `maskPlaceholder`
246
303
 
247
304
  ```jsx
package/dist/time.cjs ADDED
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/time/index.ts
31
+ var time_exports = {};
32
+ __export(time_exports, {
33
+ TimeFormat: () => TimeFormat,
34
+ formatTime: () => formatTime,
35
+ parseTime: () => parseTime,
36
+ useTimeFormat: () => useTimeFormat
37
+ });
38
+ module.exports = __toCommonJS(time_exports);
39
+
40
+ // src/time/time-format.tsx
41
+ var import_react = __toESM(require("react"), 1);
42
+
43
+ // src/time/format-time.ts
44
+ function tokenizeTime(raw) {
45
+ const digits = String(raw != null ? raw : "").replace(/\D/g, "").slice(0, 4);
46
+ let hours = digits.slice(0, 2);
47
+ let minutes = digits.slice(2, 4);
48
+ if (hours.length === 2) {
49
+ hours = String(Math.min(parseInt(hours, 10), 23)).padStart(2, "0");
50
+ }
51
+ if (minutes.length === 2) {
52
+ minutes = String(Math.min(parseInt(minutes, 10), 59)).padStart(2, "0");
53
+ }
54
+ return { hours, minutes };
55
+ }
56
+ function buildFormatted(parts, separator) {
57
+ const { hours, minutes } = parts;
58
+ if (hours === "") return "";
59
+ if (hours.length < 2) return hours;
60
+ if (minutes === "") return hours + separator;
61
+ return hours + separator + minutes;
62
+ }
63
+ function formatTime(input, options) {
64
+ var _a;
65
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
66
+ return buildFormatted(tokenizeTime(input), separator);
67
+ }
68
+
69
+ // src/time/parse-time.ts
70
+ function collapseTrailingSeparator(raw, previous, separator) {
71
+ return previous.endsWith(separator) && raw === previous.slice(0, -separator.length) ? raw.slice(0, -1) : raw;
72
+ }
73
+ function parseTime(formatted, options) {
74
+ var _a;
75
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
76
+ const parts = tokenizeTime(formatted);
77
+ const formattedValue = buildFormatted(parts, separator);
78
+ const hours = parts.hours.length === 2 ? parseInt(parts.hours, 10) : void 0;
79
+ const minutes = parts.minutes.length === 2 ? parseInt(parts.minutes, 10) : void 0;
80
+ const value = hours !== void 0 && minutes !== void 0 ? `${parts.hours}:${parts.minutes}` : "";
81
+ return { value, formattedValue, hours, minutes };
82
+ }
83
+ function digitsBeforeCaret(value, caret) {
84
+ let count = 0;
85
+ for (let i = 0; i < caret && i < value.length; i++) {
86
+ if (value[i] >= "0" && value[i] <= "9") count++;
87
+ }
88
+ return count;
89
+ }
90
+ function caretAfterDigits(formatted, digitsBefore) {
91
+ if (digitsBefore <= 0) {
92
+ const first = formatted.search(/\d/);
93
+ return first === -1 ? formatted.length : first;
94
+ }
95
+ let seen = 0;
96
+ for (let i = 0; i < formatted.length; i++) {
97
+ if (formatted[i] >= "0" && formatted[i] <= "9") {
98
+ seen++;
99
+ if (seen === digitsBefore) return i + 1;
100
+ }
101
+ }
102
+ return formatted.length;
103
+ }
104
+ function resolveCaret(values, digitsBefore, options) {
105
+ var _a;
106
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
107
+ const caret = caretAfterDigits(values.formattedValue, digitsBefore);
108
+ if (separator && values.formattedValue.slice(caret, caret + separator.length) === separator) {
109
+ return caret + separator.length;
110
+ }
111
+ return caret;
112
+ }
113
+
114
+ // src/utils/input.ts
115
+ function setInputSelection(input, start, end) {
116
+ if (end === void 0) {
117
+ end = start;
118
+ }
119
+ input.setSelectionRange(start, end);
120
+ }
121
+ function isInputFocused(input) {
122
+ const inputDocument = input.ownerDocument;
123
+ return inputDocument.hasFocus() && inputDocument.activeElement === input;
124
+ }
125
+
126
+ // src/time/time-format.tsx
127
+ var TimeFormat = (0, import_react.forwardRef)(function TimeFormat2(props, forwardedRef) {
128
+ const { value = "", onValueChange, separator, ...domProps } = props;
129
+ const options = { separator };
130
+ const innerRef = (0, import_react.useRef)(null);
131
+ const caretRef = (0, import_react.useRef)(null);
132
+ const lastValuesRef = (0, import_react.useRef)(null);
133
+ const [, forceUpdate] = (0, import_react.useReducer)((n) => n + 1, 0);
134
+ const last = lastValuesRef.current;
135
+ const formatted = last !== null && (value === last.value || value === last.formattedValue) ? last.formattedValue : formatTime(value != null ? value : "", options);
136
+ function handleChange(event) {
137
+ var _a, _b;
138
+ const el = event.target;
139
+ const caret = (_a = el.selectionStart) != null ? _a : el.value.length;
140
+ const raw = collapseTrailingSeparator(el.value, formatted, (_b = options.separator) != null ? _b : ":");
141
+ const digits = digitsBeforeCaret(raw, caret);
142
+ const values = parseTime(raw, options);
143
+ caretRef.current = resolveCaret(values, digits, options);
144
+ lastValuesRef.current = values;
145
+ forceUpdate();
146
+ onValueChange == null ? void 0 : onValueChange(values);
147
+ }
148
+ (0, import_react.useLayoutEffect)(() => {
149
+ const el = innerRef.current;
150
+ if (el && caretRef.current !== null && isInputFocused(el)) {
151
+ setInputSelection(el, caretRef.current, caretRef.current);
152
+ caretRef.current = null;
153
+ }
154
+ });
155
+ function setRef(el) {
156
+ innerRef.current = el;
157
+ if (typeof forwardedRef === "function") forwardedRef(el);
158
+ else if (forwardedRef)
159
+ forwardedRef.current = el;
160
+ }
161
+ return /* @__PURE__ */ import_react.default.createElement("input", { ...domProps, ref: setRef, value: formatted, onChange: handleChange });
162
+ });
163
+ TimeFormat.displayName = "TimeFormat";
164
+
165
+ // src/time/use-time-format.ts
166
+ var import_react2 = require("react");
167
+
168
+ // src/set-native-value.ts
169
+ function setNativeValue(input, value) {
170
+ const descriptor = Object.getOwnPropertyDescriptor(
171
+ HTMLInputElement.prototype,
172
+ "value"
173
+ );
174
+ const setter = descriptor && descriptor.set;
175
+ if (setter) {
176
+ setter.call(input, value);
177
+ } else {
178
+ input.value = value;
179
+ }
180
+ }
181
+
182
+ // src/time/use-time-format.ts
183
+ var IS_JSDOM = typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
184
+ function useTimeFormat(options) {
185
+ const optionsRef = (0, import_react2.useRef)(options);
186
+ optionsRef.current = options;
187
+ const inputRef = (0, import_react2.useRef)(null);
188
+ const handlerRef = (0, import_react2.useRef)(null);
189
+ return (0, import_react2.useCallback)((el) => {
190
+ if (el) {
191
+ let previous = el.value;
192
+ const handler = () => {
193
+ var _a, _b, _c;
194
+ const opts = optionsRef.current;
195
+ const caret = (_a = el.selectionStart) != null ? _a : el.value.length;
196
+ const raw = collapseTrailingSeparator(el.value, previous, (_b = opts.separator) != null ? _b : ":");
197
+ const digits = digitsBeforeCaret(raw, caret);
198
+ const values = parseTime(raw, opts);
199
+ setNativeValue(el, values.formattedValue);
200
+ previous = values.formattedValue;
201
+ if (isInputFocused(el)) {
202
+ const next = resolveCaret(values, digits, opts);
203
+ setInputSelection(el, next, next);
204
+ }
205
+ if (IS_JSDOM) {
206
+ queueMicrotask(() => {
207
+ if (el.value !== values.formattedValue) return;
208
+ el.value = values.formattedValue;
209
+ if (isInputFocused(el)) {
210
+ const c = resolveCaret(values, digits, opts);
211
+ setInputSelection(el, c, c);
212
+ }
213
+ });
214
+ }
215
+ (_c = opts.onValueChange) == null ? void 0 : _c.call(opts, values);
216
+ };
217
+ el.addEventListener("input", handler);
218
+ handlerRef.current = handler;
219
+ inputRef.current = el;
220
+ if (el.value) {
221
+ setNativeValue(el, formatTime(el.value, optionsRef.current));
222
+ previous = formatTime(el.value, optionsRef.current);
223
+ }
224
+ } else {
225
+ if (inputRef.current && handlerRef.current) {
226
+ inputRef.current.removeEventListener("input", handlerRef.current);
227
+ }
228
+ inputRef.current = null;
229
+ handlerRef.current = null;
230
+ }
231
+ }, []);
232
+ }
233
+ // Annotate the CommonJS export names for ESM import in node:
234
+ 0 && (module.exports = {
235
+ TimeFormat,
236
+ formatTime,
237
+ parseTime,
238
+ useTimeFormat
239
+ });
240
+ //# sourceMappingURL=time.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/time/index.ts","../src/time/time-format.tsx","../src/time/format-time.ts","../src/time/parse-time.ts","../src/utils/input.ts","../src/time/use-time-format.ts","../src/set-native-value.ts"],"sourcesContent":["export { TimeFormat } from \"./time-format\";\nexport type { TimeFormatProps } from \"./time-format\";\nexport { useTimeFormat } from \"./use-time-format\";\nexport type { UseTimeFormatOptions } from \"./use-time-format\";\nexport { formatTime } from \"./format-time\";\nexport { parseTime } from \"./parse-time\";\nexport type { TimeFormatOptions, TimeFormatValues } from \"./types\";\n","import React, { forwardRef, useLayoutEffect, useReducer, useRef } from \"react\";\nimport type { TimeFormatOptions, TimeFormatValues } from \"./types\";\nimport { formatTime } from \"./format-time\";\nimport {\n collapseTrailingSeparator,\n parseTime,\n digitsBeforeCaret,\n resolveCaret,\n} from \"./parse-time\";\nimport { isInputFocused, setInputSelection } from \"../utils/input\";\n\nexport interface TimeFormatProps\n extends TimeFormatOptions,\n Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"onChange\"> {\n value?: string;\n onValueChange?: (values: TimeFormatValues) => void;\n}\n\nexport const TimeFormat = forwardRef<HTMLInputElement, TimeFormatProps>(function TimeFormat(\n props,\n forwardedRef\n) {\n const { value = \"\", onValueChange, separator, ...domProps } = props;\n const options: TimeFormatOptions = { separator };\n\n const innerRef = useRef<HTMLInputElement | null>(null);\n const caretRef = useRef<number | null>(null);\n const lastValuesRef = useRef<TimeFormatValues | null>(null);\n\n const [, forceUpdate] = useReducer((n: number) => n + 1, 0);\n\n const last = lastValuesRef.current;\n const formatted =\n last !== null && (value === last.value || value === last.formattedValue)\n ? last.formattedValue\n : formatTime(value ?? \"\", options);\n\n function handleChange(event: React.ChangeEvent<HTMLInputElement>): void {\n const el = event.target;\n const caret = el.selectionStart ?? el.value.length;\n const raw = collapseTrailingSeparator(el.value, formatted, options.separator ?? \":\");\n const digits = digitsBeforeCaret(raw, caret);\n const values = parseTime(raw, options);\n caretRef.current = resolveCaret(values, digits, options);\n lastValuesRef.current = values;\n forceUpdate();\n onValueChange?.(values);\n }\n\n useLayoutEffect(() => {\n const el = innerRef.current;\n if (el && caretRef.current !== null && isInputFocused(el)) {\n setInputSelection(el, caretRef.current, caretRef.current);\n caretRef.current = null;\n }\n });\n\n function setRef(el: HTMLInputElement | null): void {\n innerRef.current = el;\n if (typeof forwardedRef === \"function\") forwardedRef(el);\n else if (forwardedRef)\n (forwardedRef as React.MutableRefObject<HTMLInputElement | null>).current = el;\n }\n\n return <input {...domProps} ref={setRef} value={formatted} onChange={handleChange} />;\n});\n\nTimeFormat.displayName = \"TimeFormat\";\n","import type { TimeFormatOptions } from \"./types\";\n\nexport interface TimeParts {\n hours: string;\n minutes: string;\n}\n\nexport function tokenizeTime(raw: string | number): TimeParts {\n const digits = String(raw ?? \"\").replace(/\\D/g, \"\").slice(0, 4);\n let hours = digits.slice(0, 2);\n let minutes = digits.slice(2, 4);\n if (hours.length === 2) {\n hours = String(Math.min(parseInt(hours, 10), 23)).padStart(2, \"0\");\n }\n if (minutes.length === 2) {\n minutes = String(Math.min(parseInt(minutes, 10), 59)).padStart(2, \"0\");\n }\n return { hours, minutes };\n}\n\nexport function buildFormatted(parts: TimeParts, separator: string): string {\n const { hours, minutes } = parts;\n if (hours === \"\") return \"\";\n if (hours.length < 2) return hours;\n if (minutes === \"\") return hours + separator;\n return hours + separator + minutes;\n}\n\nexport function formatTime(input: string | number, options?: TimeFormatOptions): string {\n const separator = options?.separator ?? \":\";\n return buildFormatted(tokenizeTime(input), separator);\n}\n","import type { TimeFormatOptions, TimeFormatValues } from \"./types\";\nimport { tokenizeTime, buildFormatted } from \"./format-time\";\n\nexport function collapseTrailingSeparator(\n raw: string,\n previous: string,\n separator: string\n): string {\n return previous.endsWith(separator) && raw === previous.slice(0, -separator.length)\n ? raw.slice(0, -1)\n : raw;\n}\n\nexport function parseTime(formatted: string, options?: TimeFormatOptions): TimeFormatValues {\n const separator = options?.separator ?? \":\";\n const parts = tokenizeTime(formatted);\n const formattedValue = buildFormatted(parts, separator);\n const hours = parts.hours.length === 2 ? parseInt(parts.hours, 10) : undefined;\n const minutes = parts.minutes.length === 2 ? parseInt(parts.minutes, 10) : undefined;\n const value =\n hours !== undefined && minutes !== undefined ? `${parts.hours}:${parts.minutes}` : \"\";\n return { value, formattedValue, hours, minutes };\n}\n\nexport function digitsBeforeCaret(value: string, caret: number): number {\n let count = 0;\n for (let i = 0; i < caret && i < value.length; i++) {\n if (value[i] >= \"0\" && value[i] <= \"9\") count++;\n }\n return count;\n}\n\nfunction caretAfterDigits(formatted: string, digitsBefore: number): number {\n if (digitsBefore <= 0) {\n const first = formatted.search(/\\d/);\n return first === -1 ? formatted.length : first;\n }\n let seen = 0;\n for (let i = 0; i < formatted.length; i++) {\n if (formatted[i] >= \"0\" && formatted[i] <= \"9\") {\n seen++;\n if (seen === digitsBefore) return i + 1;\n }\n }\n return formatted.length;\n}\n\nexport function resolveCaret(\n values: TimeFormatValues,\n digitsBefore: number,\n options?: TimeFormatOptions\n): number {\n const separator = options?.separator ?? \":\";\n const caret = caretAfterDigits(values.formattedValue, digitsBefore);\n if (separator && values.formattedValue.slice(caret, caret + separator.length) === separator) {\n return caret + separator.length;\n }\n return caret;\n}\n","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","import { useRef, useCallback } from \"react\";\nimport type React from \"react\";\nimport type { TimeFormatValues } from \"./types\";\nimport type { TimeFormatOptions } from \"./types\";\nimport { formatTime } from \"./format-time\";\nimport {\n collapseTrailingSeparator,\n parseTime,\n digitsBeforeCaret,\n resolveCaret,\n} from \"./parse-time\";\nimport { setNativeValue } from \"../set-native-value\";\nimport { isInputFocused, setInputSelection } from \"../utils/input\";\n\nexport interface UseTimeFormatOptions extends TimeFormatOptions {\n onValueChange?: (values: TimeFormatValues) => void;\n}\n\nconst IS_JSDOM =\n typeof navigator !== \"undefined\" && navigator.userAgent.includes(\"jsdom\");\n\nexport function useTimeFormat(\n options: UseTimeFormatOptions\n): React.RefCallback<HTMLInputElement> {\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const inputRef = useRef<HTMLInputElement | null>(null);\n const handlerRef = useRef<((event: Event) => void) | null>(null);\n\n return useCallback((el: HTMLInputElement | null) => {\n if (el) {\n let previous = el.value;\n const handler = (): void => {\n const opts = optionsRef.current;\n const caret = el.selectionStart ?? el.value.length;\n const raw = collapseTrailingSeparator(el.value, previous, opts.separator ?? \":\");\n const digits = digitsBeforeCaret(raw, caret);\n const values = parseTime(raw, opts);\n setNativeValue(el, values.formattedValue);\n previous = values.formattedValue;\n if (isInputFocused(el)) {\n const next = resolveCaret(values, digits, opts);\n setInputSelection(el, next, next);\n }\n // JSDOM-ONLY: resync @testing-library/user-event v14's shadow-value cache.\n // setNativeValue above bypasses React's value tracker; under jsdom that\n // leaves user-event's per-input shadow value stale, corrupting masking from\n // ~the 4th keystroke. A plain (idempotent) reassignment resyncs it.\n // MUST NOT run in a real browser: there, microtasks drain BETWEEN event\n // listeners, so this write would fight React; real browsers read the live\n // DOM and need no resync. onValueChange (below) is called directly, not via\n // React's tracker, so it is unaffected in either environment.\n if (IS_JSDOM) {\n queueMicrotask(() => {\n if (el.value !== values.formattedValue) return;\n el.value = values.formattedValue;\n if (isInputFocused(el)) {\n const c = resolveCaret(values, digits, opts);\n setInputSelection(el, c, c);\n }\n });\n }\n opts.onValueChange?.(values);\n };\n el.addEventListener(\"input\", handler);\n handlerRef.current = handler;\n inputRef.current = el;\n if (el.value) {\n setNativeValue(el, formatTime(el.value, optionsRef.current));\n previous = formatTime(el.value, optionsRef.current);\n }\n } else {\n if (inputRef.current && handlerRef.current) {\n inputRef.current.removeEventListener(\"input\", handlerRef.current);\n }\n inputRef.current = null;\n handlerRef.current = null;\n }\n }, []);\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":"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","names":["TimeFormat","React","import_react"]}
@@ -0,0 +1,28 @@
1
+ import React__default from 'react';
2
+
3
+ interface TimeFormatOptions {
4
+ separator?: string;
5
+ }
6
+ interface TimeFormatValues {
7
+ value: string;
8
+ formattedValue: string;
9
+ hours: number | undefined;
10
+ minutes: number | undefined;
11
+ }
12
+
13
+ interface TimeFormatProps extends TimeFormatOptions, Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> {
14
+ value?: string;
15
+ onValueChange?: (values: TimeFormatValues) => void;
16
+ }
17
+ declare const TimeFormat: React__default.ForwardRefExoticComponent<TimeFormatProps & React__default.RefAttributes<HTMLInputElement>>;
18
+
19
+ interface UseTimeFormatOptions extends TimeFormatOptions {
20
+ onValueChange?: (values: TimeFormatValues) => void;
21
+ }
22
+ declare function useTimeFormat(options: UseTimeFormatOptions): React__default.RefCallback<HTMLInputElement>;
23
+
24
+ declare function formatTime(input: string | number, options?: TimeFormatOptions): string;
25
+
26
+ declare function parseTime(formatted: string, options?: TimeFormatOptions): TimeFormatValues;
27
+
28
+ export { TimeFormat, type TimeFormatOptions, type TimeFormatProps, type TimeFormatValues, type UseTimeFormatOptions, formatTime, parseTime, useTimeFormat };
package/dist/time.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import React__default from 'react';
2
+
3
+ interface TimeFormatOptions {
4
+ separator?: string;
5
+ }
6
+ interface TimeFormatValues {
7
+ value: string;
8
+ formattedValue: string;
9
+ hours: number | undefined;
10
+ minutes: number | undefined;
11
+ }
12
+
13
+ interface TimeFormatProps extends TimeFormatOptions, Omit<React__default.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> {
14
+ value?: string;
15
+ onValueChange?: (values: TimeFormatValues) => void;
16
+ }
17
+ declare const TimeFormat: React__default.ForwardRefExoticComponent<TimeFormatProps & React__default.RefAttributes<HTMLInputElement>>;
18
+
19
+ interface UseTimeFormatOptions extends TimeFormatOptions {
20
+ onValueChange?: (values: TimeFormatValues) => void;
21
+ }
22
+ declare function useTimeFormat(options: UseTimeFormatOptions): React__default.RefCallback<HTMLInputElement>;
23
+
24
+ declare function formatTime(input: string | number, options?: TimeFormatOptions): string;
25
+
26
+ declare function parseTime(formatted: string, options?: TimeFormatOptions): TimeFormatValues;
27
+
28
+ export { TimeFormat, type TimeFormatOptions, type TimeFormatProps, type TimeFormatValues, type UseTimeFormatOptions, formatTime, parseTime, useTimeFormat };
package/dist/time.js ADDED
@@ -0,0 +1,178 @@
1
+ import {
2
+ isInputFocused,
3
+ setInputSelection,
4
+ setNativeValue
5
+ } from "./chunk-CEZXZ7IJ.js";
6
+
7
+ // src/time/time-format.tsx
8
+ import React, { forwardRef, useLayoutEffect, useReducer, useRef } from "react";
9
+
10
+ // src/time/format-time.ts
11
+ function tokenizeTime(raw) {
12
+ const digits = String(raw != null ? raw : "").replace(/\D/g, "").slice(0, 4);
13
+ let hours = digits.slice(0, 2);
14
+ let minutes = digits.slice(2, 4);
15
+ if (hours.length === 2) {
16
+ hours = String(Math.min(parseInt(hours, 10), 23)).padStart(2, "0");
17
+ }
18
+ if (minutes.length === 2) {
19
+ minutes = String(Math.min(parseInt(minutes, 10), 59)).padStart(2, "0");
20
+ }
21
+ return { hours, minutes };
22
+ }
23
+ function buildFormatted(parts, separator) {
24
+ const { hours, minutes } = parts;
25
+ if (hours === "") return "";
26
+ if (hours.length < 2) return hours;
27
+ if (minutes === "") return hours + separator;
28
+ return hours + separator + minutes;
29
+ }
30
+ function formatTime(input, options) {
31
+ var _a;
32
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
33
+ return buildFormatted(tokenizeTime(input), separator);
34
+ }
35
+
36
+ // src/time/parse-time.ts
37
+ function collapseTrailingSeparator(raw, previous, separator) {
38
+ return previous.endsWith(separator) && raw === previous.slice(0, -separator.length) ? raw.slice(0, -1) : raw;
39
+ }
40
+ function parseTime(formatted, options) {
41
+ var _a;
42
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
43
+ const parts = tokenizeTime(formatted);
44
+ const formattedValue = buildFormatted(parts, separator);
45
+ const hours = parts.hours.length === 2 ? parseInt(parts.hours, 10) : void 0;
46
+ const minutes = parts.minutes.length === 2 ? parseInt(parts.minutes, 10) : void 0;
47
+ const value = hours !== void 0 && minutes !== void 0 ? `${parts.hours}:${parts.minutes}` : "";
48
+ return { value, formattedValue, hours, minutes };
49
+ }
50
+ function digitsBeforeCaret(value, caret) {
51
+ let count = 0;
52
+ for (let i = 0; i < caret && i < value.length; i++) {
53
+ if (value[i] >= "0" && value[i] <= "9") count++;
54
+ }
55
+ return count;
56
+ }
57
+ function caretAfterDigits(formatted, digitsBefore) {
58
+ if (digitsBefore <= 0) {
59
+ const first = formatted.search(/\d/);
60
+ return first === -1 ? formatted.length : first;
61
+ }
62
+ let seen = 0;
63
+ for (let i = 0; i < formatted.length; i++) {
64
+ if (formatted[i] >= "0" && formatted[i] <= "9") {
65
+ seen++;
66
+ if (seen === digitsBefore) return i + 1;
67
+ }
68
+ }
69
+ return formatted.length;
70
+ }
71
+ function resolveCaret(values, digitsBefore, options) {
72
+ var _a;
73
+ const separator = (_a = options == null ? void 0 : options.separator) != null ? _a : ":";
74
+ const caret = caretAfterDigits(values.formattedValue, digitsBefore);
75
+ if (separator && values.formattedValue.slice(caret, caret + separator.length) === separator) {
76
+ return caret + separator.length;
77
+ }
78
+ return caret;
79
+ }
80
+
81
+ // src/time/time-format.tsx
82
+ var TimeFormat = forwardRef(function TimeFormat2(props, forwardedRef) {
83
+ const { value = "", onValueChange, separator, ...domProps } = props;
84
+ const options = { separator };
85
+ const innerRef = useRef(null);
86
+ const caretRef = useRef(null);
87
+ const lastValuesRef = useRef(null);
88
+ const [, forceUpdate] = useReducer((n) => n + 1, 0);
89
+ const last = lastValuesRef.current;
90
+ const formatted = last !== null && (value === last.value || value === last.formattedValue) ? last.formattedValue : formatTime(value != null ? value : "", options);
91
+ function handleChange(event) {
92
+ var _a, _b;
93
+ const el = event.target;
94
+ const caret = (_a = el.selectionStart) != null ? _a : el.value.length;
95
+ const raw = collapseTrailingSeparator(el.value, formatted, (_b = options.separator) != null ? _b : ":");
96
+ const digits = digitsBeforeCaret(raw, caret);
97
+ const values = parseTime(raw, options);
98
+ caretRef.current = resolveCaret(values, digits, options);
99
+ lastValuesRef.current = values;
100
+ forceUpdate();
101
+ onValueChange == null ? void 0 : onValueChange(values);
102
+ }
103
+ useLayoutEffect(() => {
104
+ const el = innerRef.current;
105
+ if (el && caretRef.current !== null && isInputFocused(el)) {
106
+ setInputSelection(el, caretRef.current, caretRef.current);
107
+ caretRef.current = null;
108
+ }
109
+ });
110
+ function setRef(el) {
111
+ innerRef.current = el;
112
+ if (typeof forwardedRef === "function") forwardedRef(el);
113
+ else if (forwardedRef)
114
+ forwardedRef.current = el;
115
+ }
116
+ return /* @__PURE__ */ React.createElement("input", { ...domProps, ref: setRef, value: formatted, onChange: handleChange });
117
+ });
118
+ TimeFormat.displayName = "TimeFormat";
119
+
120
+ // src/time/use-time-format.ts
121
+ import { useRef as useRef2, useCallback } from "react";
122
+ var IS_JSDOM = typeof navigator !== "undefined" && navigator.userAgent.includes("jsdom");
123
+ function useTimeFormat(options) {
124
+ const optionsRef = useRef2(options);
125
+ optionsRef.current = options;
126
+ const inputRef = useRef2(null);
127
+ const handlerRef = useRef2(null);
128
+ return useCallback((el) => {
129
+ if (el) {
130
+ let previous = el.value;
131
+ const handler = () => {
132
+ var _a, _b, _c;
133
+ const opts = optionsRef.current;
134
+ const caret = (_a = el.selectionStart) != null ? _a : el.value.length;
135
+ const raw = collapseTrailingSeparator(el.value, previous, (_b = opts.separator) != null ? _b : ":");
136
+ const digits = digitsBeforeCaret(raw, caret);
137
+ const values = parseTime(raw, opts);
138
+ setNativeValue(el, values.formattedValue);
139
+ previous = values.formattedValue;
140
+ if (isInputFocused(el)) {
141
+ const next = resolveCaret(values, digits, opts);
142
+ setInputSelection(el, next, next);
143
+ }
144
+ if (IS_JSDOM) {
145
+ queueMicrotask(() => {
146
+ if (el.value !== values.formattedValue) return;
147
+ el.value = values.formattedValue;
148
+ if (isInputFocused(el)) {
149
+ const c = resolveCaret(values, digits, opts);
150
+ setInputSelection(el, c, c);
151
+ }
152
+ });
153
+ }
154
+ (_c = opts.onValueChange) == null ? void 0 : _c.call(opts, values);
155
+ };
156
+ el.addEventListener("input", handler);
157
+ handlerRef.current = handler;
158
+ inputRef.current = el;
159
+ if (el.value) {
160
+ setNativeValue(el, formatTime(el.value, optionsRef.current));
161
+ previous = formatTime(el.value, optionsRef.current);
162
+ }
163
+ } else {
164
+ if (inputRef.current && handlerRef.current) {
165
+ inputRef.current.removeEventListener("input", handlerRef.current);
166
+ }
167
+ inputRef.current = null;
168
+ handlerRef.current = null;
169
+ }
170
+ }, []);
171
+ }
172
+ export {
173
+ TimeFormat,
174
+ formatTime,
175
+ parseTime,
176
+ useTimeFormat
177
+ };
178
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/time/time-format.tsx","../src/time/format-time.ts","../src/time/parse-time.ts","../src/time/use-time-format.ts"],"sourcesContent":["import React, { forwardRef, useLayoutEffect, useReducer, useRef } from \"react\";\nimport type { TimeFormatOptions, TimeFormatValues } from \"./types\";\nimport { formatTime } from \"./format-time\";\nimport {\n collapseTrailingSeparator,\n parseTime,\n digitsBeforeCaret,\n resolveCaret,\n} from \"./parse-time\";\nimport { isInputFocused, setInputSelection } from \"../utils/input\";\n\nexport interface TimeFormatProps\n extends TimeFormatOptions,\n Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"onChange\"> {\n value?: string;\n onValueChange?: (values: TimeFormatValues) => void;\n}\n\nexport const TimeFormat = forwardRef<HTMLInputElement, TimeFormatProps>(function TimeFormat(\n props,\n forwardedRef\n) {\n const { value = \"\", onValueChange, separator, ...domProps } = props;\n const options: TimeFormatOptions = { separator };\n\n const innerRef = useRef<HTMLInputElement | null>(null);\n const caretRef = useRef<number | null>(null);\n const lastValuesRef = useRef<TimeFormatValues | null>(null);\n\n const [, forceUpdate] = useReducer((n: number) => n + 1, 0);\n\n const last = lastValuesRef.current;\n const formatted =\n last !== null && (value === last.value || value === last.formattedValue)\n ? last.formattedValue\n : formatTime(value ?? \"\", options);\n\n function handleChange(event: React.ChangeEvent<HTMLInputElement>): void {\n const el = event.target;\n const caret = el.selectionStart ?? el.value.length;\n const raw = collapseTrailingSeparator(el.value, formatted, options.separator ?? \":\");\n const digits = digitsBeforeCaret(raw, caret);\n const values = parseTime(raw, options);\n caretRef.current = resolveCaret(values, digits, options);\n lastValuesRef.current = values;\n forceUpdate();\n onValueChange?.(values);\n }\n\n useLayoutEffect(() => {\n const el = innerRef.current;\n if (el && caretRef.current !== null && isInputFocused(el)) {\n setInputSelection(el, caretRef.current, caretRef.current);\n caretRef.current = null;\n }\n });\n\n function setRef(el: HTMLInputElement | null): void {\n innerRef.current = el;\n if (typeof forwardedRef === \"function\") forwardedRef(el);\n else if (forwardedRef)\n (forwardedRef as React.MutableRefObject<HTMLInputElement | null>).current = el;\n }\n\n return <input {...domProps} ref={setRef} value={formatted} onChange={handleChange} />;\n});\n\nTimeFormat.displayName = \"TimeFormat\";\n","import type { TimeFormatOptions } from \"./types\";\n\nexport interface TimeParts {\n hours: string;\n minutes: string;\n}\n\nexport function tokenizeTime(raw: string | number): TimeParts {\n const digits = String(raw ?? \"\").replace(/\\D/g, \"\").slice(0, 4);\n let hours = digits.slice(0, 2);\n let minutes = digits.slice(2, 4);\n if (hours.length === 2) {\n hours = String(Math.min(parseInt(hours, 10), 23)).padStart(2, \"0\");\n }\n if (minutes.length === 2) {\n minutes = String(Math.min(parseInt(minutes, 10), 59)).padStart(2, \"0\");\n }\n return { hours, minutes };\n}\n\nexport function buildFormatted(parts: TimeParts, separator: string): string {\n const { hours, minutes } = parts;\n if (hours === \"\") return \"\";\n if (hours.length < 2) return hours;\n if (minutes === \"\") return hours + separator;\n return hours + separator + minutes;\n}\n\nexport function formatTime(input: string | number, options?: TimeFormatOptions): string {\n const separator = options?.separator ?? \":\";\n return buildFormatted(tokenizeTime(input), separator);\n}\n","import type { TimeFormatOptions, TimeFormatValues } from \"./types\";\nimport { tokenizeTime, buildFormatted } from \"./format-time\";\n\nexport function collapseTrailingSeparator(\n raw: string,\n previous: string,\n separator: string\n): string {\n return previous.endsWith(separator) && raw === previous.slice(0, -separator.length)\n ? raw.slice(0, -1)\n : raw;\n}\n\nexport function parseTime(formatted: string, options?: TimeFormatOptions): TimeFormatValues {\n const separator = options?.separator ?? \":\";\n const parts = tokenizeTime(formatted);\n const formattedValue = buildFormatted(parts, separator);\n const hours = parts.hours.length === 2 ? parseInt(parts.hours, 10) : undefined;\n const minutes = parts.minutes.length === 2 ? parseInt(parts.minutes, 10) : undefined;\n const value =\n hours !== undefined && minutes !== undefined ? `${parts.hours}:${parts.minutes}` : \"\";\n return { value, formattedValue, hours, minutes };\n}\n\nexport function digitsBeforeCaret(value: string, caret: number): number {\n let count = 0;\n for (let i = 0; i < caret && i < value.length; i++) {\n if (value[i] >= \"0\" && value[i] <= \"9\") count++;\n }\n return count;\n}\n\nfunction caretAfterDigits(formatted: string, digitsBefore: number): number {\n if (digitsBefore <= 0) {\n const first = formatted.search(/\\d/);\n return first === -1 ? formatted.length : first;\n }\n let seen = 0;\n for (let i = 0; i < formatted.length; i++) {\n if (formatted[i] >= \"0\" && formatted[i] <= \"9\") {\n seen++;\n if (seen === digitsBefore) return i + 1;\n }\n }\n return formatted.length;\n}\n\nexport function resolveCaret(\n values: TimeFormatValues,\n digitsBefore: number,\n options?: TimeFormatOptions\n): number {\n const separator = options?.separator ?? \":\";\n const caret = caretAfterDigits(values.formattedValue, digitsBefore);\n if (separator && values.formattedValue.slice(caret, caret + separator.length) === separator) {\n return caret + separator.length;\n }\n return caret;\n}\n","import { useRef, useCallback } from \"react\";\nimport type React from \"react\";\nimport type { TimeFormatValues } from \"./types\";\nimport type { TimeFormatOptions } from \"./types\";\nimport { formatTime } from \"./format-time\";\nimport {\n collapseTrailingSeparator,\n parseTime,\n digitsBeforeCaret,\n resolveCaret,\n} from \"./parse-time\";\nimport { setNativeValue } from \"../set-native-value\";\nimport { isInputFocused, setInputSelection } from \"../utils/input\";\n\nexport interface UseTimeFormatOptions extends TimeFormatOptions {\n onValueChange?: (values: TimeFormatValues) => void;\n}\n\nconst IS_JSDOM =\n typeof navigator !== \"undefined\" && navigator.userAgent.includes(\"jsdom\");\n\nexport function useTimeFormat(\n options: UseTimeFormatOptions\n): React.RefCallback<HTMLInputElement> {\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const inputRef = useRef<HTMLInputElement | null>(null);\n const handlerRef = useRef<((event: Event) => void) | null>(null);\n\n return useCallback((el: HTMLInputElement | null) => {\n if (el) {\n let previous = el.value;\n const handler = (): void => {\n const opts = optionsRef.current;\n const caret = el.selectionStart ?? el.value.length;\n const raw = collapseTrailingSeparator(el.value, previous, opts.separator ?? \":\");\n const digits = digitsBeforeCaret(raw, caret);\n const values = parseTime(raw, opts);\n setNativeValue(el, values.formattedValue);\n previous = values.formattedValue;\n if (isInputFocused(el)) {\n const next = resolveCaret(values, digits, opts);\n setInputSelection(el, next, next);\n }\n // JSDOM-ONLY: resync @testing-library/user-event v14's shadow-value cache.\n // setNativeValue above bypasses React's value tracker; under jsdom that\n // leaves user-event's per-input shadow value stale, corrupting masking from\n // ~the 4th keystroke. A plain (idempotent) reassignment resyncs it.\n // MUST NOT run in a real browser: there, microtasks drain BETWEEN event\n // listeners, so this write would fight React; real browsers read the live\n // DOM and need no resync. onValueChange (below) is called directly, not via\n // React's tracker, so it is unaffected in either environment.\n if (IS_JSDOM) {\n queueMicrotask(() => {\n if (el.value !== values.formattedValue) return;\n el.value = values.formattedValue;\n if (isInputFocused(el)) {\n const c = resolveCaret(values, digits, opts);\n setInputSelection(el, c, c);\n }\n });\n }\n opts.onValueChange?.(values);\n };\n el.addEventListener(\"input\", handler);\n handlerRef.current = handler;\n inputRef.current = el;\n if (el.value) {\n setNativeValue(el, formatTime(el.value, optionsRef.current));\n previous = formatTime(el.value, optionsRef.current);\n }\n } else {\n if (inputRef.current && handlerRef.current) {\n inputRef.current.removeEventListener(\"input\", handlerRef.current);\n }\n inputRef.current = null;\n handlerRef.current = null;\n }\n }, []);\n}\n"],"mappings":"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","names":["TimeFormat","useRef","useRef"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-input-mask-format",
3
3
  "description": "Masked input component for React",
4
- "version": "2.2.0",
4
+ "version": "2.3.0",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Temirtator/react-input-mask-format",
7
7
  "license": "MIT",
@@ -62,6 +62,10 @@
62
62
  "import": { "types": "./dist/number.d.ts", "default": "./dist/number.js" },
63
63
  "require": { "types": "./dist/number.d.cts", "default": "./dist/number.cjs" }
64
64
  },
65
+ "./time": {
66
+ "import": { "types": "./dist/time.d.ts", "default": "./dist/time.js" },
67
+ "require": { "types": "./dist/time.d.cts", "default": "./dist/time.cjs" }
68
+ },
65
69
  "./package.json": "./package.json"
66
70
  },
67
71
  "sideEffects": false,
@@ -72,7 +76,8 @@
72
76
  { "path": "dist/index.js", "limit": "6 KB" },
73
77
  { "path": "dist/presets.js", "limit": "2 KB" },
74
78
  { "path": "dist/validators.js", "limit": "2 KB" },
75
- { "path": "dist/number.js", "limit": "2.5 KB" }
79
+ { "path": "dist/number.js", "limit": "2.5 KB" },
80
+ { "path": "dist/time.js", "limit": "2 KB" }
76
81
  ],
77
82
  "scripts": {
78
83
  "build": "tsup",