Skip to content

Commit 59a6a87

Browse files
committed
feat(manager): focus on error input
1 parent 2ae7a57 commit 59a6a87

File tree

6 files changed

+83
-11
lines changed

6 files changed

+83
-11
lines changed

packages/form-state-manager/demo/index.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import React from 'react';
22
import ReactDOM from 'react-dom';
33

44
import FormStateManager from '../src/files/form-state-manager';
5+
import FormSpy from '../src/files/form-spy';
6+
57
import TextField from '../src/tests/helpers/text-field';
68
import { Validator } from '../src/types/validate';
79

8-
const asyncValidator: Validator = (value) => new Promise((res, rej) => setTimeout(() => (value === 'foo' ? rej('No async foo') : res()), 250));
10+
const asyncValidator: Validator = (value) => new Promise((res, rej) => setTimeout(() => (value === 'foo' ? rej('No async foo') : res()), 1000));
11+
const asyncValidator1: Validator = (value) => new Promise((res, rej) => setTimeout(() => (value === 'foo' ? rej('No async foo') : res()), 3000));
912

1013
const App = () => {
1114
return (
@@ -15,14 +18,22 @@ const App = () => {
1518
return (
1619
<form onSubmit={handleSubmit}>
1720
<h1>There will be children</h1>
18-
<TextField
19-
validate={(value) => (value === 'foo' ? 'no-foo-allowed' : undefined)}
20-
label="Field 1"
21-
name="nested.field-1"
22-
id="field-1"
23-
type="text"
24-
/>
25-
<TextField validate={asyncValidator} label="Field 2" name="field-2" id="field-2" type="text" />
21+
<TextField initialValue="foo" autocomplete="off" validate={asyncValidator} label="Field 2" name="field-2-23" id="field-2" type="text" />
22+
<TextField autocomplete="off" validate={asyncValidator} label="Field 2" name="field-2" id="field-2" type="text" />
23+
<FormSpy>
24+
{(props) => (
25+
<pre>
26+
{JSON.stringify(
27+
{
28+
validate: props.valid,
29+
validating: props.validating
30+
},
31+
null,
32+
2
33+
)}
34+
</pre>
35+
)}
36+
</FormSpy>
2637
<div style={{ margin: 16 }}>
2738
<button type="submit">Submit</button>
2839
</div>

packages/form-state-manager/src/tests/helpers/text-field.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ export interface TextFieldProps {
1010
const TextField: React.ComponentType<React.HTMLProps<HTMLInputElement> & TextFieldProps & UseFieldConfig> = ({ label, id, ...props }) => {
1111
const {
1212
input,
13-
meta: { valid, error },
13+
meta: { valid, error, validating, touched },
1414
...rest
1515
} = useField(props);
1616
return (
1717
<div style={{ display: 'flex', flexDirection: 'column', margin: 16 }}>
1818
<label htmlFor={id}>{label}</label>
1919
<input id={id} {...input} {...rest} />
20-
{!valid && <p style={{ color: 'red' }}>{error}</p>}
20+
{validating && <div>I am being validated</div>}
21+
{touched && <p style={{ color: 'red' }}>{error}</p>}
2122
</div>
2223
);
2324
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import focusError from '../../utils/focus-error';
2+
3+
describe('focusError', () => {
4+
it('focus first error element', async () => {
5+
document.body.innerHTML = `<form name="foo">
6+
<input name="field-1" />
7+
<input name="field-2" />
8+
<input name="field-3" />
9+
</form>`;
10+
11+
const listener = jest.fn();
12+
13+
document.querySelector('[name="field-2"]').addEventListener('focus', listener);
14+
15+
expect(listener).not.toHaveBeenCalled();
16+
17+
focusError({ 'field-3': 'error', 'field-2': 'error' });
18+
19+
expect(listener).toHaveBeenCalled();
20+
});
21+
});

packages/form-state-manager/src/tests/utils/manager-api.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import createManagerApi, { initialMeta, flatObject } from '../../utils/manager-api';
22
import FORM_ERROR from '../../files/form-error';
3+
import * as focusError from '../../utils/focus-error';
34

45
describe('managerApi', () => {
6+
let focusErrorSpy;
7+
8+
beforeEach(() => {
9+
focusErrorSpy = jest.spyOn(focusError, 'default');
10+
});
11+
12+
afterEach(() => {
13+
focusErrorSpy.mockClear();
14+
});
15+
516
it('should create managerApi getter', () => {
617
const managerApi = createManagerApi({});
718
expect(managerApi).toEqual(expect.any(Function));
@@ -583,6 +594,7 @@ describe('managerApi', () => {
583594
});
584595

585596
it('onsubmit receives an error', () => {
597+
const focusErrorSpy = jest.spyOn(focusError, 'default');
586598
const error = 'some-error';
587599
const onSubmit = jest.fn().mockImplementation(() => error);
588600
const managerApi = createManagerApi({ onSubmit });
@@ -603,6 +615,7 @@ describe('managerApi', () => {
603615
expect(managerApi().submitErrors).toEqual(error);
604616
expect(managerApi().hasSubmitErrors).toEqual(true);
605617
expect(managerApi().hasValidationErrors).toEqual(false);
618+
expect(focusErrorSpy).toHaveBeenCalled();
606619
});
607620

608621
it('onsubmit receives an error - form level', () => {
@@ -2071,6 +2084,7 @@ describe('managerApi', () => {
20712084
expect(managerApi().submitSucceeded).toEqual(false);
20722085
expect(managerApi().submitErrors).toEqual(error);
20732086
expect(managerApi().hasSubmitErrors).toEqual(true);
2087+
expect(focusErrorSpy).toHaveBeenCalled();
20742088

20752089
expect(managerApi().getFieldState('nested.field').submitError).toEqual('some evil error');
20762090
expect(managerApi().getFieldState('nested.field').submitting).toEqual(false);
@@ -2194,9 +2208,11 @@ describe('managerApi', () => {
21942208

21952209
expect(managerApi().getFieldState('field').touched).toEqual(false);
21962210
expect(managerApi().getFieldState('field').touched).toEqual(false);
2211+
expect(focusErrorSpy).not.toHaveBeenCalled();
21972212

21982213
managerApi().handleSubmit();
21992214

2215+
expect(focusErrorSpy).toHaveBeenCalled();
22002216
expect(managerApi().getFieldState('field').touched).toEqual(true);
22012217
expect(managerApi().getFieldState('field').touched).toEqual(true);
22022218

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import AnyObject from '../types/any-object';
2+
3+
const focusError = (errors: AnyObject) => {
4+
if (document) {
5+
const allInputs = Array.from(document.forms)
6+
.reduce((acc: any, curr: any) => [...acc, ...curr.elements], [])
7+
.filter((el: HTMLElement) => el.focus);
8+
9+
const errorKeys = Object.keys(errors);
10+
11+
const firstError = allInputs.find((inp) => errorKeys.includes(inp.name));
12+
13+
firstError && firstError.focus();
14+
}
15+
};
16+
17+
export default focusError;

packages/form-state-manager/src/utils/manager-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { formLevelValidator, isPromise } from './validate';
2929
import { FormValidator, FormLevelError, Validator } from '../types/validate';
3030
import findDifference from './find-difference';
3131
import FORM_ERROR from '../files/form-error';
32+
import focusError from './focus-error';
3233

3334
export const defaultIsEqual = (a: any, b: any) => a === b;
3435

@@ -611,6 +612,8 @@ const createManagerApi: CreateManagerApi = ({
611612
}));
612613
});
613614

615+
focusError(state.errors);
616+
614617
return;
615618
}
616619

@@ -636,6 +639,7 @@ const createManagerApi: CreateManagerApi = ({
636639
handleSubmitError(errors);
637640
updateFieldSubmitMeta();
638641
render();
642+
focusError(flatSubmitErrors);
639643

640644
runAfterSubmit();
641645
})
@@ -652,6 +656,8 @@ const createManagerApi: CreateManagerApi = ({
652656

653657
render();
654658

659+
focusError(flatSubmitErrors);
660+
655661
runAfterSubmit();
656662
}
657663
}

0 commit comments

Comments
 (0)