use-mask-input 3.6.0 → 3.6.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +47 -76
  2. package/README.md +2 -251
  3. package/dist/index.cjs +157 -84
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +52 -11
  6. package/dist/index.d.ts +52 -11
  7. package/dist/index.js +158 -85
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -21
  10. package/src/api/index.ts +4 -0
  11. package/src/api/useHookFormMask.spec.ts +146 -0
  12. package/src/api/useHookFormMask.ts +56 -0
  13. package/src/api/useMaskInput-server.spec.tsx +30 -0
  14. package/src/api/useMaskInput.spec.tsx +220 -0
  15. package/src/api/useMaskInput.ts +64 -0
  16. package/src/api/withHookFormMask.spec.ts +155 -0
  17. package/src/api/withHookFormMask.ts +54 -0
  18. package/src/api/withMask.spec.ts +93 -0
  19. package/src/api/withMask.ts +25 -0
  20. package/src/core/elementResolver.spec.ts +175 -0
  21. package/src/core/elementResolver.ts +84 -0
  22. package/src/core/index.ts +3 -0
  23. package/src/core/maskConfig.spec.ts +183 -0
  24. package/src/{utils/getMaskOptions.ts → core/maskConfig.ts} +12 -3
  25. package/src/core/maskEngine.spec.ts +108 -0
  26. package/src/core/maskEngine.ts +47 -0
  27. package/src/index.tsx +12 -5
  28. package/src/{types.ts → types/index.ts} +13 -0
  29. package/src/utils/flow.spec.ts +27 -30
  30. package/src/utils/flow.ts +2 -2
  31. package/src/utils/index.ts +1 -1
  32. package/src/utils/isServer.spec.ts +15 -0
  33. package/src/utils/moduleInterop.spec.ts +37 -0
  34. package/src/useHookFormMask.ts +0 -47
  35. package/src/useMaskInput.ts +0 -41
  36. package/src/utils/getMaskOptions.spec.ts +0 -126
  37. package/src/withHookFormMask.ts +0 -34
  38. package/src/withMask.ts +0 -18
  39. /package/src/{inputmask.types.ts → types/inputmask.types.ts} +0 -0
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "use-mask-input",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
+ "private": false,
4
5
  "description": "A react Hook for build elegant input masks. Compatible with React Hook Form",
5
6
  "author": "Eduardo Borges<euduardoborges@gmail.com>",
6
7
  "type": "module",
