Skip to content

Commit 199489e

Browse files
authored
feat: Improve forms (#107)
* feat: improve forms * chore: update documentation
1 parent ca8b5ab commit 199489e

File tree

4 files changed

+133
-98
lines changed

4 files changed

+133
-98
lines changed

docs/components/Form.md

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ The provider hosts the hosts the `ref` values used by the [TextInput](./TextInpu
99
## Usage
1010

1111
```jsx
12-
<Form onSubmit={handleSubmit}>
12+
13+
// To manually focus the first invalid field
14+
ref.current?.focusFirstInvalidField()
15+
16+
<Form onSubmit={handleSubmit} ref={ref}>
1317
{...}
1418
</Form>
1519
```
1620
17-
1821
## Example
22+
1923
```jsx
2024
<Form onSubmit={handleSubmit}>
2125
<TextInput
@@ -110,17 +114,17 @@ To fix the problem, we can manually tell the `TextInput` the return key to displ
110114
111115
```jsx
112116
<Form onSubmit={handleSubmit}>
113-
<TextInput
114-
onChangeText={newText => setFirstName(newText)}
115-
defaultValue={text}
116-
label={<Text>First name:</Text>}
117-
/>
117+
<TextInput
118+
onChangeText={newText => setFirstName(newText)}
119+
defaultValue={text}
120+
label={<Text>First name:</Text>}
121+
/>
118122

119-
<SwitchListItem
120-
label={<Text>Show last name</Text>}
121-
value={isLastNameVisible}
122-
onValueChange={toggleLastName}
123-
/>
123+
<SwitchListItem
124+
label={<Text>Show last name</Text>}
125+
value={isLastNameVisible}
126+
onValueChange={toggleLastName}
127+
/>
124128

125129
{isLastNameVisible ? (
126130
<TextInput
@@ -141,7 +145,6 @@ To fix the problem, we can manually tell the `TextInput` the return key to displ
141145
</Form>
142146
```
143147
144-
145148
#### 2. Specifying the ref
146149
147150
```jsx
@@ -176,7 +179,7 @@ const emailRef = React.useRef(null);
176179
label={<Text>Email address:</Text>}
177180
ref={emailRef}
178181
/>
179-
</Form>
182+
</Form>;
180183
```
181184
182185
## Props
@@ -186,9 +189,26 @@ const emailRef = React.useRef(null);
186189
The callback to be called when the [`TextInput`](./TextInput.mdx) `returnKeyboardType` is **done**.
187190
188191
| Type |
189-
|----------|
192+
| -------- |
190193
| callback |
191194
195+
## Methods
196+
197+
### `focusFirstInvalidField`
198+
199+
Focuses the first field of the Form that has been marked as `invalid`
200+
201+
```
202+
// To manually focus the first invalid field
203+
const focusInvalidField = () => {
204+
ref.current?.focusFirstInvalidField()
205+
}
206+
207+
<Form onSubmit={handleSubmit} ref={ref}>
208+
<Pressable onPress={focusInvalidField} />
209+
</Form>
210+
```
211+
192212
## Related guidelines
193213
194214
- [Forms](../guidelines/forms)

docs/guidelines/headers.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Headers
22

3-
Headers are defined by setting the property: `accessibilityRole = "header"` and are equivalent to the HTML _H*_ tags.
3+
Headers are defined by setting the property: `accessibilityRole = "header"` and are equivalent to the HTML _H\*_ tags.
44

55
:::note
66

@@ -19,9 +19,9 @@ On iOS, the user will use the [VoiceOver rotor](https://support.apple.com/en-gb/
1919
You can have "invisible" header on your screen, for example:
2020

2121
```jsx
22-
<Text accessibilityLabel="This is the header" />
22+
<Text accessibilityLabel="This is the header" accessibilityRole="header" />
2323
```
24-
24+
2525
:::
2626

2727
## Related AMA components

lib/providers/Form.tsx

Lines changed: 93 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,89 +9,103 @@ export type FormProps = React.PropsWithChildren<{
99
onSubmit: () => boolean | Promise<boolean>;
1010
}>;
1111

12-
export const Form = ({ children, onSubmit }: FormProps) => {
13-
const refs = React.useRef<FormRef[]>([]);
14-
const { setFocus } = useFocus();
15-
16-
const checks = __DEV__ ? useChecks?.() : undefined;
17-
18-
const focusField = (nextField?: Partial<FormRef> | undefined) => {
19-
/**
20-
* Refs passed as prop have another ".current"
21-
*/
22-
const nextRefElement = nextField?.ref?.current?.current
23-
? nextField?.ref.current
24-
: nextField?.ref;
25-
26-
const callFocus =
27-
// @ts-ignore
28-
nextRefElement?.current?.focus &&
29-
nextField?.hasFocusCallback &&
30-
nextField?.isEditable;
12+
export type FormActions = {
13+
focusFirstInvalidField: () => void;
14+
};
3115

32-
__DEV__ &&
33-
nextRefElement == null &&
34-
checks?.logResult('nextRefElement', {
35-
message:
36-
'No next field found. Make sure you wrapped your form inside the <Form /> component',
37-
rule: 'NO_UNDEFINED',
38-
});
16+
export const Form = React.forwardRef<FormActions, FormProps>(
17+
({ children, onSubmit }, ref) => {
18+
const refs = React.useRef<FormRef[]>([]);
19+
const { setFocus } = useFocus();
20+
21+
const checks = __DEV__ ? useChecks?.() : undefined;
3922

40-
if (callFocus) {
23+
const focusField = (nextField?: Partial<FormRef> | undefined) => {
4124
/**
42-
* On some apps, if we call focus immediately and the field is already focused we lose the focus.
43-
* Same happens if we do not call `focus` if the field is already focused.
25+
* Refs passed as prop have another ".current"
4426
*/
45-
const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;
46-
47-
setTimeout(() => {
48-
nextRefElement?.current?.focus();
49-
50-
__DEV__ &&
51-
nextRefElement &&
52-
checks?.checkFocusTrap({
53-
ref: nextRefElement,
54-
shouldHaveFocus: true,
55-
});
56-
}, timeoutValue);
57-
} else if (nextRefElement?.current) {
58-
setFocus(nextRefElement?.current);
59-
}
60-
};
61-
62-
const submitForm = async () => {
63-
const isValid = await onSubmit();
64-
65-
if (isValid) {
66-
return;
67-
}
68-
69-
InteractionManager.runAfterInteractions(() => {
70-
setTimeout(() => {
71-
const fieldWithError = (refs.current || []).find(
72-
ref => ref.hasValidation && ref.hasError,
73-
);
74-
75-
__DEV__ &&
76-
fieldWithError == null &&
77-
checks?.logResult('Form', {
78-
message:
79-
'The form validation has failed, but no component with error was found',
80-
rule: 'NO_UNDEFINED',
81-
});
82-
83-
focusField(fieldWithError);
84-
}, 0);
85-
});
86-
};
87-
88-
return (
89-
<FormContext.Provider
90-
value={{ refs: refs.current, submitForm, focusField }}>
91-
{children}
92-
</FormContext.Provider>
93-
);
94-
};
27+
const nextRefElement = nextField?.ref?.current?.current
28+
? nextField?.ref.current
29+
: nextField?.ref;
30+
31+
const callFocus =
32+
// @ts-ignore
33+
nextRefElement?.current?.focus &&
34+
nextField?.hasFocusCallback &&
35+
nextField?.isEditable;
36+
37+
__DEV__ &&
38+
nextRefElement == null &&
39+
checks?.logResult('nextRefElement', {
40+
message:
41+
'No next field found. Make sure you wrapped your form inside the <Form /> component',
42+
rule: 'NO_UNDEFINED',
43+
});
44+
45+
if (callFocus) {
46+
/**
47+
* On some apps, if we call focus immediately and the field is already focused we lose the focus.
48+
* Same happens if we do not call `focus` if the field is already focused.
49+
*/
50+
const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;
51+
52+
setTimeout(() => {
53+
nextRefElement?.current?.focus();
54+
55+
__DEV__ &&
56+
nextRefElement &&
57+
checks?.checkFocusTrap({
58+
ref: nextRefElement,
59+
shouldHaveFocus: true,
60+
});
61+
}, timeoutValue);
62+
} else if (nextRefElement?.current) {
63+
setFocus(nextRefElement?.current);
64+
}
65+
};
66+
67+
const submitForm = async () => {
68+
const isValid = await onSubmit();
69+
70+
if (isValid) {
71+
return;
72+
}
73+
74+
focusFirstInvalidField();
75+
};
76+
77+
const focusFirstInvalidField = () => {
78+
InteractionManager.runAfterInteractions(() => {
79+
setTimeout(() => {
80+
const fieldWithError = (refs.current || []).find(
81+
fieldRef => fieldRef.hasValidation && fieldRef.hasError,
82+
);
83+
84+
__DEV__ &&
85+
fieldWithError == null &&
86+
checks?.logResult('Form', {
87+
message:
88+
'The form validation has failed, but no component with error was found',
89+
rule: 'NO_UNDEFINED',
90+
});
91+
92+
focusField(fieldWithError);
93+
}, 0);
94+
});
95+
};
96+
97+
React.useImperativeHandle(ref, () => ({
98+
focusFirstInvalidField,
99+
}));
100+
101+
return (
102+
<FormContext.Provider
103+
value={{ refs: refs.current, submitForm, focusField }}>
104+
{children}
105+
</FormContext.Provider>
106+
);
107+
},
108+
);
95109

96110
export type FormContextValue = {
97111
refs?: FormRef[];

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"pods": "cd example && pod-install --quiet",
2020
"bootstrap": "yarn example && yarn && yarn pods",
2121
"postinstall": "./scripts/init.sh",
22-
"coverage": "yarn run test -- --coverage --watchAll=false"
22+
"coverage": "yarn run test -- --coverage --watchAll=false",
23+
"doc": "cd website && yarn start"
2324
},
2425
"husky": {
2526
"hooks": {

0 commit comments

Comments
 (0)