@availity/mui-controlled-form 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +65 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +71 -0
- package/dist/index.mjs +47 -0
- package/docs/propDefinitions.tsx +31 -0
- package/introduction.stories.mdx +7 -0
- package/jest.config.js +7 -0
- package/package.json +66 -0
- package/project.json +41 -0
- package/src/index.ts +1 -0
- package/src/lib/AsyncAutocomplete.stories.tsx +113 -0
- package/src/lib/AsyncAutocomplete.test.tsx +162 -0
- package/src/lib/AsyncAutocomplete.tsx +92 -0
- package/src/lib/Autocomplete.stories.tsx +60 -0
- package/src/lib/Autocomplete.test.tsx +70 -0
- package/src/lib/Autocomplete.tsx +96 -0
- package/src/lib/Checkbox.stories.tsx +67 -0
- package/src/lib/Checkbox.test.tsx +73 -0
- package/src/lib/Checkbox.tsx +37 -0
- package/src/lib/CodesAutocomplete.stories.tsx +79 -0
- package/src/lib/CodesAutocomplete.test.tsx +128 -0
- package/src/lib/CodesAutocomplete.tsx +76 -0
- package/src/lib/ControlledForm.stories.tsx +74 -0
- package/src/lib/ControlledForm.test.tsx +77 -0
- package/src/lib/ControlledForm.tsx +35 -0
- package/src/lib/Datepicker.stories.tsx +63 -0
- package/src/lib/Datepicker.test.tsx +73 -0
- package/src/lib/Datepicker.tsx +49 -0
- package/src/lib/Input.stories.tsx +60 -0
- package/src/lib/Input.test.tsx +98 -0
- package/src/lib/Input.tsx +54 -0
- package/src/lib/OrganizationAutocomplete.stories.tsx +77 -0
- package/src/lib/OrganizationAutocomplete.test.tsx +125 -0
- package/src/lib/OrganizationAutocomplete.tsx +75 -0
- package/src/lib/ProviderAutocomplete.stories.tsx +79 -0
- package/src/lib/ProviderAutocomplete.test.tsx +128 -0
- package/src/lib/ProviderAutocomplete.tsx +80 -0
- package/src/lib/RadioGroup.stories.tsx +63 -0
- package/src/lib/RadioGroup.test.tsx +66 -0
- package/src/lib/RadioGroup.tsx +68 -0
- package/src/lib/Select.stories.tsx +74 -0
- package/src/lib/Select.test.tsx +68 -0
- package/src/lib/Select.tsx +55 -0
- package/src/lib/TextField.stories.tsx +67 -0
- package/src/lib/TextField.test.tsx +99 -0
- package/src/lib/TextField.tsx +67 -0
- package/tsconfig.json +5 -0
- package/tsconfig.spec.json +10 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ControlledCodesAutocomplete } from './CodesAutocomplete';
|
|
3
|
+
import { ControlledForm } from './ControlledForm';
|
|
4
|
+
import { Button } from '@availity/mui-button';
|
|
5
|
+
import { useFormContext } from 'react-hook-form';
|
|
6
|
+
import { Paper } from '@availity/mui-paper';
|
|
7
|
+
import { Typography } from '@availity/mui-typography';
|
|
8
|
+
import { Grid } from '@availity/mui-layout';
|
|
9
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
10
|
+
import { missingRHFprops } from '../../docs/propDefinitions';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof ControlledCodesAutocomplete> = {
|
|
13
|
+
title: 'Form Components/Controlled Form/Autocomplete/ControlledCodesAutocomplete',
|
|
14
|
+
component: ControlledCodesAutocomplete,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
argTypes: missingRHFprops,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
const client = new QueryClient({
|
|
22
|
+
defaultOptions: {
|
|
23
|
+
queries: {
|
|
24
|
+
refetchOnWindowFocus: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const _ControlledCodesAutoComplete: StoryObj<typeof ControlledCodesAutocomplete> = {
|
|
30
|
+
render: (args) => {
|
|
31
|
+
const SubmittedValues = () => {
|
|
32
|
+
const {
|
|
33
|
+
getValues,
|
|
34
|
+
formState: { isSubmitSuccessful },
|
|
35
|
+
} = useFormContext();
|
|
36
|
+
|
|
37
|
+
return isSubmitSuccessful ? (
|
|
38
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
39
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
40
|
+
<pre>{JSON.stringify(getValues(), null, 2)}</pre>
|
|
41
|
+
</Paper>
|
|
42
|
+
) : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const Actions = () => {
|
|
46
|
+
const {
|
|
47
|
+
reset,
|
|
48
|
+
formState: { isSubmitSuccessful },
|
|
49
|
+
} = useFormContext();
|
|
50
|
+
return (
|
|
51
|
+
<Grid container direction="row" justifyContent="space-between" marginTop={1}>
|
|
52
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
53
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
54
|
+
</Grid>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
return (
|
|
58
|
+
<QueryClientProvider client={client}>
|
|
59
|
+
<ControlledForm values={{}} onSubmit={(data) => data}>
|
|
60
|
+
<ControlledCodesAutocomplete {...args} />
|
|
61
|
+
<Actions />
|
|
62
|
+
<SubmittedValues />
|
|
63
|
+
</ControlledForm>
|
|
64
|
+
</QueryClientProvider>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
args: {
|
|
68
|
+
name: 'controlledCodesAutocomplete',
|
|
69
|
+
list: 'ABC',
|
|
70
|
+
FieldProps: {
|
|
71
|
+
label: 'Code Select',
|
|
72
|
+
helperText: 'Select a code from the list',
|
|
73
|
+
placeholder: 'Select...',
|
|
74
|
+
fullWidth: false,
|
|
75
|
+
},
|
|
76
|
+
limit: 15,
|
|
77
|
+
required: 'This is required.',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { Paper } from '@availity/mui-paper';
|
|
4
|
+
import { Typography } from '@availity/mui-typography';
|
|
5
|
+
import { useFormContext } from 'react-hook-form';
|
|
6
|
+
import { Grid } from '@availity/mui-layout';
|
|
7
|
+
import { Button } from '@availity/mui-button';
|
|
8
|
+
// eslint-disable-next-line @nx/enforce-module-boundaries
|
|
9
|
+
import { server } from '@availity/mock/src/lib/server';
|
|
10
|
+
import { ControlledForm } from './ControlledForm';
|
|
11
|
+
import { ControlledCodesAutocomplete } from './CodesAutocomplete';
|
|
12
|
+
|
|
13
|
+
const SubmittedValues = () => {
|
|
14
|
+
const {
|
|
15
|
+
getValues,
|
|
16
|
+
formState: { isSubmitSuccessful },
|
|
17
|
+
} = useFormContext();
|
|
18
|
+
|
|
19
|
+
return isSubmitSuccessful ? (
|
|
20
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
21
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
22
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
23
|
+
</Paper>
|
|
24
|
+
) : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const Actions = () => {
|
|
28
|
+
const {
|
|
29
|
+
formState: { isSubmitSuccessful },
|
|
30
|
+
} = useFormContext();
|
|
31
|
+
return (
|
|
32
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
33
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
34
|
+
</Grid>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const onSubmit = jest.fn();
|
|
39
|
+
|
|
40
|
+
describe('ControlledAsyncAutocomplete', () => {
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
// Start the interception.
|
|
43
|
+
server.listen();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
// Remove any handlers you may have added
|
|
48
|
+
// in individual tests (runtime handlers).
|
|
49
|
+
server.resetHandlers();
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const client = new QueryClient({
|
|
54
|
+
defaultOptions: {
|
|
55
|
+
queries: {
|
|
56
|
+
refetchOnWindowFocus: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should loadOptions successfully', async () => {
|
|
62
|
+
const screen = render(
|
|
63
|
+
<QueryClientProvider client={client}>
|
|
64
|
+
<ControlledForm values={{}} onSubmit={onSubmit}>
|
|
65
|
+
<ControlledCodesAutocomplete
|
|
66
|
+
name="controlledCodesAutocomplete"
|
|
67
|
+
list="ABC"
|
|
68
|
+
FieldProps={{
|
|
69
|
+
label: 'Code Select',
|
|
70
|
+
helperText: 'Select a code from the list',
|
|
71
|
+
placeholder: 'Select...',
|
|
72
|
+
fullWidth: false,
|
|
73
|
+
}}
|
|
74
|
+
limit={15}
|
|
75
|
+
/>
|
|
76
|
+
<Actions />
|
|
77
|
+
<SubmittedValues />
|
|
78
|
+
</ControlledForm>
|
|
79
|
+
</QueryClientProvider>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const dropdown = screen.getByRole('combobox');
|
|
83
|
+
fireEvent.click(dropdown);
|
|
84
|
+
fireEvent.keyDown(dropdown, { key: 'ArrowDown' });
|
|
85
|
+
|
|
86
|
+
await waitFor(() => expect(screen.getByText('171100000X - Acupuncturist')).toBeDefined());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should set the value and submit the form data', async () => {
|
|
90
|
+
const screen = render(
|
|
91
|
+
<QueryClientProvider client={client}>
|
|
92
|
+
<ControlledForm values={{}} onSubmit={onSubmit}>
|
|
93
|
+
<ControlledCodesAutocomplete
|
|
94
|
+
name="controlledCodesAutocomplete"
|
|
95
|
+
list="ABC"
|
|
96
|
+
FieldProps={{
|
|
97
|
+
label: 'Code Select',
|
|
98
|
+
helperText: 'Select a code from the list',
|
|
99
|
+
placeholder: 'Select...',
|
|
100
|
+
fullWidth: false,
|
|
101
|
+
}}
|
|
102
|
+
limit={15}
|
|
103
|
+
/>
|
|
104
|
+
<Actions />
|
|
105
|
+
<SubmittedValues />
|
|
106
|
+
</ControlledForm>
|
|
107
|
+
</QueryClientProvider>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const dropdown = screen.getByRole('combobox');
|
|
111
|
+
fireEvent.click(dropdown);
|
|
112
|
+
fireEvent.keyDown(dropdown, { key: 'ArrowDown' });
|
|
113
|
+
|
|
114
|
+
await waitFor(() => screen.getByText('171100000X - Acupuncturist'));
|
|
115
|
+
|
|
116
|
+
fireEvent.click(screen.getByText('171100000X - Acupuncturist'));
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
119
|
+
|
|
120
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
121
|
+
const result = screen.getByTestId('result');
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
const controlledCodesAutocompleteValue = JSON.parse(result.innerHTML).controlledCodesAutocomplete;
|
|
124
|
+
expect(controlledCodesAutocompleteValue.code).toBe('171100000X');
|
|
125
|
+
expect(controlledCodesAutocompleteValue.value).toBe('Acupuncturist');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CodesAutocomplete, CodesAutocompleteProps } from '@availity/mui-autocomplete';
|
|
2
|
+
import { useFormContext, Controller, RegisterOptions, ControllerProps, FieldValues } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type ControlledCodesAutocompleteProps = Omit<CodesAutocompleteProps, 'name'> &
|
|
5
|
+
Omit<RegisterOptions<FieldValues, string>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'> &
|
|
6
|
+
Pick<ControllerProps, 'defaultValue' | 'shouldUnregister' | 'name'>;
|
|
7
|
+
|
|
8
|
+
export const ControlledCodesAutocomplete = ({
|
|
9
|
+
name,
|
|
10
|
+
defaultValue,
|
|
11
|
+
deps,
|
|
12
|
+
max,
|
|
13
|
+
maxLength,
|
|
14
|
+
onBlur,
|
|
15
|
+
onChange,
|
|
16
|
+
pattern,
|
|
17
|
+
required,
|
|
18
|
+
shouldUnregister,
|
|
19
|
+
validate,
|
|
20
|
+
value,
|
|
21
|
+
FieldProps,
|
|
22
|
+
...rest
|
|
23
|
+
}: ControlledCodesAutocompleteProps) => {
|
|
24
|
+
const {
|
|
25
|
+
control,
|
|
26
|
+
formState: { errors },
|
|
27
|
+
} = useFormContext();
|
|
28
|
+
const errorMessage = errors[name]?.message;
|
|
29
|
+
return (
|
|
30
|
+
<Controller
|
|
31
|
+
name={name}
|
|
32
|
+
control={control}
|
|
33
|
+
defaultValue={defaultValue}
|
|
34
|
+
rules={{
|
|
35
|
+
deps,
|
|
36
|
+
max,
|
|
37
|
+
maxLength,
|
|
38
|
+
onBlur,
|
|
39
|
+
onChange,
|
|
40
|
+
pattern,
|
|
41
|
+
required,
|
|
42
|
+
shouldUnregister,
|
|
43
|
+
validate,
|
|
44
|
+
value,
|
|
45
|
+
}}
|
|
46
|
+
shouldUnregister={shouldUnregister}
|
|
47
|
+
render={({ field: { onChange, value, onBlur } }) => (
|
|
48
|
+
<CodesAutocomplete
|
|
49
|
+
{...rest}
|
|
50
|
+
FieldProps={{
|
|
51
|
+
...FieldProps,
|
|
52
|
+
error: !!errorMessage,
|
|
53
|
+
helperText:
|
|
54
|
+
errorMessage && typeof errorMessage === 'string' ? (
|
|
55
|
+
<>
|
|
56
|
+
{errorMessage}
|
|
57
|
+
<br />
|
|
58
|
+
{FieldProps?.helperText}
|
|
59
|
+
</>
|
|
60
|
+
) : (
|
|
61
|
+
FieldProps?.helperText
|
|
62
|
+
),
|
|
63
|
+
}}
|
|
64
|
+
onChange={(event, value, reason) => {
|
|
65
|
+
if (reason === 'clear') {
|
|
66
|
+
onChange(null);
|
|
67
|
+
}
|
|
68
|
+
onChange(value);
|
|
69
|
+
}}
|
|
70
|
+
onBlur={onBlur}
|
|
71
|
+
value={value || null}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Each exported component in the package should have its own stories file
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { useFormContext } from 'react-hook-form';
|
|
5
|
+
import { Paper } from '@availity/mui-paper';
|
|
6
|
+
import { Typography } from '@availity/mui-typography';
|
|
7
|
+
import { Grid } from '@availity/mui-layout';
|
|
8
|
+
import { Button } from '@availity/mui-button';
|
|
9
|
+
import { ControlledForm, ControlledFormProps } from './ControlledForm';
|
|
10
|
+
import { ControlledTextField } from './TextField';
|
|
11
|
+
import * as yup from 'yup';
|
|
12
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof ControlledForm> = {
|
|
15
|
+
title: 'Form Components/Controlled Form/ControlledForm',
|
|
16
|
+
component: ControlledForm,
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
export const _ControlledForm: StoryObj<typeof ControlledForm> = {
|
|
23
|
+
render: ({ values, onSubmit, ...args }: ControlledFormProps) => {
|
|
24
|
+
const SubmittedValues = () => {
|
|
25
|
+
const {
|
|
26
|
+
getValues,
|
|
27
|
+
formState: { isSubmitSuccessful },
|
|
28
|
+
} = useFormContext();
|
|
29
|
+
|
|
30
|
+
return isSubmitSuccessful ? (
|
|
31
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
32
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
33
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
34
|
+
</Paper>
|
|
35
|
+
) : null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const Actions = () => {
|
|
39
|
+
const {
|
|
40
|
+
reset,
|
|
41
|
+
formState: { isSubmitSuccessful },
|
|
42
|
+
} = useFormContext();
|
|
43
|
+
return (
|
|
44
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
45
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
46
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
47
|
+
</Grid>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<ControlledForm
|
|
52
|
+
values={values}
|
|
53
|
+
onSubmit={onSubmit}
|
|
54
|
+
schema={yup.object({ controlledTextField: yup.string().max(10) })}
|
|
55
|
+
validationResolver={yupResolver}
|
|
56
|
+
{...args}
|
|
57
|
+
>
|
|
58
|
+
<ControlledTextField
|
|
59
|
+
name="controlledTextField"
|
|
60
|
+
placeholder="Name"
|
|
61
|
+
inputProps={{
|
|
62
|
+
'data-testid': 'testTextField',
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
<Actions />
|
|
66
|
+
<SubmittedValues />
|
|
67
|
+
</ControlledForm>
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
args: {
|
|
71
|
+
values: { controlledTextField: undefined },
|
|
72
|
+
onSubmit: (data) => data,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import * as yup from 'yup';
|
|
3
|
+
import { ControlledForm } from './ControlledForm';
|
|
4
|
+
import { ControlledTextField } from './TextField';
|
|
5
|
+
import { useFormContext } from 'react-hook-form';
|
|
6
|
+
import { Paper } from '@availity/mui-paper';
|
|
7
|
+
import { Typography } from '@availity/mui-typography';
|
|
8
|
+
import { Grid } from '@availity/mui-layout';
|
|
9
|
+
import { Button } from '@availity/mui-button';
|
|
10
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
11
|
+
|
|
12
|
+
const SubmittedValues = () => {
|
|
13
|
+
const {
|
|
14
|
+
getValues,
|
|
15
|
+
formState: { isSubmitSuccessful },
|
|
16
|
+
} = useFormContext();
|
|
17
|
+
|
|
18
|
+
return isSubmitSuccessful ? (
|
|
19
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
20
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
21
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
22
|
+
</Paper>
|
|
23
|
+
) : null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Actions = () => {
|
|
27
|
+
const {
|
|
28
|
+
formState: { isSubmitSuccessful },
|
|
29
|
+
} = useFormContext();
|
|
30
|
+
return (
|
|
31
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
32
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
33
|
+
</Grid>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onSubmit = jest.fn();
|
|
38
|
+
|
|
39
|
+
describe('ControlledForm', () => {
|
|
40
|
+
test('should render successfully', () => {
|
|
41
|
+
const { getByText } = render(
|
|
42
|
+
<ControlledForm onSubmit={(data) => data} values={{}}>
|
|
43
|
+
Test
|
|
44
|
+
</ControlledForm>
|
|
45
|
+
);
|
|
46
|
+
expect(getByText('Test')).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
test('should handle yup schema resolver', async () => {
|
|
49
|
+
const screen = render(
|
|
50
|
+
<ControlledForm
|
|
51
|
+
values={{ controlledTextField: undefined }}
|
|
52
|
+
onSubmit={onSubmit}
|
|
53
|
+
schema={yup.object({ controlledTextField: yup.string().max(10) })}
|
|
54
|
+
validationResolver={yupResolver}
|
|
55
|
+
>
|
|
56
|
+
<ControlledTextField
|
|
57
|
+
name="controlledTextField"
|
|
58
|
+
placeholder="Name"
|
|
59
|
+
inputProps={{
|
|
60
|
+
'data-testid': 'testTextField',
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
<Actions />
|
|
64
|
+
<SubmittedValues />
|
|
65
|
+
</ControlledForm>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const input = screen.getByTestId('testTextField');
|
|
69
|
+
|
|
70
|
+
fireEvent.change(input, { target: { value: 'This is too much text' } });
|
|
71
|
+
|
|
72
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
73
|
+
|
|
74
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(0));
|
|
75
|
+
await waitFor(() => expect(screen.findByText('controlledTextField must be at most 10 characters')).toBeDefined());
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FormHTMLAttributes } from 'react';
|
|
2
|
+
import { useForm, SubmitHandler, FormProvider, Resolver } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
export type ControlledFormProps = {
|
|
5
|
+
/** This function will receive the form data if form validation is successful. */
|
|
6
|
+
onSubmit: SubmitHandler<any>;
|
|
7
|
+
/** Reactive values to update the form values. */
|
|
8
|
+
values: Record<string, any>;
|
|
9
|
+
/** The schema used by the validationResolver. */
|
|
10
|
+
schema?: unknown;
|
|
11
|
+
/** Integrates with your preferred schema validation library.
|
|
12
|
+
* More information on react-hook-form's resolvers can be
|
|
13
|
+
* found here: https://github.com/react-hook-form/resolvers#quickstart
|
|
14
|
+
*/
|
|
15
|
+
validationResolver?: (schema: unknown) => Resolver;
|
|
16
|
+
} & FormHTMLAttributes<HTMLFormElement>;
|
|
17
|
+
|
|
18
|
+
type UseFormOptions = {
|
|
19
|
+
values: Record<string, any>;
|
|
20
|
+
resolver?: Resolver;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const ControlledForm = ({ onSubmit, values, schema, validationResolver, ...rest }: ControlledFormProps) => {
|
|
24
|
+
const useFormOptions: UseFormOptions = { values };
|
|
25
|
+
if (schema && validationResolver) {
|
|
26
|
+
useFormOptions.resolver = validationResolver(schema);
|
|
27
|
+
}
|
|
28
|
+
const methods = useForm(useFormOptions);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<FormProvider {...methods}>
|
|
32
|
+
<form onSubmit={methods.handleSubmit(onSubmit)} {...rest} />
|
|
33
|
+
</FormProvider>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ControlledDatepicker, ControlledDatepickerProps } from './Datepicker';
|
|
3
|
+
import { ControlledForm } from './ControlledForm';
|
|
4
|
+
import { Button } from '@availity/mui-button';
|
|
5
|
+
import { useFormContext } from 'react-hook-form';
|
|
6
|
+
import { Paper } from '@availity/mui-paper';
|
|
7
|
+
import { Typography } from '@availity/mui-typography';
|
|
8
|
+
import { Grid } from '@availity/mui-layout';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof ControlledDatepicker> = {
|
|
11
|
+
title: 'Form Components/Controlled Form/ControlledDatepicker',
|
|
12
|
+
component: ControlledDatepicker,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
export const _ControlledInput: StoryObj<typeof ControlledDatepicker> = {
|
|
19
|
+
render: (args: ControlledDatepickerProps) => {
|
|
20
|
+
const SubmittedValues = () => {
|
|
21
|
+
const {
|
|
22
|
+
getValues,
|
|
23
|
+
formState: { isSubmitSuccessful },
|
|
24
|
+
} = useFormContext();
|
|
25
|
+
|
|
26
|
+
return isSubmitSuccessful ? (
|
|
27
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
28
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
29
|
+
<pre>{JSON.stringify(getValues(), null, 2)}</pre>
|
|
30
|
+
</Paper>
|
|
31
|
+
) : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const Actions = () => {
|
|
35
|
+
const {
|
|
36
|
+
reset,
|
|
37
|
+
formState: { isSubmitSuccessful },
|
|
38
|
+
} = useFormContext();
|
|
39
|
+
return (
|
|
40
|
+
<Grid container direction="row" justifyContent="space-between" marginTop={1}>
|
|
41
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
42
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
43
|
+
</Grid>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
return (
|
|
47
|
+
<ControlledForm values={{ controlledInput: undefined }} onSubmit={(data) => data}>
|
|
48
|
+
<ControlledDatepicker {...args} />
|
|
49
|
+
<Actions />
|
|
50
|
+
<SubmittedValues />
|
|
51
|
+
</ControlledForm>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
name: 'controlledDatepicker',
|
|
56
|
+
FieldProps: {
|
|
57
|
+
fullWidth: false,
|
|
58
|
+
helperText: 'Help text for the field',
|
|
59
|
+
helpTopicId: '1234',
|
|
60
|
+
label: 'Date',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
import { Paper } from '@availity/mui-paper';
|
|
4
|
+
import { Typography } from '@availity/mui-typography';
|
|
5
|
+
import { Grid } from '@availity/mui-layout';
|
|
6
|
+
import { Button } from '@availity/mui-button';
|
|
7
|
+
import { ThemeProvider } from '@availity/theme-provider';
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
import { ControlledForm } from './ControlledForm';
|
|
10
|
+
import { ControlledDatepicker } from './Datepicker';
|
|
11
|
+
|
|
12
|
+
const SubmittedValues = () => {
|
|
13
|
+
const {
|
|
14
|
+
getValues,
|
|
15
|
+
formState: { isSubmitSuccessful },
|
|
16
|
+
} = useFormContext();
|
|
17
|
+
|
|
18
|
+
return isSubmitSuccessful ? (
|
|
19
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
20
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
21
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
22
|
+
</Paper>
|
|
23
|
+
) : null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Actions = () => {
|
|
27
|
+
const {
|
|
28
|
+
formState: { isSubmitSuccessful },
|
|
29
|
+
} = useFormContext();
|
|
30
|
+
return (
|
|
31
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
32
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
33
|
+
</Grid>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onSubmit = jest.fn();
|
|
38
|
+
|
|
39
|
+
describe('Datepicker', () => {
|
|
40
|
+
test('should render successfully and submit selection', async () => {
|
|
41
|
+
const screen = render(
|
|
42
|
+
<ThemeProvider>
|
|
43
|
+
<ControlledForm values={{ controlledDatepicker: undefined }} onSubmit={onSubmit}>
|
|
44
|
+
<ControlledDatepicker
|
|
45
|
+
name="controlledDatepicker"
|
|
46
|
+
FieldProps={{
|
|
47
|
+
fullWidth: false,
|
|
48
|
+
helperText: 'Help text for the field',
|
|
49
|
+
helpTopicId: '1234',
|
|
50
|
+
label: 'Date',
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<Actions />
|
|
54
|
+
<SubmittedValues />
|
|
55
|
+
</ControlledForm>
|
|
56
|
+
</ThemeProvider>
|
|
57
|
+
);
|
|
58
|
+
expect(screen.getAllByText('Date')).toBeTruthy();
|
|
59
|
+
const input = screen.getByLabelText('Choose date');
|
|
60
|
+
fireEvent.click(input);
|
|
61
|
+
const date = screen.getAllByRole('gridcell');
|
|
62
|
+
fireEvent.click(date[7]); // The 8th gridcell will always be a day because it is the first day of the second week of the month.
|
|
63
|
+
|
|
64
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
65
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
66
|
+
const result = screen.getByTestId('result');
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
const controlledDatepickerValue = JSON.parse(result.innerHTML).controlledDatepicker;
|
|
69
|
+
expect(controlledDatepickerValue).toBeDefined();
|
|
70
|
+
expect(dayjs(controlledDatepickerValue).isValid()).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
}, 10000);
|
|
73
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Datepicker, DatepickerProps } from '@availity/mui-datepicker';
|
|
2
|
+
import { useFormContext, RegisterOptions, FieldValues, Controller, ControllerProps } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
export type ControlledDatepickerProps = DatepickerProps &
|
|
5
|
+
Omit<RegisterOptions<FieldValues, string>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'> &
|
|
6
|
+
Pick<ControllerProps, 'defaultValue' | 'shouldUnregister' | 'name'>;
|
|
7
|
+
|
|
8
|
+
export const ControlledDatepicker = ({
|
|
9
|
+
name,
|
|
10
|
+
defaultValue,
|
|
11
|
+
deps,
|
|
12
|
+
max,
|
|
13
|
+
maxLength,
|
|
14
|
+
min,
|
|
15
|
+
minLength,
|
|
16
|
+
onBlur,
|
|
17
|
+
onChange,
|
|
18
|
+
pattern,
|
|
19
|
+
required,
|
|
20
|
+
shouldUnregister,
|
|
21
|
+
validate,
|
|
22
|
+
value,
|
|
23
|
+
...rest
|
|
24
|
+
}: ControlledDatepickerProps) => {
|
|
25
|
+
const { control } = useFormContext();
|
|
26
|
+
return (
|
|
27
|
+
<Controller
|
|
28
|
+
name={name}
|
|
29
|
+
control={control}
|
|
30
|
+
defaultValue={defaultValue}
|
|
31
|
+
rules={{
|
|
32
|
+
deps,
|
|
33
|
+
max,
|
|
34
|
+
maxLength,
|
|
35
|
+
min,
|
|
36
|
+
minLength,
|
|
37
|
+
onBlur,
|
|
38
|
+
onChange,
|
|
39
|
+
pattern,
|
|
40
|
+
required,
|
|
41
|
+
shouldUnregister,
|
|
42
|
+
validate,
|
|
43
|
+
value,
|
|
44
|
+
}}
|
|
45
|
+
shouldUnregister={shouldUnregister}
|
|
46
|
+
render={({ field: { onChange, value } }) => <Datepicker {...rest} onChange={onChange} value={value || null} />}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|