@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,162 @@
|
|
|
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
|
+
import AvApi, { ApiConfig } from '@availity/api-axios';
|
|
9
|
+
// eslint-disable-next-line @nx/enforce-module-boundaries
|
|
10
|
+
import { server } from '@availity/mock/src/lib/server';
|
|
11
|
+
import { ControlledForm } from './ControlledForm';
|
|
12
|
+
import { ControlledAsyncAutocomplete } from './AsyncAutocomplete';
|
|
13
|
+
|
|
14
|
+
const api = new AvApi({ name: 'example' } as ApiConfig);
|
|
15
|
+
|
|
16
|
+
type Option = {
|
|
17
|
+
label: string;
|
|
18
|
+
value: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ExampleResponse = {
|
|
22
|
+
totalCount: number;
|
|
23
|
+
options: Option[];
|
|
24
|
+
count: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getResults = async (offset: number, limit: number) => {
|
|
28
|
+
// const offset = page * limit;
|
|
29
|
+
const resp = await api.post<ExampleResponse>({ offset, limit }, { params: {} });
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
totalCount: resp.data.totalCount,
|
|
33
|
+
offset,
|
|
34
|
+
limit,
|
|
35
|
+
options: resp.data.options,
|
|
36
|
+
count: resp.data.count,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const loadOptions = async (offset: number, limit: number) => {
|
|
41
|
+
const { options, totalCount } = await getResults(offset, limit);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
options,
|
|
45
|
+
hasMore: offset + limit < totalCount,
|
|
46
|
+
offset,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SubmittedValues = () => {
|
|
51
|
+
const {
|
|
52
|
+
getValues,
|
|
53
|
+
formState: { isSubmitSuccessful },
|
|
54
|
+
} = useFormContext();
|
|
55
|
+
|
|
56
|
+
return isSubmitSuccessful ? (
|
|
57
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
58
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
59
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
60
|
+
</Paper>
|
|
61
|
+
) : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const Actions = () => {
|
|
65
|
+
const {
|
|
66
|
+
reset,
|
|
67
|
+
formState: { isSubmitSuccessful },
|
|
68
|
+
} = useFormContext();
|
|
69
|
+
return (
|
|
70
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
71
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
72
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
73
|
+
</Grid>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const onSubmit = jest.fn();
|
|
78
|
+
|
|
79
|
+
describe('ControlledAsyncAutocomplete', () => {
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
// Start the interception.
|
|
82
|
+
server.listen();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
// Remove any handlers you may have added
|
|
87
|
+
// in individual tests (runtime handlers).
|
|
88
|
+
server.resetHandlers();
|
|
89
|
+
jest.restoreAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const client = new QueryClient({
|
|
93
|
+
defaultOptions: {
|
|
94
|
+
queries: {
|
|
95
|
+
refetchOnWindowFocus: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should loadOptions successfully', async () => {
|
|
101
|
+
const screen = render(
|
|
102
|
+
<QueryClientProvider client={client}>
|
|
103
|
+
<ControlledForm values={{ controlledAutocomplete: undefined }} onSubmit={(data) => data}>
|
|
104
|
+
<ControlledAsyncAutocomplete
|
|
105
|
+
name="controlledAsyncAutocomplete"
|
|
106
|
+
FieldProps={{ label: 'Async Select', helperText: 'Helper Text', fullWidth: false }}
|
|
107
|
+
getOptionLabel={(val: Option) => val.label}
|
|
108
|
+
loadOptions={loadOptions}
|
|
109
|
+
limit={10}
|
|
110
|
+
queryKey="example"
|
|
111
|
+
/>
|
|
112
|
+
<Actions />
|
|
113
|
+
<SubmittedValues />
|
|
114
|
+
</ControlledForm>
|
|
115
|
+
</QueryClientProvider>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const dropdown = screen.getByRole('combobox');
|
|
119
|
+
fireEvent.click(dropdown);
|
|
120
|
+
fireEvent.keyDown(dropdown, { key: 'ArrowDown' });
|
|
121
|
+
|
|
122
|
+
await waitFor(() => expect(screen.getByText('Option 1')).toBeDefined());
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should set the value and submit the form data', async () => {
|
|
126
|
+
const screen = render(
|
|
127
|
+
<QueryClientProvider client={client}>
|
|
128
|
+
<ControlledForm values={{ controlledAutocomplete: undefined }} onSubmit={onSubmit}>
|
|
129
|
+
<ControlledAsyncAutocomplete
|
|
130
|
+
name="controlledAsyncAutocomplete"
|
|
131
|
+
FieldProps={{ label: 'Async Select', helperText: 'Helper Text', fullWidth: false }}
|
|
132
|
+
getOptionLabel={(val: Option) => val.label}
|
|
133
|
+
loadOptions={loadOptions}
|
|
134
|
+
limit={10}
|
|
135
|
+
queryKey="example"
|
|
136
|
+
/>
|
|
137
|
+
<Actions />
|
|
138
|
+
<SubmittedValues />
|
|
139
|
+
</ControlledForm>
|
|
140
|
+
</QueryClientProvider>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const dropdown = screen.getByRole('combobox');
|
|
144
|
+
fireEvent.click(dropdown);
|
|
145
|
+
fireEvent.keyDown(dropdown, { key: 'ArrowDown' });
|
|
146
|
+
|
|
147
|
+
await waitFor(() => screen.getByText('Option 1'));
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByText('Option 1'));
|
|
150
|
+
|
|
151
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
152
|
+
|
|
153
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
154
|
+
const result = screen.getByTestId('result');
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
const controlledAsyncAutocompleteValue = JSON.parse(result.innerHTML).controlledAsyncAutocomplete;
|
|
157
|
+
expect(controlledAsyncAutocompleteValue.label).toBe('Option 1');
|
|
158
|
+
expect(controlledAsyncAutocompleteValue.value).toBe(1);
|
|
159
|
+
expect(controlledAsyncAutocompleteValue.id).toBeDefined(); // This is a unique id
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AsyncAutocomplete, AsyncAutocompleteProps } from '@availity/mui-autocomplete';
|
|
2
|
+
import { useFormContext, RegisterOptions, FieldValues, Controller, ControllerProps } from 'react-hook-form';
|
|
3
|
+
import { ChipTypeMap } from '@mui/material/Chip';
|
|
4
|
+
|
|
5
|
+
type ControlledAsyncAutocompleteProps<
|
|
6
|
+
Option,
|
|
7
|
+
Multiple extends boolean | undefined,
|
|
8
|
+
DisableClearable extends boolean | undefined,
|
|
9
|
+
FreeSolo extends boolean | undefined,
|
|
10
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
|
|
11
|
+
> = Omit<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'name'> &
|
|
12
|
+
Omit<RegisterOptions<FieldValues, string>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'> &
|
|
13
|
+
Pick<ControllerProps, 'defaultValue' | 'shouldUnregister' | 'name'>;
|
|
14
|
+
|
|
15
|
+
export const ControlledAsyncAutocomplete = <
|
|
16
|
+
Option,
|
|
17
|
+
Multiple extends boolean | undefined = false,
|
|
18
|
+
DisableClearable extends boolean | undefined = false,
|
|
19
|
+
FreeSolo extends boolean | undefined = false,
|
|
20
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
|
|
21
|
+
>({
|
|
22
|
+
name,
|
|
23
|
+
deps,
|
|
24
|
+
max,
|
|
25
|
+
maxLength,
|
|
26
|
+
min,
|
|
27
|
+
minLength,
|
|
28
|
+
onBlur,
|
|
29
|
+
onChange,
|
|
30
|
+
pattern,
|
|
31
|
+
required,
|
|
32
|
+
shouldUnregister,
|
|
33
|
+
validate,
|
|
34
|
+
value,
|
|
35
|
+
FieldProps,
|
|
36
|
+
...rest
|
|
37
|
+
}: ControlledAsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
|
|
38
|
+
const {
|
|
39
|
+
control,
|
|
40
|
+
formState: { errors },
|
|
41
|
+
} = useFormContext();
|
|
42
|
+
const errorMessage = errors[name]?.message;
|
|
43
|
+
return (
|
|
44
|
+
<Controller
|
|
45
|
+
name={name}
|
|
46
|
+
control={control}
|
|
47
|
+
defaultValue={rest.defaultValue}
|
|
48
|
+
rules={{
|
|
49
|
+
deps,
|
|
50
|
+
max,
|
|
51
|
+
maxLength,
|
|
52
|
+
min,
|
|
53
|
+
minLength,
|
|
54
|
+
onBlur,
|
|
55
|
+
onChange,
|
|
56
|
+
pattern,
|
|
57
|
+
required,
|
|
58
|
+
shouldUnregister,
|
|
59
|
+
validate,
|
|
60
|
+
value,
|
|
61
|
+
}}
|
|
62
|
+
shouldUnregister={shouldUnregister}
|
|
63
|
+
render={({ field: { onChange, value, onBlur } }) => (
|
|
64
|
+
<AsyncAutocomplete
|
|
65
|
+
{...rest}
|
|
66
|
+
FieldProps={{
|
|
67
|
+
...FieldProps,
|
|
68
|
+
error: !!errorMessage,
|
|
69
|
+
helperText:
|
|
70
|
+
errorMessage && typeof errorMessage === 'string' ? (
|
|
71
|
+
<>
|
|
72
|
+
{errorMessage}
|
|
73
|
+
<br />
|
|
74
|
+
{FieldProps?.helperText}
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
FieldProps?.helperText
|
|
78
|
+
),
|
|
79
|
+
}}
|
|
80
|
+
onChange={(event, value, reason) => {
|
|
81
|
+
if (reason === 'clear') {
|
|
82
|
+
onChange(null);
|
|
83
|
+
}
|
|
84
|
+
onChange(value);
|
|
85
|
+
}}
|
|
86
|
+
onBlur={onBlur}
|
|
87
|
+
value={value || null}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ControlledAutocomplete } from './Autocomplete';
|
|
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 ControlledAutocomplete> = {
|
|
11
|
+
title: 'Form Components/Controlled Form/Autocomplete/ControlledAutocomplete',
|
|
12
|
+
component: ControlledAutocomplete,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
export const _ControlledAutoComplete: StoryObj<typeof ControlledAutocomplete> = {
|
|
19
|
+
render: (args) => {
|
|
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={{ controlledAutocomplete: undefined }} onSubmit={(data) => data}>
|
|
48
|
+
<ControlledAutocomplete {...args} />
|
|
49
|
+
<Actions />
|
|
50
|
+
<SubmittedValues />
|
|
51
|
+
</ControlledForm>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
name: 'controlledAutocomplete',
|
|
56
|
+
options: ['Option 1', 'Option 2'],
|
|
57
|
+
required: 'This is required.',
|
|
58
|
+
FieldProps: { label: 'Autocomplete Label' },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fireEvent, render, 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 { ControlledForm } from './ControlledForm';
|
|
8
|
+
import { ControlledAutocomplete } from './Autocomplete';
|
|
9
|
+
|
|
10
|
+
const onSubmit = jest.fn();
|
|
11
|
+
|
|
12
|
+
describe('ControlledAsyncAutocomplete', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
// Remove any handlers you may have added
|
|
15
|
+
// in individual tests (runtime handlers).
|
|
16
|
+
jest.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should set the value and submit the form', async () => {
|
|
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 data-testid="result">{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">
|
|
41
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
42
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
43
|
+
</Grid>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
const screen = render(
|
|
47
|
+
<ControlledForm values={{ controlledAutocomplete: undefined }} onSubmit={onSubmit}>
|
|
48
|
+
<ControlledAutocomplete name="controlledAutocomplete" options={['Option 1', 'Option 2']} />
|
|
49
|
+
<Actions />
|
|
50
|
+
<SubmittedValues />
|
|
51
|
+
</ControlledForm>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const dropdown = screen.getByRole('combobox');
|
|
55
|
+
fireEvent.click(dropdown);
|
|
56
|
+
fireEvent.keyDown(dropdown, { key: 'ArrowDown' });
|
|
57
|
+
|
|
58
|
+
fireEvent.click(screen.getByText('Option 1'));
|
|
59
|
+
|
|
60
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
61
|
+
|
|
62
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
63
|
+
|
|
64
|
+
const result = screen.getByTestId('result');
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
const controlledAutocompleteValue = JSON.parse(result.innerHTML).controlledAutocomplete;
|
|
67
|
+
expect(controlledAutocompleteValue).toBe('Option 1');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Autocomplete, AutocompleteProps } from '@availity/mui-autocomplete';
|
|
2
|
+
import { useFormContext, RegisterOptions, FieldValues, Controller, ControllerProps } from 'react-hook-form';
|
|
3
|
+
import { ChipTypeMap } from '@mui/material/Chip';
|
|
4
|
+
|
|
5
|
+
export type ControlledAutocompleteProps<
|
|
6
|
+
T,
|
|
7
|
+
Multiple extends boolean | undefined,
|
|
8
|
+
DisableClearable extends boolean | undefined,
|
|
9
|
+
FreeSolo extends boolean | undefined,
|
|
10
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
|
|
11
|
+
> = Omit<
|
|
12
|
+
AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>,
|
|
13
|
+
'onChange' | 'onBlur' | 'value' | 'name'
|
|
14
|
+
> &
|
|
15
|
+
Omit<RegisterOptions<FieldValues, string>, 'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'> &
|
|
16
|
+
Pick<ControllerProps, 'defaultValue' | 'shouldUnregister' | 'name'>;
|
|
17
|
+
|
|
18
|
+
export const ControlledAutocomplete = <
|
|
19
|
+
T,
|
|
20
|
+
Multiple extends boolean | undefined = false,
|
|
21
|
+
DisableClearable extends boolean | undefined = false,
|
|
22
|
+
FreeSolo extends boolean | undefined = false,
|
|
23
|
+
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
|
|
24
|
+
>({
|
|
25
|
+
name,
|
|
26
|
+
FieldProps,
|
|
27
|
+
defaultValue,
|
|
28
|
+
deps,
|
|
29
|
+
max,
|
|
30
|
+
maxLength,
|
|
31
|
+
min,
|
|
32
|
+
minLength,
|
|
33
|
+
onBlur,
|
|
34
|
+
onChange,
|
|
35
|
+
pattern,
|
|
36
|
+
required,
|
|
37
|
+
shouldUnregister,
|
|
38
|
+
validate,
|
|
39
|
+
value,
|
|
40
|
+
...rest
|
|
41
|
+
}: ControlledAutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
|
|
42
|
+
const {
|
|
43
|
+
control,
|
|
44
|
+
formState: { errors },
|
|
45
|
+
} = useFormContext();
|
|
46
|
+
const errorMessage = errors[name]?.message;
|
|
47
|
+
return (
|
|
48
|
+
<Controller
|
|
49
|
+
control={control}
|
|
50
|
+
name={name}
|
|
51
|
+
defaultValue={defaultValue}
|
|
52
|
+
rules={{
|
|
53
|
+
deps,
|
|
54
|
+
max,
|
|
55
|
+
maxLength,
|
|
56
|
+
min,
|
|
57
|
+
minLength,
|
|
58
|
+
onBlur,
|
|
59
|
+
onChange,
|
|
60
|
+
pattern,
|
|
61
|
+
required,
|
|
62
|
+
shouldUnregister,
|
|
63
|
+
validate,
|
|
64
|
+
value,
|
|
65
|
+
}}
|
|
66
|
+
shouldUnregister={shouldUnregister}
|
|
67
|
+
render={({ field: { onChange, value, onBlur } }) => (
|
|
68
|
+
<Autocomplete
|
|
69
|
+
{...rest}
|
|
70
|
+
FieldProps={{
|
|
71
|
+
...FieldProps,
|
|
72
|
+
error: !!errorMessage,
|
|
73
|
+
helperText:
|
|
74
|
+
errorMessage && typeof errorMessage === 'string' ? (
|
|
75
|
+
<>
|
|
76
|
+
{errorMessage}
|
|
77
|
+
<br />
|
|
78
|
+
{FieldProps?.helperText}
|
|
79
|
+
</>
|
|
80
|
+
) : (
|
|
81
|
+
FieldProps?.helperText
|
|
82
|
+
),
|
|
83
|
+
}}
|
|
84
|
+
onChange={(event, value, reason) => {
|
|
85
|
+
if (reason === 'clear') {
|
|
86
|
+
onChange(null);
|
|
87
|
+
}
|
|
88
|
+
onChange(value);
|
|
89
|
+
}}
|
|
90
|
+
onBlur={onBlur}
|
|
91
|
+
value={value || null}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ControlledCheckbox } from './Checkbox';
|
|
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 { FormControlLabel, FormGroup, FormControl, FormLabel } from '@availity/mui-form-utils';
|
|
9
|
+
import { Grid } from '@availity/mui-layout';
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof ControlledCheckbox> = {
|
|
12
|
+
title: 'Form Components/Controlled Form/ControlledCheckbox',
|
|
13
|
+
component: ControlledCheckbox,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
export const _ControlledCheckbox: StoryObj<typeof ControlledCheckbox> = {
|
|
20
|
+
render: () => {
|
|
21
|
+
const SubmittedValues = () => {
|
|
22
|
+
const {
|
|
23
|
+
getValues,
|
|
24
|
+
formState: { isSubmitSuccessful },
|
|
25
|
+
} = useFormContext();
|
|
26
|
+
|
|
27
|
+
return isSubmitSuccessful ? (
|
|
28
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
29
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
30
|
+
<pre>{JSON.stringify(getValues(), null, 2)}</pre>
|
|
31
|
+
</Paper>
|
|
32
|
+
) : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const Actions = () => {
|
|
36
|
+
const {
|
|
37
|
+
reset,
|
|
38
|
+
formState: { isSubmitSuccessful },
|
|
39
|
+
} = useFormContext();
|
|
40
|
+
return (
|
|
41
|
+
<Grid container direction="row" justifyContent="space-between" marginTop={1}>
|
|
42
|
+
<Button disabled={!isSubmitSuccessful} children="Reset" color="secondary" onClick={() => reset()} />
|
|
43
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
44
|
+
</Grid>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
<ControlledForm onSubmit={(data) => data} values={{ controlledCheckbox: undefined }}>
|
|
49
|
+
<FormControl>
|
|
50
|
+
<FormLabel id="radio-group" component="div">
|
|
51
|
+
Radio Group
|
|
52
|
+
</FormLabel>
|
|
53
|
+
<FormGroup>
|
|
54
|
+
<FormControlLabel label="Option 1" control={<ControlledCheckbox name="Option 1" />} />
|
|
55
|
+
<FormControlLabel label="Option 2" control={<ControlledCheckbox name="Option 2" />} />
|
|
56
|
+
<FormControlLabel label="Option 3" control={<ControlledCheckbox name="Option 3" />} />
|
|
57
|
+
</FormGroup>
|
|
58
|
+
</FormControl>
|
|
59
|
+
<Actions />
|
|
60
|
+
<SubmittedValues />
|
|
61
|
+
</ControlledForm>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
args: {
|
|
65
|
+
name: 'controlledCheckbox',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import { FormControl, FormLabel, FormControlLabel, FormGroup } from '@availity/mui-form-utils';
|
|
3
|
+
import { useFormContext } from 'react-hook-form';
|
|
4
|
+
import { Paper } from '@availity/mui-paper';
|
|
5
|
+
import { Typography } from '@availity/mui-typography';
|
|
6
|
+
import { Grid } from '@availity/mui-layout';
|
|
7
|
+
import { Button } from '@availity/mui-button';
|
|
8
|
+
import { ControlledForm } from './ControlledForm';
|
|
9
|
+
import { ControlledCheckbox } from './Checkbox';
|
|
10
|
+
|
|
11
|
+
const SubmittedValues = () => {
|
|
12
|
+
const {
|
|
13
|
+
getValues,
|
|
14
|
+
formState: { isSubmitSuccessful },
|
|
15
|
+
} = useFormContext();
|
|
16
|
+
|
|
17
|
+
return isSubmitSuccessful ? (
|
|
18
|
+
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
|
|
19
|
+
<Typography variant="h2">Submitted Values</Typography>
|
|
20
|
+
<pre data-testid="result">{JSON.stringify(getValues(), null, 2)}</pre>
|
|
21
|
+
</Paper>
|
|
22
|
+
) : null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const Actions = () => {
|
|
26
|
+
const {
|
|
27
|
+
formState: { isSubmitSuccessful },
|
|
28
|
+
} = useFormContext();
|
|
29
|
+
return (
|
|
30
|
+
<Grid container direction="row" justifyContent="space-between">
|
|
31
|
+
<Button type="submit" disabled={isSubmitSuccessful} children="Submit" />
|
|
32
|
+
</Grid>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onSubmit = jest.fn();
|
|
37
|
+
|
|
38
|
+
describe('ControlledCheckbox', () => {
|
|
39
|
+
test('should set the value and submit the form data', async () => {
|
|
40
|
+
const screen = render(
|
|
41
|
+
<ControlledForm onSubmit={onSubmit} values={{ controlledCheckbox: undefined }}>
|
|
42
|
+
<FormControl>
|
|
43
|
+
<FormLabel id="radio-group" component="div">
|
|
44
|
+
Radio Group
|
|
45
|
+
</FormLabel>
|
|
46
|
+
<FormGroup>
|
|
47
|
+
<FormControlLabel label="Option 1" control={<ControlledCheckbox name="option1" />} />
|
|
48
|
+
<FormControlLabel label="Option 2" control={<ControlledCheckbox name="option2" />} />
|
|
49
|
+
<FormControlLabel label="Option 3" control={<ControlledCheckbox name="option3" />} />
|
|
50
|
+
</FormGroup>
|
|
51
|
+
</FormControl>
|
|
52
|
+
<Actions />
|
|
53
|
+
<SubmittedValues />
|
|
54
|
+
</ControlledForm>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const option1 = screen.getByText('Option 1');
|
|
58
|
+
|
|
59
|
+
fireEvent.click(option1);
|
|
60
|
+
|
|
61
|
+
fireEvent.click(screen.getByText('Submit'));
|
|
62
|
+
|
|
63
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
64
|
+
|
|
65
|
+
const result = screen.getByTestId('result');
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
const formValues = JSON.parse(result.innerHTML);
|
|
68
|
+
expect(formValues.option1).toBeTruthy();
|
|
69
|
+
expect(formValues.option2).toBeFalsy();
|
|
70
|
+
expect(formValues.option3).toBeFalsy();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Checkbox, CheckboxProps } from '@availity/mui-checkbox';
|
|
2
|
+
import { useFormContext, RegisterOptions, FieldValues } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type ControlledCheckboxProps = CheckboxProps & {
|
|
5
|
+
name: string;
|
|
6
|
+
} & Omit<
|
|
7
|
+
RegisterOptions<FieldValues, string>,
|
|
8
|
+
'required' | 'max' | 'maxLength' | 'min' | 'minLength' | 'pattern' | 'validate'
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export const ControlledCheckbox = ({
|
|
12
|
+
name,
|
|
13
|
+
setValueAs,
|
|
14
|
+
disabled,
|
|
15
|
+
onChange,
|
|
16
|
+
onBlur,
|
|
17
|
+
value,
|
|
18
|
+
shouldUnregister,
|
|
19
|
+
deps,
|
|
20
|
+
...rest
|
|
21
|
+
}: ControlledCheckboxProps) => {
|
|
22
|
+
const { register } = useFormContext();
|
|
23
|
+
return (
|
|
24
|
+
<Checkbox
|
|
25
|
+
{...rest}
|
|
26
|
+
{...register(name, {
|
|
27
|
+
setValueAs,
|
|
28
|
+
disabled,
|
|
29
|
+
onChange,
|
|
30
|
+
onBlur,
|
|
31
|
+
value,
|
|
32
|
+
shouldUnregister,
|
|
33
|
+
deps,
|
|
34
|
+
})}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|