@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.
@@ -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,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["."],
4
+ "exclude": ["dist", "build", "node_modules"]
5
+ }
@@ -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
+ }