7
- "repository": "https://github.com/eduardoborges/use-mask-input",
8
+ "repository": {
9
+ "url": "https://github.com/eduardoborges/use-mask-input"
10
+ },
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "source": "./src/index.tsx",
8
15
  "exports": {
9
16
  ".": {
10
17
  "types": "./dist/index.d.ts",
@@ -16,13 +23,6 @@
16
23
  "node": ">=16",
17
24
  "npm": ">=7"
18
25
  },
19
- "scripts": {
20
- "build": "./scripts.sh build",
21
- "dev": "./scripts.sh dev",
22
- "lint": "./scripts.sh lint",
23
- "test": "./scripts.sh test",
24
- "prepare": "./scripts.sh prepare"
25
- },
26
26
  "files": [
27
27
  "dist",
28
28
  "src",
@@ -36,13 +36,9 @@
36
36
  "devDependencies": {
37
37
  "@eslint/compat": "^1.3.2",
38
38
  "@eslint/js": "^9.35.0",
39
- "@semantic-release/changelog": "6.0.3",
40
- "@semantic-release/commit-analyzer": "13.0.1",
41
- "@semantic-release/git": "10.0.1",
42
- "@semantic-release/github": "11.0.5",
43
- "@semantic-release/npm": "12.0.2",
44
- "@semantic-release/release-notes-generator": "14.0.3",
45
39
  "@stylistic/eslint-plugin": "3.1.0",
40
+ "@testing-library/dom": "^10.4.0",
41
+ "@testing-library/react": "^16.1.0",
46
42
  "@types/inputmask": "5.0.7",
47
43
  "@types/node": "22",
48
44
  "@types/react": ">=17",
@@ -56,16 +52,20 @@
56
52
  "eslint-plugin-react": "^7.37.5",
57
53
  "eslint-plugin-react-hooks": "^5.2.0",
58
54
  "inputmask": "5.0.10-beta.61",
55
+ "jsdom": "^25.0.1",
59
56
  "react-hook-form": "7.62.0",
60
- "read-pkg": "9.0.1",
61
- "semantic-release": "24.2.7",
62
- "simple-git-hooks": "2.13.1",
63
57
  "tsup": "8.5.0",
64
58
  "typescript": "5.1",
65
59
  "typescript-eslint": "^8.42.0",
66
60
  "vitest": "3.2.4"
67
61
  },
68
- "simple-git-hooks": {
69
- "pre-commit": "npm run lint && npm run test && npm run build"
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "lint": "eslint ./src --ext ts,tsx",
66
+ "test": "vitest --dir ./src --run --coverage",
67
+ "type-check": "tsc --noEmit",
68
+ "clean": "rm -rf dist",
69
+ "postbuild": "cp ../../README.md README.md"
70
70
  }
71
- }
71
+ }
@@ -0,0 +1,4 @@
1
+ export { default as useMaskInput } from './useMaskInput';
2
+ export { default as useHookFormMask } from './useHookFormMask';
3
+ export { default as withMask } from './withMask';
4
+ export { default as withHookFormMask } from './withHookFormMask';
@@ -0,0 +1,146 @@
1
+ import inputmask from 'inputmask';
2
+ import {
3
+ beforeEach,
4
+ describe, expect, it, vi,
5
+ } from 'vitest';
6
+
7
+ import useHookFormMask from './useHookFormMask';
8
+
9
+ import type { FieldValues, UseFormRegister } from 'react-hook-form';
10
+
11
+ vi.mock('inputmask', () => ({
12
+ default: vi.fn((options) => ({
13
+ mask: vi.fn(),
14
+ options,
15
+ })),
16
+ }));
17
+
18
+ describe('useHookFormMask', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it('returns a function', () => {
24
+ const registerFn = vi.fn(() => ({
25
+ ref: vi.fn(),
26
+ prevRef: vi.fn(),
27
+ onChange: vi.fn(),
28
+ onBlur: vi.fn(),
29
+ name: 'test',
30
+ }));
31
+
32
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
33
+ expect(typeof maskedRegister).toBe('function');
34
+ });
35
+
36
+ it('registers field with mask', () => {
37
+ const input = document.createElement('input');
38
+ const refCallback = vi.fn();
39
+ const registerFn = vi.fn(() => ({
40
+ ref: refCallback,
41
+ prevRef: vi.fn(),
42
+ onChange: vi.fn(),
43
+ onBlur: vi.fn(),
44
+ name: 'phone',
45
+ }));
46
+ const maskFn = vi.fn();
47
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
48
+
49
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
50
+ const result = maskedRegister('phone', '999-999');
51
+
52
+ expect(registerFn).toHaveBeenCalledWith('phone', undefined);
53
+ expect(result.ref).toBeDefined();
54
+ expect(typeof result.ref).toBe('function');
55
+
56
+ // call the ref callback
57
+ result.ref?.(input);
58
+
59
+ expect(maskFn).toHaveBeenCalled();
60
+ });
61
+
62
+ it('merges register options with mask options', () => {
63
+ const registerFn = vi.fn(() => ({
64
+ ref: vi.fn(),
65
+ prevRef: vi.fn(),
66
+ onChange: vi.fn(),
67
+ onBlur: vi.fn(),
68
+ name: 'phone',
69
+ }));
70
+ const maskFn = vi.fn();
71
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
72
+
73
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
74
+ maskedRegister('phone', '999-999', { required: true });
75
+
76
+ expect(registerFn).toHaveBeenCalledWith('phone', { required: true });
77
+ });
78
+
79
+ it('works with alias masks', () => {
80
+ const registerFn = vi.fn(() => ({
81
+ ref: vi.fn(),
82
+ prevRef: vi.fn(),
83
+ onChange: vi.fn(),
84
+ onBlur: vi.fn(),
85
+ name: 'cpf',
86
+ }));
87
+ const maskFn = vi.fn();
88
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
89
+
90
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
91
+ const result = maskedRegister('cpf', 'cpf');
92
+
93
+ expect(result.ref).toBeDefined();
94
+ });
95
+
96
+ it('works with array masks', () => {
97
+ const registerFn = vi.fn(() => ({
98
+ ref: vi.fn(),
99
+ prevRef: vi.fn(),
100
+ onChange: vi.fn(),
101
+ onBlur: vi.fn(),
102
+ name: 'phone',
103
+ }));
104
+ const maskFn = vi.fn();
105
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
106
+
107
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
108
+ const result = maskedRegister('phone', ['999-999', '9999-9999']);
109
+
110
+ expect(result.ref).toBeDefined();
111
+ });
112
+
113
+ it('preserves all register return properties', () => {
114
+ const onChange = vi.fn();
115
+ const onBlur = vi.fn();
116
+ const registerFn = vi.fn(() => ({
117
+ ref: vi.fn(),
118
+ prevRef: vi.fn(),
119
+ onChange,
120
+ onBlur,
121
+ name: 'phone',
122
+ }));
123
+
124
+ const maskedRegister = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
125
+ const result = maskedRegister('phone', '999-999');
126
+
127
+ expect(result.onChange).toBe(onChange);
128
+ expect(result.onBlur).toBe(onBlur);
129
+ expect(result.name).toBe('phone');
130
+ });
131
+
132
+ it('handles null ref from register', () => {
133
+ const registerFn = vi.fn(() => ({
134
+ ref: undefined,
135
+ prevRef: vi.fn(),
136
+ onChange: vi.fn(),
137
+ onBlur: vi.fn(),
138
+ name: 'phone',
139
+ }));
140
+
141
+ const maskedRegister = useHookFormMask(registerFn as unknown as UseFormRegister<FieldValues>);
142
+ const result = maskedRegister('phone', '999-999');
143
+
144
+ expect(result.ref).toBeDefined();
145
+ });
146
+ });
@@ -0,0 +1,56 @@
1
+ import { applyMaskToElement } from '../core';
2
+ import { flow } from '../utils';
3
+
4
+ import type { RefCallback } from 'react';
5
+ import type {
6
+ FieldValues, Path,
7
+ RegisterOptions,
8
+ UseFormRegister,
9
+ } from 'react-hook-form';
10
+
11
+ import type { Mask, Options, UseHookFormMaskReturn } from '../types';
12
+
13
+ /**
14
+ * Creates a masked version of React Hook Form's register function.
15
+ * Takes react-hook-form's register and adds automatic masking. Like an upgrade.
16
+ *
17
+ * @template T - The form data type
18
+ * @template D - The register options type
19
+ * @param registerFn - The register function from useForm hook
20
+ * @returns A function that registers a field with mask support
21
+ */
22
+ export default function useHookFormMask<
23
+ T extends FieldValues, D extends RegisterOptions,
24
+ >(registerFn: UseFormRegister<T>) {
25
+ return (fieldName: Path<T>, mask: Mask, options?: (
26
+ D & Options) | Options | D): UseHookFormMaskReturn<T> => {
27
+ if (!registerFn) throw new Error('registerFn is required');
28
+
29
+ const registerReturn = registerFn(fieldName, options as Options);
30
+ const { ref } = registerReturn as UseHookFormMaskReturn<T>;
31
+
32
+ const applyMaskToRef = (_ref: HTMLElement | null) => {
33
+ if (_ref) applyMaskToElement(_ref, mask, options as Options);
34
+ return _ref;
35
+ };
36
+
37
+ const refWithMask = ref
38
+ ? flow(applyMaskToRef, ref)
39
+ : applyMaskToRef;
40
+
41
+ const result = {
42
+ ...registerReturn,
43
+ ref: refWithMask as RefCallback<HTMLElement | null>,
44
+ } as UseHookFormMaskReturn<T>;
45
+
46
+ // change prevRef to non-enumerable
47
+ Object.defineProperty(result, 'prevRef', {
48
+ value: ref,
49
+ enumerable: false,
50
+ writable: true,
51
+ configurable: true,
52
+ });
53
+
54
+ return result;
55
+ };
56
+ }
@@ -0,0 +1,30 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import {
3
+ beforeEach,
4
+ describe, expect, it, vi,
5
+ } from 'vitest';
6
+
7
+ vi.mock('../utils/isServer', () => ({
8
+ default: true,
9
+ }));
10
+
11
+ describe('useMaskInput server-side', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ vi.resetModules();
15
+ });
16
+
17
+ it('returns no-op function on server', async () => {
18
+ const { default: useMaskInput } = await import('./useMaskInput');
19
+ const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
20
+
21
+ expect(typeof result.current).toBe('function');
22
+
23
+ act(() => {
24
+ result.current(document.createElement('input'));
25
+ });
26
+
27
+ // should do nothing on server
28
+ expect(result.current).toBeDefined();
29
+ });
30
+ });
@@ -0,0 +1,220 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import inputmask from 'inputmask';
3
+ import {
4
+ beforeEach,
5
+ describe, expect, it, vi,
6
+ } from 'vitest';
7
+
8
+ import useMaskInput from './useMaskInput';
9
+ import * as core from '../core';
10
+
11
+ import type { Input } from '../types';
12
+
13
+ vi.mock('inputmask', () => ({
14
+ default: vi.fn((options) => ({
15
+ mask: vi.fn(),
16
+ options,
17
+ })),
18
+ }));
19
+
20
+ vi.mock('../utils/isServer', () => ({
21
+ default: false,
22
+ }));
23
+
24
+ describe('useMaskInput', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ it('returns a ref callback function', () => {
30
+ const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
31
+ expect(typeof result.current).toBe('function');
32
+ });
33
+
34
+ it('handles null input', () => {
35
+ const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
36
+
37
+ act(() => {
38
+ result.current(null);
39
+ });
40
+
41
+ expect(result.current).toBeDefined();
42
+ });
43
+
44
+ it('handles direct input element', () => {
45
+ const input = document.createElement('input');
46
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
47
+
48
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
49
+
50
+ act(() => {
51
+ result.current(input);
52
+ });
53
+
54
+ rerender();
55
+
56
+ expect(inputmask).toHaveBeenCalled();
57
+ });
58
+
59
+ it('handles ref object', () => {
60
+ const input = document.createElement('input');
61
+ const ref = { current: input };
62
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
63
+
64
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
65
+
66
+ act(() => {
67
+ result.current(ref as unknown as Input);
68
+ });
69
+
70
+ rerender();
71
+
72
+ expect(inputmask).toHaveBeenCalled();
73
+ });
74
+
75
+ it('handles wrapper element with input inside', () => {
76
+ const wrapper = document.createElement('div');
77
+ const input = document.createElement('input');
78
+ wrapper.appendChild(input);
79
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
80
+
81
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
82
+
83
+ act(() => {
84
+ result.current(wrapper);
85
+ });
86
+
87
+ rerender();
88
+
89
+ expect(inputmask).toHaveBeenCalled();
90
+ });
91
+
92
+ it('handles invalid element in ref', () => {
93
+ const invalidRef = { current: 'not an element' };
94
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
95
+
96
+ const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
97
+
98
+ act(() => {
99
+ result.current(invalidRef as unknown as Input);
100
+ });
101
+
102
+ expect(result.current).toBeDefined();
103
+ });
104
+
105
+ it('handles element that is not HTMLElement in useEffect', () => {
106
+ vi.spyOn(core, 'isHTMLElement').mockReturnValueOnce(false);
107
+
108
+ const invalidElement = { nodeType: 1 } as unknown as Input;
109
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
110
+
111
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
112
+
113
+ act(() => {
114
+ result.current(invalidElement as unknown as Input);
115
+ });
116
+
117
+ rerender();
118
+
119
+ expect(result.current).toBeDefined();
120
+ });
121
+
122
+ it('handles wrapper without input inside', () => {
123
+ const wrapper = document.createElement('div');
124
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
125
+
126
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
127
+
128
+ act(() => {
129
+ result.current(wrapper);
130
+ });
131
+
132
+ rerender();
133
+
134
+ expect(inputmask).toHaveBeenCalled();
135
+ });
136
+
137
+ it('works with custom options', () => {
138
+ const input = document.createElement('input');
139
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
140
+
141
+ const { result, rerender } = renderHook(() => useMaskInput({
142
+ mask: '999-999',
143
+ options: { placeholder: '_' },
144
+ }));
145
+
146
+ act(() => {
147
+ result.current(input);
148
+ });
149
+
150
+ rerender();
151
+
152
+ expect(inputmask).toHaveBeenCalled();
153
+ });
154
+
155
+ it('works with alias masks', () => {
156
+ const input = document.createElement('input');
157
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
158
+
159
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: 'cpf' }));
160
+
161
+ act(() => {
162
+ result.current(input);
163
+ });
164
+
165
+ rerender();
166
+
167
+ expect(inputmask).toHaveBeenCalled();
168
+ });
169
+
170
+ it('calls register callback when provided', () => {
171
+ const input = document.createElement('input');
172
+ const register = vi.fn();
173
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
174
+
175
+ const { result, rerender } = renderHook(() => useMaskInput({
176
+ mask: '999-999',
177
+ register,
178
+ }));
179
+
180
+ act(() => {
181
+ result.current(input);
182
+ });
183
+
184
+ rerender();
185
+
186
+ expect(inputmask).toHaveBeenCalled();
187
+ });
188
+
189
+ it('handles textarea element', () => {
190
+ const textarea = document.createElement('textarea');
191
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
192
+
193
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
194
+
195
+ act(() => {
196
+ result.current(textarea);
197
+ });
198
+
199
+ rerender();
200
+
201
+ expect(inputmask).toHaveBeenCalled();
202
+ });
203
+
204
+ it('handles case where findInputElement returns valid element', () => {
205
+ const wrapper = document.createElement('div');
206
+ const input = document.createElement('input');
207
+ wrapper.appendChild(input);
208
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as unknown as Inputmask.Instance);
209
+
210
+ const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
211
+
212
+ act(() => {
213
+ result.current(wrapper);
214
+ });
215
+
216
+ rerender();
217
+
218
+ expect(inputmask).toHaveBeenCalled();
219
+ });
220
+ });
@@ -0,0 +1,64 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+
3
+ import {
4
+ createMaskInstance, findInputElement, isHTMLElement, resolveInputRef,
5
+ } from '../core';
6
+ import isServer from '../utils/isServer';
7
+
8
+ import type { Input, Mask, Options } from '../types';
9
+
10
+ interface UseMaskInputOptions {
11
+ mask: Mask;
12
+ register?: (element: HTMLElement) => void;
13
+ options?: Options;
14
+ }
15
+
16
+ /**
17
+ * React hook for applying input masks to form elements.
18
+ * Works with Ant Design and other wrapped components too.
19
+ *
20
+ * @param props - Configuration object
21
+ * @param props.mask - The mask pattern to apply
22
+ * @param props.register - Optional callback that receives the element
23
+ * @param props.options - Optional mask configuration options
24
+ * @returns A ref callback function to attach to the input element
25
+ */
26
+ export default function useMaskInput(props: UseMaskInputOptions): ((input: Input | null) => void) {
27
+ const { mask, register, options } = props;
28
+ const ref = useRef<HTMLInputElement | null>(null);
29
+ const maskInput = useMemo(() => createMaskInstance(mask, options), [mask, options]);
30
+
31
+ if (isServer) {
32
+ return (): void => {
33
+ // server doesn't have dom, so just do nothing
34
+ };
35
+ }
36
+
37
+ // eslint-disable-next-line react-hooks/rules-of-hooks
38
+ useEffect(() => {
39
+ if (isServer || !ref.current) return;
40
+
41
+ if (!isHTMLElement(ref.current)) {
42
+ return;
43
+ }
44
+
45
+ const inputElement = findInputElement(ref.current);
46
+
47
+ if (inputElement && isHTMLElement(inputElement)) {
48
+ maskInput.mask(inputElement);
49
+ }
50
+
51
+ if (register && isHTMLElement(ref.current)) {
52
+ register(ref.current);
53
+ }
54
+ }, [mask, register, options, maskInput, ref]);
55
+
56
+ return (input: Input | null): void => {
57
+ if (!input) {
58
+ ref.current = null;
59
+ return;
60
+ }
61
+
62
+ ref.current = resolveInputRef(input);
63
+ };
64
+ }