@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,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePressable = void 0;
4
+ const useButtonChecks_1 = require("@react-native-ama/core/src/hooks/useButtonChecks");
5
+ const internal_1 = require("@react-native-ama/internal");
6
+ const react_native_1 = require("react-native");
7
+ const usePressable = (props, children) => {
8
+ const accessibilityRole = react_native_1.Platform.OS === 'ios' &&
9
+ IOS_BUTTON_ACCESSIBILITY_ROLES.includes(props.accessibilityRole)
10
+ ? 'button'
11
+ : props.accessibilityRole;
12
+ const accessibilityState = (0, internal_1.generateAccessibilityStateFromProp)(Object.assign(Object.assign({}, props), { accessibilityRole }));
13
+ const ignoreContrastCheck = __DEV__
14
+ ? // @ts-ignore
15
+ props.disabled && (0, internal_1.shouldIgnoreContrastCheckForDisabledElement)()
16
+ : null;
17
+ const checks = __DEV__
18
+ ? useButtonChecks_1.useButtonChecks === null || useButtonChecks_1.useButtonChecks === void 0 ? void 0 : (0, useButtonChecks_1.useButtonChecks)(Object.assign(Object.assign({}, props), { accessibilityRole, accessibilityState }), children, !ignoreContrastCheck)
19
+ : null;
20
+ return __DEV__
21
+ ? {
22
+ accessibilityState,
23
+ // @ts-ignore
24
+ onLayout: evt => {
25
+ if (props.onLayout) {
26
+ props.onLayout(evt);
27
+ }
28
+ checks === null || checks === void 0 ? void 0 : checks.onLayout(evt);
29
+ },
30
+ // @ts-ignore
31
+ style: checks.debugStyle,
32
+ accessibilityRole,
33
+ }
34
+ : {
35
+ accessibilityState,
36
+ accessibilityRole,
37
+ };
38
+ };
39
+ exports.usePressable = usePressable;
40
+ const IOS_BUTTON_ACCESSIBILITY_ROLES = [
41
+ 'checkbox',
42
+ 'togglebutton',
43
+ 'radio',
44
+ ];
@@ -0,0 +1,10 @@
1
+ import type { ViewProps } from 'react-native';
2
+ type UseSwitch = Omit<ViewProps, 'children' | 'accessibilityRole' | 'accessibilityLabel'> & {
3
+ accessibilityLabel: string;
4
+ };
5
+ type UseSwitchReturn = Omit<ViewProps, 'children' | 'accessibilityRole' | 'accessibilityLabel'> & {
6
+ accessibilityLabel: string;
7
+ accessibilityRole: 'switch';
8
+ };
9
+ export declare const useSwitch: (props: UseSwitch) => UseSwitchReturn;
10
+ export {};
@@ -0,0 +1,25 @@
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.useSwitch = void 0;
7
+ const core_1 = require("@react-native-ama/core");
8
+ const internal_1 = require("@react-native-ama/internal");
9
+ const react_1 = __importDefault(require("react"));
10
+ const useSwitch = (props) => {
11
+ const accessibilityState = react_1.default.useMemo(() => (0, internal_1.generateAccessibilityStateFromProp)(props), [props]);
12
+ const checks = __DEV__
13
+ ? core_1.useButtonChecks === null || core_1.useButtonChecks === void 0 ? void 0 : (0, core_1.useButtonChecks)(Object.assign(Object.assign({}, props), { accessibilityRole: 'switch' }))
14
+ : undefined;
15
+ return __DEV__
16
+ ? Object.assign(Object.assign({ accessibilityRole: 'switch', accessibilityState }, props), {
17
+ // @ts-ignore
18
+ onLayout: checks.onLayout, style: internal_1.applyStyle === null || internal_1.applyStyle === void 0 ? void 0 : (0, internal_1.applyStyle)({
19
+ // @ts-ignore
20
+ style: props.style,
21
+ // @ts-ignore
22
+ debugStyle: checks.debugStyle,
23
+ }) }) : Object.assign({ accessibilityRole: 'switch', accessibilityState }, props);
24
+ };
25
+ exports.useSwitch = useSwitch;
@@ -0,0 +1,10 @@
1
+ export { ExpandablePressable } from './components/ExpandablePressable';
2
+ export { SwitchListItem, type SwitchListItemProps, } from './components/SwitchListItem';
3
+ export { SwitchWrapper } from './components/SwitchWrapper';
4
+ export { Text } from './components/Text';
5
+ export { TouchableOpacity, type TouchableOpacityProps, } from './components/TouchableOpacity';
6
+ export { TouchableWithoutFeedback, type TouchableWithoutFeedbackProps, } from './components/TouchableWithoutFeedback';
7
+ export { Pressable, type PressableProps } from './components/Pressable';
8
+ export { useExpandable } from './hooks/useExpandable';
9
+ export { usePressable, type UsePressable } from './hooks/usePressable';
10
+ export { useSwitch } from './hooks/useSwitch';
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSwitch = exports.usePressable = exports.useExpandable = exports.Pressable = exports.TouchableWithoutFeedback = exports.TouchableOpacity = exports.Text = exports.SwitchWrapper = exports.SwitchListItem = exports.ExpandablePressable = void 0;
4
+ // Components
5
+ var ExpandablePressable_1 = require("./components/ExpandablePressable");
6
+ Object.defineProperty(exports, "ExpandablePressable", { enumerable: true, get: function () { return ExpandablePressable_1.ExpandablePressable; } });
7
+ var SwitchListItem_1 = require("./components/SwitchListItem");
8
+ Object.defineProperty(exports, "SwitchListItem", { enumerable: true, get: function () { return SwitchListItem_1.SwitchListItem; } });
9
+ var SwitchWrapper_1 = require("./components/SwitchWrapper");
10
+ Object.defineProperty(exports, "SwitchWrapper", { enumerable: true, get: function () { return SwitchWrapper_1.SwitchWrapper; } });
11
+ var Text_1 = require("./components/Text");
12
+ Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return Text_1.Text; } });
13
+ var TouchableOpacity_1 = require("./components/TouchableOpacity");
14
+ Object.defineProperty(exports, "TouchableOpacity", { enumerable: true, get: function () { return TouchableOpacity_1.TouchableOpacity; } });
15
+ var TouchableWithoutFeedback_1 = require("./components/TouchableWithoutFeedback");
16
+ Object.defineProperty(exports, "TouchableWithoutFeedback", { enumerable: true, get: function () { return TouchableWithoutFeedback_1.TouchableWithoutFeedback; } });
17
+ var Pressable_1 = require("./components/Pressable");
18
+ Object.defineProperty(exports, "Pressable", { enumerable: true, get: function () { return Pressable_1.Pressable; } });
19
+ // Hooks
20
+ var useExpandable_1 = require("./hooks/useExpandable");
21
+ Object.defineProperty(exports, "useExpandable", { enumerable: true, get: function () { return useExpandable_1.useExpandable; } });
22
+ var usePressable_1 = require("./hooks/usePressable");
23
+ Object.defineProperty(exports, "usePressable", { enumerable: true, get: function () { return usePressable_1.usePressable; } });
24
+ var useSwitch_1 = require("./hooks/useSwitch");
25
+ Object.defineProperty(exports, "useSwitch", { enumerable: true, get: function () { return useSwitch_1.useSwitch; } });
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@react-native-ama/forms",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "react-native": "src/index",
6
+ "types": "dist/index.d.ts",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": [
10
+ {
11
+ "imports": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./dist/index.js"
15
+ ]
16
+ },
17
+ "files": [
18
+ "src",
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "rm -rf dist && tsc -p ./tsconfig.build.json",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "jest"
25
+ },
26
+ "dependencies": {
27
+ "@react-native-ama/core": "*",
28
+ "@react-native-ama/internal": "*",
29
+ "@react-native-ama/react-native": "*"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "*",
33
+ "react-native": "*"
34
+ },
35
+ "keywords": [
36
+ "react-native",
37
+ "a11y",
38
+ "accessibility"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/FormidableLabs/react-native-ama",
43
+ "directory": "packages/forms"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/FormidableLabs/react-native-ama/issues"
47
+ },
48
+ "homepage": "https://github.com/FormidableLabs/react-native-ama#readme",
49
+ "publishConfig": {
50
+ "provenance": true
51
+ },
52
+ "author": "",
53
+ "license": "MIT",
54
+ "jest": {
55
+ "preset": "jest-expo",
56
+ "modulePathIgnorePatterns": [
57
+ "<rootDir>/node_modules",
58
+ "<rootDir>/dist/"
59
+ ],
60
+ "collectCoverageFrom": [
61
+ "src/**/*.{ts,tsx}"
62
+ ],
63
+ "coverageThreshold": {
64
+ "global": {
65
+ "statements": 90,
66
+ "branches": 82,
67
+ "functions": 83,
68
+ "lines": 91
69
+ }
70
+ },
71
+ "transformIgnorePatterns": [
72
+ "node_modules/(?!(@react-native|react-native|react-native-reanimated/))"
73
+ ],
74
+ "transform": {
75
+ "\\.[jt]sx?$": "babel-jest"
76
+ },
77
+ "verbose": true
78
+ },
79
+ "devDependencies": {
80
+ "jest-expo": "^49.0.0"
81
+ }
82
+ }
@@ -0,0 +1,262 @@
1
+ import * as UseChecks from '@react-native-ama/core/src/hooks/useChecks';
2
+ import { render, waitFor } from '@testing-library/react-native';
3
+ import * as React from 'react';
4
+
5
+ import { Form, FormContext } from './Form';
6
+
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ describe('Form', () => {
12
+ let checkFocusTrap: jest.Mock;
13
+
14
+ beforeEach(function () {
15
+ checkFocusTrap = jest.fn().mockResolvedValue(null);
16
+
17
+ // @ts-ignore
18
+ jest.spyOn(UseChecks, 'useChecks').mockReturnValue({
19
+ checkFocusTrap,
20
+ hasErrors: true,
21
+ } as any);
22
+ });
23
+
24
+ it('checks that next field has the focus if it has the focus callback', async () => {
25
+ const focus = jest.fn();
26
+ const ref = { current: { focus, isFocused: jest.fn() } };
27
+ const secondField = {
28
+ ref,
29
+ hasFocusCallback: true,
30
+ hasValidation: false,
31
+ isEditable: true,
32
+ id: 'second',
33
+ };
34
+
35
+ let providerValues = customRender();
36
+
37
+ providerValues.refs?.push({
38
+ ref: { current: 'random ref' },
39
+ hasFocusCallback: false,
40
+ hasValidation: false,
41
+ id: 'first',
42
+ });
43
+
44
+ // next field
45
+ providerValues.refs?.push(secondField);
46
+
47
+ providerValues.focusField?.(secondField);
48
+
49
+ await waitFor(() =>
50
+ expect(checkFocusTrap).toHaveBeenCalledWith({
51
+ ref: {
52
+ current: {
53
+ focus: expect.any(Function),
54
+ isFocused: expect.any(Function),
55
+ },
56
+ },
57
+ shouldHaveFocus: true,
58
+ }),
59
+ );
60
+ });
61
+
62
+ it('calls setFocus if the next field has no .focus callback', () => {
63
+ const ref = { current: 'whatever' };
64
+ const secondField = {
65
+ ref,
66
+ id: 'second',
67
+ };
68
+
69
+ let providerValues = customRender();
70
+
71
+ providerValues.refs?.push({
72
+ ref: { current: 'random ref' },
73
+ hasFocusCallback: false,
74
+ hasValidation: false,
75
+ id: 'first',
76
+ });
77
+
78
+ // next field
79
+ providerValues.refs?.push(secondField);
80
+
81
+ providerValues.focusField?.(secondField);
82
+
83
+ expect(setFocus).toHaveBeenCalledWith('whatever');
84
+ });
85
+
86
+ it('calls the .focus callback if the next field has it and is editable', async () => {
87
+ const focus = jest.fn();
88
+ const ref = { current: { focus, isFocused: jest.fn() } };
89
+ const secondField = {
90
+ ref,
91
+ hasFocusCallback: true,
92
+ hasValidation: false,
93
+ isEditable: true,
94
+ id: 'second',
95
+ };
96
+
97
+ let providerValues = customRender();
98
+
99
+ providerValues.refs?.push({
100
+ ref: { current: 'random ref' },
101
+ hasFocusCallback: false,
102
+ hasValidation: false,
103
+ id: 'first',
104
+ });
105
+
106
+ // next field
107
+ providerValues.refs?.push(secondField);
108
+
109
+ providerValues.focusField?.(secondField);
110
+
111
+ await waitFor(() => expect(focus).toHaveBeenCalledWith());
112
+ });
113
+
114
+ it('awaits 50ms before triggering again the `focus` event if the component already have the focus', async () => {
115
+ jest.useFakeTimers();
116
+
117
+ const focus = jest.fn();
118
+ const ref = {
119
+ current: { focus, isFocused: jest.fn().mockReturnValue(true) },
120
+ };
121
+ const secondField = {
122
+ ref,
123
+ hasFocusCallback: true,
124
+ hasValidation: false,
125
+ isEditable: true,
126
+ id: 'second',
127
+ };
128
+
129
+ let providerValues = customRender();
130
+
131
+ providerValues.refs?.push({
132
+ ref: { current: 'random ref' },
133
+ hasFocusCallback: false,
134
+ hasValidation: false,
135
+ id: 'first',
136
+ });
137
+
138
+ // next field
139
+ providerValues.refs?.push(secondField);
140
+
141
+ providerValues.focusField?.(secondField);
142
+
143
+ expect(focus).not.toHaveBeenCalled();
144
+ expect(checkFocusTrap).not.toHaveBeenCalled();
145
+
146
+ jest.advanceTimersByTime(51);
147
+
148
+ expect(focus).toHaveBeenCalledWith();
149
+
150
+ await waitFor(() =>
151
+ expect(checkFocusTrap).toHaveBeenCalledWith({
152
+ ref: {
153
+ current: {
154
+ focus: expect.any(Function),
155
+ isFocused: expect.any(Function),
156
+ },
157
+ },
158
+ shouldHaveFocus: true,
159
+ }),
160
+ );
161
+
162
+ jest.useRealTimers();
163
+ });
164
+
165
+ it('calls the setFocus callback if the next field is not editable', () => {
166
+ const focus = jest.fn();
167
+ const ref = { current: { focus, isFocused: jest.fn() } };
168
+ const secondField = {
169
+ ref,
170
+ hasFocusCallback: true,
171
+ hasValidation: false,
172
+ isEditable: false,
173
+ id: 'second',
174
+ };
175
+
176
+ let providerValues = customRender();
177
+
178
+ providerValues.refs?.push({
179
+ ref: { current: 'random ref' },
180
+ hasFocusCallback: false,
181
+ hasValidation: false,
182
+ id: 'first',
183
+ });
184
+
185
+ // next field
186
+ providerValues.refs?.push(secondField);
187
+
188
+ providerValues.focusField?.(secondField);
189
+
190
+ expect(setFocus).toHaveBeenCalledWith(ref.current);
191
+ });
192
+
193
+ it('focuses the first failed component when submitForm fails', async () => {
194
+ const onSubmit = jest.fn().mockReturnValue(false);
195
+
196
+ const focus = jest.fn();
197
+ const ref = { current: { focus, isFocused: jest.fn() } };
198
+ const secondField = {
199
+ ref,
200
+ hasFocusCallback: true,
201
+ hasValidation: true,
202
+ hasError: true,
203
+ isEditable: false,
204
+ id: 'second',
205
+ };
206
+
207
+ let providerValues = customRender(onSubmit);
208
+
209
+ providerValues.refs?.push({
210
+ ref: { current: 'random ref' },
211
+ hasFocusCallback: false,
212
+ hasValidation: false,
213
+ id: 'first',
214
+ });
215
+
216
+ // next field
217
+ providerValues.refs?.push(secondField);
218
+
219
+ providerValues.submitForm?.();
220
+
221
+ await waitFor(() => expect(setFocus).toHaveBeenCalledWith(ref.current));
222
+ });
223
+ });
224
+
225
+ let setFocus: jest.Mock;
226
+
227
+ function mockUseFocus() {
228
+ setFocus = jest.fn();
229
+
230
+ return {
231
+ useFocus: () => {
232
+ return {
233
+ setFocus,
234
+ };
235
+ },
236
+ };
237
+ }
238
+
239
+ function customRender(onSubmit = jest.fn()) {
240
+ let providerValues: any;
241
+
242
+ render(
243
+ <Form onSubmit={onSubmit}>
244
+ <FormContext.Consumer>
245
+ {value => {
246
+ providerValues = value;
247
+
248
+ return null;
249
+ }}
250
+ </FormContext.Consumer>
251
+ </Form>,
252
+ );
253
+
254
+ return providerValues;
255
+ }
256
+
257
+ jest.mock('../hooks/useFocus', () => mockUseFocus());
258
+ jest.mock('../internal/checks/checkFocusTrap', () => {
259
+ return {
260
+ checkFocusTrap: jest.fn().mockResolvedValue(null),
261
+ };
262
+ });
@@ -0,0 +1,138 @@
1
+ import { useChecks, useFocus } from '@react-native-ama/core';
2
+ import { isFocused } from '@react-native-ama/internal';
3
+ import * as React from 'react';
4
+ import { InteractionManager } from 'react-native';
5
+
6
+ export type FormProps = React.PropsWithChildren<{
7
+ onSubmit: () => boolean | Promise<boolean>;
8
+ ref?: React.RefObject<FormActions>;
9
+ }>;
10
+
11
+ export type FormActions = {
12
+ focusFirstInvalidField: () => void;
13
+ };
14
+
15
+ export const Form = React.forwardRef<FormActions, FormProps>(
16
+ ({ children, onSubmit }, ref) => {
17
+ const refs = React.useRef<FormRef[]>([]);
18
+ const { setFocus } = useFocus();
19
+
20
+ const checks = __DEV__ ? useChecks?.() : undefined;
21
+
22
+ const focusField = (nextField?: Partial<FormRef> | undefined) => {
23
+ /**
24
+ * Refs passed as prop have another ".current"
25
+ */
26
+ const nextRefElement = nextField?.ref?.current?.current
27
+ ? nextField?.ref.current
28
+ : nextField?.ref;
29
+
30
+ const callFocus =
31
+ // @ts-ignore
32
+ nextRefElement?.current?.focus &&
33
+ nextField?.hasFocusCallback &&
34
+ nextField?.isEditable;
35
+
36
+ __DEV__ &&
37
+ nextRefElement == null &&
38
+ checks?.logResult('nextRefElement', {
39
+ message:
40
+ 'No next field found. Make sure you wrapped your form inside the <Form /> component',
41
+ rule: 'NO_UNDEFINED',
42
+ });
43
+
44
+ if (callFocus) {
45
+ /**
46
+ * On some apps, if we call focus immediately and the field is already focused we lose the focus.
47
+ * Same happens if we do not call `focus` if the field is already focused.
48
+ */
49
+ const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;
50
+
51
+ setTimeout(() => {
52
+ nextRefElement?.current?.focus();
53
+
54
+ __DEV__ &&
55
+ nextRefElement &&
56
+ checks?.checkFocusTrap({
57
+ ref: nextRefElement,
58
+ shouldHaveFocus: true,
59
+ });
60
+ }, timeoutValue);
61
+ } else if (nextRefElement?.current) {
62
+ setFocus(nextRefElement?.current);
63
+ }
64
+ };
65
+
66
+ const submitForm = async () => {
67
+ const isValid = await onSubmit();
68
+
69
+ if (isValid) {
70
+ return;
71
+ }
72
+
73
+ focusFirstInvalidField();
74
+ };
75
+
76
+ const focusFirstInvalidField = () => {
77
+ InteractionManager.runAfterInteractions(() => {
78
+ setTimeout(() => {
79
+ const fieldWithError = (refs.current || []).find(
80
+ fieldRef => fieldRef.hasValidation && fieldRef.hasError,
81
+ );
82
+
83
+ __DEV__ &&
84
+ fieldWithError == null &&
85
+ checks?.logResult('Form', {
86
+ message:
87
+ 'The form validation has failed, but no component with error was found',
88
+ rule: 'NO_UNDEFINED',
89
+ });
90
+
91
+ focusField(fieldWithError);
92
+ }, 0);
93
+ });
94
+ };
95
+
96
+ React.useImperativeHandle(ref, () => ({
97
+ focusFirstInvalidField,
98
+ }));
99
+
100
+ return (
101
+ <FormContext.Provider
102
+ value={{ refs: refs.current, submitForm, focusField }}>
103
+ {children}
104
+ </FormContext.Provider>
105
+ );
106
+ },
107
+ );
108
+
109
+ export type FormContextValue = {
110
+ refs?: FormRef[];
111
+ submitForm: () => Promise<void>;
112
+ focusField?: (nextField?: Partial<FormRef> | undefined) => void;
113
+ };
114
+
115
+ export type FormRef = {
116
+ ref: React.RefObject<any>;
117
+ hasFocusCallback: boolean;
118
+ id: string | undefined;
119
+ hasValidation: boolean;
120
+ hasError?: boolean;
121
+ isEditable?: boolean;
122
+ };
123
+
124
+ export const FormContext = React.createContext<FormContextValue | null>(null);
125
+
126
+ export const useForm = () => {
127
+ const context = React.useContext(FormContext);
128
+ if (!context) {
129
+ __DEV__ &&
130
+ console.error(
131
+ 'Please wrap your form field inside the <Form /> component',
132
+ );
133
+ throw new Error(
134
+ 'useForm must be used within a FormContextProvider, please wrap your form field inside the <Form /> component',
135
+ );
136
+ }
137
+ return context;
138
+ };
@@ -0,0 +1,54 @@
1
+ import { render } from '@testing-library/react-native';
2
+ import * as React from 'react';
3
+ import { Text } from 'react-native';
4
+
5
+ import * as UseFormField from '../hooks/useFormField';
6
+ import { FormField } from './FormField';
7
+
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+
12
+ describe('FormField', () => {
13
+ it('register itself using useFormField', () => {
14
+ const useFormField = jest.spyOn(UseFormField, 'useFormField');
15
+
16
+ render(
17
+ <FormField hasValidation={false}>
18
+ <Text>Test</Text>
19
+ </FormField>,
20
+ );
21
+
22
+ expect(useFormField.mock.calls[0][0]).toMatchObject({
23
+ hasFocusCallback: false,
24
+ ref: expect.objectContaining({ current: expect.any(Object) }),
25
+ });
26
+ });
27
+
28
+ it('wraps the children in a focusable View', () => {
29
+ const { toJSON } = render(
30
+ <FormField hasValidation={false}>
31
+ <Text>Test</Text>
32
+ </FormField>,
33
+ );
34
+
35
+ expect(toJSON()).toMatchInlineSnapshot(`
36
+ <View
37
+ accessibilityHint=""
38
+ accessible={true}
39
+ focusable={false}
40
+ onClick={[Function]}
41
+ onResponderGrant={[Function]}
42
+ onResponderMove={[Function]}
43
+ onResponderRelease={[Function]}
44
+ onResponderTerminate={[Function]}
45
+ onResponderTerminationRequest={[Function]}
46
+ onStartShouldSetResponder={[Function]}
47
+ >
48
+ <Text>
49
+ Test
50
+ </Text>
51
+ </View>
52
+ `);
53
+ });
54
+ });