Skip to content

Commit afe5448

Browse files
committed
Merge branch 'development'
2 parents 040933c + d0e1e52 commit afe5448

File tree

10 files changed

+203
-37
lines changed

10 files changed

+203
-37
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@ yarn-error.log*
2525
# IDE
2626
/.idea
2727
/.run/
28+
29+
# Example
2830
/example/dist/
2931
/example/node_modules/
32+
/example/yarn.lock

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ yarn add formik-semantic-ui-react
8383
| Form | [FormProps](https://react.semantic-ui.com/collections/form/) |
8484
| Field | [FieldProps](#FieldProps) |
8585
| FormikDebug | CSS Style Props |
86-
| Input | [FieldProps](#FieldProps) & [InputProps](https://react.semantic-ui.com/elements/input/) & [FieldErrorProps](#FieldErrorProps) |
86+
| Input | [FieldProps](#FieldProps) & [InputProps](https://react.semantic-ui.com/elements/input/) & [FieldErrorProps](#FieldErrorProps) & `inputLabel` |
8787
| Radio | [FieldProps](#FieldProps) & [RadioProps](https://react.semantic-ui.com/addons/radio/) & [FieldErrorProps](#FieldErrorProps) |
8888
| Select | [FieldProps](#FieldProps) & [SelectProps](https://react.semantic-ui.com/addons/select/) & [FieldErrorProps](#FieldErrorProps) |
8989
| TextArea | [FieldProps](#FieldProps) & [TextAreaProps](https://react.semantic-ui.com/addons/text-area/) & [FieldErrorProps](#FieldErrorProps) |

example/index.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'react-app-polyfill/ie11';
22
import * as React from 'react';
33
import * as ReactDOM from 'react-dom';
44
import { Formik } from 'formik';
5-
import { Form, FormikDebug, Input, SubmitButton } from '../src';
5+
import { Checkbox, Form, Input, ResetButton, SubmitButton } from '../src';
66
import 'semantic-ui-css/semantic.min.css';
77
import styled from 'styled-components';
88
import * as Yup from 'yup';
@@ -23,13 +23,15 @@ const App = () => {
2323
const initialValues = {
2424
email: '',
2525
password: '',
26+
remember: false,
2627
};
2728

2829
const validationSchema = Yup.object({
2930
email: Yup.string().email('Invalid email address').required('Required'),
3031
password: Yup.string()
3132
.min(8, 'Must be at least 8 characters')
3233
.required('Required'),
34+
remember: Yup.boolean().required('Required').oneOf([true], 'Required'),
3335
});
3436

3537
return (
@@ -41,30 +43,36 @@ const App = () => {
4143
onSubmit={(values, { setSubmitting }) => {
4244
setTimeout(() => setSubmitting(false), 1000);
4345
}}
46+
onReset={() => console.log('YO')}
4447
>
4548
<Form size="large">
4649
<Input
47-
inputLabel={{ color: 'orange', content: 'email' }}
50+
id="input-email"
51+
inputLabel={{ color: 'orange', children: 'email' }}
4852
name="email"
4953
placeholder="Email"
50-
focus
5154
fluid
5255
errorPrompt
5356
/>
5457
<Input
58+
id="input-password"
5559
inputLabel={{ color: 'violet', content: 'password' }}
5660
name="password"
5761
type="password"
5862
placeholder="Password"
5963
autoComplete="on"
60-
focus
6164
fluid
6265
errorPrompt
6366
/>
64-
<SubmitButton primary fluid>
65-
Login
66-
</SubmitButton>
67-
<FormikDebug />
67+
<Checkbox
68+
id="checkbox-remember"
69+
name="remember"
70+
label="Remember ?"
71+
errorPrompt
72+
/>
73+
<SubmitButton primary fluid content="Login" />
74+
<ResetButton secondary fluid content="Reset" />
75+
{/*<FormikDebug />*/}
6876
</Form>
6977
</Formik>
7078
</Wrapper>

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"devDependencies": {
2525
"@types/react": "^16.9.11",
2626
"@types/react-dom": "^16.8.4",
27+
"@types/yup": "^0.29.9",
2728
"parcel": "^1.12.3",
2829
"typescript": "^3.4.5"
2930
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"@types/jest": "^26.0.15",
9797
"@types/react": "^16.9.55",
9898
"@types/react-dom": "^16.9.9",
99+
"@types/yup": "^0.29.9",
99100
"eslint-plugin-prettier": "^3.1.4",
100101
"formik": "^2.2.1",
101102
"husky": "^4.3.0",
@@ -110,7 +111,8 @@
110111
"ts-jest": "^26.4.3",
111112
"tsdx": "^0.14.1",
112113
"tslib": "^2.0.3",
113-
"typescript": "^4.0.5"
114+
"typescript": "^4.0.5",
115+
"yup": "^0.29.3"
114116
},
115117
"dependencies": {}
116118
}

src/Input.tsx

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
import React, { forwardRef, Ref } from 'react';
2-
import { FieldMetaProps, FieldProps as FormikFieldProps } from 'formik';
2+
import {
3+
FieldMetaProps,
4+
FieldProps as FormikFieldProps,
5+
isObject,
6+
isString,
7+
} from 'formik';
38
import {
49
FormField,
510
Input as _Input,
611
InputOnChangeData,
712
InputProps as _InputProps,
813
Label,
14+
LabelProps,
15+
SemanticShorthandItem,
916
} from 'semantic-ui-react';
1017
import { FieldErrorProps, FieldProps } from './types';
1118
import Field from './Field';
19+
import { RESET_BUTTON_ID } from './ResetButton';
20+
import { SUBMIT_BUTTON_ID } from './SubmitButton';
1221

1322
export interface InputProps extends FieldProps, _InputProps, FieldErrorProps {
14-
inputLabel?: string;
23+
inputLabel?: SemanticShorthandItem<LabelProps>;
1524
}
1625

1726
export const Input = (
1827
{
28+
id,
1929
name,
2030
validate,
2131
fast,
2232
onChange: _onChange,
33+
onBlur: _onBlur,
2334
errorPrompt,
2435
errorConfig,
2536
label,
@@ -28,13 +39,25 @@ export const Input = (
2839
}: InputProps,
2940
ref: Ref<_Input>,
3041
) => {
42+
const fieldLabelId = (id && label && `${id}-field-label`) || undefined;
43+
const inputLabelId = (id && inputLabel && `${id}-input-label`) || undefined;
44+
45+
const fieldLabel = label && (
46+
<label id={fieldLabelId} htmlFor={id}>
47+
{label}
48+
</label>
49+
);
50+
3151
const errorLabel = (error: string | undefined) => (
3252
<Label
53+
id={id ? `${id}-error-message` : undefined}
54+
role="alert"
55+
aria-atomic
56+
content={error}
3357
prompt={errorConfig?.prompt ?? true}
3458
basic={errorConfig?.basic}
3559
pointing={errorConfig?.pointing ?? true}
3660
color={errorConfig?.color}
37-
content={error}
3861
/>
3962
);
4063

@@ -57,27 +80,52 @@ export const Input = (
5780
return (
5881
<Field name={name} validate={validate} fast={fast}>
5982
{({ field: { value, onChange, onBlur }, meta }: FormikFieldProps) => (
60-
<FormField
61-
ref={ref}
62-
name={name}
63-
value={value}
64-
onChange={(
65-
event: React.ChangeEvent<HTMLInputElement>,
66-
data: InputOnChangeData,
67-
) => {
68-
onChange(event);
69-
_onChange && _onChange(event, data);
70-
}}
71-
onBlur={onBlur}
72-
error={meta.touched && meta.error}
73-
>
74-
{label && <Label>{label}</Label>}
83+
<FormField error={meta.touched && !!meta.error}>
84+
{fieldLabel}
7585
{errorLabelBefore(meta)}
7686
<_Input
87+
id={id}
88+
ref={ref}
7789
name={name}
78-
label={inputLabel}
90+
value={value}
91+
label={
92+
(isString(inputLabel) && {
93+
children: inputLabel,
94+
htmlFor: id,
95+
id: inputLabelId,
96+
}) ||
97+
(isObject(inputLabel) && {
98+
...(inputLabel as object),
99+
htmlFor: id,
100+
id: inputLabelId,
101+
})
102+
}
103+
onChange={(
104+
event: React.ChangeEvent<HTMLInputElement>,
105+
data: InputOnChangeData,
106+
) => {
107+
onChange(event);
108+
_onChange && _onChange(event, data);
109+
}}
110+
onBlur={(event: FocusEvent) => {
111+
if (event.relatedTarget instanceof Element) {
112+
/*
113+
Skip validation onBlur when reset / submit button is clicked or
114+
It will block reset / submit button onClick event
115+
*/
116+
if (
117+
event.relatedTarget.id === RESET_BUTTON_ID ||
118+
event.relatedTarget.id === SUBMIT_BUTTON_ID
119+
) {
120+
return;
121+
}
122+
}
123+
onBlur(event);
124+
}}
125+
aria-describedby={id && !!meta.error ? `${id}-error-message` : null}
126+
aria-invalid={!!meta.error ? true : undefined}
127+
aria-labelledby={`${fieldLabelId ?? ''} ${inputLabelId ?? ''}`}
79128
{...restProps}
80-
defaultValue={value}
81129
/>
82130
{errorLabelAfter(meta)}
83131
</FormField>

src/ResetButton.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ import React from 'react';
22
import { ButtonProps, FormButton } from 'semantic-ui-react';
33
import { useFormikContext } from 'formik';
44

5-
export const ResetButton = (props: ButtonProps) => {
5+
export const RESET_BUTTON_ID = 'formik-semantic-ui-react-reset-button';
6+
7+
export const ResetButton = ({ onClick, ...restProps }: ButtonProps) => {
68
const formContext = useFormikContext();
79
return (
8-
<FormButton {...props} type="button" onClick={formContext.handleReset} />
10+
<FormButton
11+
id={RESET_BUTTON_ID}
12+
type="button"
13+
onClick={(event, data) => {
14+
formContext.resetForm();
15+
onClick && onClick(event, data);
16+
}}
17+
{...restProps}
18+
/>
919
);
1020
};
1121

src/SubmitButton.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ interface Props {
66
disableLoading?: boolean;
77
}
88

9+
export const SUBMIT_BUTTON_ID = 'formik-semantic-ui-react-submit-button';
10+
911
export const SubmitButton = ({
1012
disableLoading,
1113
loading,
@@ -14,6 +16,7 @@ export const SubmitButton = ({
1416
const context = useFormikContext();
1517
return (
1618
<FormButton
19+
id={SUBMIT_BUTTON_ID}
1720
type="submit"
1821
loading={
1922
disableLoading

test/Input.test.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,70 @@
11
import { Formik } from 'formik';
22
import React from 'react';
3-
import { Form, Input } from '../src';
3+
import { Form, Input, ResetButton, SubmitButton } from '../src';
44
import { render, waitFor } from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
6+
import { LabelProps, SemanticShorthandItem } from 'semantic-ui-react';
7+
import * as Yup from 'yup';
68

79
interface Props {
810
label?: string;
11+
inputLabel?: SemanticShorthandItem<LabelProps>;
912
value: string;
1013
validate?: any;
14+
validationSchema?: any;
1115
}
1216

13-
const Container = ({ label, value, validate }: Props) => {
17+
const Container = ({
18+
label,
19+
inputLabel,
20+
value,
21+
validate,
22+
validationSchema,
23+
}: Props) => {
1424
return (
15-
<Formik initialValues={{ field: value }} onSubmit={() => {}}>
25+
<Formik
26+
initialValues={{ field: value }}
27+
validationSchema={validationSchema}
28+
onSubmit={() => {}}
29+
>
1630
<Form>
1731
<Input
32+
id="input-test"
1833
data-testid="input"
1934
name="field"
2035
label={label}
36+
inputLabel={inputLabel}
2137
validate={validate}
38+
errorPrompt
2239
/>
40+
<SubmitButton content="Submit" />
41+
<ResetButton content="Reset" />
2342
</Form>
2443
</Formik>
2544
);
2645
};
2746

2847
describe('Input', () => {
2948
it('should render Input component', async function () {
30-
const { getByTestId, getByText, getByRole } = render(
49+
const { getByTestId, getByText, getByRole, getByLabelText } = render(
3150
<Container label="Testing" value="value" />,
3251
);
3352
expect(getByTestId('input')).toBeInTheDocument();
53+
// Get input element by label text
54+
expect(getByLabelText('Testing')).toBeInTheDocument();
3455
expect(getByRole('textbox')).toHaveValue('value');
3556
expect(getByText('Testing')).toBeInTheDocument();
3657
});
3758

59+
it('should render Input label', function () {
60+
const { getByLabelText, getByText } = render(
61+
<Container inputLabel={'Testing'} value={''} />,
62+
);
63+
expect(getByText('Testing')).toBeInTheDocument();
64+
// Get input element by label text
65+
expect(getByLabelText('Testing')).toBeInTheDocument();
66+
});
67+
3868
it('should change value', async function () {
3969
const { getByRole } = render(<Container value="" />);
4070
expect(getByRole('textbox')).toHaveValue('');
@@ -48,4 +78,27 @@ describe('Input', () => {
4878
await userEvent.type(getByRole('textbox'), 'new value');
4979
await waitFor(() => expect(validate).toBeCalledTimes('new value'.length));
5080
});
81+
82+
it('should reset value after reset button is clicked', async function () {
83+
const { getByRole } = render(<Container value={''} />);
84+
expect(getByRole('textbox')).toHaveValue('');
85+
await userEvent.type(getByRole('textbox'), 'new value');
86+
await waitFor(() => expect(getByRole('textbox')).toHaveValue('new value'));
87+
await userEvent.click(getByRole('button', { name: 'Reset' }));
88+
await waitFor(() => expect(getByRole('textbox')).toHaveValue(''));
89+
});
90+
91+
it('should show error prompt if input is invalid', async function () {
92+
const validationSchema = Yup.object({
93+
field: Yup.string().required('Required'),
94+
});
95+
const { getByRole } = render(
96+
<Container value={''} validationSchema={validationSchema} />,
97+
);
98+
expect(getByRole('button', { name: 'Submit' })).toBeInTheDocument();
99+
await userEvent.click(getByRole('button', { name: 'Submit' }));
100+
await waitFor(() =>
101+
expect(getByRole('alert')).toHaveTextContent('Required'),
102+
);
103+
});
51104
});

0 commit comments

Comments
 (0)