Skip to content

Commit

Permalink
feat: Improve forms (#107)
Browse files Browse the repository at this point in the history
* feat: improve forms

* chore: update documentation
  • Loading branch information
ceceppa authored Aug 1, 2023
1 parent ca8b5ab commit 199489e
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 98 deletions.
50 changes: 35 additions & 15 deletions docs/components/Form.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ The provider hosts the hosts the `ref` values used by the [TextInput](./TextInpu
## Usage

```jsx
<Form onSubmit={handleSubmit}>

// To manually focus the first invalid field
ref.current?.focusFirstInvalidField()

<Form onSubmit={handleSubmit} ref={ref}>
{...}
</Form>
```

## Example
```jsx
<Form onSubmit={handleSubmit}>
<TextInput
Expand Down Expand Up @@ -110,17 +114,17 @@ To fix the problem, we can manually tell the `TextInput` the return key to displ
```jsx
<Form onSubmit={handleSubmit}>
<TextInput
onChangeText={newText => setFirstName(newText)}
defaultValue={text}
label={<Text>First name:</Text>}
/>
<TextInput
onChangeText={newText => setFirstName(newText)}
defaultValue={text}
label={<Text>First name:</Text>}
/>

<SwitchListItem
label={<Text>Show last name</Text>}
value={isLastNameVisible}
onValueChange={toggleLastName}
/>
<SwitchListItem
label={<Text>Show last name</Text>}
value={isLastNameVisible}
onValueChange={toggleLastName}
/>

{isLastNameVisible ? (
<TextInput
Expand All @@ -141,7 +145,6 @@ To fix the problem, we can manually tell the `TextInput` the return key to displ
</Form>
```

#### 2. Specifying the ref
```jsx
Expand Down Expand Up @@ -176,7 +179,7 @@ const emailRef = React.useRef(null);
label={<Text>Email address:</Text>}
ref={emailRef}
/>
</Form>
</Form>;
```
## Props
Expand All @@ -186,9 +189,26 @@ const emailRef = React.useRef(null);
The callback to be called when the [`TextInput`](./TextInput.mdx) `returnKeyboardType` is **done**.
| Type |
|----------|
| -------- |
| callback |
## Methods
### `focusFirstInvalidField`
Focuses the first field of the Form that has been marked as `invalid`
```
// To manually focus the first invalid field
const focusInvalidField = () => {
ref.current?.focusFirstInvalidField()
}

<Form onSubmit={handleSubmit} ref={ref}>
<Pressable onPress={focusInvalidField} />
</Form>
```
## Related guidelines
- [Forms](../guidelines/forms)
6 changes: 3 additions & 3 deletions docs/guidelines/headers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Headers

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

:::note

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

```jsx
<Text accessibilityLabel="This is the header" />
<Text accessibilityLabel="This is the header" accessibilityRole="header" />
```

:::

## Related AMA components
Expand Down
172 changes: 93 additions & 79 deletions lib/providers/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,89 +9,103 @@ export type FormProps = React.PropsWithChildren<{
onSubmit: () => boolean | Promise<boolean>;
}>;

export const Form = ({ children, onSubmit }: FormProps) => {
const refs = React.useRef<FormRef[]>([]);
const { setFocus } = useFocus();

const checks = __DEV__ ? useChecks?.() : undefined;

const focusField = (nextField?: Partial<FormRef> | undefined) => {
/**
* Refs passed as prop have another ".current"
*/
const nextRefElement = nextField?.ref?.current?.current
? nextField?.ref.current
: nextField?.ref;

const callFocus =
// @ts-ignore
nextRefElement?.current?.focus &&
nextField?.hasFocusCallback &&
nextField?.isEditable;
export type FormActions = {
focusFirstInvalidField: () => void;
};

__DEV__ &&
nextRefElement == null &&
checks?.logResult('nextRefElement', {
message:
'No next field found. Make sure you wrapped your form inside the <Form /> component',
rule: 'NO_UNDEFINED',
});
export const Form = React.forwardRef<FormActions, FormProps>(
({ children, onSubmit }, ref) => {
const refs = React.useRef<FormRef[]>([]);
const { setFocus } = useFocus();

const checks = __DEV__ ? useChecks?.() : undefined;

if (callFocus) {
const focusField = (nextField?: Partial<FormRef> | undefined) => {
/**
* On some apps, if we call focus immediately and the field is already focused we lose the focus.
* Same happens if we do not call `focus` if the field is already focused.
* Refs passed as prop have another ".current"
*/
const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;

setTimeout(() => {
nextRefElement?.current?.focus();

__DEV__ &&
nextRefElement &&
checks?.checkFocusTrap({
ref: nextRefElement,
shouldHaveFocus: true,
});
}, timeoutValue);
} else if (nextRefElement?.current) {
setFocus(nextRefElement?.current);
}
};

const submitForm = async () => {
const isValid = await onSubmit();

if (isValid) {
return;
}

InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
const fieldWithError = (refs.current || []).find(
ref => ref.hasValidation && ref.hasError,
);

__DEV__ &&
fieldWithError == null &&
checks?.logResult('Form', {
message:
'The form validation has failed, but no component with error was found',
rule: 'NO_UNDEFINED',
});

focusField(fieldWithError);
}, 0);
});
};

return (
<FormContext.Provider
value={{ refs: refs.current, submitForm, focusField }}>
{children}
</FormContext.Provider>
);
};
const nextRefElement = nextField?.ref?.current?.current
? nextField?.ref.current
: nextField?.ref;

const callFocus =
// @ts-ignore
nextRefElement?.current?.focus &&
nextField?.hasFocusCallback &&
nextField?.isEditable;

__DEV__ &&
nextRefElement == null &&
checks?.logResult('nextRefElement', {
message:
'No next field found. Make sure you wrapped your form inside the <Form /> component',
rule: 'NO_UNDEFINED',
});

if (callFocus) {
/**
* On some apps, if we call focus immediately and the field is already focused we lose the focus.
* Same happens if we do not call `focus` if the field is already focused.
*/
const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;

setTimeout(() => {
nextRefElement?.current?.focus();

__DEV__ &&
nextRefElement &&
checks?.checkFocusTrap({
ref: nextRefElement,
shouldHaveFocus: true,
});
}, timeoutValue);
} else if (nextRefElement?.current) {
setFocus(nextRefElement?.current);
}
};

const submitForm = async () => {
const isValid = await onSubmit();

if (isValid) {
return;
}

focusFirstInvalidField();
};

const focusFirstInvalidField = () => {
InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
const fieldWithError = (refs.current || []).find(
fieldRef => fieldRef.hasValidation && fieldRef.hasError,
);

__DEV__ &&
fieldWithError == null &&
checks?.logResult('Form', {
message:
'The form validation has failed, but no component with error was found',
rule: 'NO_UNDEFINED',
});

focusField(fieldWithError);
}, 0);
});
};

React.useImperativeHandle(ref, () => ({
focusFirstInvalidField,
}));

return (
<FormContext.Provider
value={{ refs: refs.current, submitForm, focusField }}>
{children}
</FormContext.Provider>
);
},
);

export type FormContextValue = {
refs?: FormRef[];
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"pods": "cd example && pod-install --quiet",
"bootstrap": "yarn example && yarn && yarn pods",
"postinstall": "./scripts/init.sh",
"coverage": "yarn run test -- --coverage --watchAll=false"
"coverage": "yarn run test -- --coverage --watchAll=false",
"doc": "cd website && yarn start"
},
"husky": {
"hooks": {
Expand Down

1 comment on commit 199489e

@vercel
Copy link

@vercel vercel bot commented on 199489e Aug 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.