@react-native-ama/forms 1.0.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 (110) hide show
  1. package/dist/core/src/components/AMAProvider.d.ts +26 -0
  2. package/dist/core/src/components/AMAProvider.js +147 -0
  3. package/dist/core/src/components/AutofocusContainer.d.ts +9 -0
  4. package/dist/core/src/components/AutofocusContainer.js +58 -0
  5. package/dist/core/src/components/HideChildrenFromAccessibilityTree.d.ts +6 -0
  6. package/dist/core/src/components/HideChildrenFromAccessibilityTree.js +40 -0
  7. package/dist/core/src/hooks/useButtonChecks.d.ts +6 -0
  8. package/dist/core/src/hooks/useButtonChecks.js +51 -0
  9. package/dist/core/src/hooks/useChecks.d.ts +15 -0
  10. package/dist/core/src/hooks/useChecks.js +152 -0
  11. package/dist/core/src/hooks/useFocus.d.ts +4 -0
  12. package/dist/core/src/hooks/useFocus.js +55 -0
  13. package/dist/core/src/hooks/useTimedAction.d.ts +3 -0
  14. package/dist/core/src/hooks/useTimedAction.js +63 -0
  15. package/dist/core/src/index.d.ts +7 -0
  16. package/dist/core/src/index.js +20 -0
  17. package/dist/forms/src/components/Form.d.ts +24 -0
  18. package/dist/forms/src/components/Form.js +122 -0
  19. package/dist/forms/src/components/FormField.d.ts +7 -0
  20. package/dist/forms/src/components/FormField.js +52 -0
  21. package/dist/forms/src/components/FormSubmit.d.ts +10 -0
  22. package/dist/forms/src/components/FormSubmit.js +25 -0
  23. package/dist/forms/src/components/FormSwitch.d.ts +4 -0
  24. package/dist/forms/src/components/FormSwitch.js +15 -0
  25. package/dist/forms/src/components/TextInput.d.ts +26 -0
  26. package/dist/forms/src/components/TextInput.js +123 -0
  27. package/dist/forms/src/hooks/useFormField.d.ts +26 -0
  28. package/dist/forms/src/hooks/useFormField.js +92 -0
  29. package/dist/forms/src/hooks/useTextInput.d.ts +350 -0
  30. package/dist/forms/src/hooks/useTextInput.js +102 -0
  31. package/dist/forms/src/index.d.ts +16 -0
  32. package/dist/forms/src/index.js +24 -0
  33. package/dist/internal/src/checks/checkAccessibilityRole.d.ts +3 -0
  34. package/dist/internal/src/checks/checkAccessibilityRole.js +32 -0
  35. package/dist/internal/src/checks/checkFocusTrap.d.ts +8 -0
  36. package/dist/internal/src/checks/checkFocusTrap.js +34 -0
  37. package/dist/internal/src/checks/checkForAccessibilityState.d.ts +7 -0
  38. package/dist/internal/src/checks/checkForAccessibilityState.js +48 -0
  39. package/dist/internal/src/checks/checkMinimumSize.d.ts +3 -0
  40. package/dist/internal/src/checks/checkMinimumSize.js +29 -0
  41. package/dist/internal/src/checks/contrastChecker.d.ts +8 -0
  42. package/dist/internal/src/checks/contrastChecker.js +77 -0
  43. package/dist/internal/src/checks/noUndefinedProperty.d.ts +8 -0
  44. package/dist/internal/src/checks/noUndefinedProperty.js +14 -0
  45. package/dist/internal/src/checks/uppercaseChecker.d.ts +8 -0
  46. package/dist/internal/src/checks/uppercaseChecker.js +18 -0
  47. package/dist/internal/src/checks/uppercaseStringChecker.d.ts +8 -0
  48. package/dist/internal/src/checks/uppercaseStringChecker.js +29 -0
  49. package/dist/internal/src/index.d.ts +20 -0
  50. package/dist/internal/src/index.js +56 -0
  51. package/dist/internal/src/types.d.ts +17 -0
  52. package/dist/internal/src/types.js +2 -0
  53. package/dist/internal/src/utils/applyStyle.d.ts +9 -0
  54. package/dist/internal/src/utils/applyStyle.js +33 -0
  55. package/dist/internal/src/utils/constants.d.ts +3 -0
  56. package/dist/internal/src/utils/constants.js +32 -0
  57. package/dist/internal/src/utils/error.style.d.ts +6 -0
  58. package/dist/internal/src/utils/error.style.js +11 -0
  59. package/dist/internal/src/utils/generateAccessibilityStateFromProp.d.ts +9 -0
  60. package/dist/internal/src/utils/generateAccessibilityStateFromProp.js +35 -0
  61. package/dist/internal/src/utils/getPropertyFromStyle.d.ts +2 -0
  62. package/dist/internal/src/utils/getPropertyFromStyle.js +10 -0
  63. package/dist/internal/src/utils/interpolateAnimationStates.d.ts +2 -0
  64. package/dist/internal/src/utils/interpolateAnimationStates.js +43 -0
  65. package/dist/internal/src/utils/isFocused.d.ts +2 -0
  66. package/dist/internal/src/utils/isFocused.js +7 -0
  67. package/dist/internal/src/utils/logger.d.ts +16 -0
  68. package/dist/internal/src/utils/logger.js +53 -0
  69. package/dist/internal/src/utils/logger.rules.d.ts +15 -0
  70. package/dist/internal/src/utils/logger.rules.js +74 -0
  71. package/dist/internal/src/utils/maybeGenerateStringFromElement.d.ts +2 -0
  72. package/dist/internal/src/utils/maybeGenerateStringFromElement.js +51 -0
  73. package/dist/internal/src/utils/minimumTouchableSize.d.ts +3 -0
  74. package/dist/internal/src/utils/minimumTouchableSize.js +9 -0
  75. package/dist/react-native/src/components/ExpandablePressable.d.ts +6 -0
  76. package/dist/react-native/src/components/ExpandablePressable.js +26 -0
  77. package/dist/react-native/src/components/Pressable.d.ts +5 -0
  78. package/dist/react-native/src/components/Pressable.js +49 -0
  79. package/dist/react-native/src/components/SwitchListItem.d.ts +12 -0
  80. package/dist/react-native/src/components/SwitchListItem.js +44 -0
  81. package/dist/react-native/src/components/SwitchWrapper.d.ts +11 -0
  82. package/dist/react-native/src/components/SwitchWrapper.js +36 -0
  83. package/dist/react-native/src/components/Text.d.ts +6 -0
  84. package/dist/react-native/src/components/Text.js +77 -0
  85. package/dist/react-native/src/components/TouchableOpacity.d.ts +6 -0
  86. package/dist/react-native/src/components/TouchableOpacity.js +48 -0
  87. package/dist/react-native/src/components/TouchableWithoutFeedback.d.ts +5 -0
  88. package/dist/react-native/src/components/TouchableWithoutFeedback.js +48 -0
  89. package/dist/react-native/src/hooks/useExpandable.d.ts +13 -0
  90. package/dist/react-native/src/hooks/useExpandable.js +31 -0
  91. package/dist/react-native/src/hooks/usePressable.d.ts +16 -0
  92. package/dist/react-native/src/hooks/usePressable.js +44 -0
  93. package/dist/react-native/src/hooks/useSwitch.d.ts +10 -0
  94. package/dist/react-native/src/hooks/useSwitch.js +25 -0
  95. package/dist/react-native/src/index.d.ts +10 -0
  96. package/dist/react-native/src/index.js +25 -0
  97. package/package.json +82 -0
  98. package/src/components/Form.test.tsx +262 -0
  99. package/src/components/Form.tsx +138 -0
  100. package/src/components/FormField.test.tsx +54 -0
  101. package/src/components/FormField.tsx +45 -0
  102. package/src/components/FormSubmit.test.tsx +71 -0
  103. package/src/components/FormSubmit.tsx +43 -0
  104. package/src/components/FormSwitch.tsx +17 -0
  105. package/src/components/TextInput.test.tsx +540 -0
  106. package/src/components/TextInput.tsx +168 -0
  107. package/src/hooks/useFormField.test.tsx +183 -0
  108. package/src/hooks/useFormField.ts +133 -0
  109. package/src/hooks/useTextInput.ts +128 -0
  110. package/src/index.ts +39 -0
