UNPKG

@availity/feedback

Version:
313 lines (295 loc) 10.3 kB
import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { Button, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'reactstrap'; import { avLogMessagesApiV2, avRegionsApi } from '@availity/api-axios'; import { Form, Field } from '@availity/form'; import { SelectField } from '@availity/select'; import * as yup from 'yup'; import SmileField from './SmileField'; yup.addMethod(yup.string, 'isRequired', function format(isRequired, msg) { return this.test({ name: 'dateRange', exclusive: true, message: msg || 'This field is required.', test(value) { if (isRequired) { return value !== undefined; } return true; }, }); }); const fieldStyles = { resize: 'none' }; const inlineStyles = { display: 'inline-block', margin: 0 }; const FeedbackForm = ({ name, onClose, faceOptions, aboutOptions = [], feedbackText = true, aboutLabel = 'This is about', onFeedbackSent, prompt, additionalComments = false, staticFields, analytics = avLogMessagesApiV2, modalHeaderProps, showSupport = false, setSupportIsActive, autoFocusFeedbackButton, modal = true, ...formProps }) => { const [active, setActive] = useState(null); const [sent, setSent] = useState(null); const [loading, setLoading] = useState(null); const ref = useRef(); const sendFeedback = async ({ smileField, ...values }) => { setLoading(true); const response = await avRegionsApi.getCurrentRegion(); await analytics.info({ surveyId: `${name.replaceAll(/\s/g, '_')}_Smile_Survey`, smileLocation: `${name}`, smile: `icon-${smileField.icon}`, url: window.location.href, region: response.data.regions[0] && response.data.regions[0].id, userAgent: window.navigator.userAgent, submitTime: new Date(), ...values, // Spread the form values onto the logger ...staticFields, // Spread the static key value pairs onto the logger }); setSent(values); setLoading(false); }; // Close the Modal once sent after 2 seconds useEffect(() => { if (sent) { setTimeout(() => { if (onClose) { onClose(); // Mostly for Screen Reader use but a nice to have for all } if (onFeedbackSent) { for (const key of Object.keys(sent)) { if (sent[key] === undefined) { delete sent[key]; } } onFeedbackSent({ active: active.icon, ...sent, }); } }, 2000); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [sent]); return sent ? ( <ModalHeader role="status" id="feedback-form-header" tabIndex="0" className="d-flex justify-content-center" {...modalHeaderProps} > Thank you for your feedback. </ModalHeader> ) : ( <> <ModalHeader id="feedback-form-header" role="heading" aria-level="2" className="h5" tag="div" {...modalHeaderProps} > {prompt || `Tell us what you think about ${name}`} </ModalHeader> <Form innerRef={ref} aria-label="Feedback Form" aria-describedby="feedback-form-header" role="form" onKeyDown={({ key }) => key === 'Escape' && onClose()} data-testid="feedback-form" initialValues={{ 'face-options': undefined, additionalFeedback: undefined, feedback: undefined, feedbackApp: undefined, smileField: undefined, }} validationSchema={yup.object().shape({ feedback: yup .string() .max(200, 'Feedback cannot exceed 200 characters.') .isRequired(feedbackText, 'This field is required.'), additionalFeedback: yup.string().max(200, 'Additional Feedback cannot exceed 200 characters.'), smileField: yup .object() .shape({ icon: yup.string().required(), description: yup.string(), label: yup.string(), }) .required('This field is required.'), feedbackApp: yup.string().isRequired(aboutOptions.length > 0, 'This field is required.'), })} {...formProps} onSubmit={(values) => sendFeedback(values)} > <ModalBody> <FormGroup size="lg" id="face-options" role="group" aria-labelledby="feedback-form-header" data-testid="face-options" className="d-flex flex-row justify-content-between" > <SmileField options={faceOptions} name="smileField" onChange={(option) => setActive(option)} onClose={onClose} autoFocusFeedbackButton={autoFocusFeedbackButton} modal={modal} /> </FormGroup> {active ? ( <> {aboutOptions.length > 0 && ( <SelectField name="feedbackApp" id="about-options" data-testid="about-options" label={aboutLabel} options={aboutOptions} /> )} <Field type="textarea" name="feedback" label={(active && active.label) || 'Feedback? Requests? Defects?'} style={fieldStyles} rows="2" /> {additionalComments && ( <Field type="textarea" name="additionalFeedback" label="Additional Comments... (Optional)" style={fieldStyles} rows="2" /> )} </> ) : null} </ModalBody> <ModalFooter> {showSupport ? ( <> <span className="d-none d-md-block" style={inlineStyles}> Need Help? </span> <Button className="pl-0" onClick={() => setSupportIsActive(true)} color="link" type="button" onKeyDown={({ key }) => key === 'Enter' && setSupportIsActive(true)} > Open a support ticket </Button> </> ) : null} {onClose ? ( <Button type="button" onClick={onClose} color="secondary" onKeyDown={({ key, shiftKey }) => { if (key === 'Enter') { onClose(); } if (key === 'Tab' && !active && !shiftKey && !modal) { onClose(); } }} > Close </Button> ) : null} <Button onKeyDown={({ key, shiftKey }) => { if (key === 'Enter') { ref.current.submitForm(); } if (key === 'Tab' && !shiftKey && !modal) { onClose(); } }} type="submit" color="primary" disabled={Boolean(!active || loading)} > Send Feedback </Button> </ModalFooter> </Form> </> ); }; FeedbackForm.propTypes = { /** The name of the application this feedback is for. It is used in the API request to indicate where the feedback came from. */ name: PropTypes.string.isRequired, /** Callback for when the feedback is submitted. It is called with the feedback object. */ onFeedbackSent: PropTypes.func, /** Array of Objects containing icon (String), description (String), and label (String) properties. * Allows you to override the smiley face options which appear. * Default: Smiley Face, Meh Face, and Frowny Face. * Previous placeholder property removed as of v6.0.0. Use label instead. */ faceOptions: PropTypes.arrayOf( PropTypes.shape({ icon: PropTypes.string, description: PropTypes.string, label: PropTypes.string, }) ), /** Array of Objects containing value (String,Number) and label (String) properties. * Allows a dropdown displaying the options provided to let the user indicate what the feedback is about. */ aboutOptions: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }) ), /** If false, then feedback text area is made optional */ feedbackText: PropTypes.bool, /** Label text for the dropdown created via the aboutOptions prop. Default: "This is about". *Previously aboutPlaceholder. All placeholders replaced with labels starting v6.0.0. */ aboutLabel: PropTypes.node, /** When provided, a "Close" button is rendered and onClose is excuted when it's clicked. */ onClose: PropTypes.func, /** Text that prompts the user to provider feedback. Default: "Tell us what you think about ${appName}.". */ prompt: PropTypes.string, /** If true, shows an optional comments field below. */ additionalComments: PropTypes.bool, /** Static (non-user-entered) key/value pairs to be sent in feedback submission. */ staticFields: PropTypes.object, /** Props to be spread onto the <ModalHeader /> rendered inside of the <FeedbackForm />. See ModalHeader *For accessibility use className instead of tag to adjust size and style of header. */ modalHeaderProps: PropTypes.shape({ ...ModalHeader.propTypes }), /** Override the analytics instance that is passed in. Default avLogMessagesApiV2 */ analytics: PropTypes.shape({ info: PropTypes.func.isRequired, }), /** Toggle whether or not to show the "Open a Support ticket" link in the FeedbackForm */ showSupport: PropTypes.bool, setSupportIsActive: PropTypes.func, /** Default: true. When set to false, the first feedback button is not focused. * This is to avoid issues with focus causing other elements to close (e.g. dropdowns) */ autoFocusFeedbackButton: PropTypes.bool, modal: PropTypes.bool, }; export default FeedbackForm;