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