@@ -0,0 +1,183 @@
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';
4
+ import * as React from 'react';
5
+
6
+ import * as Form from '../components/Form';
7
+ import { useFormField } from './useFormField';
8
+
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ let submitForm: jest.Mock = jest.fn();
14
+
15
+ describe('useFormField', () => {
16
+ let localRef: any[] = [];
17
+
18
+ beforeEach(() => {
19
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
20
+ isScreenReaderEnabled: true,
21
+ } as any);
22
+
23
+ jest.spyOn(Form, 'useForm').mockImplementation(() => {
24
+ return { refs: localRef, submitForm };
25
+ });
26
+ });
27
+
28
+ afterEach(function () {
29
+ localRef.splice(-1);
30
+ });
31
+
32
+ it('register itself as ref when is mounted', async () => {
33
+ renderHook(
34
+ () => {
35
+ return useFormField({
36
+ ref: { current: 'hello' },
37
+ hasFocusCallback: false,
38
+ id: 'my-id',
39
+ nextFieldId: 'next-field-id',
40
+ nextFormFieldRef: { current: 'nextFormFieldRef' },
41
+ hasValidation: false,
42
+ });
43
+ },
44
+ { wrapper: renderHookWrapper },
45
+ );
46
+
47
+ await waitFor(() => {
48
+ expect(localRef).toHaveLength(1);
49
+ expect(localRef[0]).toEqual({
50
+ hasFocusCallback: false,
51
+ ref: { current: { current: 'hello' } },
52
+ id: 'my-id',
53
+ hasError: undefined,
54
+ hasValidation: false,
55
+ isEditable: true,
56
+ });
57
+ });
58
+ });
59
+
60
+ it('unregister itself from the refs when is mounted', () => {
61
+ const { unmount } = renderHook(
62
+ () => {
63
+ return useFormField({
64
+ ref: { current: 'hello' },
65
+ hasFocusCallback: false,
66
+ hasValidation: false,
67
+ });
68
+ },
69
+ { wrapper: renderHookWrapper },
70
+ );
71
+
72
+ unmount();
73
+
74
+ expect(localRef.length).toBe(0);
75
+ });
76
+
77
+ 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
+ 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
+ const ref = { current: 'whatever' };
124
+
125
+ const { result } = renderHook(
126
+ () => {
127
+ useFormField({
128
+ ref: { current: 'random ref' },
129
+ hasFocusCallback: false,
130
+ hasValidation: false,
131
+ });
132
+
133
+ // next field
134
+ return useFormField({
135
+ ref,
136
+ hasFocusCallback: false,
137
+ hasValidation: false,
138
+ });
139
+ },
140
+ { wrapper: renderHookWrapper },
141
+ );
142
+
143
+ await waitFor(() => {
144
+ result.current.focusNextFormField();
145
+
146
+ expect(submitForm).toHaveBeenCalledWith();
147
+ });
148
+ });
149
+ });
150
+ });
151
+
152
+ let setFocus: jest.Mock;
153
+ let checkFocusTrap: jest.Mock;
154
+
155
+ function mockAMACore() {
156
+ const original = jest.requireActual('@react-native-ama/core');
157
+
158
+ setFocus = jest.fn();
159
+
160
+ return {
161
+ ...original,
162
+ useFocus: () => {
163
+ return {
164
+ setFocus,
165
+ };
166
+ },
167
+ };
168
+ }
169
+
170
+ function mockAMAInternal() {
171
+ const original = jest.requireActual('@react-native-ama/internal');
172
+
173
+ checkFocusTrap = jest.fn().mockResolvedValue(null);
174
+
175
+ return { ...original, checkFocusTrap };
176
+ }
177
+
178
+ const renderHookWrapper = ({ children }) => (
179
+ <AMAProvider.AMAProvider>{children}</AMAProvider.AMAProvider>
180
+ );
181
+
182
+ jest.mock('@react-native-ama/core', () => mockAMACore());
183
+ jest.mock('@react-native-ama/internal', () => mockAMAInternal());
@@ -0,0 +1,133 @@
1
+ import { useChecks } from '@react-native-ama/core';
2
+ import { applyStyle } from '@react-native-ama/internal';
3
+ import React from 'react';
4
+ import { Keyboard, ViewStyle } from 'react-native';
5
+
6
+ import { useForm } from '../components/Form';
7
+
8
+ export type UseFormField = {
9
+ ref?: React.RefObject<any> | React.ForwardedRef<any> | null;
10
+ hasFocusCallback: boolean;
11
+ id?: string;
12
+ nextFieldId?: string;
13
+ nextFormFieldRef?: React.RefObject<any>;
14
+ accessibilityHint?: string;
15
+ style?: ViewStyle;
16
+ editable?: boolean;
17
+ hasValidation: boolean;
18
+ hasError?: boolean;
19
+ errorMessage?: string;
20
+ };
21
+
22
+ export const useFormField = ({
23
+ ref,
24
+ hasFocusCallback,
25
+ id,
26
+ nextFieldId,
27
+ nextFormFieldRef,
28
+ hasValidation,
29
+ hasError,
30
+ accessibilityHint,
31
+ errorMessage,
32
+ editable = true,
33
+ style = {},
34
+ }: UseFormField) => {
35
+ const { refs, submitForm, focusField } = useForm();
36
+ const fieldRef = React.useRef(ref);
37
+
38
+ const checks = __DEV__ ? useChecks?.() : undefined;
39
+
40
+ const getMyIndex = () => {
41
+ const allRefs = refs!;
42
+ const item = allRefs.find(r => r.ref === fieldRef);
43
+
44
+ return item ? allRefs.indexOf(item) : -1;
45
+ };
46
+
47
+ const focusNextFormField = () => {
48
+ const isLastItem = isLastField();
49
+
50
+ if (isLastItem) {
51
+ Keyboard.dismiss();
52
+
53
+ submitForm();
54
+
55
+ return;
56
+ }
57
+
58
+ const currentField = refs![getMyIndex()];
59
+
60
+ focusField?.(getNextFieldRef());
61
+
62
+ __DEV__ &&
63
+ currentField?.hasFocusCallback &&
64
+ checks?.checkFocusTrap({
65
+ ref: currentField?.ref?.current,
66
+ shouldHaveFocus: false,
67
+ });
68
+ };
69
+
70
+ const getNextFieldRef = () => {
71
+ const allRefs = refs || [];
72
+
73
+ if (nextFormFieldRef) {
74
+ return {
75
+ ref: nextFormFieldRef,
76
+ };
77
+ } else if (nextFieldId) {
78
+ return allRefs.find(item => item.id === nextFieldId);
79
+ }
80
+
81
+ return allRefs[getMyIndex() + 1];
82
+ };
83
+
84
+ const isLastField = () => {
85
+ const myIndex = getMyIndex();
86
+
87
+ return myIndex === refs!.length - 1;
88
+ };
89
+
90
+ React.useEffect(() => {
91
+ refs?.push({
92
+ ref: fieldRef,
93
+ hasFocusCallback,
94
+ id,
95
+ hasValidation,
96
+ hasError,
97
+ isEditable: editable,
98
+ });
99
+
100
+ return () => {
101
+ const myIndex = getMyIndex();
102
+
103
+ refs?.splice(myIndex, 1);
104
+ };
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, []);
107
+
108
+ React.useEffect(() => {
109
+ const myRefIndex = refs?.findIndex(item => item.ref === fieldRef);
110
+
111
+ // @ts-ignore
112
+ refs[myRefIndex].hasError = hasError;
113
+ // @ts-ignore
114
+ refs[myRefIndex].isEditable = editable;
115
+ }, [editable, hasError, refs]);
116
+
117
+ const fullAccessibilityHint = [accessibilityHint, errorMessage]
118
+ .filter(Boolean)
119
+ .join(',');
120
+
121
+ return __DEV__
122
+ ? {
123
+ focusNextFormField,
124
+ isLastField,
125
+ style: applyStyle?.({ style, debugStyle: checks?.debugStyle }),
126
+ accessibilityHint: fullAccessibilityHint,
127
+ }
128
+ : {
129
+ focusNextFormField,
130
+ accessibilityHint: fullAccessibilityHint,
131
+ isLastField,
132
+ };
133
+ };
@@ -0,0 +1,128 @@
1
+ import { useChecks } from '@react-native-ama/core';
2
+ import { applyStyle } from '@react-native-ama/internal';
3
+ import * as React from 'react';
4
+ import type {
5
+ LayoutChangeEvent,
6
+ NativeSyntheticEvent,
7
+ ReturnKeyTypeOptions,
8
+ StyleProp,
9
+ TextInputSubmitEditingEventData,
10
+ TextStyle,
11
+ ViewStyle,
12
+ } from 'react-native';
13
+
14
+ import { TextInputProps } from '../components/TextInput';
15
+ import { UseFormField, useFormField } from './useFormField';
16
+
17
+ type UseTextInput = Omit<UseFormField, 'hasFocusCallback'> & {
18
+ onLayout?: (event: LayoutChangeEvent) => void;
19
+ returnKeyType?: ReturnKeyTypeOptions;
20
+ onSubmitEditing?: (
21
+ event: NativeSyntheticEvent<TextInputSubmitEditingEventData>,
22
+ ) => void;
23
+ accessibilityLabel: string;
24
+ editable?: boolean;
25
+ style?: ViewStyle | StyleProp<TextStyle> | null;
26
+ required: boolean;
27
+ requiredMessage?: string;
28
+ hasValidation: boolean;
29
+ hasError?: boolean;
30
+ errorMessage?: string;
31
+ };
32
+
33
+ export const useTextInput = ({
34
+ onLayout,
35
+ returnKeyType,
36
+ onSubmitEditing,
37
+ accessibilityLabel,
38
+ requiredMessage,
39
+ required,
40
+ ...rest
41
+ }: UseTextInput) => {
42
+ const [theReturnKeyType, setTheReturnKeyType] = React.useState(
43
+ returnKeyType || 'next',
44
+ );
45
+ const { style: formFieldStyle, ...formField } = useFormField({
46
+ hasFocusCallback: true,
47
+ ...rest,
48
+ });
49
+
50
+ const handleOnSubmitEditing = (
51
+ event: NativeSyntheticEvent<TextInputSubmitEditingEventData>,
52
+ ) => {
53
+ onSubmitEditing?.(event);
54
+
55
+ formField.focusNextFormField();
56
+ };
57
+
58
+ const checkReturnKeyType = (event: LayoutChangeEvent) => {
59
+ const setReturnKeyTypeAsDone =
60
+ formField.isLastField() && returnKeyType == null;
61
+
62
+ if (setReturnKeyTypeAsDone) {
63
+ setTheReturnKeyType('done');
64
+ }
65
+
66
+ onLayout?.(event);
67
+ };
68
+
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
+ const fullAccessibilityLabel = required
101
+ ? `${accessibilityLabel}, ${requiredMessage}`
102
+ : accessibilityLabel;
103
+
104
+ const advancedProps: Partial<TextInputProps> = {
105
+ accessibilityLabel: fullAccessibilityLabel,
106
+ returnKeyType: theReturnKeyType,
107
+ onSubmitEditing: handleOnSubmitEditing,
108
+ onLayout: checkReturnKeyType,
109
+ blurOnSubmit: theReturnKeyType === 'done',
110
+ };
111
+
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
+ };
128
+ };
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ // Components
2
+ import {
3
+ FormActions,
4
+ FormProps,
5
+ Form as FormProvider,
6
+ } from './components/Form';
7
+ import { FormField, FormFieldProps } from './components/FormField';
8
+ import { FormSubmit, FormSubmitProps } from './components/FormSubmit';
9
+ import { FormSwitch, FormSwitchProps } from './components/FormSwitch';
10
+
11
+ type FormComponent = React.FunctionComponent<FormProps> & {
12
+ Submit: (props: FormSubmitProps) => JSX.Element;
13
+ Field: React.FunctionComponent<FormFieldProps>;
14
+ Switch: (props: FormSwitchProps) => JSX.Element;
15
+ };
16
+
17
+ // @ts-ignore
18
+ const Form: FormComponent = FormProvider;
19
+ Form.Submit = FormSubmit;
20
+ Form.Field = FormField;
21
+ Form.Switch = FormSwitch;
22
+
23
+ export { Form, FormField, FormSubmit };
24
+
25
+ // Components
26
+ export { TextInput } from './components/TextInput';
27
+
28
+ // Hooks
29
+ export { useFormField } from './hooks/useFormField';
30
+ export { useTextInput } from './hooks/useTextInput';
31
+
32
+ // Types
33
+ export {
34
+ type FormProps,
35
+ type FormActions,
36
+ type FormFieldProps,
37
+ type FormSubmitProps,
38
+ type FormSwitchProps,
39
+ };