@availity/mui-feedback 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 +20 -0
- package/README.md +61 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +582 -0
- package/dist/index.mjs +552 -0
- package/introduction.mdx +7 -0
- package/jest.config.js +7 -0
- package/package.json +57 -0
- package/project.json +42 -0
- package/src/index.ts +1 -0
- package/src/lib/Feedback.stories.tsx +19 -0
- package/src/lib/Feedback.test.tsx +15 -0
- package/src/lib/Feedback.tsx +67 -0
- package/src/lib/FeedbackForm.test.tsx +48 -0
- package/src/lib/FeedbackForm.tsx +188 -0
- package/src/lib/FeedbackHeader.test.tsx +28 -0
- package/src/lib/FeedbackHeader.tsx +36 -0
- package/tsconfig.json +5 -0
- package/tsconfig.spec.json +10 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { Feedback } from './Feedback';
|
|
3
|
+
|
|
4
|
+
describe('Feedback', () => {
|
|
5
|
+
test('should render successfully', () => {
|
|
6
|
+
const { getByText } = render(<Feedback appName="This App" />);
|
|
7
|
+
const button = getByText('Give Feedback');
|
|
8
|
+
|
|
9
|
+
expect(button).toBeTruthy();
|
|
10
|
+
|
|
11
|
+
fireEvent.click(button);
|
|
12
|
+
|
|
13
|
+
expect(getByText('Tell us what you think about This App')).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Popover } from '@availity/mui-popover';
|
|
3
|
+
import { Button } from '@availity/mui-button';
|
|
4
|
+
import { Container, styled } from '@mui/material';
|
|
5
|
+
import { avLogMessagesApi } from '@availity/api-axios';
|
|
6
|
+
import { FeedbackForm } from './FeedbackForm';
|
|
7
|
+
import { FeedbackHeader } from './FeedbackHeader';
|
|
8
|
+
|
|
9
|
+
export interface FeedbackProps {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
analytics?: { info: (entries: any) => any };
|
|
12
|
+
appName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FeedbackContainer = styled(Container, { name: 'AvFeedbackContainer', slot: 'root' })({});
|
|
16
|
+
|
|
17
|
+
export const Feedback = ({ analytics = avLogMessagesApi, appName }: FeedbackProps): JSX.Element => {
|
|
18
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
19
|
+
const [sent, setSent] = useState<boolean>(false);
|
|
20
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
21
|
+
|
|
22
|
+
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
23
|
+
setAnchorEl(event.currentTarget);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handlePopoverClose = () => {
|
|
27
|
+
setAnchorEl(null);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const open = Boolean(anchorEl);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<Button onClick={handlePopoverOpen} color="secondary" size="large">
|
|
35
|
+
Give Feedback
|
|
36
|
+
</Button>
|
|
37
|
+
<Popover
|
|
38
|
+
anchorEl={anchorEl}
|
|
39
|
+
anchorOrigin={{
|
|
40
|
+
vertical: 'bottom',
|
|
41
|
+
horizontal: 'right',
|
|
42
|
+
}}
|
|
43
|
+
sx={{ marginTop: '4px' }}
|
|
44
|
+
disableRestoreFocus
|
|
45
|
+
onClose={handlePopoverClose}
|
|
46
|
+
open={open}
|
|
47
|
+
transformOrigin={{
|
|
48
|
+
vertical: 'top',
|
|
49
|
+
horizontal: 'right',
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<FeedbackContainer>
|
|
53
|
+
<FeedbackHeader appName={appName} handleClose={handlePopoverClose} loading={loading} sent={sent} />
|
|
54
|
+
<FeedbackForm
|
|
55
|
+
analytics={analytics}
|
|
56
|
+
appName={appName}
|
|
57
|
+
handleClose={handlePopoverClose}
|
|
58
|
+
loading={loading}
|
|
59
|
+
sent={sent}
|
|
60
|
+
setLoading={setLoading}
|
|
61
|
+
setSent={setSent}
|
|
62
|
+
/>
|
|
63
|
+
</FeedbackContainer>
|
|
64
|
+
</Popover>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { FeedbackForm } from './FeedbackForm';
|
|
3
|
+
|
|
4
|
+
const analytics = { info: jest.fn() };
|
|
5
|
+
const handleClose = jest.fn();
|
|
6
|
+
const setLoading = jest.fn();
|
|
7
|
+
const setSent = jest.fn();
|
|
8
|
+
|
|
9
|
+
describe('Feedback', () => {
|
|
10
|
+
test('should render Send Feedback button disabled', () => {
|
|
11
|
+
const { getAllByRole } = render(
|
|
12
|
+
<FeedbackForm
|
|
13
|
+
appName="This App"
|
|
14
|
+
analytics={analytics}
|
|
15
|
+
handleClose={handleClose}
|
|
16
|
+
loading={false}
|
|
17
|
+
sent={false}
|
|
18
|
+
setLoading={setLoading}
|
|
19
|
+
setSent={setSent}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
const submitButton = getAllByRole('button')[4];
|
|
23
|
+
|
|
24
|
+
expect(submitButton).toHaveAttribute('disabled');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should not render Send Feedback button disabled if smile is selected', () => {
|
|
28
|
+
const { getAllByRole } = render(
|
|
29
|
+
<FeedbackForm
|
|
30
|
+
appName="This App"
|
|
31
|
+
analytics={analytics}
|
|
32
|
+
handleClose={handleClose}
|
|
33
|
+
loading={false}
|
|
34
|
+
sent={false}
|
|
35
|
+
setLoading={setLoading}
|
|
36
|
+
setSent={setSent}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const smileButton = getAllByRole('button')[0];
|
|
41
|
+
|
|
42
|
+
fireEvent.click(smileButton);
|
|
43
|
+
|
|
44
|
+
const submitButton = getAllByRole('button')[4];
|
|
45
|
+
|
|
46
|
+
expect(submitButton).not.toHaveAttribute('disabled');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { TextField } from '@availity/mui-textfield';
|
|
3
|
+
import { LoadingButton, Button } from '@availity/mui-button';
|
|
4
|
+
import { ToggleButtonGroup, ToggleButton } from '@availity/mui-toggle-button';
|
|
5
|
+
import { Grid, SvgIconProps, ToggleButtonProps, styled } from '@mui/material';
|
|
6
|
+
import { FaceFrownIcon, FaceNeutralIcon, FaceSmileIcon } from '@availity/mui-icon';
|
|
7
|
+
import { FormLabel } from '@availity/mui-form-utils';
|
|
8
|
+
import { avRegionsApi } from '@availity/api-axios';
|
|
9
|
+
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
|
|
10
|
+
|
|
11
|
+
interface Inputs {
|
|
12
|
+
feedback: string;
|
|
13
|
+
smileField: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SmileButtonProps extends ToggleButtonProps {
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
Icon: (props: SvgIconProps) => JSX.Element;
|
|
19
|
+
label: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FeedbackFormProps {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
analytics: { info: (entries: Record<string, unknown>) => any };
|
|
26
|
+
appName: string;
|
|
27
|
+
handleClose: () => void;
|
|
28
|
+
loading: boolean;
|
|
29
|
+
sent: boolean;
|
|
30
|
+
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
|
31
|
+
setSent: React.Dispatch<React.SetStateAction<boolean>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SmileButtons = styled(ToggleButtonGroup, { name: 'AvFeedbackContainer', slot: 'SmileButtons' })({});
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
37
|
+
const SmileButton = ({ disabled, Icon, label, value, ...props }: SmileButtonProps) => (
|
|
38
|
+
<div>
|
|
39
|
+
<ToggleButton aria-label={value} value={value} {...props} disabled={disabled}>
|
|
40
|
+
<Icon fontSize="large" />
|
|
41
|
+
</ToggleButton>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const FeedbackForm = ({
|
|
46
|
+
analytics,
|
|
47
|
+
appName,
|
|
48
|
+
handleClose,
|
|
49
|
+
loading,
|
|
50
|
+
sent,
|
|
51
|
+
setLoading,
|
|
52
|
+
setSent,
|
|
53
|
+
}: FeedbackFormProps): JSX.Element | null => {
|
|
54
|
+
const {
|
|
55
|
+
control,
|
|
56
|
+
formState: { errors },
|
|
57
|
+
handleSubmit,
|
|
58
|
+
register,
|
|
59
|
+
setValue,
|
|
60
|
+
watch,
|
|
61
|
+
} = useForm<Inputs>();
|
|
62
|
+
|
|
63
|
+
const onSubmit: SubmitHandler<Inputs> = async ({ smileField, ...values }) => {
|
|
64
|
+
setLoading(true);
|
|
65
|
+
try {
|
|
66
|
+
const response = await avRegionsApi.getCurrentRegion();
|
|
67
|
+
|
|
68
|
+
await analytics.info({
|
|
69
|
+
surveyId: `${appName.replace(/\s/g, '_')}_Smile_Survey`,
|
|
70
|
+
smileLocation: `${appName}`,
|
|
71
|
+
smile: `icon-${smileField}`,
|
|
72
|
+
url: window.location.href,
|
|
73
|
+
region: response.data.regions[0] && response.data.regions[0].id,
|
|
74
|
+
userAgent: window.navigator.userAgent,
|
|
75
|
+
submitTime: new Date(),
|
|
76
|
+
...values, // Spread the form values onto the logger
|
|
77
|
+
});
|
|
78
|
+
setSent(true);
|
|
79
|
+
setLoading(false);
|
|
80
|
+
} catch {
|
|
81
|
+
setSent(false);
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (sent) {
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
handleClose();
|
|
90
|
+
}, 2000);
|
|
91
|
+
}
|
|
92
|
+
}, [sent]);
|
|
93
|
+
|
|
94
|
+
const options = [
|
|
95
|
+
{
|
|
96
|
+
Icon: FaceSmileIcon,
|
|
97
|
+
label: 'What do you like?',
|
|
98
|
+
value: 'smile',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
Icon: FaceNeutralIcon,
|
|
102
|
+
label: 'What would you improve?',
|
|
103
|
+
value: 'meh',
|
|
104
|
+
},
|
|
105
|
+
{ Icon: FaceFrownIcon, label: "What don't you like?", value: 'frown' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const getFeedbackLabel = () => {
|
|
109
|
+
const smile = watch('smileField');
|
|
110
|
+
|
|
111
|
+
const option = options.find((option) => option.value === smile);
|
|
112
|
+
|
|
113
|
+
return option?.label || 'What would you improve?';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (!sent) {
|
|
117
|
+
return (
|
|
118
|
+
<Grid
|
|
119
|
+
component="form"
|
|
120
|
+
container
|
|
121
|
+
justifyContent="center"
|
|
122
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
123
|
+
aria-label="Feedback Form"
|
|
124
|
+
aria-describedby="feedback-form-header"
|
|
125
|
+
>
|
|
126
|
+
<Controller
|
|
127
|
+
control={control}
|
|
128
|
+
name="smileField"
|
|
129
|
+
render={({ field }) => {
|
|
130
|
+
return (
|
|
131
|
+
<SmileButtons
|
|
132
|
+
children={options.map((props, index) => (
|
|
133
|
+
<SmileButton autoFocus={index === 0} disabled={loading} key={props.value} {...props} />
|
|
134
|
+
))}
|
|
135
|
+
{...field}
|
|
136
|
+
aria-labelledby="feedback-form-header"
|
|
137
|
+
onChange={(event: React.MouseEvent<HTMLElement>, value: string) => {
|
|
138
|
+
setValue(field.name, value);
|
|
139
|
+
}}
|
|
140
|
+
size="medium"
|
|
141
|
+
exclusive
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
<TextField
|
|
147
|
+
{...register('feedback', {
|
|
148
|
+
required: 'This field is required',
|
|
149
|
+
maxLength: { value: 200, message: 'This field must not exceed 200 characters' },
|
|
150
|
+
})}
|
|
151
|
+
fullWidth
|
|
152
|
+
multiline
|
|
153
|
+
minRows={3}
|
|
154
|
+
maxRows={3}
|
|
155
|
+
label={getFeedbackLabel()}
|
|
156
|
+
inputProps={{ 'aria-required': 'true' }}
|
|
157
|
+
InputLabelProps={{
|
|
158
|
+
component: FormLabel,
|
|
159
|
+
required: true,
|
|
160
|
+
}}
|
|
161
|
+
helperText={errors.feedback?.message || 'Max 200 characters'}
|
|
162
|
+
error={!!errors.feedback}
|
|
163
|
+
disabled={loading}
|
|
164
|
+
/>
|
|
165
|
+
<Grid container direction="row" marginTop="16px" spacing={1}>
|
|
166
|
+
<Grid item xs={6}>
|
|
167
|
+
<Button color="secondary" disabled={loading} fullWidth onClick={handleClose}>
|
|
168
|
+
Cancel
|
|
169
|
+
</Button>
|
|
170
|
+
</Grid>
|
|
171
|
+
<Grid item xs={6}>
|
|
172
|
+
<LoadingButton
|
|
173
|
+
disabled={!watch('smileField')}
|
|
174
|
+
fullWidth
|
|
175
|
+
loading={loading}
|
|
176
|
+
type="submit"
|
|
177
|
+
variant="contained"
|
|
178
|
+
>
|
|
179
|
+
Send Feedback
|
|
180
|
+
</LoadingButton>
|
|
181
|
+
</Grid>
|
|
182
|
+
</Grid>
|
|
183
|
+
</Grid>
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { FeedbackHeader } from './FeedbackHeader';
|
|
3
|
+
|
|
4
|
+
describe('FeedbackHeader', () => {
|
|
5
|
+
test('should render successfully', () => {
|
|
6
|
+
const handleClose = jest.fn();
|
|
7
|
+
const { getByText, getByLabelText } = render(
|
|
8
|
+
<FeedbackHeader appName="This App" handleClose={handleClose} loading={false} sent={false} />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(getByText('Tell us what you think about This App')).toBeTruthy();
|
|
12
|
+
|
|
13
|
+
const closeButton = getByLabelText('Close');
|
|
14
|
+
|
|
15
|
+
fireEvent.click(closeButton);
|
|
16
|
+
|
|
17
|
+
expect(handleClose).toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should render sent text successfully', () => {
|
|
21
|
+
const handleClose = jest.fn();
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<FeedbackHeader appName="This App" handleClose={handleClose} loading={false} sent={true} />
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(getByText('Thank you for your feedback.')).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IconButton } from '@availity/mui-button';
|
|
2
|
+
import { CloseIcon } from '@availity/mui-icon';
|
|
3
|
+
import { Typography } from '@availity/mui-typography';
|
|
4
|
+
import { Grid } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
interface FeedbackHeaderProps {
|
|
7
|
+
appName: string;
|
|
8
|
+
handleClose: () => void;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
sent: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const FeedbackHeader = ({ appName, handleClose, loading, sent }: FeedbackHeaderProps): JSX.Element => {
|
|
14
|
+
return (
|
|
15
|
+
<Grid
|
|
16
|
+
alignItems="center"
|
|
17
|
+
container
|
|
18
|
+
direction="row"
|
|
19
|
+
marginBottom={!sent ? '8px' : '0px'}
|
|
20
|
+
justifyContent="space-between"
|
|
21
|
+
flexWrap="nowrap"
|
|
22
|
+
id="feedback-form-header"
|
|
23
|
+
>
|
|
24
|
+
<Grid item whiteSpace="normal">
|
|
25
|
+
<Typography component="h2" variant="h5">
|
|
26
|
+
{sent ? 'Thank you for your feedback.' : `Tell us what you think about ${appName}`}
|
|
27
|
+
</Typography>
|
|
28
|
+
</Grid>
|
|
29
|
+
<Grid item marginRight="-8px">
|
|
30
|
+
<IconButton disabled={loading} title="Close" onClick={handleClose}>
|
|
31
|
+
<CloseIcon />
|
|
32
|
+
</IconButton>
|
|
33
|
+
</Grid>
|
|
34
|
+
</Grid>
|
|
35
|
+
);
|
|
36
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"types": ["jest", "node", "@testing-library/jest-dom"],
|
|
7
|
+
"allowJs": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["**/*.test.js", "**/*.test.ts", "**/*.test.tsx", "**/*.d.ts"]
|
|
10
|
+
}
|