use-mask-input 3.8.0 → 3.10.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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +55 -7
  3. package/dist/antd.cjs +14 -12
  4. package/dist/antd.cjs.map +1 -1
  5. package/dist/antd.d.cts +3 -2
  6. package/dist/antd.d.ts +3 -2
  7. package/dist/antd.js +6 -4
  8. package/dist/antd.js.map +1 -1
  9. package/dist/{chunk-X5SEJVSB.cjs → chunk-DTC7JTZP.cjs} +78 -26
  10. package/dist/{chunk-X5SEJVSB.cjs.map → chunk-DTC7JTZP.cjs.map} +1 -1
  11. package/dist/{chunk-ICLWBMH4.js → chunk-TVCNC3TP.js} +77 -27
  12. package/dist/{chunk-ICLWBMH4.js.map → chunk-TVCNC3TP.js.map} +1 -1
  13. package/dist/{index-S8txl6uK.d.cts → index-D8KkaDbQ.d.cts} +15 -2
  14. package/dist/{index-S8txl6uK.d.ts → index-D8KkaDbQ.d.ts} +15 -2
  15. package/dist/index.cjs +83 -29
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +17 -4
  18. package/dist/index.d.ts +17 -4
  19. package/dist/index.js +71 -19
  20. package/dist/index.js.map +1 -1
  21. package/package.json +7 -7
  22. package/src/antd/useMaskInputAntd.spec.tsx +22 -0
  23. package/src/antd/useMaskInputAntd.ts +11 -7
  24. package/src/api/index.ts +2 -0
  25. package/src/api/useHookFormMask.ts +4 -1
  26. package/src/api/useMaskInput.spec.tsx +18 -0
  27. package/src/api/useMaskInput.ts +11 -5
  28. package/src/api/useTanStackFormMask.ts +24 -0
  29. package/src/api/withHookFormMask.spec.ts +43 -74
  30. package/src/api/withHookFormMask.ts +18 -10
  31. package/src/api/withMask.spec.ts +16 -0
  32. package/src/api/withMask.ts +11 -8
  33. package/src/api/withTanStackFormMask.spec.ts +76 -0
  34. package/src/api/withTanStackFormMask.ts +73 -0
  35. package/src/core/maskEngine.spec.ts +12 -6
  36. package/src/index.tsx +6 -0
  37. package/src/types/index.ts +19 -1
  38. package/src/utils/index.ts +6 -1
  39. package/src/utils/maskHelpers.ts +44 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-mask-input",
3
- "version": "3.8.0",
3
+ "version": "3.10.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>",
@@ -42,18 +42,18 @@
42
42
  "@testing-library/dom": "^10.4.1",
43
43
  "@testing-library/react": "^16.3.2",
44
44
  "@types/inputmask": "5.0.7",
45
- "@types/node": "25",
45
+ "@types/node": "^25.5.0",
46
46
  "@types/react": ">=19",
47
47
  "@types/react-dom": ">=19",
48
- "@vitest/coverage-v8": "4.0.18",
49
- "antd": "^6.3.2",
50
- "oxlint": "1.53.0",
48
+ "@vitest/coverage-v8": "4.1.1",
49
+ "antd": "^6.3.4",
50
+ "oxlint": "1.57.0",
51
51
  "inputmask": "5.0.10-beta.61",
52
52
  "jsdom": "^28.1.0",
53
- "react-hook-form": "7.71.2",
53
+ "react-hook-form": "7.72.0",
54
54
  "tsup": "8.5.1",
55
55
  "typescript": "5.9",
