@react-native-ama/forms 1.2.0 → 2.0.0-beta.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 (40) hide show
  1. package/dist/components/Form.d.ts +5 -2
  2. package/dist/components/Form.js +14 -31
  3. package/dist/components/FormSubmit.d.ts +7 -9
  4. package/dist/components/FormSubmit.js +2 -14
  5. package/dist/components/TextInput.d.ts +36 -4
  6. package/dist/components/TextInput.js +20 -73
  7. package/dist/hooks/useFocus.d.ts +4 -0
  8. package/dist/hooks/useFocus.js +69 -0
  9. package/dist/hooks/useFormField.d.ts +1 -7
  10. package/dist/hooks/useFormField.js +32 -25
  11. package/dist/hooks/useFormSubmit.d.ts +4 -0
  12. package/dist/hooks/useFormSubmit.js +14 -0
  13. package/dist/hooks/useTextInput.d.ts +59 -231
  14. package/dist/hooks/useTextInput.js +2 -37
  15. package/dist/index.d.ts +10 -13
  16. package/dist/index.js +3 -8
  17. package/dist/utils/checkFocusTrap.d.ts +8 -0
  18. package/dist/utils/checkFocusTrap.js +30 -0
  19. package/package.json +59 -18
  20. package/src/components/Form.test.tsx +63 -70
  21. package/src/components/Form.tsx +28 -52
  22. package/src/components/FormField.test.tsx +32 -16
  23. package/src/components/FormField.tsx +0 -1
  24. package/src/components/FormSubmit.test.tsx +17 -55
  25. package/src/components/FormSubmit.tsx +9 -36
  26. package/src/components/TextInput.test.tsx +83 -397
  27. package/src/components/TextInput.tsx +41 -109
  28. package/src/hooks/useFocus.test.ts +130 -0
  29. package/src/hooks/useFocus.ts +62 -0
  30. package/src/hooks/useFormField.test.tsx +183 -78
  31. package/src/hooks/useFormField.ts +36 -28
  32. package/src/hooks/useFormSubmit.test.tsx +18 -0
  33. package/src/hooks/useFormSubmit.ts +17 -0
  34. package/src/hooks/useTextInput.ts +10 -55
  35. package/src/index.ts +15 -20
  36. package/src/utils/checkFocusTrap.test.ts +80 -0
  37. package/src/utils/checkFocusTrap.ts +40 -0
  38. package/dist/components/FormSwitch.d.ts +0 -4
  39. package/dist/components/FormSwitch.js +0 -15
  40. 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 type TextInputProps = RNTextInputProps &
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<React.ElementRef<typeof TextInput> | null>(
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 checks = __DEV__ ? useChecks?.() : undefined;
70
- __DEV__ &&
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
- return __DEV__ ? (
133
- <>
134
- {showLabelBeforeInput ? accessibilityHiddenLabel : null}
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
- {showLabelBeforeInput ? accessibilityHiddenLabel : null}
151
- {hasError && errorPosition === 'belowLabel' ? renderError() : null}
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
- {showLabelBeforeInput ? null : accessibilityHiddenLabel}
161
- {hasError && errorPosition === 'afterInput' ? renderError() : null}
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 * as AMAProvider from '@react-native-ama/core';
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
- let setFocus: jest.Mock;
153
- let checkFocusTrap: jest.Mock;
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
- function mockAMACore() {
156
- const original = jest.requireActual('@react-native-ama/core');
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
- setFocus = jest.fn();
117
+ // second field
118
+ useFormField({
119
+ ref: { current: 'second' },
120
+ hasFocusCallback: false,
121
+ hasValidation: false,
122
+ });
159
123
 
160
- return {
161
- ...original,
162
- useFocus: () => {
163
- return {
164
- setFocus,
165
- };
166
- },
167
- };
168
- }
124
+ return first;
125
+ },
126
+ { wrapper: renderHookWrapper },
127
+ );
169
128
 
170
- function mockAMAInternal() {
171
- const original = jest.requireActual('@react-native-ama/internal');
129
+ await waitFor(() => {
130
+ result.current.focusNextFormField();
131
+ expect(focusField).toHaveBeenCalled();
132
+ });
133
+ });
172
134
 
173
- checkFocusTrap = jest.fn().mockResolvedValue(null);
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
- return { ...original, checkFocusTrap };
176
- }
143
+ const nextFormFieldRef = { current: 'explicit-next' };
177
144
 
178
- const renderHookWrapper = ({ children }) => (
179
- <AMAProvider.AMAProvider>{children}</AMAProvider.AMAProvider>
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
- jest.mock('@react-native-ama/core', () => mockAMACore());
183
- jest.mock('@react-native-ama/internal', () => mockAMAInternal());
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
+ );