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.
Files changed (36) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +105 -29
  3. package/dist/antd.cjs +9 -9
  4. package/dist/antd.d.cts +1 -1
  5. package/dist/antd.d.ts +1 -1
  6. package/dist/antd.js +1 -1
  7. package/dist/{chunk-QCWLMMDI.js → chunk-ICLWBMH4.js} +21 -2
  8. package/dist/{chunk-QCWLMMDI.js.map → chunk-ICLWBMH4.js.map} +1 -1
  9. package/dist/{chunk-VK6LQ75W.cjs → chunk-X5SEJVSB.cjs} +21 -2
  10. package/dist/{chunk-VK6LQ75W.cjs.map → chunk-X5SEJVSB.cjs.map} +1 -1
  11. package/dist/{index-F3rlTTTe.d.cts → index-BoaVtWUr.d.cts} +13 -4
  12. package/dist/{index-F3rlTTTe.d.ts → index-BoaVtWUr.d.ts} +13 -4
  13. package/dist/index.cjs +92 -25
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +14 -2
  16. package/dist/index.d.ts +14 -2
  17. package/dist/index.js +80 -15
  18. package/dist/index.js.map +1 -1
  19. package/package.json +15 -25
  20. package/src/antd/useHookFormMaskAntd.spec.ts +2 -0
  21. package/src/antd/useMaskInputAntd.spec.tsx +3 -3
  22. package/src/api/index.ts +2 -0
  23. package/src/api/useHookFormMask.spec.ts +52 -5
  24. package/src/api/useHookFormMask.ts +49 -9
  25. package/src/api/useMaskInput.spec.tsx +11 -11
  26. package/src/api/useTanStackFormMask.ts +24 -0
  27. package/src/api/withHookFormMask.spec.ts +7 -7
  28. package/src/api/withMask.spec.ts +6 -6
  29. package/src/api/withTanStackFormMask.spec.ts +76 -0
  30. package/src/api/withTanStackFormMask.ts +64 -0
  31. package/src/core/maskConfig.spec.ts +24 -0
  32. package/src/core/maskConfig.ts +14 -0
  33. package/src/core/maskEngine.spec.ts +12 -6
  34. package/src/index.tsx +4 -0
  35. package/src/types/index.ts +14 -0
  36. 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.7.4",
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
- "@eslint/compat": "^1.3.2",
43
- "@eslint/js": "^9.35.0",
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": "22",
49
- "@types/react": ">=17",
50
- "@types/react-dom": ">=17",
51
- "@vitest/coverage-v8": "3.2.4",
52
- "antd": "^6.2.3",
53
- "eslint": "^9.35.0",
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": "^25.0.1",
62
- "react-hook-form": "7.62.0",
63
- "tsup": "8.5.0",
64
- "typescript": "5.1",
65
- "typescript-eslint": "^8.42.0",
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": "eslint ./src --ext ts,tsx",
61
+ "lint": "oxlint ./src",
72
62
  "test": "vitest --dir ./src --run --coverage",
73
63
  "type-check": "tsc --noEmit",
74
64
  "clean": "rm -rf dist",
@@ -8,6 +8,8 @@ import {
8
8
  } from 'vitest';
9
9
 
10
10
  import { applyMaskToElement, resolveInputRef } from '../core';
11
+
12
+
11
13
  import useHookFormMaskAntd from './useHookFormMaskAntd';
12
14
 
13
15
  import type { InputRef } from 'antd';
@@ -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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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
- const refCache = new Map<string, RefCallback<HTMLElement | null>>();
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
- if (!refCache.has(cacheKey)) {
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
- refCache.set(
47
- cacheKey,
48
- (ref ? flow(applyMaskToRef, ref) : applyMaskToRef) as RefCallback<HTMLElement | null>,
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: refCache.get(cacheKey),
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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, UseFormRegisterReturn } from 'react-hook-form';
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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);
@@ -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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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 unknown as Inputmask.Instance);
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
+ }