56
- "vitest": "4.0.18"
56
+ "vitest": "4.1.1"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup",
@@ -31,6 +31,7 @@ describe('useMaskInputAntd', () => {
31
31
  it('returns a ref callback function', () => {
32
32
  const { result } = renderHook(() => useMaskInputAntd({ mask: '999-999' }));
33
33
  expect(typeof result.current).toBe('function');
34
+ expect(typeof result.current.unmaskedValue).toBe('function');
34
35
  });
35
36
 
36
37
  it('handles null input ref', () => {
@@ -62,6 +63,27 @@ describe('useMaskInputAntd', () => {
62
63
  expect(inputmask).toHaveBeenCalled();
63
64
  });
64
65
 
66
+ it('exposes the unmasked value from the masked Ant Design input', () => {
67
+ const inputElement = document.createElement('input');
68
+ vi.mocked(inputmask).mockReturnValue({
69
+ mask: vi.fn(),
70
+ } as any);
71
+
72
+ const { result } = renderHook(
73
+ () => useMaskInputAntd({ mask: '999-999' }),
74
+ );
75
+
76
+ act(() => {
77
+ result.current({ input: inputElement } as unknown as InputRef);
78
+ });
79
+
80
+ inputElement.inputmask = {
81
+ unmaskedvalue: vi.fn(() => '2026-04-01'),
82
+ } as any;
83
+
84
+ expect(result.current.unmaskedValue()).toBe('2026-04-01');
85
+ });
86
+
65
87
  it('works with custom options', () => {
66
88
  const inputElement = document.createElement('input');
67
89
  vi.mocked(inputmask).mockReturnValue({
@@ -3,10 +3,13 @@ import { useCallback, useEffect, useRef } from 'react';
3
3
  import withMask from '../api/withMask';
4
4
  import { resolveInputRef } from '../core';
5
5
  import isServer from '../utils/isServer';
6
+ import { getUnmaskedValue, setUnmaskedValue } from '../utils';
6
7
 
7
8
  import type { InputRef } from 'antd';
8
9
 
9
- import type { Mask, Options } from '../types';
10
+ import type { Mask, Options, UnmaskedValueApi } from '../types';
11
+
12
+ type UseMaskInputAntdReturn = ((input: InputRef | null) => void) & UnmaskedValueApi;
10
13
 
11
14
  interface UseMaskInputOptions {
12
15
  mask: Mask;
@@ -23,14 +26,13 @@ interface UseMaskInputOptions {
23
26
  * @param props.options - Optional mask configuration options
24
27
  * @returns A ref callback function to attach to the Ant Design Input element
25
28
  */
26
- export default function useMaskInputAntd(props: UseMaskInputOptions): (
27
- input: InputRef | null
28
- ) => void {
29
+ export default function useMaskInputAntd(props: UseMaskInputOptions): UseMaskInputAntdReturn {
29
30
  const { mask, register, options } = props;
30
31
  const ref = useRef<HTMLInputElement | null>(null);
31
32
  const maskRef = useRef(mask);
32
33
  const optionsRef = useRef(options);
33
34
  const maskedElementRef = useRef<HTMLInputElement | null>(null);
35
+ const unmaskedValue = useCallback(() => getUnmaskedValue(ref.current), []);
34
36
 
35
37
  maskRef.current = mask;
36
38
  optionsRef.current = options;
@@ -55,10 +57,12 @@ export default function useMaskInputAntd(props: UseMaskInputOptions): (
55
57
  }, [register]);
56
58
 
57
59
  if (isServer) {
58
- return (): void => {
60
+ const noop = (() => {
59
61
  // server doesn't have dom, so just do nothing
60
- };
62
+ }) as unknown as UseMaskInputAntdReturn;
63
+
64
+ return setUnmaskedValue(noop, () => '');
61
65
  }
62
66
 
63
- return refCallback;
67
+ return setUnmaskedValue(refCallback as UseMaskInputAntdReturn, unmaskedValue);
64
68
  }
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,7 +1,9 @@
1
1
  import { useLayoutEffect, useMemo, useRef } from 'react';
2
2
 
3
3
  import { applyMaskToElement } from '../core';
4
- import { flow, makeMaskCacheKey, setPrevRef } from '../utils';
4
+ import {
5
+ flow, getUnmaskedValue, makeMaskCacheKey, setPrevRef, setUnmaskedValue,
6
+ } from '../utils';
5
7
 
6
8
  import type { RefCallback } from 'react';
7
9
  import type {
@@ -93,6 +95,7 @@ export default function useHookFormMask<
93
95
  ...registerReturn,
94
96
  ref: entry.stableRef,
95
97
  } as UseHookFormMaskReturn<T>;
98
+ setUnmaskedValue(result, () => getUnmaskedValue(entry?.element ?? null));
96
99
 
97
100
  setPrevRef(result, ref);
98
101
 
@@ -29,6 +29,7 @@ describe('useMaskInput', () => {
29
29
  it('returns a ref callback function', () => {
30
30
  const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
31
31
  expect(typeof result.current).toBe('function');
32
+ expect(typeof result.current.unmaskedValue).toBe('function');
32
33
  });
33
34
 
34
35
  it('handles null input', () => {
@@ -56,6 +57,23 @@ describe('useMaskInput', () => {
56
57
  expect(inputmask).toHaveBeenCalled();
57
58
  });
58
59
 
60
+ it('exposes the unmasked value from the masked input', () => {
61
+ const input = document.createElement('input');
62
+ vi.mocked(inputmask).mockReturnValue({ mask: vi.fn() } as any);
63
+
64
+ const { result } = renderHook(() => useMaskInput({ mask: '999-999' }));
65
+
66
+ act(() => {
67
+ result.current(input);
68
+ });
69
+
70
+ input.inputmask = {
71
+ unmaskedvalue: vi.fn(() => '2026-04-01'),
72
+ } as any;
73
+
74
+ expect(result.current.unmaskedValue()).toBe('2026-04-01');
75
+ });
76
+
59
77
  it('handles ref object', () => {
60
78
  const input = document.createElement('input');
61
79
  const ref = { current: input };
@@ -5,8 +5,11 @@ import {
5
5
  import { resolveInputRef } from '../core';
6
6
  import withMask from './withMask';
7
7
  import isServer from '../utils/isServer';
8
+ import { getUnmaskedValue, setUnmaskedValue } from '../utils';
8
9
 
9
- import type { Input, Mask, Options } from '../types';
10
+ import type {
11
+ Input, Mask, Options, UseMaskInputReturn,
12
+ } from '../types';
10
13
 
11
14
  interface UseMaskInputOptions {
12
15
  mask: Mask;
@@ -24,11 +27,12 @@ interface UseMaskInputOptions {
24
27
  * @param props.options - Optional mask configuration options
25
28
  * @returns A ref callback function to attach to the input element
26
29
  */
27
- export default function useMaskInput(props: UseMaskInputOptions): ((input: Input | null) => void) {
30
+ export default function useMaskInput(props: UseMaskInputOptions): UseMaskInputReturn {
28
31
  const { mask, register, options } = props;
29
32
  const ref = useRef<HTMLInputElement | null>(null);
30
33
  const maskRef = useRef(mask);
31
34
  const optionsRef = useRef(options);
35
+ const unmaskedValue = useCallback(() => getUnmaskedValue(ref.current), []);
32
36
 
33
37
  const refCallback = useCallback((input: Input | null): void => {
34
38
  if (!input) {
@@ -46,10 +50,12 @@ export default function useMaskInput(props: UseMaskInputOptions): ((input: Input
46
50
  }, [register]);
47
51
 
48
52
  if (isServer) {
49
- return (): void => {
53
+ const noop = (() => {
50
54
  // server doesn't have dom, so just do nothing
51
- };
55
+ }) as unknown as UseMaskInputReturn;
56
+
57
+ return setUnmaskedValue(noop, () => '');
52
58
  }
53
59
 
54
- return refCallback;
60
+ return setUnmaskedValue(refCallback, unmaskedValue);
55
61
  }
@@ -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
+ }
@@ -18,6 +18,18 @@ vi.mock('inputmask', () => ({
18
18
  })),
19
19
  }));
20
20
 
21
+ const createRegister = (
22
+ overrides: Partial<UseHookFormMaskReturn<FieldValues>> = {},
23
+ ): UseHookFormMaskReturn<FieldValues> => ({
24
+ prevRef: vi.fn(),
25
+ ref: vi.fn(),
26
+ onChange: vi.fn(),
27
+ onBlur: vi.fn(),
28
+ name: 'phone',
29
+ unmaskedValue: vi.fn(() => ''),
30
+ ...overrides,
31
+ });
32
+
21
33
  describe('withHookFormMask', () => {
22
34
  beforeEach(() => {
23
35
  vi.clearAllMocks();
@@ -25,13 +37,7 @@ describe('withHookFormMask', () => {
25
37
 
26
38
  it('returns register object with masked ref', () => {
27
39
  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
- };
40
+ const register = createRegister({ ref: originalRef });
35
41
  const maskFn = vi.fn();
36
42
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
37
43
 
@@ -42,18 +48,13 @@ describe('withHookFormMask', () => {
42
48
  expect(result.onChange).toBe(register.onChange);
43
49
  expect(result.onBlur).toBe(register.onBlur);
44
50
  expect(result.name).toBe(register.name);
51
+ expect(typeof result.unmaskedValue).toBe('function');
45
52
  });
46
53
 
47
54
  it('applies mask when ref is called', () => {
48
55
  const input = document.createElement('input');
49
56
  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 register = createRegister({ ref: originalRef });
57
58
  const maskFn = vi.fn();
58
59
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
59
60
 
@@ -63,16 +64,27 @@ describe('withHookFormMask', () => {
63
64
  expect(maskFn).toHaveBeenCalled();
64
65
  });
65
66
 
67
+ it('exposes the unmasked value from the masked input', () => {
68
+ const input = document.createElement('input');
69
+ const originalRef = vi.fn();
70
+ const register = createRegister({ ref: originalRef });
71
+ const maskFn = vi.fn();
72
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
73
+
74
+ const result = withHookFormMask(register, '999-999');
75
+ result.ref?.(input);
76
+
77
+ input.inputmask = {
78
+ unmaskedvalue: vi.fn(() => '2026-04-01'),
79
+ } as any;
80
+
81
+ expect(result.unmaskedValue()).toBe('2026-04-01');
82
+ });
83
+
66
84
  it('calls original ref after applying mask', () => {
67
85
  const input = document.createElement('input');
68
86
  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
- };
87
+ const register = createRegister({ ref: originalRef });
76
88
  const maskFn = vi.fn();
77
89
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
78
90
 
@@ -85,13 +97,7 @@ describe('withHookFormMask', () => {
85
97
  it('works with alias masks', () => {
86
98
  const input = document.createElement('input');
87
99
  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
- };
100
+ const register = createRegister({ ref: originalRef, name: 'cpf' });
95
101
  const maskFn = vi.fn();
96
102
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
97
103
 
@@ -104,13 +110,7 @@ describe('withHookFormMask', () => {
104
110
  it('works with custom options', () => {
105
111
  const input = document.createElement('input');
106
112
  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
- };
113
+ const register = createRegister({ ref: originalRef });
114
114
  const maskFn = vi.fn();
115
115
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
116
116
 
@@ -121,13 +121,10 @@ describe('withHookFormMask', () => {
121
121
  });
122
122
 
123
123
  it('handles null ref gracefully', () => {
124
- const register: UseHookFormMaskReturn<FieldValues> = {
124
+ const register = createRegister({
125
125
  prevRef: null as unknown as RefCallback<HTMLElement | null>,
126
126
  ref: null as unknown as RefCallback<HTMLElement | null>,
127
- onChange: vi.fn(),
128
- onBlur: vi.fn(),
129
- name: 'phone',
130
- };
127
+ });
131
128
 
132
129
  const result = withHookFormMask(register, '999-999');
133
130
  expect(result.ref).toBeNull();
@@ -137,13 +134,7 @@ describe('withHookFormMask', () => {
137
134
 
138
135
  it('handles null input in ref callback', () => {
139
136
  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
- };
137
+ const register = createRegister({ ref: originalRef });
147
138
  const maskFn = vi.fn();
148
139
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
149
140
 
@@ -155,13 +146,7 @@ describe('withHookFormMask', () => {
155
146
 
156
147
  it('returns the same ref callback reference across multiple calls (stable identity)', () => {
157
148
  const originalRef = vi.fn();
158
- const register: UseHookFormMaskReturn<FieldValues> = {
159
- prevRef: vi.fn(),
160
- ref: originalRef,
161
- onChange: vi.fn(),
162
- onBlur: vi.fn(),
163
- name: 'phone',
164
- };
149
+ const register = createRegister({ ref: originalRef });
165
150
 
166
151
  const first = withHookFormMask(register, '999-999');
167
152
  const second = withHookFormMask(register, '999-999');
@@ -171,20 +156,8 @@ describe('withHookFormMask', () => {
171
156
 
172
157
  it('returns different ref callbacks for different field/mask combinations', () => {
173
158
  const originalRef = vi.fn();
174
- const registerPhone: UseHookFormMaskReturn<FieldValues> = {
175
- prevRef: vi.fn(),
176
- ref: originalRef,
177
- onChange: vi.fn(),
178
- onBlur: vi.fn(),
179
- name: 'phone',
180
- };
181
- const registerCpf: UseHookFormMaskReturn<FieldValues> = {
182
- prevRef: vi.fn(),
183
- ref: originalRef,
184
- onChange: vi.fn(),
185
- onBlur: vi.fn(),
186
- name: 'cpf',
187
- };
159
+ const registerPhone = createRegister({ ref: originalRef, name: 'phone' });
160
+ const registerCpf = createRegister({ ref: originalRef, name: 'cpf' });
188
161
 
189
162
  const phone = withHookFormMask(registerPhone, '999-999');
190
163
  const cpf = withHookFormMask(registerCpf, 'cpf');
@@ -195,12 +168,8 @@ describe('withHookFormMask', () => {
195
168
  it('returns a new ref callback when the original ref changes', () => {
196
169
  const ref1 = vi.fn();
197
170
  const ref2 = vi.fn();
198
- const register1: UseHookFormMaskReturn<FieldValues> = {
199
- prevRef: vi.fn(), ref: ref1, onChange: vi.fn(), onBlur: vi.fn(), name: 'phone',
200
- };
201
- const register2: UseHookFormMaskReturn<FieldValues> = {
202
- prevRef: vi.fn(), ref: ref2, onChange: vi.fn(), onBlur: vi.fn(), name: 'phone',
203
- };
171
+ const register1 = createRegister({ prevRef: vi.fn(), ref: ref1 });
172
+ const register2 = createRegister({ prevRef: vi.fn(), ref: ref2 });
204
173
 
205
174
  const result1 = withHookFormMask(register1, '999-999');
206
175
  const result2 = withHookFormMask(register2, '999-999');
@@ -1,5 +1,7 @@
1
1
  import { applyMaskToElement } from '../core';
2
- import { flow, makeMaskCacheKey, setPrevRef } from '../utils';
2
+ import {
3
+ getUnmaskedValue, makeMaskCacheKey, setPrevRef, setUnmaskedValue,
4
+ } from '../utils';
3
5
 
4
6
  import type { RefCallback } from 'react';
5
7
  import type { FieldValues } from 'react-hook-form';
@@ -8,6 +10,10 @@ import type {
8
10
  Mask, Options, UseFormRegisterReturn, UseHookFormMaskReturn,
9
11
  } from '../types';
10
12
 
13
+ type MaskedRefCallback = RefCallback<HTMLElement | null> & {
14
+ currentElement?: HTMLElement | null;
15
+ };
16
+
11
17
  const refCache = new WeakMap<
12
18
  RefCallback<HTMLElement | null>,
13
19
  Map<string, RefCallback<HTMLElement | null>>
@@ -36,6 +42,7 @@ export default function withHookFormMask(
36
42
  ...register,
37
43
  ref: null as unknown as RefCallback<HTMLElement | null>,
38
44
  } as UseHookFormMaskReturn<FieldValues>;
45
+ setUnmaskedValue(result, () => '');
39
46
  setPrevRef(result, ref);
40
47
  return result;
41
48
  }
@@ -47,20 +54,21 @@ export default function withHookFormMask(
47
54
  const cacheKey = makeMaskCacheKey(register.name, mask);
48
55
 
49
56
  if (!maskCache?.has(cacheKey)) {
50
- const applyMaskToRef = (_ref: HTMLElement | null) => {
51
- if (_ref) applyMaskToElement(_ref, mask, options);
52
- return _ref;
53
- };
54
- maskCache?.set(
55
- cacheKey,
56
- flow(applyMaskToRef, ref) as RefCallback<HTMLElement | null>,
57
- );
57
+ const maskedRef = ((input: HTMLElement | null) => {
58
+ maskedRef.currentElement = input;
59
+ if (input) applyMaskToElement(input, mask, options);
60
+ return ref(input);
61
+ }) as MaskedRefCallback;
62
+
63
+ maskCache?.set(cacheKey, maskedRef);
58
64
  }
59
65
 
66
+ const maskedRef = maskCache?.get(cacheKey) as MaskedRefCallback | undefined;
60
67
  const result = {
61
68
  ...register,
62
- ref: maskCache?.get(cacheKey),
69
+ ref: maskedRef,
63
70
  } as UseHookFormMaskReturn<FieldValues>;
71
+ setUnmaskedValue(result, () => getUnmaskedValue(maskedRef?.currentElement ?? null));
64
72
 
65
73
  setPrevRef(result, ref);
66
74
 
@@ -24,6 +24,7 @@ describe('withMask', () => {
24
24
  it('returns a function', () => {
25
25
  const result = withMask('999-999');
26
26
  expect(typeof result).toBe('function');
27
+ expect(typeof result.unmaskedValue).toBe('function');
27
28
  });
28
29
 
29
30
  it('applies mask to input element', () => {
@@ -37,6 +38,21 @@ describe('withMask', () => {
37
38
  expect(maskFn).toHaveBeenCalledWith(input);
38
39
  });
39
40
 
41
+ it('exposes the unmasked value from the masked input', () => {
42
+ const input = document.createElement('input');
43
+ const maskFn = vi.fn();
44
+ vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
45
+
46
+ const refCallback = withMask('999-999');
47
+ refCallback(input);
48
+
49
+ input.inputmask = {
50
+ unmaskedvalue: vi.fn(() => '2026-04-01'),
51
+ } as any;
52
+
53
+ expect(refCallback.unmaskedValue()).toBe('2026-04-01');
54
+ });
55
+
40
56
  it('does nothing if input is null', () => {
41
57
  const maskFn = vi.fn();
42
58
  vi.mocked(inputmask).mockReturnValue({ mask: maskFn } as any);
@@ -2,13 +2,13 @@
2
2
  import inputmask from 'inputmask';
3
3
 
4
4
  import { getMaskOptions } from '../core/maskConfig';
5
- import { makeMaskCacheKey } from '../utils';
5
+ import { getUnmaskedValue, makeMaskCacheKey, setUnmaskedValue } from '../utils';
6
6
  import isServer from '../utils/isServer';
7
7
  import interopDefaultSync from '../utils/moduleInterop';
8
8
 
9
- import type { Input, Mask, Options } from '../types';
9
+ import type { Input, Mask, Options, UseMaskInputReturn } from '../types';
10
10
 
11
- const callbackCache = new Map<string, (input: Input | null) => void>();
11
+ const callbackCache = new Map<string, UseMaskInputReturn>();
12
12
 
13
13
  /**
14
14
  * Higher-order function that creates a ref callback for applying input masks.
@@ -18,26 +18,29 @@ const callbackCache = new Map<string, (input: Input | null) => void>();
18
18
  * @param options - Optional mask configuration options
19
19
  * @returns A ref callback function that applies the mask
20
20
  */
21
- export default function withMask(mask: Mask, options?: Options): ((input: Input | null) => void) {
21
+ export default function withMask(mask: Mask, options?: Options): UseMaskInputReturn {
22
22
  // without options, we cant cache, so we always return a fresh callback. :P
23
23
  if (!options) {
24
24
  const cacheKey = makeMaskCacheKey('', mask);
25
25
  if (callbackCache.has(cacheKey)) {
26
- return callbackCache.get(cacheKey) as (input: Input | null) => void;
26
+ return callbackCache.get(cacheKey) as UseMaskInputReturn;
27
27
  }
28
28
  }
29
29
 
30
- const callback = (input: Input | null): void => {
30
+ let currentInput: Input | null = null;
31
+
32
+ const callback = ((input: Input | null): void => {
31
33
  if (isServer || mask === null || !input) return;
32
34
 
35
+ currentInput = input;
33
36
  const maskInput = interopDefaultSync(inputmask)(getMaskOptions(mask, options));
34
37
  maskInput.mask(input as HTMLElement);
35
- };
38
+ }) as UseMaskInputReturn;
36
39
 
37
40
  if (!options) {
38
41
  const cacheKey = makeMaskCacheKey('', mask);
39
42
  callbackCache.set(cacheKey, callback);
40
43
  }
41
44
 
42
- return callback;
45
+ return setUnmaskedValue(callback, () => getUnmaskedValue(currentInput));
43
46
  }
@@ -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
+ });