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.
- package/CHANGELOG.md +12 -0
- package/README.md +55 -7
- package/dist/antd.cjs +14 -12
- package/dist/antd.cjs.map +1 -1
- package/dist/antd.d.cts +3 -2
- package/dist/antd.d.ts +3 -2
- package/dist/antd.js +6 -4
- package/dist/antd.js.map +1 -1
- package/dist/{chunk-X5SEJVSB.cjs → chunk-DTC7JTZP.cjs} +78 -26
- package/dist/{chunk-X5SEJVSB.cjs.map → chunk-DTC7JTZP.cjs.map} +1 -1
- package/dist/{chunk-ICLWBMH4.js → chunk-TVCNC3TP.js} +77 -27
- package/dist/{chunk-ICLWBMH4.js.map → chunk-TVCNC3TP.js.map} +1 -1
- package/dist/{index-S8txl6uK.d.cts → index-D8KkaDbQ.d.cts} +15 -2
- package/dist/{index-S8txl6uK.d.ts → index-D8KkaDbQ.d.ts} +15 -2
- package/dist/index.cjs +83 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +71 -19
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/antd/useMaskInputAntd.spec.tsx +22 -0
- package/src/antd/useMaskInputAntd.ts +11 -7
- package/src/api/index.ts +2 -0
- package/src/api/useHookFormMask.ts +4 -1
- package/src/api/useMaskInput.spec.tsx +18 -0
- package/src/api/useMaskInput.ts +11 -5
- package/src/api/useTanStackFormMask.ts +24 -0
- package/src/api/withHookFormMask.spec.ts +43 -74
- package/src/api/withHookFormMask.ts +18 -10
- package/src/api/withMask.spec.ts +16 -0
- package/src/api/withMask.ts +11 -8
- package/src/api/withTanStackFormMask.spec.ts +76 -0
- package/src/api/withTanStackFormMask.ts +73 -0
- package/src/core/maskEngine.spec.ts +12 -6
- package/src/index.tsx +6 -0
- package/src/types/index.ts +19 -1
- package/src/utils/index.ts +6 -1
- 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.
|
|
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.
|
|
49
|
-
"antd": "^6.3.
|
|
50
|
-
"oxlint": "1.
|
|
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.
|
|
53
|
+
"react-hook-form": "7.72.0",
|
|
54
54
|
"tsup": "8.5.1",
|
|
55
55
|
"typescript": "5.9",
|
|
56
|
-
"vitest": "4.
|
|
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
|
-
|
|
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 {
|
|
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 };
|
package/src/api/useMaskInput.ts
CHANGED
|
@@ -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 {
|
|
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):
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
175
|
-
|
|
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:
|
|
199
|
-
|
|
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 {
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
69
|
+
ref: maskedRef,
|
|
63
70
|
} as UseHookFormMaskReturn<FieldValues>;
|
|
71
|
+
setUnmaskedValue(result, () => getUnmaskedValue(maskedRef?.currentElement ?? null));
|
|
64
72
|
|
|
65
73
|
setPrevRef(result, ref);
|
|
66
74
|
|
package/src/api/withMask.spec.ts
CHANGED
|
@@ -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);
|
package/src/api/withMask.ts
CHANGED
|
@@ -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,
|
|
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):
|
|
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
|
|
26
|
+
return callbackCache.get(cacheKey) as UseMaskInputReturn;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
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
|
+
});
|