Skip to content
This repository was archived by the owner on Dec 1, 2025. It is now read-only.

Commit 105c5c4

Browse files
committed
forms new components Phone and Email with validation, also Inputs will required label prop to be present for accessibility
1 parent a68fba3 commit 105c5c4

22 files changed

Lines changed: 273 additions & 157 deletions

File tree

packages/forms/src/__mocks__/setup.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ export function formSetup({
3434
</Form>,
3535
);
3636

37+
// console.log(renderArg);
38+
3739
return { ...renderArg, ...utils };
3840
}

packages/forms/src/__tests__/checkbox.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { renderFields } from '../renderFields';
55
import { fireEvent } from '@testing-library/react';
66
import { axe } from 'jest-axe';
77

8-
describe('Checkbox', () => {
8+
describe('Checkbox input', () => {
99
test('is accessible', async () => {
1010
const handleSubmit = jest.fn();
1111
const { container } = formSetup({

packages/forms/src/__tests__/date.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { axe } from 'jest-axe';
66

77
// TODO: write more test when there are clear specs for date input validation
88

9-
describe('Date', () => {
9+
describe('Date input', () => {
1010
test('is accessible', async () => {
1111
const fields: FieldProps[] = [
1212
{
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { formSetup } from '../__mocks__/setup';
3+
import { FFInputEmail } from '../elements/email/email';
4+
import { axe } from 'jest-axe';
5+
import userEvent from '@testing-library/user-event';
6+
7+
describe('Email input', () => {
8+
test('is accessible', async () => {
9+
const { container } = formSetup({
10+
render: <FFInputEmail label="Email" testId="email-input" name="email" />,
11+
});
12+
const results = await axe(container);
13+
expect(results).toHaveNoViolations();
14+
});
15+
16+
test('values get captured correctly', async () => {
17+
const testId = 'email-input';
18+
const handleSubmit = jest.fn();
19+
const { getByText, getByTestId } = formSetup({
20+
render: <FFInputEmail label="Email" testId={testId} name="email" />,
21+
onSubmit: handleSubmit,
22+
});
23+
24+
userEvent.type(getByTestId(testId), 'david.alekna@tpr.gov.uk');
25+
getByText('Submit').click();
26+
27+
expect(getByTestId(testId)).toHaveValue('david.alekna@tpr.gov.uk');
28+
});
29+
30+
test('should not accept invalid email addresses', async () => {
31+
const testId = 'email-input';
32+
const handleSubmit = jest.fn();
33+
const { getByText, getByTestId, form } = formSetup({
34+
render: <FFInputEmail label="Email" testId={testId} name="email" />,
35+
onSubmit: handleSubmit,
36+
});
37+
38+
userEvent.type(getByTestId(testId), 'this is not an email address');
39+
getByText('Submit').click();
40+
41+
expect(form.getState().valid).toBeFalsy();
42+
});
43+
44+
test('accepts only valid emails', async () => {
45+
const testId = 'email-input';
46+
const handleSubmit = jest.fn();
47+
const { getByText, getByTestId, form } = formSetup({
48+
render: <FFInputEmail label="Email" testId={testId} name="email" />,
49+
onSubmit: handleSubmit,
50+
});
51+
52+
userEvent.type(getByTestId(testId), 'david.alekna@tpr.gov.uk');
53+
getByText('Submit').click();
54+
55+
expect(form.getState().valid).toBeTruthy();
56+
});
57+
58+
test('composes custom validation function', async () => {
59+
const testId = 'email-input';
60+
const errorMessage = 'Must be a TPR email';
61+
const handleSubmit = jest.fn();
62+
const { getByText, getByTestId, queryByText, form } = formSetup({
63+
render: (
64+
<FFInputEmail
65+
label="Email"
66+
testId={testId}
67+
name="email"
68+
validate={(email) =>
69+
email && email.includes('tpr.gov.uk') ? undefined : errorMessage
70+
}
71+
/>
72+
),
73+
onSubmit: handleSubmit,
74+
});
75+
76+
userEvent.type(getByTestId(testId), 'david.alekna@gmail.com');
77+
getByText('Submit').click();
78+
79+
expect(queryByText(errorMessage)).toBeInTheDocument();
80+
expect(form.getState().valid).toBeFalsy();
81+
});
82+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { formSetup } from '../__mocks__/setup';
3+
import { FFInputPhone } from '../elements/phone/phone';
4+
import { axe } from 'jest-axe';
5+
import userEvent from '@testing-library/user-event';
6+
7+
describe('Phone input', () => {
8+
test('is accessible', async () => {
9+
const { container } = formSetup({
10+
render: (
11+
<FFInputPhone label="Phone number" testId="phone-input" name="phone" />
12+
),
13+
});
14+
const results = await axe(container);
15+
expect(results).toHaveNoViolations();
16+
});
17+
18+
test('values get captured correctly', async () => {
19+
const testId = 'phone-input';
20+
const handleSubmit = jest.fn();
21+
const { getByText, getByTestId } = formSetup({
22+
render: (
23+
<FFInputPhone label="Phone number" testId={testId} name="phone" />
24+
),
25+
onSubmit: handleSubmit,
26+
});
27+
28+
userEvent.type(getByTestId(testId), '07543 221 321');
29+
getByText('Submit').click();
30+
31+
expect(getByTestId(testId)).toHaveValue('07543 221 321');
32+
});
33+
34+
test('should not accept invalid numbers', async () => {
35+
const testId = 'phone-input';
36+
const handleSubmit = jest.fn();
37+
const { getByText, getByTestId, form } = formSetup({
38+
render: (
39+
<FFInputPhone label="Phone number" testId={testId} name="phone" />
40+
),
41+
onSubmit: handleSubmit,
42+
});
43+
44+
userEvent.type(
45+
getByTestId(testId),
46+
'this is not a valid phone number address',
47+
);
48+
getByText('Submit').click();
49+
50+
expect(form.getState().valid).toBeFalsy();
51+
expect(getByText('Invalid phone number')).toBeInTheDocument();
52+
});
53+
54+
test('accepts only valid numbers', async () => {
55+
const testId = 'phone-input';
56+
const handleSubmit = jest.fn();
57+
const { getByText, getByTestId, form } = formSetup({
58+
render: (
59+
<FFInputPhone label="Phone number" testId={testId} name="phone" />
60+
),
61+
onSubmit: handleSubmit,
62+
});
63+
64+
userEvent.type(getByTestId(testId), '07543 221 321');
65+
getByText('Submit').click();
66+
67+
expect(form.getState().valid).toBeTruthy();
68+
});
69+
70+
test('composes custom validation function', async () => {
71+
const testId = 'phone-input';
72+
const errorMessage = 'Must include tripple 7';
73+
const handleSubmit = jest.fn();
74+
const { getByText, getByTestId, queryByText, form } = formSetup({
75+
render: (
76+
<FFInputPhone
77+
label="Phone number"
78+
testId={testId}
79+
name="phone"
80+
validate={(phone) =>
81+
phone && phone.includes('777') ? undefined : errorMessage
82+
}
83+
/>
84+
),
85+
onSubmit: handleSubmit,
86+
});
87+
88+
userEvent.type(getByTestId(testId), '07543 221 321');
89+
getByText('Submit').click();
90+
91+
expect(queryByText(errorMessage)).toBeInTheDocument();
92+
expect(form.getState().valid).toBeFalsy();
93+
});
94+
});

packages/forms/src/__tests__/radio.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { renderFields } from '../index';
55
import { fireEvent } from '@testing-library/react';
66
import { axe } from 'jest-axe';
77

8-
describe('RadioButton', () => {
8+
describe('Radio input', () => {
99
test('is accessible', async () => {
1010
const handleSubmit = jest.fn();
1111
const { container } = formSetup({

packages/forms/src/__tests__/select.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { formSetup } from '../__mocks__/setup';
33
import { FFSelect } from '../elements/select/select';
44
import { axe } from 'jest-axe';
55

6-
describe('Select', () => {
6+
describe('Select input', () => {
77
test('is accessible', async () => {
88
const testId = 'select-input';
99
const items = [

packages/forms/src/__tests__/text.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { validate, renderFields } from '../index';
66
import { FieldProps } from '../renderFields';
77
import { axe } from 'jest-axe';
88

9-
describe('Text', () => {
9+
describe('Text input', () => {
1010
test('is accessible', async () => {
1111
const { container } = formSetup({
1212
render: (
@@ -80,7 +80,7 @@ describe('Text', () => {
8080
test('handles input', async () => {
8181
const handleSubmit = jest.fn();
8282
const { container, form } = formSetup({
83-
render: <FFInputText name="name" type="text" />,
83+
render: <FFInputText label="Input Text" name="name" type="text" />,
8484
onSubmit: handleSubmit,
8585
});
8686
const name = container.querySelector('input[name="name"]');

packages/forms/src/elements/email/email.mdx

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ route: /forms/email
66

77
import { Playground } from '@playground';
88
import { Form } from 'react-final-form';
9-
import { FFInputEmail, validateEmail } from './email';
9+
import { FFInputEmail } from './email';
1010
import { validate } from '../../validation';
1111
import { renderFields } from '../../renderFields';
1212

@@ -54,65 +54,20 @@ Example using field level validation
5454
label="Event name"
5555
placeholder="john.smith@tpr.gov.uk"
5656
hint="Must be between 6 and 8 digits long"
57+
validate={(email) =>
58+
email && email.includes('tpr.gov.uk')
59+
? undefined
60+
: 'Must be a TPR email'
61+
}
5762
inputWidth={5}
5863
cfg={{ my: 5 }}
5964
/>
60-
<button type="submit" children="Submit" />
65+
<button type="submit" style={{ display: 'none' }} children="Submit" />
6166
</form>
6267
)}
6368
</Form>
6469
</Playground>
6570

66-
<br />
67-
Example using record level validation
68-
69-
```js
70-
import { Form, validate, renderFields } from '@tpr/forms';
71-
```
72-
73-
<Playground>
74-
{() => {
75-
const fields = [
76-
{
77-
type: 'email',
78-
name: 'event_place',
79-
label: 'Event place',
80-
hint: 'The word must be London exactly',
81-
error: (value, _fields) => {
82-
return value === 'London' ? undefined : 'Must be in London';
83-
},
84-
placeholder: 'add some text here...',
85-
inputWidth: 5,
86-
cfg: { my: 5 },
87-
},
88-
{
89-
type: 'email',
90-
name: 'event_name',
91-
label: 'Event name',
92-
hint: 'Cannot be empty',
93-
error: 'Enter an event name',
94-
placeholder: 'add some text here...',
95-
inputWidth: 5,
96-
cfg: { my: 5 },
97-
},
98-
];
99-
return (
100-
<Form onSubmit={console.log} validate={validate(fields)}>
101-
{({ handleSubmit }) => (
102-
<form onSubmit={handleSubmit}>
103-
{renderFields(fields)}
104-
<button
105-
type="submit"
106-
style={{ display: 'none' }}
107-
children="Submit"
108-
/>
109-
</form>
110-
)}
111-
</Form>
112-
);
113-
}}
114-
</Playground>
115-
11671
## API
11772

11873
```

packages/forms/src/elements/email/email.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,11 @@ import { Field, FieldRenderProps } from 'react-final-form';
33
import { StyledInputLabel, InputElementHeading } from '../elements';
44
import { FieldProps, FieldExtraProps } from '../../renderFields';
55
import { Input } from '../input/input';
6-
7-
// NOTE: composition option for validate on FFInputEmail
8-
// const compose = (...functions: Function[]) => (args: any[]) => {
9-
// return functions.reduceRight((arg, fn) => {
10-
// if (typeof fn === 'function') {
11-
// const fnValue = fn(arg);
12-
// console.log(fnValue);
13-
// if (fnValue) {
14-
// return fnValue;
15-
// }
16-
// }
17-
// }, args);
18-
// };
19-
20-
export function validEmail(email: string) {
21-
const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
22-
return regex.test(String(email).toLowerCase());
23-
}
6+
import {
7+
composeValidators,
8+
isEmailValid,
9+
executeClientValidation,
10+
} from '../../validators';
2411

2512
type InputEmailProps = FieldRenderProps<string> & FieldExtraProps;
2613
const InputEmail: React.FC<InputEmailProps> = ({
@@ -30,6 +17,7 @@ const InputEmail: React.FC<InputEmailProps> = ({
3017
testId,
3118
meta,
3219
required,
20+
placeholder,
3321
inputWidth: width,
3422
cfg,
3523
}) => {
@@ -49,6 +37,7 @@ const InputEmail: React.FC<InputEmailProps> = ({
4937
width={width}
5038
testId={testId}
5139
label={label}
40+
placeholder={placeholder}
5241
touched={meta && meta.touched && meta.error}
5342
{...input}
5443
/>
@@ -63,15 +52,10 @@ export const FFInputEmail: React.FC<FieldProps & FieldExtraProps> = (
6352
<Field
6453
{...fieldProps}
6554
required={typeof fieldProps.validate === 'function' || fieldProps.error}
66-
validate={(email, allValues) => {
67-
// NOTE: might be a good option to use currying but then we would
68-
// need to provide default error message
69-
if (fieldProps.validate) {
70-
return fieldProps.validate(email, allValues);
71-
} else {
72-
return validEmail(email) ? undefined : 'Invalid email address';
73-
}
74-
}}
55+
validate={composeValidators(
56+
executeClientValidation(fieldProps.validate),
57+
isEmailValid('Invalid email address'),
58+
)}
7559
component={InputEmail}
7660
/>
7761
);

0 commit comments

Comments
 (0)