@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,9 +1,27 @@
1
- import { useChecks } from '@react-native-ama/core';
2
- import { applyStyle } from '@react-native-ama/internal';
3
1
  import React from 'react';
4
2
  import { Keyboard, ViewStyle } from 'react-native';
5
-
6
3
  import { useForm } from '../components/Form';
4
+ import { checkFocusTrap } from '../utils/checkFocusTrap';
5
+
6
+ let _useAMAContext:
7
+ | (() => {
8
+ trackError?: (rule: string, ref?: React.RefObject<any>) => void;
9
+ } | null)
10
+ | null = null;
11
+ try {
12
+
13
+ _useAMAContext = require('@react-native-ama/core').useAMAContext;
14
+ } catch {
15
+ // core is an optional peer — fall back to null
16
+ }
17
+
18
+ const getTrackError = () => {
19
+ try {
20
+ return _useAMAContext?.()?.trackError ?? null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ };
7
25
 
8
26
  export type UseFormField = {
9
27
  ref?: React.RefObject<any> | React.ForwardedRef<any> | null;
@@ -32,16 +50,14 @@ export const useFormField = ({
32
50
  errorMessage,
33
51
  editable = true,
34
52
  suppressError,
35
- style = {},
36
53
  }: UseFormField) => {
37
54
  const { refs, submitForm, focusField } = useForm({ suppressError });
38
55
  const fieldRef = React.useRef(ref);
39
-
40
- const checks = __DEV__ ? useChecks?.() : undefined;
56
+ const trackError = __DEV__ ? getTrackError() : null;
41
57
 
42
58
  const getMyIndex = () => {
43
59
  const allRefs = refs!;
44
- const item = allRefs.find(r => r.ref === fieldRef);
60
+ const item = allRefs.find((r) => r.ref === fieldRef);
45
61
 
46
62
  return item ? allRefs.indexOf(item) : -1;
47
63
  };
@@ -57,16 +73,15 @@ export const useFormField = ({
57
73
  return;
58
74
  }
59
75
 
60
- const currentField = refs![getMyIndex()];
61
-
62
- focusField?.(getNextFieldRef());
63
-
64
76
  __DEV__ &&
65
- currentField?.hasFocusCallback &&
66
- checks?.checkFocusTrap({
67
- ref: currentField?.ref?.current,
77
+ hasFocusCallback &&
78
+ checkFocusTrap?.({
79
+ ref: fieldRef.current as React.RefObject<any>,
68
80
  shouldHaveFocus: false,
81
+ trackError: trackError ?? null,
69
82
  });
83
+
84
+ focusField?.(getNextFieldRef());
70
85
  };
71
86
 
72
87
  const getNextFieldRef = () => {
@@ -77,7 +92,7 @@ export const useFormField = ({
77
92
  ref: nextFormFieldRef,
78
93
  };
79
94
  } else if (nextFieldId) {
80
- return allRefs.find(item => item.id === nextFieldId);
95
+ return allRefs.find((item) => item.id === nextFieldId);
81
96
  }
82
97
 
83
98
  return allRefs[getMyIndex() + 1];
@@ -108,7 +123,7 @@ export const useFormField = ({
108
123
  }, []);
109
124
 
110
125
  React.useEffect(() => {
111
- const myRefIndex = refs?.findIndex(item => item.ref === fieldRef);
126
+ const myRefIndex = refs?.findIndex((item) => item.ref === fieldRef);
112
127
 
113
128
  // @ts-ignore
114
129
  refs[myRefIndex].hasError = hasError;
@@ -120,16 +135,9 @@ export const useFormField = ({
120
135
  .filter(Boolean)
121
136
  .join(',');
122
137
 
123
- return __DEV__
124
- ? {
125
- focusNextFormField,
126
- isLastField,
127
- style: applyStyle?.({ style, debugStyle: checks?.debugStyle }),
128
- accessibilityHint: fullAccessibilityHint,
129
- }
130
- : {
131
- focusNextFormField,
132
- accessibilityHint: fullAccessibilityHint,
133
- isLastField,
134
- };
138
+ return {
139
+ focusNextFormField,
140
+ accessibilityHint: fullAccessibilityHint,
141
+ isLastField,
142
+ };
135
143
  };
@@ -0,0 +1,18 @@
1
+ import { renderHook } from '@testing-library/react-native';
2
+ import * as Form from '../components/Form';
3
+ import { useFormSubmit } from './useFormSubmit';
4
+
5
+ describe('useFormSubmit', () => {
6
+ it('returns a submitForm callback from form context', async () => {
7
+ const submitForm = jest.fn().mockResolvedValue(undefined);
8
+ jest.spyOn(Form, 'useForm').mockImplementation(
9
+ () => ({ submitForm }) as any,
10
+ );
11
+
12
+ const { result } = renderHook(() => useFormSubmit());
13
+
14
+ await result.current.submitForm();
15
+
16
+ expect(submitForm).toHaveBeenCalledTimes(1);
17
+ });
18
+ });
@@ -0,0 +1,17 @@
1
+ import { useForm } from '../components/Form';
2
+
3
+ export type UseFormSubmit = {
4
+ submitForm: () => Promise<void>;
5
+ };
6
+
7
+ export const useFormSubmit = (): UseFormSubmit => {
8
+ const { submitForm } = useForm();
9
+
10
+ const handleSubmit = () => {
11
+ return submitForm();
12
+ };
13
+
14
+ return {
15
+ submitForm: handleSubmit,
16
+ };
17
+ };
@@ -1,5 +1,3 @@
1
- import { useChecks } from '@react-native-ama/core';
2
- import { applyStyle } from '@react-native-ama/internal';
3
1
  import * as React from 'react';
4
2
  import type {
5
3
  LayoutChangeEvent,
@@ -10,7 +8,6 @@ import type {
10
8
  TextStyle,
11
9
  ViewStyle,
12
10
  } from 'react-native';
13
-
14
11
  import { TextInputProps } from '../components/TextInput';
15
12
  import { UseFormField, useFormField } from './useFormField';
16
13
 
@@ -18,9 +15,9 @@ export type UseTextInput = Omit<UseFormField, 'hasFocusCallback'> & {
18
15
  onLayout?: (event: LayoutChangeEvent) => void;
19
16
  returnKeyType?: ReturnKeyTypeOptions;
20
17
  onSubmitEditing?: (
21
- event: NativeSyntheticEvent<TextInputSubmitEditingEventData>,
18
+ event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
22
19
  ) => void;
23
- accessibilityLabel: string;
20
+ accessibilityLabel?: string;
24
21
  editable?: boolean;
25
22
  style?: ViewStyle | StyleProp<TextStyle> | null;
26
23
  required: boolean;
@@ -40,15 +37,15 @@ export const useTextInput = ({
40
37
  ...rest
41
38
  }: UseTextInput) => {
42
39
  const [theReturnKeyType, setTheReturnKeyType] = React.useState(
43
- returnKeyType || 'next',
40
+ returnKeyType || 'next'
44
41
  );
45
- const { style: formFieldStyle, ...formField } = useFormField({
42
+ const formField = useFormField({
46
43
  hasFocusCallback: true,
47
44
  ...rest,
48
45
  });
49
46
 
50
47
  const handleOnSubmitEditing = (
51
- event: NativeSyntheticEvent<TextInputSubmitEditingEventData>,
48
+ event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
52
49
  ) => {
53
50
  onSubmitEditing?.(event);
54
51
 
@@ -66,37 +63,6 @@ export const useTextInput = ({
66
63
  onLayout?.(event);
67
64
  };
68
65
 
69
- const checks = __DEV__ ? useChecks?.() : undefined;
70
-
71
- __DEV__ &&
72
- checks?.noUndefinedProperty({
73
- properties: { accessibilityLabel },
74
- property: 'accessibilityLabel',
75
- rule: 'NO_FORM_LABEL',
76
- });
77
- __DEV__ &&
78
- checks?.noUppercaseStringChecker({
79
- text: accessibilityLabel,
80
- });
81
- __DEV__ &&
82
- checks?.noUppercaseStringChecker({
83
- text: rest.accessibilityHint,
84
- });
85
- __DEV__ &&
86
- accessibilityLabel?.trim().slice(-1) === '*' &&
87
- checks?.logResult('NO_FORM_LABEL_ENDING_WITH_ASTERISK', {
88
- rule: 'NO_FORM_LABEL_ENDING_WITH_ASTERISK',
89
- message:
90
- 'Form labels should not end with an asterisk. Please remove it and provide a required message instead.',
91
- });
92
- __DEV__ &&
93
- required &&
94
- checks?.noUndefinedProperty({
95
- properties: { requiredMessage },
96
- property: 'requiredMessage',
97
- rule: 'NO_FORM_LABEL_ENDING_WITH_ASTERISK',
98
- });
99
-
100
66
  const fullAccessibilityLabel = required
101
67
  ? `${accessibilityLabel}, ${requiredMessage}`
102
68
  : accessibilityLabel;
@@ -109,20 +75,9 @@ export const useTextInput = ({
109
75
  blurOnSubmit: theReturnKeyType === 'done',
110
76
  };
111
77
 
112
- return __DEV__
113
- ? {
114
- ...rest,
115
- ...formField,
116
- ...advancedProps,
117
- style: applyStyle?.({
118
- // @ts-ignore
119
- style: formFieldStyle,
120
- debugStyle: checks?.debugStyle,
121
- }) as any,
122
- }
123
- : {
124
- ...rest,
125
- ...formField,
126
- ...advancedProps,
127
- };
78
+ return {
79
+ ...rest,
80
+ ...formField,
81
+ ...advancedProps,
82
+ };
128
83
  };
package/src/index.ts CHANGED
@@ -1,45 +1,40 @@
1
- // Components
2
-
3
- import { isFocused } from '@react-native-ama/internal';
4
-
1
+ import React from 'react';
5
2
  import {
6
- FormActions,
7
3
  Form as FormProvider,
4
+ type FormActions,
8
5
  type FormProps,
9
6
  } from './components/Form';
10
- import { FormField, FormFieldProps } from './components/FormField';
11
- import { FormSubmit, FormSubmitProps } from './components/FormSubmit';
12
- import { FormSwitch, FormSwitchProps } from './components/FormSwitch';
7
+ import { FormField, type FormFieldProps } from './components/FormField';
8
+ import {
9
+ FormSubmit,
10
+ type FormSubmitProps,
11
+ type FormSubmitRenderProps,
12
+ } from './components/FormSubmit';
13
13
 
14
- type FormComponent = React.FunctionComponent<FormProps> & {
15
- Submit: (props: FormSubmitProps) => JSX.Element;
16
- Field: React.FunctionComponent<FormFieldProps>;
17
- Switch: (props: FormSwitchProps) => JSX.Element;
14
+ type FormComponent = typeof FormProvider & {
15
+ Submit: (props: FormSubmitProps) => React.ReactElement;
16
+ Field: typeof FormField;
18
17
  };
19
18
 
20
- // @ts-ignore
21
- const Form: FormComponent = FormProvider;
19
+ const Form = FormProvider as unknown as FormComponent;
22
20
  Form.Submit = FormSubmit;
23
21
  Form.Field = FormField;
24
- Form.Switch = FormSwitch;
25
22
 
26
- export { Form, FormField, FormSubmit, FormSwitch };
23
+ export { Form, FormField, FormSubmit };
27
24
 
28
25
  // Components
29
26
  export { TextInput, type TextInputProps } from './components/TextInput';
30
27
 
31
28
  // Hooks
32
29
  export { useFormField, type UseFormField } from './hooks/useFormField';
30
+ export { useFormSubmit, type UseFormSubmit } from './hooks/useFormSubmit';
33
31
  export { useTextInput, type UseTextInput } from './hooks/useTextInput';
34
32
 
35
- // utils
36
- export { isFocused };
37
-
38
33
  // Types
39
34
  export {
40
35
  type FormProps,
41
36
  type FormActions,
42
37
  type FormFieldProps,
43
38
  type FormSubmitProps,
44
- type FormSwitchProps,
39
+ type FormSubmitRenderProps,
45
40
  };
@@ -0,0 +1,80 @@
1
+ import { checkFocusTrap } from './checkFocusTrap';
2
+
3
+ beforeEach(() => {
4
+ jest.clearAllMocks();
5
+ jest.useFakeTimers();
6
+ // @ts-ignore
7
+ global.__DEV__ = true;
8
+ });
9
+
10
+ afterEach(() => {
11
+ jest.useRealTimers();
12
+ // @ts-ignore
13
+ global.__DEV__ = true;
14
+ });
15
+
16
+ describe('checkFocusTrap', () => {
17
+ it('calls trackError with NO_KEYBOARD_TRAP when ref is focused and shouldHaveFocus is false', async () => {
18
+ const trackError = jest.fn();
19
+ const ref = { current: { isFocused: () => true } } as any;
20
+
21
+ const promise = checkFocusTrap!({ ref, shouldHaveFocus: false, trackError });
22
+ jest.runAllTimers();
23
+ await promise;
24
+
25
+ expect(trackError).toHaveBeenCalledWith('NO_KEYBOARD_TRAP', ref);
26
+ });
27
+
28
+ it('calls trackError with NO_KEYBOARD_TRAP when ref is not focused and shouldHaveFocus is true', async () => {
29
+ const trackError = jest.fn();
30
+ const ref = { current: { isFocused: () => false } } as any;
31
+
32
+ const promise = checkFocusTrap!({ ref, shouldHaveFocus: true, trackError });
33
+ jest.runAllTimers();
34
+ await promise;
35
+
36
+ expect(trackError).toHaveBeenCalledWith('NO_KEYBOARD_TRAP', ref);
37
+ });
38
+
39
+ it('does not call trackError when no trap condition exists', async () => {
40
+ const trackError = jest.fn();
41
+ const ref = { current: { isFocused: () => false } } as any;
42
+
43
+ const promise = checkFocusTrap!({ ref, shouldHaveFocus: false, trackError });
44
+ jest.runAllTimers();
45
+ await promise;
46
+
47
+ expect(trackError).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it('calls console.error when trackError is unavailable and trap is detected', async () => {
51
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
52
+ const ref = { current: { isFocused: () => true } } as any;
53
+
54
+ const promise = checkFocusTrap!({ ref, shouldHaveFocus: false, trackError: null });
55
+ jest.runAllTimers();
56
+ await promise;
57
+
58
+ expect(errorSpy).toHaveBeenCalledWith('The component did trap the focus');
59
+ errorSpy.mockRestore();
60
+ });
61
+
62
+ it.each([null, undefined])('does not throw when ref.current is %s', async (currentValue) => {
63
+ const trackError = jest.fn();
64
+ const ref = { current: currentValue } as any;
65
+
66
+ const promise = checkFocusTrap!({ ref, shouldHaveFocus: false, trackError });
67
+ jest.runAllTimers();
68
+ await promise;
69
+
70
+ // No throw — test passes if we reach here
71
+ });
72
+ });
73
+
74
+ jest.mock('react-native', () => mockReactNative());
75
+
76
+ function mockReactNative() {
77
+ return {
78
+ Platform: { OS: 'ios' },
79
+ };
80
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import type { TextInput } from 'react-native';
3
+
4
+ export type CheckFocusTrap = {
5
+ ref: React.RefObject<TextInput>;
6
+ shouldHaveFocus: boolean;
7
+ trackError?:
8
+ | ((rule: 'NO_KEYBOARD_TRAP', ref: React.RefObject<any>) => void)
9
+ | null;
10
+ };
11
+
12
+ const CHECK_FOCUS_TRAP_DELAY_MS = 100;
13
+
14
+ const isFocused = (ref: TextInput | null): boolean => {
15
+ return ref?.isFocused?.() ?? false;
16
+ };
17
+
18
+ export const checkFocusTrap = __DEV__
19
+ ? ({ ref, shouldHaveFocus, trackError }: CheckFocusTrap): Promise<void> => {
20
+ return new Promise((resolve) => {
21
+ setTimeout(() => {
22
+ const hasFocus = isFocused(ref?.current ?? null) === shouldHaveFocus;
23
+
24
+ if (!hasFocus) {
25
+ const message = shouldHaveFocus
26
+ ? 'The component did not receive the focus'
27
+ : 'The component did trap the focus';
28
+
29
+ if (trackError) {
30
+ trackError('NO_KEYBOARD_TRAP', ref);
31
+ } else {
32
+ console.error(message);
33
+ }
34
+ }
35
+
36
+ resolve();
37
+ }, CHECK_FOCUS_TRAP_DELAY_MS);
38
+ });
39
+ }
40
+ : null;
@@ -1,4 +0,0 @@
1
- import { type SwitchListItemProps } from '@react-native-ama/react-native';
2
- import React from 'react';
3
- export type FormSwitchProps = SwitchListItemProps;
4
- export declare const FormSwitch: (props: FormSwitchProps) => React.JSX.Element;
@@ -1,15 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FormSwitch = void 0;
7
- const react_native_1 = require("@react-native-ama/react-native");
8
- const react_1 = __importDefault(require("react"));
9
- const FormField_1 = require("./FormField");
10
- const FormSwitch = (props) => {
11
- return (<FormField_1.FormField hasValidation={false}>
12
- <react_native_1.SwitchListItem {...props}/>
13
- </FormField_1.FormField>);
14
- };
15
- exports.FormSwitch = FormSwitch;
@@ -1,17 +0,0 @@
1
- import {
2
- SwitchListItem,
3
- type SwitchListItemProps,
4
- } from '@react-native-ama/react-native';
5
- import React from 'react';
6
-
7
- import { FormField } from './FormField';
8
-
9
- export type FormSwitchProps = SwitchListItemProps;
10
-
11
- export const FormSwitch = (props: FormSwitchProps) => {
12
- return (
13
- <FormField hasValidation={false}>
14
- <SwitchListItem {...props} />
15
- </FormField>
16
- );
17
- };