use-mask-input 3.7.4 → 3.9.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/CHANGELOG.md +34 -0
- package/README.md +105 -29
- package/dist/antd.cjs +9 -9
- package/dist/antd.d.cts +1 -1
- package/dist/antd.d.ts +1 -1
- package/dist/antd.js +1 -1
- package/dist/{chunk-QCWLMMDI.js → chunk-ICLWBMH4.js} +21 -2
- package/dist/{chunk-QCWLMMDI.js.map → chunk-ICLWBMH4.js.map} +1 -1
- package/dist/{chunk-VK6LQ75W.cjs → chunk-X5SEJVSB.cjs} +21 -2
- package/dist/{chunk-VK6LQ75W.cjs.map → chunk-X5SEJVSB.cjs.map} +1 -1
- package/dist/{index-F3rlTTTe.d.cts → index-BoaVtWUr.d.cts} +13 -4
- package/dist/{index-F3rlTTTe.d.ts → index-BoaVtWUr.d.ts} +13 -4
- package/dist/index.cjs +92 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +80 -15
- package/dist/index.js.map +1 -1
- package/package.json +15 -25
- package/src/antd/useHookFormMaskAntd.spec.ts +2 -0
- package/src/antd/useMaskInputAntd.spec.tsx +3 -3
- package/src/api/index.ts +2 -0
- package/src/api/useHookFormMask.spec.ts +52 -5
- package/src/api/useHookFormMask.ts +49 -9
- package/src/api/useMaskInput.spec.tsx +11 -11
- package/src/api/useTanStackFormMask.ts +24 -0
- package/src/api/withHookFormMask.spec.ts +7 -7
- package/src/api/withMask.spec.ts +6 -6
- package/src/api/withTanStackFormMask.spec.ts +76 -0
- package/src/api/withTanStackFormMask.ts +64 -0
- package/src/core/maskConfig.spec.ts +24 -0
- package/src/core/maskConfig.ts +14 -0
- package/src/core/maskEngine.spec.ts +12 -6
- package/src/index.tsx +4 -0
- package/src/types/index.ts +14 -0
- package/src/types/inputmask.types.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-mask-input",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A react Hook for build elegant input masks. Compatible with React Hook Form",
|
|
6
6
|
"author": "Eduardo Borges<euduardoborges@gmail.com>",
|
|
@@ -39,36 +39,26 @@
|
|
|
39
39
|
"react-dom": ">=17"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@stylistic/eslint-plugin": "3.1.0",
|
|
45
|
-
"@testing-library/dom": "^10.4.0",
|
|
46
|
-
"@testing-library/react": "^16.1.0",
|
|
42
|
+
"@testing-library/dom": "^10.4.1",
|
|
43
|
+
"@testing-library/react": "^16.3.2",
|
|
47
44
|
"@types/inputmask": "5.0.7",
|
|
48
|
-
"@types/node": "
|
|
49
|
-
"@types/react": ">=
|
|
50
|
-
"@types/react-dom": ">=
|
|
51
|
-
"@vitest/coverage-v8": "
|
|
52
|
-
"antd": "^6.
|
|
53
|
-
"
|
|
54
|
-
"eslint-config-airbnb-extended": "^2.2.0",
|
|
55
|
-
"eslint-import-resolver-typescript": "^4.4.4",
|
|
56
|
-
"eslint-plugin-import-x": "^4.16.1",
|
|
57
|
-
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
58
|
-
"eslint-plugin-react": "^7.37.5",
|
|
59
|
-
"eslint-plugin-react-hooks": "^5.2.0",
|
|
45
|
+
"@types/node": "^25.5.0",
|
|
46
|
+
"@types/react": ">=19",
|
|
47
|
+
"@types/react-dom": ">=19",
|
|
48
|
+
"@vitest/coverage-v8": "4.1.1",
|
|
49
|
+
"antd": "^6.3.4",
|
|
50
|
+
"oxlint": "1.57.0",
|
|
60
51
|
"inputmask": "5.0.10-beta.61",
|
|
61
|
-
"jsdom": "^
|
|
62
|
-
"react-hook-form": "7.
|
|
63
|
-
"tsup": "8.5.
|
|
64
|
-
"typescript": "5.
|
|
65
|
-
"
|
|
66
|
-
"vitest": "3.2.4"
|
|
52
|
+
"jsdom": "^28.1.0",
|
|
53
|
+
"react-hook-form": "7.72.0",
|
|
54
|
+
"tsup": "8.5.1",
|
|
55
|
+
"typescript": "5.9",
|
|
56
|
+
"vitest": "4.1.1"
|
|
67
57
|
},
|
|
68
58
|
"scripts": {
|
|
69
59
|
"build": "tsup",
|
|
70
60
|
"dev": "tsup --watch",
|
|
71
|
-
"lint": "
|
|
61
|
+
"lint": "oxlint ./src",
|
|
72
62
|
"test": "vitest --dir ./src --run --coverage",
|
|
73
63
|
"type-check": "tsc --noEmit",
|
|
74
64
|
"clean": "rm -rf dist",
|
|
@@ -47,7 +47,7 @@ describe('useMaskInputAntd', () => {
|
|
|
47
47
|
const inputElement = document.createElement('input');
|
|
48
48
|
vi.mocked(inputmask).mockReturnValue({
|
|
49
49
|
mask: vi.fn(),
|
|
50
|
-
} as
|
|
50
|
+
} as any);
|
|
51
51
|
|
|
52
52
|
const { result, rerender } = renderHook(
|
|
53
53
|
() => useMaskInputAntd({ mask: '999-999' }),
|
|
@@ -66,7 +66,7 @@ describe('useMaskInputAntd', () => {
|
|
|
66
66
|
const inputElement = document.createElement('input');
|
|
67
67
|
vi.mocked(inputmask).mockReturnValue({
|
|
68
68
|
mask: vi.fn(),
|
|
69
|
-
} as
|
|
69
|
+
} as any);
|
|
70
70
|
|
|
71
71
|
const { result, rerender } = renderHook(
|
|
72
72
|
() => useMaskInputAntd({
|
|
@@ -89,7 +89,7 @@ describe('useMaskInputAntd', () => {
|
|
|
89
89
|
const register = vi.fn();
|
|
90
90
|
vi.mocked(inputmask).mockReturnValue({
|
|
91
91
|
mask: vi.fn(),
|
|
92
|
-
} as
|
|
92
|
+
} as any);
|
|
93
93
|
|
|
94
94
|
const { result, rerender } = renderHook(
|
|
95
95
|
() => useMaskInputAntd({
|
package/src/api/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as useMaskInput } from './useMaskInput';
|
|
2
2
|
export { default as useHookFormMask } from './useHookFormMask';
|
|
3
|
+
export { default as useTanStackFormMask } from './useTanStackFormMask';
|
|
3
4
|
export { default as withMask } from './withMask';
|
|
4
5
|
export { default as withHookFormMask } from './withHookFormMask';
|
|
6
|
+
export { default as withTanStackFormMask } from './withTanStackFormMask';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react';
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
2
|
import inputmask from 'inputmask';
|
|
3
3
|
import {
|
|
4
4
|
beforeEach,
|
|
@@ -50,7 +50,7 @@ describe('useHookFormMask', () => {
|
|
|
50
50
|
name: 'phone',
|
|
51
51
|
}));
|
|
52
52
|
const maskFn = vi.fn();
|
|
53
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
53
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
54
54
|
|
|
55
55
|
const { result } = renderHook(
|
|
56
56
|
() => useHookFormMask(registerFn as UseFormRegister<FieldValues>),
|
|
@@ -68,7 +68,7 @@ describe('useHookFormMask', () => {
|
|
|
68
68
|
|
|
69
69
|
it('merges register options with mask options', () => {
|
|
70
70
|
const registerFn = makeRegisterFn('phone');
|
|
71
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
71
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
72
72
|
|
|
73
73
|
const { result } = renderHook(
|
|
74
74
|
() => useHookFormMask(registerFn as UseFormRegister<FieldValues>),
|
|
@@ -80,7 +80,7 @@ describe('useHookFormMask', () => {
|
|
|
80
80
|
|
|
81
81
|
it('works with alias masks', () => {
|
|
82
82
|
const registerFn = makeRegisterFn('cpf');
|
|
83
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
83
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
84
84
|
|
|
85
85
|
const { result } = renderHook(
|
|
86
86
|
() => useHookFormMask(registerFn as UseFormRegister<FieldValues>),
|
|
@@ -92,7 +92,7 @@ describe('useHookFormMask', () => {
|
|
|
92
92
|
|
|
93
93
|
it('works with array masks', () => {
|
|
94
94
|
const registerFn = makeRegisterFn('phone');
|
|
95
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
95
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
96
96
|
|
|
97
97
|
const { result } = renderHook(
|
|
98
98
|
() => useHookFormMask(registerFn as UseFormRegister<FieldValues>),
|
|
@@ -190,6 +190,53 @@ describe('useHookFormMask', () => {
|
|
|
190
190
|
expect(refBefore).not.toBe(refAfter);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it('calls the latest RHF ref with the element after re-render (reset() regression)', async () => {
|
|
194
|
+
// Simulate react-hook-form's reset() behaviour: it clears _fields and
|
|
195
|
+
// returns a brand-new ref callback from register() on the next render.
|
|
196
|
+
// The cached stable ref must still forward to the new RHF ref so that
|
|
197
|
+
// RHF's internal T()/Z() logic can sync the DOM value to the reset value.
|
|
198
|
+
const input = document.createElement('input');
|
|
199
|
+
const refFn1 = vi.fn();
|
|
200
|
+
const refFn2 = vi.fn(); // "new" ref returned after reset()
|
|
201
|
+
|
|
202
|
+
const makeRegisterReturn = (ref: ReturnType<typeof vi.fn>) => ({
|
|
203
|
+
ref,
|
|
204
|
+
prevRef: vi.fn(),
|
|
205
|
+
onChange: vi.fn(),
|
|
206
|
+
onBlur: vi.fn(),
|
|
207
|
+
name: 'phone',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let currentRef = refFn1;
|
|
211
|
+
const registerFn = vi.fn(() => makeRegisterReturn(currentRef));
|
|
212
|
+
|
|
213
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
214
|
+
|
|
215
|
+
// Mimic a real component: the registration function is called on every
|
|
216
|
+
// render (which is what triggers the queue-push logic for reset support).
|
|
217
|
+
const { result, rerender } = renderHook(
|
|
218
|
+
() => {
|
|
219
|
+
const registerWithMask = useHookFormMask(registerFn as UseFormRegister<FieldValues>);
|
|
220
|
+
return registerWithMask('phone', '999-999');
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Mount the element – stable cached ref is called once
|
|
225
|
+
result.current.ref?.(input);
|
|
226
|
+
expect(refFn1).toHaveBeenCalledWith(input);
|
|
227
|
+
|
|
228
|
+
// Simulate reset(): register() now returns a different ref (refFn2)
|
|
229
|
+
currentRef = refFn2;
|
|
230
|
+
|
|
231
|
+
await act(async () => {
|
|
232
|
+
rerender();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// After the re-render + useLayoutEffect, the new RHF ref must have been
|
|
236
|
+
// called with the stored element so RHF can re-register it and sync values.
|
|
237
|
+
expect(refFn2).toHaveBeenCalledWith(input);
|
|
238
|
+
});
|
|
239
|
+
|
|
193
240
|
it('defines prevRef as a non-enumerable property', () => {
|
|
194
241
|
const prevRef = vi.fn();
|
|
195
242
|
const registerFn = vi.fn(() => ({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import { applyMaskToElement } from '../core';
|
|
4
4
|
import { flow, makeMaskCacheKey, setPrevRef } from '../utils';
|
|
@@ -12,6 +12,13 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
import type { Mask, Options, UseHookFormMaskReturn } from '../types';
|
|
14
14
|
|
|
15
|
+
interface CacheEntry {
|
|
16
|
+
stableRef: RefCallback<HTMLElement | null>;
|
|
17
|
+
element: HTMLElement | null;
|
|
18
|
+
latestRHFRef?: RefCallback<HTMLElement | null>;
|
|
19
|
+
syncedRHFRef?: RefCallback<HTMLElement | null>;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Creates a masked version of React Hook Form's register function.
|
|
17
24
|
* Takes react-hook-form's register and adds automatic masking. Like an upgrade.
|
|
@@ -25,9 +32,26 @@ export default function useHookFormMask<
|
|
|
25
32
|
T extends FieldValues, D extends RegisterOptions,
|
|
26
33
|
>(registerFn: UseFormRegister<T>): ((fieldName: Path<T>, mask: Mask, options?: (
|
|
27
34
|
D & Options) | Options | D) => UseHookFormMaskReturn<T>) {
|
|
28
|
-
|
|
35
|
+
const entryCacheRef = useRef(new Map<string, CacheEntry>());
|
|
36
|
+
|
|
37
|
+
useLayoutEffect(() => {
|
|
38
|
+
entryCacheRef.current.forEach((entry) => {
|
|
39
|
+
const currentEntry = entry;
|
|
40
|
+
if (!currentEntry.element || !currentEntry.latestRHFRef) return;
|
|
41
|
+
|
|
42
|
+
// After reset(), RHF gives us a new ref callback. React won't call it
|
|
43
|
+
// because our outward ref identity stays stable, so we replay it here.
|
|
44
|
+
if (currentEntry.latestRHFRef !== currentEntry.syncedRHFRef) {
|
|
45
|
+
currentEntry.latestRHFRef(currentEntry.element);
|
|
46
|
+
currentEntry.syncedRHFRef = currentEntry.latestRHFRef;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
29
51
|
return useMemo(() => {
|
|
30
|
-
|
|
52
|
+
// registerFn identity changed, so drop cached refs bound to the previous
|
|
53
|
+
// register lifecycle.
|
|
54
|
+
entryCacheRef.current = new Map<string, CacheEntry>();
|
|
31
55
|
|
|
32
56
|
return (fieldName: Path<T>, mask: Mask, options?: (
|
|
33
57
|
D & Options) | Options | D): UseHookFormMaskReturn<T> => {
|
|
@@ -38,20 +62,36 @@ export default function useHookFormMask<
|
|
|
38
62
|
|
|
39
63
|
const cacheKey = makeMaskCacheKey(fieldName, mask);
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
let entry = entryCacheRef.current.get(cacheKey);
|
|
66
|
+
if (!entry) {
|
|
67
|
+
const nextEntry: CacheEntry = {
|
|
68
|
+
element: null,
|
|
69
|
+
latestRHFRef: ref,
|
|
70
|
+
syncedRHFRef: undefined,
|
|
71
|
+
stableRef: null as unknown as RefCallback<HTMLElement | null>,
|
|
72
|
+
};
|
|
73
|
+
|
|
42
74
|
const applyMaskToRef = (_ref: HTMLElement | null) => {
|
|
75
|
+
nextEntry.element = _ref;
|
|
43
76
|
if (_ref) applyMaskToElement(_ref, mask, options as Options);
|
|
44
77
|
return _ref;
|
|
45
78
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
|
|
80
|
+
nextEntry.stableRef = (
|
|
81
|
+
nextEntry.latestRHFRef
|
|
82
|
+
? flow(applyMaskToRef, (_ref: HTMLElement | null) => nextEntry.latestRHFRef?.(_ref))
|
|
83
|
+
: applyMaskToRef
|
|
84
|
+
) as RefCallback<HTMLElement | null>;
|
|
85
|
+
|
|
86
|
+
entry = nextEntry;
|
|
87
|
+
entryCacheRef.current.set(cacheKey, nextEntry);
|
|
88
|
+
} else {
|
|
89
|
+
entry.latestRHFRef = ref;
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
const result = {
|
|
53
93
|
...registerReturn,
|
|
54
|
-
ref:
|
|
94
|
+
ref: entry.stableRef,
|
|
55
95
|
} as UseHookFormMaskReturn<T>;
|
|
56
96
|
|
|
57
97
|
setPrevRef(result, ref);
|
|
@@ -43,7 +43,7 @@ describe('useMaskInput', () => {
|
|
|
43
43
|
|
|
44
44
|
it('handles direct input element', () => {
|
|
45
45
|
const input = document.createElement('input');
|
|
46
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
46
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
47
47
|
|
|
48
48
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
49
49
|
|
|
@@ -59,7 +59,7 @@ describe('useMaskInput', () => {
|
|
|
59
59
|
it('handles ref object', () => {
|
|
60
60
|
const input = document.createElement('input');
|
|
61
61
|
const ref = { current: input };
|
|
62
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
62
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
63
63
|
|
|
64
64
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
65
65
|
|
|
@@ -76,7 +76,7 @@ describe('useMaskInput', () => {
|
|
|
76
76
|
const wrapper = document.createElement('div');
|
|
77
77
|
const input = document.createElement('input');
|
|
78
78
|
wrapper.appendChild(input);
|
|
79
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
79
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
80
80
|
|
|
81
81
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
82
82
|
|
|
@@ -91,7 +91,7 @@ describe('useMaskInput', () => {
|
|
|
91
91
|
|
|
92
92
|
it('handles invalid element in ref', () => {
|
|
93
93
|
const invalidRef = { current: 'not an element' };
|
|
94
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
94
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
95
95
|
|
|
96
96
|
const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
97
97
|
|
|
@@ -106,7 +106,7 @@ describe('useMaskInput', () => {
|
|
|
106
106
|
vi.spyOn(core, 'isHTMLElement').mockReturnValueOnce(false);
|
|
107
107
|
|
|
108
108
|
const invalidElement = { nodeType: 1 } as unknown as Input;
|
|
109
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
109
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
110
110
|
|
|
111
111
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
112
112
|
|
|
@@ -121,7 +121,7 @@ describe('useMaskInput', () => {
|
|
|
121
121
|
|
|
122
122
|
it('handles wrapper without input inside', () => {
|
|
123
123
|
const wrapper = document.createElement('div');
|
|
124
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
124
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
125
125
|
|
|
126
126
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
127
127
|
|
|
@@ -136,7 +136,7 @@ describe('useMaskInput', () => {
|
|
|
136
136
|
|
|
137
137
|
it('works with custom options', () => {
|
|
138
138
|
const input = document.createElement('input');
|
|
139
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
139
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
140
140
|
|
|
141
141
|
const { result, rerender } = renderHook(() => useMaskInput({
|
|
142
142
|
mask: '999-999',
|
|
@@ -154,7 +154,7 @@ describe('useMaskInput', () => {
|
|
|
154
154
|
|
|
155
155
|
it('works with alias masks', () => {
|
|
156
156
|
const input = document.createElement('input');
|
|
157
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
157
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
158
158
|
|
|
159
159
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: 'cpf' }));
|
|
160
160
|
|
|
@@ -170,7 +170,7 @@ describe('useMaskInput', () => {
|
|
|
170
170
|
it('calls register callback when provided', () => {
|
|
171
171
|
const input = document.createElement('input');
|
|
172
172
|
const register = vi.fn();
|
|
173
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
173
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
174
174
|
|
|
175
175
|
const { result, rerender } = renderHook(() => useMaskInput({
|
|
176
176
|
mask: '999-999',
|
|
@@ -188,7 +188,7 @@ describe('useMaskInput', () => {
|
|
|
188
188
|
|
|
189
189
|
it('handles textarea element', () => {
|
|
190
190
|
const textarea = document.createElement('textarea');
|
|
191
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
191
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
192
192
|
|
|
193
193
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
194
194
|
|
|
@@ -205,7 +205,7 @@ describe('useMaskInput', () => {
|
|
|
205
205
|
const wrapper = document.createElement('div');
|
|
206
206
|
const input = document.createElement('input');
|
|
207
207
|
wrapper.appendChild(input);
|
|
208
|
-
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as
|
|
208
|
+
vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
|
|
209
209
|
|
|
210
210
|
const { result, rerender } = renderHook(() => useMaskInput({ mask: '999-999' }));
|
|
211
211
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import withTanStackFormMask from './withTanStackFormMask';
|
|
4
|
+
|
|
5
|
+
import type { Mask, Options, TanStackFormInputProps, UseTanStackFormMaskReturn } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a helper to mask TanStack Form-compatible input props.
|
|
9
|
+
* Designed for objects returned by field.getInputProps().
|
|
10
|
+
*/
|
|
11
|
+
export default function useTanStackFormMask(): <T extends TanStackFormInputProps>(
|
|
12
|
+
mask: Mask,
|
|
13
|
+
inputProps: T,
|
|
14
|
+
options?: Options,
|
|
15
|
+
) => UseTanStackFormMaskReturn<T> {
|
|
16
|
+
return useMemo(
|
|
17
|
+
() => <T extends TanStackFormInputProps>(
|
|
18
|
+
mask: Mask,
|
|
19
|
+
inputProps: T,
|
|
20
|
+
options?: Options,
|
|
21
|
+
): UseTanStackFormMaskReturn<T> => withTanStackFormMask(inputProps, mask, options),
|
|
22
|
+
[],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import withHookFormMask from './withHookFormMask';
|
|
8
8
|
|
|
9
9
|
import type { RefCallback } from 'react';
|
|
10
|
-
import type { FieldValues
|
|
10
|
+
import type { FieldValues } from 'react-hook-form';
|
|
11
11
|
|
|
12
12
|
import type { UseHookFormMaskReturn } from '../types';
|
|
13
13
|
|
|
@@ -33,7 +33,7 @@ describe('withHookFormMask', () => {
|
|
|
33
33
|
name: 'phone',
|
|
34
34
|
};
|
|
35
35
|
const maskFn = vi.fn();
|
|
36
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
36
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
37
37
|
|
|
38
38
|
const result = withHookFormMask(register, '999-999');
|
|
39
39
|
|
|
@@ -55,7 +55,7 @@ describe('withHookFormMask', () => {
|
|
|
55
55
|
name: 'phone',
|
|
56
56
|
};
|
|
57
57
|
const maskFn = vi.fn();
|
|
58
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
58
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
59
59
|
|
|
60
60
|
const result = withHookFormMask(register, '999-999');
|
|
61
61
|
result.ref?.(input);
|
|
@@ -74,7 +74,7 @@ describe('withHookFormMask', () => {
|
|
|
74
74
|
name: 'phone',
|
|
75
75
|
};
|
|
76
76
|
const maskFn = vi.fn();
|
|
77
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
77
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
78
78
|
|
|
79
79
|
const result = withHookFormMask(register, '999-999');
|
|
80
80
|
result.ref?.(input);
|
|
@@ -93,7 +93,7 @@ describe('withHookFormMask', () => {
|
|
|
93
93
|
name: 'cpf',
|
|
94
94
|
};
|
|
95
95
|
const maskFn = vi.fn();
|
|
96
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
96
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
97
97
|
|
|
98
98
|
const result = withHookFormMask(register, 'cpf');
|
|
99
99
|
result.ref?.(input);
|
|
@@ -112,7 +112,7 @@ describe('withHookFormMask', () => {
|
|
|
112
112
|
name: 'phone',
|
|
113
113
|
};
|
|
114
114
|
const maskFn = vi.fn();
|
|
115
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
115
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
116
116
|
|
|
117
117
|
const result = withHookFormMask(register, '999-999', { placeholder: '_' });
|
|
118
118
|
result.ref?.(input);
|
|
@@ -145,7 +145,7 @@ describe('withHookFormMask', () => {
|
|
|
145
145
|
name: 'phone',
|
|
146
146
|
};
|
|
147
147
|
const maskFn = vi.fn();
|
|
148
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
148
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
149
149
|
|
|
150
150
|
const result = withHookFormMask(register, '999-999');
|
|
151
151
|
result.ref?.(null as unknown as HTMLElement);
|
package/src/api/withMask.spec.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe('withMask', () => {
|
|
|
29
29
|
it('applies mask to input element', () => {
|
|
30
30
|
const input = document.createElement('input');
|
|
31
31
|
const maskFn = vi.fn();
|
|
32
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
32
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
33
33
|
|
|
34
34
|
const refCallback = withMask('999-999');
|
|
35
35
|
refCallback(input);
|
|
@@ -39,7 +39,7 @@ describe('withMask', () => {
|
|
|
39
39
|
|
|
40
40
|
it('does nothing if input is null', () => {
|
|
41
41
|
const maskFn = vi.fn();
|
|
42
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
42
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
43
43
|
|
|
44
44
|
const refCallback = withMask('999-999');
|
|
45
45
|
refCallback(null);
|
|
@@ -50,7 +50,7 @@ describe('withMask', () => {
|
|
|
50
50
|
it('does nothing if mask is null', () => {
|
|
51
51
|
const input = document.createElement('input');
|
|
52
52
|
const maskFn = vi.fn();
|
|
53
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
53
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
54
54
|
|
|
55
55
|
const refCallback = withMask(null);
|
|
56
56
|
refCallback(input);
|
|
@@ -61,7 +61,7 @@ describe('withMask', () => {
|
|
|
61
61
|
it('applies mask with custom options', () => {
|
|
62
62
|
const input = document.createElement('input');
|
|
63
63
|
const maskFn = vi.fn();
|
|
64
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
64
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
65
65
|
|
|
66
66
|
const refCallback = withMask('999-999', { placeholder: '_' });
|
|
67
67
|
refCallback(input);
|
|
@@ -72,7 +72,7 @@ describe('withMask', () => {
|
|
|
72
72
|
it('works with alias masks', () => {
|
|
73
73
|
const input = document.createElement('input');
|
|
74
74
|
const maskFn = vi.fn();
|
|
75
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
75
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
76
76
|
|
|
77
77
|
const refCallback = withMask('cpf');
|
|
78
78
|
refCallback(input);
|
|
@@ -83,7 +83,7 @@ describe('withMask', () => {
|
|
|
83
83
|
it('works with array masks', () => {
|
|
84
84
|
const input = document.createElement('input');
|
|
85
85
|
const maskFn = vi.fn();
|
|
86
|
-
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as
|
|
86
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
87
87
|
|
|
88
88
|
const refCallback = withMask(['999-999', '9999-9999']);
|
|
89
89
|
refCallback(input);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import inputmask from 'inputmask';
|
|
2
|
+
import {
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe, expect, it, vi,
|
|
5
|
+
} from 'vitest';
|
|
6
|
+
|
|
7
|
+
import withTanStackFormMask from './withTanStackFormMask';
|
|
8
|
+
|
|
9
|
+
import type { TanStackFormInputProps } from '../types';
|
|
10
|
+
|
|
11
|
+
vi.mock('inputmask', () => ({
|
|
12
|
+
default: vi.fn((options) => ({
|
|
13
|
+
mask: vi.fn(),
|
|
14
|
+
options,
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('withTanStackFormMask', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns masked input props with stable structure', () => {
|
|
24
|
+
const inputProps: TanStackFormInputProps = {
|
|
25
|
+
name: 'cpf',
|
|
26
|
+
ref: vi.fn(),
|
|
27
|
+
onBlur: vi.fn(),
|
|
28
|
+
onChange: vi.fn(),
|
|
29
|
+
value: '',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = withTanStackFormMask(inputProps, 'cpf');
|
|
33
|
+
|
|
34
|
+
expect(typeof result.ref).toBe('function');
|
|
35
|
+
expect(result.onBlur).toBe(inputProps.onBlur);
|
|
36
|
+
expect(result.onChange).toBe(inputProps.onChange);
|
|
37
|
+
expect(result.name).toBe('cpf');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('applies mask when ref callback receives an input', () => {
|
|
41
|
+
const maskFn = vi.fn();
|
|
42
|
+
vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
|
|
43
|
+
|
|
44
|
+
const input = document.createElement('input');
|
|
45
|
+
const originalRef = vi.fn();
|
|
46
|
+
const inputProps: TanStackFormInputProps = {
|
|
47
|
+
name: 'phone',
|
|
48
|
+
ref: originalRef,
|
|
49
|
+
onBlur: vi.fn(),
|
|
50
|
+
onChange: vi.fn(),
|
|
51
|
+
value: '',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const result = withTanStackFormMask(inputProps, '(99) 99999-9999');
|
|
55
|
+
result.ref(input);
|
|
56
|
+
|
|
57
|
+
expect(maskFn).toHaveBeenCalled();
|
|
58
|
+
expect(originalRef).toHaveBeenCalledWith(input);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('keeps ref cache stable for same ref and mask', () => {
|
|
62
|
+
const originalRef = vi.fn();
|
|
63
|
+
const inputProps: TanStackFormInputProps = {
|
|
64
|
+
name: 'cpf',
|
|
65
|
+
ref: originalRef,
|
|
66
|
+
onBlur: vi.fn(),
|
|
67
|
+
onChange: vi.fn(),
|
|
68
|
+
value: '',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const first = withTanStackFormMask(inputProps, 'cpf');
|
|
72
|
+
const second = withTanStackFormMask(inputProps, 'cpf');
|
|
73
|
+
|
|
74
|
+
expect(first.ref).toBe(second.ref);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { applyMaskToElement } from '../core';
|
|
2
|
+
import { flow, makeMaskCacheKey, setPrevRef } from '../utils';
|
|
3
|
+
|
|
4
|
+
import type { RefCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Mask, Options, TanStackFormInputProps, UseTanStackFormMaskReturn,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
const refCache = new WeakMap<
|
|
11
|
+
RefCallback<HTMLElement | null>,
|
|
12
|
+
Map<string, RefCallback<HTMLElement | null>>
|
|
13
|
+
>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enhances TanStack Form-compatible input props with mask support.
|
|
17
|
+
* Works with objects returned by field.getInputProps().
|
|
18
|
+
*/
|
|
19
|
+
export default function withTanStackFormMask<T extends TanStackFormInputProps>(
|
|
20
|
+
inputProps: T,
|
|
21
|
+
mask: Mask,
|
|
22
|
+
options?: Options,
|
|
23
|
+
): UseTanStackFormMaskReturn<T> {
|
|
24
|
+
const { ref } = inputProps;
|
|
25
|
+
|
|
26
|
+
if (!ref) {
|
|
27
|
+
const result = {
|
|
28
|
+
...inputProps,
|
|
29
|
+
ref: ((input: HTMLElement | null) => {
|
|
30
|
+
if (input) applyMaskToElement(input, mask, options);
|
|
31
|
+
}) as RefCallback<HTMLElement | null>,
|
|
32
|
+
} as unknown as UseTanStackFormMaskReturn<T>;
|
|
33
|
+
|
|
34
|
+
setPrevRef(result, ref);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!refCache.has(ref)) {
|
|
39
|
+
refCache.set(ref, new Map());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const maskCache = refCache.get(ref);
|
|
43
|
+
const cacheKey = makeMaskCacheKey(inputProps.name ?? '', mask);
|
|
44
|
+
|
|
45
|
+
if (!maskCache?.has(cacheKey)) {
|
|
46
|
+
const applyMaskToRef = (_ref: HTMLElement | null) => {
|
|
47
|
+
if (_ref) applyMaskToElement(_ref, mask, options);
|
|
48
|
+
return _ref;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
maskCache?.set(
|
|
52
|
+
cacheKey,
|
|
53
|
+
flow(applyMaskToRef, ref) as RefCallback<HTMLElement | null>,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = {
|
|
58
|
+
...inputProps,
|
|
59
|
+
ref: maskCache?.get(cacheKey),
|
|
60
|
+
} as unknown as UseTanStackFormMaskReturn<T>;
|
|
61
|
+
|
|
62
|
+
setPrevRef(result, ref);
|
|
63
|
+
return result;
|
|
64
|
+
}
|