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
@@ -0,0 +1,155 @@
1
+ import inputmask from 'inputmask';
2
+ import {
3
+ beforeEach,
4
+ describe, expect, it, vi,
5
+ } from 'vitest';
6
+
7
+ import withHookFormMask from './withHookFormMask';
8
+
9
+ import type { RefCallback } from 'react';
10
+ import type { FieldValues, UseFormRegisterReturn } from 'react-hook-form';
11
+
12
+ import type { UseHookFormMaskReturn } from '../types';
13
+
14
+ vi.mock('inputmask', () => ({
15
+ default: vi.fn((options) => ({
16
+ mask: vi.fn(),
17
+ options,
18
+ })),
19
+ }));
20
+
21
+ describe('withHookFormMask', () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+
26
+ it('returns register object with masked ref', () => {
27
+ const originalRef = vi.fn();
28
+ const register: UseHookFormMaskReturn<FieldValues> = {
29
+ prevRef: vi.fn(),
30
+ ref: originalRef,
31
+ onChange: vi.fn(),
32
+ onBlur: vi.fn(),
33
+ name: 'phone',
34
+ };
35
+ const maskFn = vi.fn();
36
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
37
+
38
+ const result = withHookFormMask(register, '999-999');
39
+
40
+ expect(result.ref).toBeDefined();
41
+ expect(typeof result.ref).toBe('function');
42
+ expect(result.onChange).toBe(register.onChange);
43
+ expect(result.onBlur).toBe(register.onBlur);
44
+ expect(result.name).toBe(register.name);
45
+ });
46
+
47
+ it('applies mask when ref is called', () => {
48
+ const input = document.createElement('input');
49
+ const originalRef = vi.fn();
50
+ const register: UseHookFormMaskReturn<FieldValues> = {
51
+ prevRef: vi.fn(),
52
+ ref: originalRef,
53
+ onChange: vi.fn(),
54
+ onBlur: vi.fn(),
55
+ name: 'phone',
56
+ };
57
+ const maskFn = vi.fn();
58
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
59
+
60
+ const result = withHookFormMask(register, '999-999');
61
+ result.ref?.(input);
62
+
63
+ expect(maskFn).toHaveBeenCalled();
64
+ });
65
+
66
+ it('calls original ref after applying mask', () => {
67
+ const input = document.createElement('input');
68
+ const originalRef = vi.fn();
69
+ const register: UseHookFormMaskReturn<FieldValues> = {
70
+ ref: originalRef,
71
+ prevRef: vi.fn(),
72
+ onChange: vi.fn(),
73
+ onBlur: vi.fn(),
74
+ name: 'phone',
75
+ };
76
+ const maskFn = vi.fn();
77
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
78
+
79
+ const result = withHookFormMask(register, '999-999');
80
+ result.ref?.(input);
81
+
82
+ expect(originalRef).toHaveBeenCalled();
83
+ });
84
+
85
+ it('works with alias masks', () => {
86
+ const input = document.createElement('input');
87
+ const originalRef = vi.fn();
88
+ const register: UseHookFormMaskReturn<FieldValues> = {
89
+ ref: originalRef,
90
+ prevRef: vi.fn(),
91
+ onChange: vi.fn(),
92
+ onBlur: vi.fn(),
93
+ name: 'cpf',
94
+ };
95
+ const maskFn = vi.fn();
96
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
97
+
98
+ const result = withHookFormMask(register, 'cpf');
99
+ result.ref?.(input);
100
+
101
+ expect(maskFn).toHaveBeenCalled();
102
+ });
103
+
104
+ it('works with custom options', () => {
105
+ const input = document.createElement('input');
106
+ const originalRef = vi.fn();
107
+ const register: UseHookFormMaskReturn<FieldValues> = {
108
+ prevRef: vi.fn(),
109
+ ref: originalRef,
110
+ onChange: vi.fn(),
111
+ onBlur: vi.fn(),
112
+ name: 'phone',
113
+ };
114
+ const maskFn = vi.fn();
115
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
116
+
117
+ const result = withHookFormMask(register, '999-999', { placeholder: '_' });
118
+ result.ref?.(input);
119
+
120
+ expect(maskFn).toHaveBeenCalled();
121
+ });
122
+
123
+ it('handles null ref gracefully', () => {
124
+ const register: UseHookFormMaskReturn<FieldValues> = {
125
+ prevRef: null as unknown as RefCallback<HTMLElement | null>,
126
+ ref: null as unknown as RefCallback<HTMLElement | null>,
127
+ onChange: vi.fn(),
128
+ onBlur: vi.fn(),
129
+ name: 'phone',
130
+ };
131
+
132
+ const result = withHookFormMask(register, '999-999');
133
+ expect(result.ref).toBeNull();
134
+ expect(result.onChange).toBe(register.onChange);
135
+ expect(result.onBlur).toBe(register.onBlur);
136
+ });
137
+
138
+ it('handles null input in ref callback', () => {
139
+ const originalRef = vi.fn();
140
+ const register: UseHookFormMaskReturn<FieldValues> = {
141
+ prevRef: vi.fn(),
142
+ ref: originalRef,
143
+ onChange: vi.fn(),
144
+ onBlur: vi.fn(),
145
+ name: 'phone',
146
+ };
147
+ const maskFn = vi.fn();
148
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
149
+
150
+ const result = withHookFormMask(register, '999-999');
151
+ result.ref?.(null as unknown as HTMLElement);
152
+
153
+ expect(maskFn).not.toHaveBeenCalled();
154
+ });
155
+ });
@@ -0,0 +1,54 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { applyMaskToElement } from '../core';
3
+ import { flow } from '../utils';
4
+
5
+ import type { RefCallback } from 'react';
6
+ import type { FieldValues } from 'react-hook-form';
7
+
8
+ import type {
9
+ Mask, Options, UseFormRegisterReturn, UseHookFormMaskReturn,
10
+ } from '../types';
11
+
12
+ /**
13
+ * Enhances a React Hook Form register return object with mask support.
14
+ * Takes an already registered field and adds mask to it.
15
+ * Useful when you registered the field before.
16
+ *
17
+ * @param register - The register return object from React Hook Form
18
+ * @param mask - The mask pattern to apply
19
+ * @param options - Optional mask configuration options
20
+ * @returns A new register return object with mask applied
21
+ */
22
+ export default function withHookFormMask(
23
+ register: UseFormRegisterReturn,
24
+ mask: Mask,
25
+ options?: Options,
26
+ ): UseHookFormMaskReturn<FieldValues> {
27
+ const { ref } = register as UseHookFormMaskReturn<FieldValues>;
28
+
29
+ const applyMaskToRef = (_ref: HTMLElement | null) => {
30
+ if (_ref) applyMaskToElement(_ref, mask, options);
31
+ return _ref;
32
+ };
33
+
34
+ const refWithMask = ref === null
35
+ ? null
36
+ : ref
37
+ ? flow(applyMaskToRef, ref)
38
+ : null;
39
+
40
+ const result = {
41
+ ...register,
42
+ ref: refWithMask as RefCallback<HTMLElement | null>,
43
+ } as UseHookFormMaskReturn<FieldValues>;
44
+
45
+ // change prevRef to non-enumerable
46
+ Object.defineProperty(result, 'prevRef', {
47
+ value: ref,
48
+ enumerable: false,
49
+ writable: true,
50
+ configurable: true,
51
+ });
52
+
53
+ return result;
54
+ }
@@ -0,0 +1,93 @@
1
+ import inputmask from 'inputmask';
2
+ import {
3
+ beforeEach, describe, expect, it, vi,
4
+ } from 'vitest';
5
+
6
+ import withMask from './withMask';
7
+
8
+ vi.mock('inputmask', () => ({
9
+ default: vi.fn((options) => ({
10
+ mask: vi.fn(),
11
+ options,
12
+ })),
13
+ }));
14
+
15
+ vi.mock('../utils/isServer', () => ({
16
+ default: false,
17
+ }));
18
+
19
+ describe('withMask', () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ it('returns a function', () => {
25
+ const result = withMask('999-999');
26
+ expect(typeof result).toBe('function');
27
+ });
28
+
29
+ it('applies mask to input element', () => {
30
+ const input = document.createElement('input');
31
+ const maskFn = vi.fn();
32
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
33
+
34
+ const refCallback = withMask('999-999');
35
+ refCallback(input);
36
+
37
+ expect(maskFn).toHaveBeenCalledWith(input);
38
+ });
39
+
40
+ it('does nothing if input is null', () => {
41
+ const maskFn = vi.fn();
42
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
43
+
44
+ const refCallback = withMask('999-999');
45
+ refCallback(null);
46
+
47
+ expect(maskFn).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it('does nothing if mask is null', () => {
51
+ const input = document.createElement('input');
52
+ const maskFn = vi.fn();
53
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
54
+
55
+ const refCallback = withMask(null);
56
+ refCallback(input);
57
+
58
+ expect(maskFn).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('applies mask with custom options', () => {
62
+ const input = document.createElement('input');
63
+ const maskFn = vi.fn();
64
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
65
+
66
+ const refCallback = withMask('999-999', { placeholder: '_' });
67
+ refCallback(input);
68
+
69
+ expect(maskFn).toHaveBeenCalledWith(input);
70
+ });
71
+
72
+ it('works with alias masks', () => {
73
+ const input = document.createElement('input');
74
+ const maskFn = vi.fn();
75
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
76
+
77
+ const refCallback = withMask('cpf');
78
+ refCallback(input);
79
+
80
+ expect(maskFn).toHaveBeenCalledWith(input);
81
+ });
82
+
83
+ it('works with array masks', () => {
84
+ const input = document.createElement('input');
85
+ const maskFn = vi.fn();
86
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as unknown as Inputmask.Instance);
87
+
88
+ const refCallback = withMask(['999-999', '9999-9999']);
89
+ refCallback(input);
90
+
91
+ expect(maskFn).toHaveBeenCalledWith(input);
92
+ });
93
+ });
@@ -0,0 +1,25 @@
1
+ /* eslint-disable import-x/no-extraneous-dependencies */
2
+ import inputmask from 'inputmask';
3
+
4
+ import { getMaskOptions } from '../core/maskConfig';
5
+ import isServer from '../utils/isServer';
6
+ import interopDefaultSync from '../utils/moduleInterop';
7
+
8
+ import type { Input, Mask, Options } from '../types';
9
+
10
+ /**
11
+ * Higher-order function that creates a ref callback for applying input masks.
12
+ * Simple function to apply mask via ref. No hooks, no drama.
13
+ *
14
+ * @param mask - The mask pattern to apply
15
+ * @param options - Optional mask configuration options
16
+ * @returns A ref callback function that applies the mask
17
+ */
18
+ export default function withMask(mask: Mask, options?: Options) {
19
+ return (input: Input | null): void => {
20
+ if (isServer || mask === null || !input) return;
21
+
22
+ const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));
23
+ maskInput.mask(input as HTMLElement);
24
+ };
25
+ }
@@ -0,0 +1,175 @@
1
+ import {
2
+ describe, expect, it, vi,
3
+ } from 'vitest';
4
+
5
+ import { findInputElement, isHTMLElement, resolveInputRef } from './elementResolver';
6
+
7
+ import type { Input } from '..';
8
+
9
+ describe('elementResolver', () => {
10
+ describe('isHTMLElement', () => {
11
+ it('returns true for valid HTMLElement', () => {
12
+ const element = document.createElement('div');
13
+ expect(isHTMLElement(element)).toBe(true);
14
+ });
15
+
16
+ it('returns false for null', () => {
17
+ expect(isHTMLElement(null)).toBe(false);
18
+ });
19
+
20
+ it('returns false for undefined', () => {
21
+ expect(isHTMLElement(undefined)).toBe(false);
22
+ });
23
+
24
+ it('returns false for string', () => {
25
+ expect(isHTMLElement('string')).toBe(false);
26
+ });
27
+
28
+ it('returns false for number', () => {
29
+ expect(isHTMLElement(123)).toBe(false);
30
+ });
31
+
32
+ it('returns false for object without nodeType', () => {
33
+ expect(isHTMLElement({})).toBe(false);
34
+ });
35
+
36
+ it('returns false for object without querySelector', () => {
37
+ expect(isHTMLElement({ nodeType: 1 })).toBe(false);
38
+ });
39
+
40
+ it('returns false for object with non-function querySelector', () => {
41
+ expect(isHTMLElement({ nodeType: 1, querySelector: 'not a function' })).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('findInputElement', () => {
46
+ it('returns null for null', () => {
47
+ expect(findInputElement(null)).toBe(null);
48
+ });
49
+
50
+ it('returns null for undefined', () => {
51
+ expect(findInputElement(undefined)).toBe(null);
52
+ });
53
+
54
+ it('returns input element directly if it is an INPUT', () => {
55
+ const input = document.createElement('input');
56
+ expect(findInputElement(input)).toBe(input);
57
+ });
58
+
59
+ it('returns textarea element directly if it is a TEXTAREA', () => {
60
+ const textarea = document.createElement('textarea');
61
+ expect(findInputElement(textarea)).toBe(textarea);
62
+ });
63
+
64
+ it('finds input inside wrapper element', () => {
65
+ const wrapper = document.createElement('div');
66
+ const input = document.createElement('input');
67
+ wrapper.appendChild(input);
68
+ expect(findInputElement(wrapper)).toBe(input);
69
+ });
70
+
71
+ it('finds textarea inside wrapper element', () => {
72
+ const wrapper = document.createElement('div');
73
+ const textarea = document.createElement('textarea');
74
+ wrapper.appendChild(textarea);
75
+ expect(findInputElement(wrapper)).toBe(textarea);
76
+ });
77
+
78
+ it('returns null if no input found inside wrapper', () => {
79
+ const wrapper = document.createElement('div');
80
+ const span = document.createElement('span');
81
+ wrapper.appendChild(span);
82
+ expect(findInputElement(wrapper)).toBe(null);
83
+ });
84
+
85
+ it('returns null for invalid element', () => {
86
+ expect(findInputElement('not an element')).toBe(null);
87
+ });
88
+
89
+ it('handles querySelector error gracefully', () => {
90
+ const element = {
91
+ nodeType: 1,
92
+ nodeName: 'DIV',
93
+ querySelector: vi.fn(() => {
94
+ throw new Error('querySelector error');
95
+ }),
96
+ };
97
+ expect(findInputElement(element)).toBe(null);
98
+ });
99
+
100
+ it('handles element without querySelector method', () => {
101
+ const element = {
102
+ nodeType: 1,
103
+ nodeName: 'DIV',
104
+ };
105
+ expect(findInputElement(element)).toBe(null);
106
+ });
107
+
108
+ it('handles element with non-function querySelector', () => {
109
+ const element = {
110
+ nodeType: 1,
111
+ nodeName: 'DIV',
112
+ querySelector: 'not a function',
113
+ };
114
+ expect(findInputElement(element)).toBe(null);
115
+ });
116
+
117
+ it('handles element where querySelector is not in element', () => {
118
+ const element = {
119
+ nodeType: 1,
120
+ nodeName: 'DIV',
121
+ };
122
+ // element doesn't have querySelector property
123
+ expect(findInputElement(element)).toBe(null);
124
+ });
125
+
126
+ it('handles querySelector returning null', () => {
127
+ const element = {
128
+ nodeType: 1,
129
+ nodeName: 'DIV',
130
+ querySelector: vi.fn(() => null),
131
+ };
132
+ expect(findInputElement(element)).toBe(null);
133
+ });
134
+
135
+ it('handles querySelector returning non-HTMLElement', () => {
136
+ const element = {
137
+ nodeType: 1,
138
+ nodeName: 'DIV',
139
+ querySelector: vi.fn(() => 'not an element'),
140
+ };
141
+ expect(findInputElement(element)).toBe(null);
142
+ });
143
+ });
144
+
145
+ describe('resolveInputRef', () => {
146
+ it('returns null for null', () => {
147
+ expect(resolveInputRef(null)).toBe(null);
148
+ });
149
+
150
+ it('returns element for direct HTMLElement', () => {
151
+ const input = document.createElement('input');
152
+ expect(resolveInputRef(input)).toBe(input);
153
+ });
154
+
155
+ it('returns element from ref object with current', () => {
156
+ const input = document.createElement('input');
157
+ const ref = { current: input };
158
+ expect(resolveInputRef(ref as unknown as Input)).toBe(input);
159
+ });
160
+
161
+ it('returns null for ref object with null current', () => {
162
+ const ref = { current: null } as unknown as Input;
163
+ expect(resolveInputRef(ref)).toBe(null);
164
+ });
165
+
166
+ it('returns null for ref object with invalid current', () => {
167
+ const ref = { current: 'not an element' } as unknown as Input;
168
+ expect(resolveInputRef(ref)).toBe(null);
169
+ });
170
+
171
+ it('returns null for invalid input type', () => {
172
+ expect(resolveInputRef('not an element' as unknown as Input)).toBe(null);
173
+ });
174
+ });
175
+ });
@@ -0,0 +1,84 @@
1
+ import type { Input } from '../types';
2
+
3
+ /**
4
+ * Checks if an element is a valid DOM element (or at least looks like one).
5
+ *
6
+ * @param element - The element to check
7
+ * @returns True if it's a valid HTMLElement
8
+ */
9
+ export function isHTMLElement(element: unknown): element is HTMLElement {
10
+ return (
11
+ element !== null
12
+ && typeof element === 'object'
13
+ && 'nodeType' in element
14
+ && 'querySelector' in element
15
+ && typeof (element as HTMLElement).querySelector === 'function'
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Finds the actual input element from various component structures.
21
+ * Finds the actual input inside wrappers (ant design, etc). Like a detective.
22
+ *
23
+ * @param element - The element to search in
24
+ * @returns The found input element or null
25
+ */
26
+ export function findInputElement(element: unknown): HTMLElement | null {
27
+ if (!element) return null;
28
+
29
+ if (!isHTMLElement(element)) {
30
+ return null;
31
+ }
32
+
33
+ // if it's already an input or textarea, return it directly
34
+ if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
35
+ return element;
36
+ }
37
+
38
+ // tries to find input inside the wrapper
39
+ if (!('querySelector' in element) || typeof element.querySelector !== 'function') {
40
+ return null;
41
+ }
42
+
43
+ try {
44
+ const inputElement = element.querySelector('input') ?? element.querySelector('textarea');
45
+
46
+ if (inputElement && isHTMLElement(inputElement)) {
47
+ return inputElement;
48
+ }
49
+ } catch {
50
+ // if it errors, return null and move on
51
+ return null;
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Resolves React refs to a valid HTMLInputElement.
59
+ * Handles ref objects and direct DOM elements.
60
+ *
61
+ * @param input - The input reference to resolve
62
+ * @returns A valid HTMLInputElement or null
63
+ */
64
+ export function resolveInputRef(input: Input | null): HTMLInputElement | null {
65
+ if (!input) {
66
+ return null;
67
+ }
68
+
69
+ // react ref objects
70
+ if (typeof input === 'object' && 'current' in input) {
71
+ const refValue = (input as { current: HTMLElement | null }).current;
72
+ if (isHTMLElement(refValue)) {
73
+ return refValue as HTMLInputElement;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ // direct dom elements
79
+ if (isHTMLElement(input)) {
80
+ return input as HTMLInputElement;
81
+ }
82
+
83
+ return null;
84
+ }
@@ -0,0 +1,3 @@
1
+ export * from './elementResolver';
2
+ export * from './maskEngine';
3
+ export * from './maskConfig';