@react-native-ama/forms 1.2.1 → 2.0.0-beta.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/dist/components/Form.d.ts +5 -2
- package/dist/components/Form.js +14 -31
- package/dist/components/FormSubmit.d.ts +7 -9
- package/dist/components/FormSubmit.js +2 -14
- package/dist/components/TextInput.d.ts +36 -4
- package/dist/components/TextInput.js +20 -73
- package/dist/hooks/useFocus.d.ts +4 -0
- package/dist/hooks/useFocus.js +69 -0
- package/dist/hooks/useFormField.d.ts +1 -7
- package/dist/hooks/useFormField.js +32 -25
- package/dist/hooks/useFormSubmit.d.ts +4 -0
- package/dist/hooks/useFormSubmit.js +14 -0
- package/dist/hooks/useTextInput.d.ts +59 -231
- package/dist/hooks/useTextInput.js +2 -37
- package/dist/index.d.ts +9 -12
- package/dist/index.js +3 -8
- package/dist/utils/checkFocusTrap.d.ts +8 -0
- package/dist/utils/checkFocusTrap.js +30 -0
- package/package.json +59 -18
- package/src/components/Form.test.tsx +63 -70
- package/src/components/Form.tsx +28 -52
- package/src/components/FormField.test.tsx +32 -16
- package/src/components/FormField.tsx +0 -1
- package/src/components/FormSubmit.test.tsx +17 -55
- package/src/components/FormSubmit.tsx +9 -36
- package/src/components/TextInput.test.tsx +83 -397
- package/src/components/TextInput.tsx +41 -109
- package/src/hooks/useFocus.test.ts +130 -0
- package/src/hooks/useFocus.ts +62 -0
- package/src/hooks/useFormField.test.tsx +183 -78
- package/src/hooks/useFormField.ts +36 -28
- package/src/hooks/useFormSubmit.test.tsx +18 -0
- package/src/hooks/useFormSubmit.ts +17 -0
- package/src/hooks/useTextInput.ts +10 -55
- package/src/index.ts +14 -19
- package/src/utils/checkFocusTrap.test.ts +80 -0
- package/src/utils/checkFocusTrap.ts +40 -0
- package/dist/components/FormSwitch.d.ts +0 -4
- package/dist/components/FormSwitch.js +0 -15
- package/src/components/FormSwitch.tsx +0 -17
|
@@ -1,58 +1,64 @@
|
|
|
1
|
-
import { HideChildrenFromAccessibilityTree } from '@react-native-ama/core';
|
|
2
|
-
import { useChecks } from '@react-native-ama/core';
|
|
3
|
-
import { applyStyle } from '@react-native-ama/internal';
|
|
4
|
-
import { maybeGenerateStringFromElement } from '@react-native-ama/internal';
|
|
5
1
|
import * as React from 'react';
|
|
6
2
|
import {
|
|
7
3
|
TextInput as RNTextInput,
|
|
8
4
|
TextInputProps as RNTextInputProps,
|
|
9
5
|
} from 'react-native';
|
|
10
|
-
|
|
11
6
|
import { UseTextInput, useTextInput } from '../hooks/useTextInput';
|
|
12
7
|
|
|
13
|
-
export
|
|
8
|
+
export const a11yProps: Pick<TextInputProps, 'accessibilityElementsHidden' | 'importantForAccessibility'> = {
|
|
9
|
+
importantForAccessibility: 'no-hide-descendants',
|
|
10
|
+
accessibilityElementsHidden: true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type A11yProps = typeof a11yProps;
|
|
14
|
+
|
|
15
|
+
type TextInputBaseProps = Omit<RNTextInputProps, 'accessibilityLabel' | 'aria-label'> &
|
|
14
16
|
Omit<UseTextInput, 'required' | 'accessibilityLabel'> & {
|
|
15
|
-
labelComponent: JSX.Element;
|
|
16
17
|
labelPosition?: 'beforeInput' | 'afterInput';
|
|
17
18
|
nextFormField?: React.RefObject<RNTextInput>;
|
|
18
19
|
id?: string;
|
|
19
20
|
nextFieldId?: string;
|
|
20
|
-
errorComponent?: JSX.Element;
|
|
21
21
|
errorPosition?: 'belowLabel' | 'afterInput';
|
|
22
|
+
renderLabel?: (a11yProps: A11yProps) => React.ReactNode;
|
|
23
|
+
renderError?: (a11yProps: A11yProps) => React.ReactNode;
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
// Require either `accessibilityLabel` or `aria-label` (but at least one)
|
|
27
|
+
export type TextInputProps =
|
|
28
|
+
| (TextInputBaseProps & { accessibilityLabel: string; 'aria-label'?: never })
|
|
29
|
+
| (TextInputBaseProps & { 'aria-label': string; accessibilityLabel?: never });
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const labelA11yProps: A11yProps = {
|
|
33
|
+
importantForAccessibility: 'no-hide-descendants',
|
|
34
|
+
accessibilityElementsHidden: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
24
37
|
export const TextInput = React.forwardRef<RNTextInput, TextInputProps>(
|
|
25
38
|
(
|
|
26
39
|
{
|
|
27
|
-
labelComponent,
|
|
28
|
-
labelPosition = 'beforeInput',
|
|
29
40
|
nextFormField,
|
|
30
41
|
id,
|
|
31
42
|
nextFieldId,
|
|
32
43
|
accessibilityHint,
|
|
33
44
|
hasError,
|
|
34
|
-
errorComponent,
|
|
35
|
-
errorPosition = 'afterInput',
|
|
36
45
|
hasValidation,
|
|
37
46
|
errorMessage,
|
|
47
|
+
accessibilityLabel,
|
|
48
|
+
labelPosition = 'beforeInput',
|
|
49
|
+
errorPosition = 'afterInput',
|
|
50
|
+
renderLabel,
|
|
51
|
+
renderError,
|
|
38
52
|
...rest
|
|
39
53
|
},
|
|
40
|
-
forwardedRef
|
|
54
|
+
forwardedRef
|
|
41
55
|
) => {
|
|
42
|
-
const inputRef = React.useRef<
|
|
43
|
-
null
|
|
56
|
+
const inputRef = React.useRef<RNTextInput | null>(
|
|
57
|
+
null
|
|
44
58
|
);
|
|
45
59
|
|
|
46
60
|
React.useImperativeHandle(forwardedRef, () => inputRef.current!);
|
|
47
61
|
|
|
48
|
-
const showLabelBeforeInput = labelPosition === 'beforeInput';
|
|
49
|
-
|
|
50
|
-
const accessibilityLabel = React.useMemo(
|
|
51
|
-
() =>
|
|
52
|
-
maybeGenerateStringFromElement(labelComponent, rest.accessibilityLabel),
|
|
53
|
-
[labelComponent, rest.accessibilityLabel],
|
|
54
|
-
);
|
|
55
|
-
|
|
56
62
|
const textInputProps = useTextInput({
|
|
57
63
|
...rest,
|
|
58
64
|
ref: inputRef,
|
|
@@ -63,103 +69,29 @@ export const TextInput = React.forwardRef<RNTextInput, TextInputProps>(
|
|
|
63
69
|
hasValidation,
|
|
64
70
|
errorMessage,
|
|
65
71
|
required: false,
|
|
66
|
-
accessibilityLabel,
|
|
72
|
+
accessibilityLabel: accessibilityLabel || rest?.['aria-label'],
|
|
67
73
|
});
|
|
68
74
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
checks?.noUndefinedProperty({
|
|
72
|
-
properties: { labelComponent },
|
|
73
|
-
property: 'labelComponent',
|
|
74
|
-
rule: 'NO_FORM_LABEL',
|
|
75
|
-
});
|
|
76
|
-
__DEV__ &&
|
|
77
|
-
checks?.noUndefinedProperty({
|
|
78
|
-
properties: { accessibilityLabel },
|
|
79
|
-
property: 'accessibilityLabel',
|
|
80
|
-
rule: 'NO_ACCESSIBILITY_LABEL',
|
|
81
|
-
});
|
|
82
|
-
__DEV__ &&
|
|
83
|
-
accessibilityLabel?.trim() === '' &&
|
|
84
|
-
checks?.logResult('NO_ACCESSIBILITY_LABEL', {
|
|
85
|
-
rule: 'NO_ACCESSIBILITY_LABEL',
|
|
86
|
-
message: 'No accessibility label provided',
|
|
87
|
-
});
|
|
88
|
-
__DEV__ &&
|
|
89
|
-
hasValidation &&
|
|
90
|
-
checks?.noUndefinedProperty({
|
|
91
|
-
// @ts-ignore
|
|
92
|
-
properties: { errorComponent },
|
|
93
|
-
property: 'errorComponent',
|
|
94
|
-
rule: 'NO_FORM_ERROR',
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const style =
|
|
98
|
-
__DEV__ && // @ts-ignore
|
|
99
|
-
applyStyle?.({
|
|
100
|
-
// @ts-ignore
|
|
101
|
-
style: textInputProps?.style || {},
|
|
102
|
-
debugStyle: checks?.debugStyle,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const accessibilityHiddenLabel = React.useMemo(
|
|
106
|
-
() => (
|
|
107
|
-
<HideChildrenFromAccessibilityTree>
|
|
108
|
-
{labelComponent}
|
|
109
|
-
</HideChildrenFromAccessibilityTree>
|
|
110
|
-
),
|
|
111
|
-
[labelComponent],
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const errorText = React.useMemo(() => {
|
|
115
|
-
return hasValidation && hasError
|
|
116
|
-
? maybeGenerateStringFromElement(errorComponent, errorMessage)
|
|
117
|
-
: '';
|
|
118
|
-
}, [hasValidation, hasError, errorComponent, errorMessage]);
|
|
119
|
-
|
|
120
|
-
const renderError = () => {
|
|
121
|
-
return (
|
|
122
|
-
<HideChildrenFromAccessibilityTree>
|
|
123
|
-
{errorComponent}
|
|
124
|
-
</HideChildrenFromAccessibilityTree>
|
|
125
|
-
);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const fullAccessibilityHint = [accessibilityHint || '', errorText || '']
|
|
129
|
-
.filter(item => item.length > 0)
|
|
75
|
+
const fullAccessibilityHint = [accessibilityHint, errorMessage]
|
|
76
|
+
.filter(Boolean)
|
|
130
77
|
?.join(', ');
|
|
131
78
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{hasError && errorPosition === 'belowLabel' ? renderError() : null}
|
|
136
|
-
<RNTextInput
|
|
137
|
-
// @ts-ignore
|
|
138
|
-
ref={inputRef}
|
|
139
|
-
{...rest}
|
|
140
|
-
{...textInputProps}
|
|
141
|
-
style={style}
|
|
142
|
-
accessibilityLabel={accessibilityLabel!}
|
|
143
|
-
accessibilityHint={fullAccessibilityHint}
|
|
144
|
-
/>
|
|
145
|
-
{showLabelBeforeInput ? null : accessibilityHiddenLabel}
|
|
146
|
-
{hasError && errorPosition === 'afterInput' ? renderError() : null}
|
|
147
|
-
</>
|
|
148
|
-
) : (
|
|
79
|
+
const showError = hasError && renderError;
|
|
80
|
+
|
|
81
|
+
return (
|
|
149
82
|
<>
|
|
150
|
-
{
|
|
151
|
-
{
|
|
83
|
+
{labelPosition === 'beforeInput' ? renderLabel?.(labelA11yProps) : null}
|
|
84
|
+
{errorPosition === 'belowLabel' && showError ? renderError?.(labelA11yProps) : null}
|
|
152
85
|
<RNTextInput
|
|
153
|
-
// @ts-ignore
|
|
154
86
|
ref={inputRef}
|
|
155
87
|
{...rest}
|
|
156
88
|
{...textInputProps}
|
|
157
|
-
accessibilityLabel={accessibilityLabel
|
|
89
|
+
accessibilityLabel={accessibilityLabel ?? rest['aria-label']}
|
|
158
90
|
accessibilityHint={fullAccessibilityHint}
|
|
159
91
|
/>
|
|
160
|
-
{
|
|
161
|
-
{
|
|
92
|
+
{labelPosition === 'afterInput' ? renderLabel?.(labelA11yProps) : null}
|
|
93
|
+
{errorPosition === 'afterInput' && showError ? renderError?.(labelA11yProps) : null}
|
|
162
94
|
</>
|
|
163
95
|
);
|
|
164
|
-
}
|
|
96
|
+
}
|
|
165
97
|
);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
2
|
+
import { useFocus } from './useFocus';
|
|
3
|
+
|
|
4
|
+
const rn = jest.requireMock('react-native');
|
|
5
|
+
|
|
6
|
+
// useFocus uses SHELL_COLORS as a global — define it for the test environment
|
|
7
|
+
(global as any).SHELL_COLORS = {
|
|
8
|
+
BG_RED: '', RESET: '', BLUE: '', YELLOW: '',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
global.__DEV__ = true;
|
|
15
|
+
rn.InteractionManager.runAfterInteractions = jest.fn((cb) => cb());
|
|
16
|
+
rn.findNodeHandle = jest.fn(() => 42);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
global.__DEV__ = true;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('useFocus', () => {
|
|
25
|
+
it('returns a setFocus function', () => {
|
|
26
|
+
const { result } = renderHook(() => useFocus());
|
|
27
|
+
expect(typeof result.current.setFocus).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does nothing when called with null', () => {
|
|
31
|
+
const { result } = renderHook(() => useFocus());
|
|
32
|
+
act(() => {
|
|
33
|
+
result.current.setFocus(null);
|
|
34
|
+
});
|
|
35
|
+
expect(rn.AccessibilityInfo.setAccessibilityFocus).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('calls setAccessibilityFocus when a valid node handle is found', () => {
|
|
39
|
+
jest.useFakeTimers();
|
|
40
|
+
rn.findNodeHandle = jest.fn(() => 42);
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useFocus());
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current.setFocus({} as any);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(rn.AccessibilityInfo.setAccessibilityFocus).toHaveBeenCalledWith(42);
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
jest.runAllTimers();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(rn.AccessibilityInfo.setAccessibilityFocus).toHaveBeenCalledTimes(2);
|
|
54
|
+
jest.useRealTimers();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('warns in __DEV__ mode when node handle is not found', () => {
|
|
58
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
59
|
+
rn.findNodeHandle = jest.fn(() => null);
|
|
60
|
+
|
|
61
|
+
const { result } = renderHook(() => useFocus());
|
|
62
|
+
act(() => {
|
|
63
|
+
result.current.setFocus({} as any);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
67
|
+
warnSpy.mockRestore();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not warn when __DEV__ is false and node handle is not found', () => {
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
global.__DEV__ = false;
|
|
73
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
74
|
+
rn.findNodeHandle = jest.fn(() => null);
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useFocus());
|
|
77
|
+
act(() => {
|
|
78
|
+
result.current.setFocus({} as any);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
82
|
+
warnSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('warns in __DEV__ when findNodeHandle throws', () => {
|
|
86
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
87
|
+
rn.findNodeHandle = jest.fn(() => {
|
|
88
|
+
throw new Error('boom');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(() => useFocus());
|
|
92
|
+
act(() => {
|
|
93
|
+
result.current.setFocus({} as any);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
97
|
+
warnSpy.mockRestore();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not warn when __DEV__ is false and findNodeHandle throws', () => {
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
global.__DEV__ = false;
|
|
103
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
104
|
+
rn.findNodeHandle = jest.fn(() => {
|
|
105
|
+
throw new Error('boom');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { result } = renderHook(() => useFocus());
|
|
109
|
+
act(() => {
|
|
110
|
+
result.current.setFocus({} as any);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
114
|
+
warnSpy.mockRestore();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('auto-focuses refComponent on mount when provided', () => {
|
|
118
|
+
rn.findNodeHandle = jest.fn(() => 99);
|
|
119
|
+
const ref = { current: {} };
|
|
120
|
+
|
|
121
|
+
renderHook(() => useFocus(ref));
|
|
122
|
+
|
|
123
|
+
expect(rn.AccessibilityInfo.setAccessibilityFocus).toHaveBeenCalledWith(99);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not auto-focus when refComponent is not provided', () => {
|
|
127
|
+
renderHook(() => useFocus());
|
|
128
|
+
expect(rn.AccessibilityInfo.setAccessibilityFocus).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccessibilityInfo,
|
|
4
|
+
findNodeHandle,
|
|
5
|
+
InteractionManager,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
|
|
8
|
+
export const useFocus = (refComponent?: React.RefObject<any>) => {
|
|
9
|
+
const setFocus = React.useCallback(
|
|
10
|
+
(
|
|
11
|
+
component:
|
|
12
|
+
| null
|
|
13
|
+
| number
|
|
14
|
+
| React.Component<any, any>
|
|
15
|
+
| React.ComponentClass<any>
|
|
16
|
+
) => {
|
|
17
|
+
if (!component) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
InteractionManager.runAfterInteractions(() => {
|
|
22
|
+
try {
|
|
23
|
+
const elementId = findNodeHandle(component);
|
|
24
|
+
|
|
25
|
+
if (elementId) {
|
|
26
|
+
AccessibilityInfo.setAccessibilityFocus(elementId);
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
AccessibilityInfo.setAccessibilityFocus(elementId); //ISSUE: https://github.com/facebook/react-native/issues/30097
|
|
29
|
+
}, 100);
|
|
30
|
+
} else if (__DEV__) {
|
|
31
|
+
console.warn(
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
`${SHELL_COLORS.BG_RED}AMA.${SHELL_COLORS.RESET} ${SHELL_COLORS.BLUE}useFocus${SHELL_COLORS.RESET}: ${SHELL_COLORS.YELLOW}Ref element not found${SHELL_COLORS.RESET}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
console.warn(
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
`${SHELL_COLORS.BG_RED}AMA.${SHELL_COLORS.RESET} ${SHELL_COLORS.BLUE}useFocus${SHELL_COLORS.RESET}: ${SHELL_COLORS.YELLOW}Error finding node handle${SHELL_COLORS.RESET} \n`,
|
|
41
|
+
error
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
[]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!refComponent) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setFocus(refComponent.current);
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [refComponent]);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
setFocus,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { renderHook } from '@testing-library/react-native';
|
|
3
|
-
import { waitFor } from '@testing-library/react-native';
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react-native';
|
|
4
2
|
import * as React from 'react';
|
|
5
|
-
|
|
6
3
|
import * as Form from '../components/Form';
|
|
7
4
|
import { useFormField } from './useFormField';
|
|
5
|
+
import * as CheckFocusTrapModule from '../utils/checkFocusTrap';
|
|
8
6
|
|
|
9
7
|
beforeEach(() => {
|
|
10
8
|
jest.clearAllMocks();
|
|
@@ -16,10 +14,6 @@ describe('useFormField', () => {
|
|
|
16
14
|
let localRef: any[] = [];
|
|
17
15
|
|
|
18
16
|
beforeEach(() => {
|
|
19
|
-
jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
|
|
20
|
-
isScreenReaderEnabled: true,
|
|
21
|
-
} as any);
|
|
22
|
-
|
|
23
17
|
jest.spyOn(Form, 'useForm').mockImplementation(() => {
|
|
24
18
|
return { refs: localRef, submitForm };
|
|
25
19
|
});
|
|
@@ -75,51 +69,7 @@ describe('useFormField', () => {
|
|
|
75
69
|
});
|
|
76
70
|
|
|
77
71
|
describe('focusNextFormField', () => {
|
|
78
|
-
describe('Given the form field is not the last of the refs', () => {
|
|
79
|
-
describe('checks for focus when calling focusNextFormField', function () {
|
|
80
|
-
it('checks only against the current field if next one does not have the focus callback', async () => {
|
|
81
|
-
const focus = jest.fn();
|
|
82
|
-
const ref = { current: { focus, isFocused: jest.fn() } };
|
|
83
|
-
|
|
84
|
-
const { result } = renderHook(
|
|
85
|
-
() => {
|
|
86
|
-
const first = useFormField({
|
|
87
|
-
ref: { current: 'random ref' },
|
|
88
|
-
hasFocusCallback: true,
|
|
89
|
-
hasValidation: false,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// next field
|
|
93
|
-
useFormField({
|
|
94
|
-
ref,
|
|
95
|
-
hasFocusCallback: false,
|
|
96
|
-
hasValidation: false,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return first;
|
|
100
|
-
},
|
|
101
|
-
{ wrapper: renderHookWrapper },
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
result.current.focusNextFormField();
|
|
105
|
-
|
|
106
|
-
await waitFor(() =>
|
|
107
|
-
expect(checkFocusTrap).toHaveBeenCalledWith({
|
|
108
|
-
ref: { current: 'random ref' },
|
|
109
|
-
shouldHaveFocus: false,
|
|
110
|
-
}),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
expect(checkFocusTrap).toHaveBeenCalledTimes(1);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
72
|
it('triggers submitForm when the element is the last one of the ref', async () => {
|
|
119
|
-
jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
|
|
120
|
-
isScreenReaderEnabled: true,
|
|
121
|
-
} as any);
|
|
122
|
-
|
|
123
73
|
const ref = { current: 'whatever' };
|
|
124
74
|
|
|
125
75
|
const { result } = renderHook(
|
|
@@ -146,38 +96,193 @@ describe('useFormField', () => {
|
|
|
146
96
|
expect(submitForm).toHaveBeenCalledWith();
|
|
147
97
|
});
|
|
148
98
|
});
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
99
|
|
|
152
|
-
|
|
153
|
-
|
|
100
|
+
it('calls focusField with the next ref when not the last field', async () => {
|
|
101
|
+
const focusField = jest.fn();
|
|
102
|
+
jest.spyOn(Form, 'useForm').mockImplementation(() => ({
|
|
103
|
+
refs: localRef,
|
|
104
|
+
submitForm,
|
|
105
|
+
focusField,
|
|
106
|
+
}));
|
|
154
107
|
|
|
155
|
-
|
|
156
|
-
|
|
108
|
+
const { result } = renderHook(
|
|
109
|
+
() => {
|
|
110
|
+
// first field
|
|
111
|
+
const first = useFormField({
|
|
112
|
+
ref: { current: 'first' },
|
|
113
|
+
hasFocusCallback: false,
|
|
114
|
+
hasValidation: false,
|
|
115
|
+
});
|
|
157
116
|
|
|
158
|
-
|
|
117
|
+
// second field
|
|
118
|
+
useFormField({
|
|
119
|
+
ref: { current: 'second' },
|
|
120
|
+
hasFocusCallback: false,
|
|
121
|
+
hasValidation: false,
|
|
122
|
+
});
|
|
159
123
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
setFocus,
|
|
165
|
-
};
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
}
|
|
124
|
+
return first;
|
|
125
|
+
},
|
|
126
|
+
{ wrapper: renderHookWrapper },
|
|
127
|
+
);
|
|
169
128
|
|
|
170
|
-
|
|
171
|
-
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
result.current.focusNextFormField();
|
|
131
|
+
expect(focusField).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
172
134
|
|
|
173
|
-
|
|
135
|
+
it('uses nextFormFieldRef when provided', async () => {
|
|
136
|
+
const focusField = jest.fn();
|
|
137
|
+
jest.spyOn(Form, 'useForm').mockImplementation(() => ({
|
|
138
|
+
refs: localRef,
|
|
139
|
+
submitForm,
|
|
140
|
+
focusField,
|
|
141
|
+
}));
|
|
174
142
|
|
|
175
|
-
|
|
176
|
-
}
|
|
143
|
+
const nextFormFieldRef = { current: 'explicit-next' };
|
|
177
144
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
145
|
+
const { result } = renderHook(
|
|
146
|
+
() => {
|
|
147
|
+
const first = useFormField({
|
|
148
|
+
ref: { current: 'first' },
|
|
149
|
+
hasFocusCallback: false,
|
|
150
|
+
hasValidation: false,
|
|
151
|
+
nextFormFieldRef,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
useFormField({
|
|
155
|
+
ref: { current: 'second' },
|
|
156
|
+
hasFocusCallback: false,
|
|
157
|
+
hasValidation: false,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return first;
|
|
161
|
+
},
|
|
162
|
+
{ wrapper: renderHookWrapper },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
result.current.focusNextFormField();
|
|
167
|
+
expect(focusField).toHaveBeenCalledWith(
|
|
168
|
+
expect.objectContaining({ ref: nextFormFieldRef }),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('calls checkFocusTrap when hasFocusCallback is true and not the last field', async () => {
|
|
174
|
+
const focusField = jest.fn();
|
|
175
|
+
const checkFocusTrapSpy = jest.spyOn(CheckFocusTrapModule as { checkFocusTrap: (...args: any[]) => Promise<void> }, 'checkFocusTrap').mockResolvedValue(undefined);
|
|
176
|
+
jest.spyOn(Form, 'useForm').mockImplementation(() => ({
|
|
177
|
+
refs: localRef,
|
|
178
|
+
submitForm,
|
|
179
|
+
focusField,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
const { result } = renderHook(
|
|
183
|
+
() => {
|
|
184
|
+
const first = useFormField({
|
|
185
|
+
ref: { current: 'first' },
|
|
186
|
+
hasFocusCallback: true,
|
|
187
|
+
hasValidation: false,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
useFormField({
|
|
191
|
+
ref: { current: 'second' },
|
|
192
|
+
hasFocusCallback: false,
|
|
193
|
+
hasValidation: false,
|
|
194
|
+
});
|
|
181
195
|
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
return first;
|
|
197
|
+
},
|
|
198
|
+
{ wrapper: renderHookWrapper },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
result.current.focusNextFormField();
|
|
203
|
+
expect(checkFocusTrapSpy).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({ shouldHaveFocus: false }),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
checkFocusTrapSpy.mockRestore();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('does not call checkFocusTrap when hasFocusCallback is false', async () => {
|
|
212
|
+
const focusField = jest.fn();
|
|
213
|
+
const checkFocusTrapSpy = jest.spyOn(CheckFocusTrapModule as { checkFocusTrap: (...args: any[]) => Promise<void> }, 'checkFocusTrap').mockResolvedValue(undefined);
|
|
214
|
+
jest.spyOn(Form, 'useForm').mockImplementation(() => ({
|
|
215
|
+
refs: localRef,
|
|
216
|
+
submitForm,
|
|
217
|
+
focusField,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
const { result } = renderHook(
|
|
221
|
+
() => {
|
|
222
|
+
const first = useFormField({
|
|
223
|
+
ref: { current: 'first' },
|
|
224
|
+
hasFocusCallback: false,
|
|
225
|
+
hasValidation: false,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
useFormField({
|
|
229
|
+
ref: { current: 'second' },
|
|
230
|
+
hasFocusCallback: false,
|
|
231
|
+
hasValidation: false,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return first;
|
|
235
|
+
},
|
|
236
|
+
{ wrapper: renderHookWrapper },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await waitFor(() => {
|
|
240
|
+
result.current.focusNextFormField();
|
|
241
|
+
expect(checkFocusTrapSpy).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
checkFocusTrapSpy.mockRestore();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('uses nextFieldId to find the next ref when provided', async () => {
|
|
248
|
+
const focusField = jest.fn();
|
|
249
|
+
jest.spyOn(Form, 'useForm').mockImplementation(() => ({
|
|
250
|
+
refs: localRef,
|
|
251
|
+
submitForm,
|
|
252
|
+
focusField,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
const { result } = renderHook(
|
|
256
|
+
() => {
|
|
257
|
+
const first = useFormField({
|
|
258
|
+
ref: { current: 'first' },
|
|
259
|
+
hasFocusCallback: false,
|
|
260
|
+
hasValidation: false,
|
|
261
|
+
nextFieldId: 'second-field',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
useFormField({
|
|
265
|
+
ref: { current: 'second' },
|
|
266
|
+
hasFocusCallback: false,
|
|
267
|
+
hasValidation: false,
|
|
268
|
+
id: 'second-field',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return first;
|
|
272
|
+
},
|
|
273
|
+
{ wrapper: renderHookWrapper },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
result.current.focusNextFormField();
|
|
278
|
+
expect(focusField).toHaveBeenCalledWith(
|
|
279
|
+
expect.objectContaining({ id: 'second-field' }),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const renderHookWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
287
|
+
<>{children}</>
|
|
288
|
+
);
|