@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.
- package/dist/core/src/components/AMAProvider.d.ts +26 -0
- package/dist/core/src/components/AMAProvider.js +147 -0
- package/dist/core/src/components/AutofocusContainer.d.ts +9 -0
- package/dist/core/src/components/AutofocusContainer.js +58 -0
- package/dist/core/src/components/HideChildrenFromAccessibilityTree.d.ts +6 -0
- package/dist/core/src/components/HideChildrenFromAccessibilityTree.js +40 -0
- package/dist/core/src/hooks/useButtonChecks.d.ts +6 -0
- package/dist/core/src/hooks/useButtonChecks.js +51 -0
- package/dist/core/src/hooks/useChecks.d.ts +15 -0
- package/dist/core/src/hooks/useChecks.js +152 -0
- package/dist/core/src/hooks/useFocus.d.ts +4 -0
- package/dist/core/src/hooks/useFocus.js +55 -0
- package/dist/core/src/hooks/useTimedAction.d.ts +3 -0
- package/dist/core/src/hooks/useTimedAction.js +63 -0
- package/dist/core/src/index.d.ts +7 -0
- package/dist/core/src/index.js +20 -0
- package/dist/forms/src/components/Form.d.ts +24 -0
- package/dist/forms/src/components/Form.js +122 -0
- package/dist/forms/src/components/FormField.d.ts +7 -0
- package/dist/forms/src/components/FormField.js +52 -0
- package/dist/forms/src/components/FormSubmit.d.ts +10 -0
- package/dist/forms/src/components/FormSubmit.js +25 -0
- package/dist/forms/src/components/FormSwitch.d.ts +4 -0
- package/dist/forms/src/components/FormSwitch.js +15 -0
- package/dist/forms/src/components/TextInput.d.ts +26 -0
- package/dist/forms/src/components/TextInput.js +123 -0
- package/dist/forms/src/hooks/useFormField.d.ts +26 -0
- package/dist/forms/src/hooks/useFormField.js +92 -0
- package/dist/forms/src/hooks/useTextInput.d.ts +350 -0
- package/dist/forms/src/hooks/useTextInput.js +102 -0
- package/dist/forms/src/index.d.ts +16 -0
- package/dist/forms/src/index.js +24 -0
- package/dist/internal/src/checks/checkAccessibilityRole.d.ts +3 -0
- package/dist/internal/src/checks/checkAccessibilityRole.js +32 -0
- package/dist/internal/src/checks/checkFocusTrap.d.ts +8 -0
- package/dist/internal/src/checks/checkFocusTrap.js +34 -0
- package/dist/internal/src/checks/checkForAccessibilityState.d.ts +7 -0
- package/dist/internal/src/checks/checkForAccessibilityState.js +48 -0
- package/dist/internal/src/checks/checkMinimumSize.d.ts +3 -0
- package/dist/internal/src/checks/checkMinimumSize.js +29 -0
- package/dist/internal/src/checks/contrastChecker.d.ts +8 -0
- package/dist/internal/src/checks/contrastChecker.js +77 -0
- package/dist/internal/src/checks/noUndefinedProperty.d.ts +8 -0
- package/dist/internal/src/checks/noUndefinedProperty.js +14 -0
- package/dist/internal/src/checks/uppercaseChecker.d.ts +8 -0
- package/dist/internal/src/checks/uppercaseChecker.js +18 -0
- package/dist/internal/src/checks/uppercaseStringChecker.d.ts +8 -0
- package/dist/internal/src/checks/uppercaseStringChecker.js +29 -0
- package/dist/internal/src/index.d.ts +20 -0
- package/dist/internal/src/index.js +56 -0
- package/dist/internal/src/types.d.ts +17 -0
- package/dist/internal/src/types.js +2 -0
- package/dist/internal/src/utils/applyStyle.d.ts +9 -0
- package/dist/internal/src/utils/applyStyle.js +33 -0
- package/dist/internal/src/utils/constants.d.ts +3 -0
- package/dist/internal/src/utils/constants.js +32 -0
- package/dist/internal/src/utils/error.style.d.ts +6 -0
- package/dist/internal/src/utils/error.style.js +11 -0
- package/dist/internal/src/utils/generateAccessibilityStateFromProp.d.ts +9 -0
- package/dist/internal/src/utils/generateAccessibilityStateFromProp.js +35 -0
- package/dist/internal/src/utils/getPropertyFromStyle.d.ts +2 -0
- package/dist/internal/src/utils/getPropertyFromStyle.js +10 -0
- package/dist/internal/src/utils/interpolateAnimationStates.d.ts +2 -0
- package/dist/internal/src/utils/interpolateAnimationStates.js +43 -0
- package/dist/internal/src/utils/isFocused.d.ts +2 -0
- package/dist/internal/src/utils/isFocused.js +7 -0
- package/dist/internal/src/utils/logger.d.ts +16 -0
- package/dist/internal/src/utils/logger.js +53 -0
- package/dist/internal/src/utils/logger.rules.d.ts +15 -0
- package/dist/internal/src/utils/logger.rules.js +74 -0
- package/dist/internal/src/utils/maybeGenerateStringFromElement.d.ts +2 -0
- package/dist/internal/src/utils/maybeGenerateStringFromElement.js +51 -0
- package/dist/internal/src/utils/minimumTouchableSize.d.ts +3 -0
- package/dist/internal/src/utils/minimumTouchableSize.js +9 -0
- package/dist/react-native/src/components/ExpandablePressable.d.ts +6 -0
- package/dist/react-native/src/components/ExpandablePressable.js +26 -0
- package/dist/react-native/src/components/Pressable.d.ts +5 -0
- package/dist/react-native/src/components/Pressable.js +49 -0
- package/dist/react-native/src/components/SwitchListItem.d.ts +12 -0
- package/dist/react-native/src/components/SwitchListItem.js +44 -0
- package/dist/react-native/src/components/SwitchWrapper.d.ts +11 -0
- package/dist/react-native/src/components/SwitchWrapper.js +36 -0
- package/dist/react-native/src/components/Text.d.ts +6 -0
- package/dist/react-native/src/components/Text.js +77 -0
- package/dist/react-native/src/components/TouchableOpacity.d.ts +6 -0
- package/dist/react-native/src/components/TouchableOpacity.js +48 -0
- package/dist/react-native/src/components/TouchableWithoutFeedback.d.ts +5 -0
- package/dist/react-native/src/components/TouchableWithoutFeedback.js +48 -0
- package/dist/react-native/src/hooks/useExpandable.d.ts +13 -0
- package/dist/react-native/src/hooks/useExpandable.js +31 -0
- package/dist/react-native/src/hooks/usePressable.d.ts +16 -0
- package/dist/react-native/src/hooks/usePressable.js +44 -0
- package/dist/react-native/src/hooks/useSwitch.d.ts +10 -0
- package/dist/react-native/src/hooks/useSwitch.js +25 -0
- package/dist/react-native/src/index.d.ts +10 -0
- package/dist/react-native/src/index.js +25 -0
- package/package.json +82 -0
- package/src/components/Form.test.tsx +262 -0
- package/src/components/Form.tsx +138 -0
- package/src/components/FormField.test.tsx +54 -0
- package/src/components/FormField.tsx +45 -0
- package/src/components/FormSubmit.test.tsx +71 -0
- package/src/components/FormSubmit.tsx +43 -0
- package/src/components/FormSwitch.tsx +17 -0
- package/src/components/TextInput.test.tsx +540 -0
- package/src/components/TextInput.tsx +168 -0
- package/src/hooks/useFormField.test.tsx +183 -0
- package/src/hooks/useFormField.ts +133 -0
- package/src/hooks/useTextInput.ts +128 -0
- 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
|
+
});
|