design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
467 lines (446 loc) • 15.9 kB
JSX
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useIntl, defineMessages } from 'react-intl';
import { isCmsUi } from '@plone/volto/helpers';
import {
Container,
Row,
Col,
Spinner,
Card,
CardHeader,
CardBody,
} from 'design-react-kit';
import {
getNumberOfSteps,
getTranslatedQuestion,
HoneypotWidget,
GoogleReCaptchaWidget,
submitFeedback,
resetSubmitFeedback,
getFeedbackThreshold,
isFeedbackEnabledForRoute,
getStaticFeedbackRouteTitle,
} from 'volto-feedback';
import cx from 'classnames';
import AnswersStep from './Steps/AnswersStep';
import CommentsStep from './Steps/CommentsStep';
import RTRating from './Steps/Commons/Rating';
import { PropTypes } from 'prop-types';
import 'volto-feedback/components/FeedbackForm/feedback-form.css';
const messages = defineMessages({
title: {
id: 'feedback_form_title',
defaultMessage: 'How clear is the information on this page?',
},
aria_title_feedback: {
id: 'feedback_form_aria_title',
defaultMessage: 'Feedback form',
},
yes: {
id: 'feedback_form_yes',
defaultMessage: 'Yes',
},
no: {
id: 'feedback_form_no',
defaultMessage: 'No',
},
suggestions_placeholder: {
id: 'feedback_form_suggestions_placeholder',
defaultMessage:
'Explain us why, and help us improve the quality of the site',
},
submit: {
id: 'feedback_form_submit',
defaultMessage: 'Submit your comment',
},
thank_you: {
id: 'feedback_form_thank_you',
defaultMessage: 'Thank you for your feedback!',
},
next: {
id: 'feedback_form_button_next',
defaultMessage: 'Next',
},
prev: {
id: 'feedback_form_button_prev',
defaultMessage: 'Previous',
},
feedback_sent: {
id: 'feedback_sent',
defaultMessage: 'Your feedback was sent!',
},
unclear_instructions: {
id: 'feedback_unclear_instructions',
defaultMessage: 'Some instructions were not clear and confusin',
},
incomplete_instructions: {
id: 'feedback_incomplete_instructions',
defaultMessage: 'Some instructions were incomplete',
},
unclear_proceeding: {
id: 'feedback_unclear_proceeding',
defaultMessage:
"Sometimes I couldn't understand if I was proceeding correctly",
},
technical_problems: {
id: 'feedback_technical_problems',
defaultMessage: 'I ran into technical problems',
},
other_negative: {
id: 'feedback_other_negative',
defaultMessage: 'Other',
},
clear_instructions: {
id: 'feedback_clear_instructions',
defaultMessage: 'The instructions were clear',
},
complete_instructions: {
id: 'feedback_complete_instructions',
defaultMessage: 'The instructions were complete',
},
clear_proceeding: {
id: 'feedback_clear_proceeding',
defaultMessage: 'I always understood that I was proceeding correctly',
},
no_technical_problems: {
id: 'feedback_no_technical_problems',
defaultMessage: 'I had no technical problems',
},
other_positive: {
id: 'feedback_other_positive',
defaultMessage: 'Other',
},
feedback: {
id: 'feedback_stars',
defaultMessage: 'Valuta da 1 a 5 stelle',
},
error: {
id: 'feedback_error',
defaultMessage: 'Error',
},
});
const FeedbackForm = ({ title, pathname }) => {
const intl = useIntl();
const location = useLocation();
const path = pathname ?? location.pathname ?? '/';
const dispatch = useDispatch();
const [satisfaction, setSatisfaction] = useState(null);
const [step, setStep] = useState(0);
const [invalidForm, setInvalidForm] = useState(true);
const [formData, setFormData] = useState({});
const captcha = !!process.env.RAZZLE_RECAPTCHA_KEY ? 'GoogleReCaptcha' : null;
const submitResults = useSelector((state) => state.submitFeedback);
const [validToken, setValidToken] = useState(null);
const threshold = getFeedbackThreshold();
const fieldHoney = __CLIENT__
? window.env.RAZZLE_HONEYPOT_FIELD
: process.env.RAZZLE_HONEYPOT_FIELD;
const numberOfSteps = getNumberOfSteps();
const changeSatisfaction = (e) => {
setSatisfaction(e);
};
const updateFormData = (field, value) => {
if (field === 'comment') {
if (value?.length > 200) setInvalidForm(true);
else setInvalidForm(false);
}
if (field === 'answer') {
if (!value) setInvalidForm(true);
else setInvalidForm(false);
}
setFormData({
...formData,
[field]: value,
});
};
const getFormFieldValue = (field) => formData?.[field] ?? undefined;
const nextStep = () => {
if (!invalidForm) setStep(step + 1);
};
const prevStep = () => {
if (!invalidForm && step !== 0) {
setStep(step - 1);
}
};
useEffect(() => {
setValidToken(null);
setSatisfaction(null);
return () => {
dispatch(resetSubmitFeedback());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path]);
useEffect(() => {
const currentVote = getFormFieldValue('vote');
if (
(currentVote > threshold && satisfaction < threshold) ||
(currentVote < threshold && satisfaction > threshold)
)
updateFormData('answer', null);
updateFormData('vote', satisfaction ?? null);
setStep(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [satisfaction]);
// initialized honeypot field
useEffect(() => {
updateFormData(fieldHoney, '');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldHoney]);
// Motivazioni del focus programmatico
// - su alcuni browser viene dichiarato "object replacement character" ai rerender di react
// che sono inevitabili mentre il focus rimane sul bottone ma non viene detto
// - replichiamo 1:1 il comportamento del secondo step, quando vai avanti hai autofocus sulla
// textarea per i commenti
// - essendo annunciato ora il titolo dello step, l'utente sr sa comunque benissimo dove si trova,
// anzi, il flow di compilazione del form kb+sr e' molto piu' agevole
useEffect(() => {
if (step === 0 && getFormFieldValue('answer')) {
const selectedAnswer = document.getElementById(
`${
satisfaction > threshold ? 'positive' : 'negative'
}-${getFormFieldValue('answer')}`,
);
if (selectedAnswer) {
selectedAnswer.focus();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step]);
const onVerifyCaptcha = useCallback(
(token) => {
if (satisfaction !== null && !validToken) {
setValidToken(token);
}
},
[satisfaction, setValidToken, validToken],
);
const resetFormData = () => {
setFormData({
[fieldHoney]: '',
});
};
const sendFormData = () => {
if (invalidForm) return;
setStep(2);
let content =
isFeedbackEnabledForRoute(path) && isCmsUi(path)
? getStaticFeedbackRouteTitle(path)
: path;
if (typeof content === 'object' && content.id)
content = intl.formatMessage(content);
const data = {
...formData,
...(captcha && { 'g-recaptcha-response': validToken }),
answer: getTranslatedQuestion(intl, formData.answer),
content,
};
dispatch(submitFeedback(data));
resetFormData();
};
if (!isFeedbackEnabledForRoute(path)) {
return null;
}
let action = path?.length > 1 ? path.replace(/\//g, '') : path;
if (action?.length > 0) {
action = action?.replace(/-/g, '_');
} else {
action = 'homepage';
}
return (
<section className="bg-primary customer-satisfaction">
<Container>
<Row className="d-flex justify-content-center bg-primary">
<Col className="col-12 col-lg-6">
<div
className="feedback-form"
role="form"
aria-label={intl.formatMessage(messages.aria_title_feedback)}
>
<Card
className="shadow card-wrapper py-4 px-4"
data-element="feedback"
>
{!submitResults?.loading &&
!submitResults.loaded &&
step !== 2 && (
<>
<h2
id="vf-radiogroup-label"
className="title-medium-2-semi-bold mb-0"
data-element="feedback-title"
>
{title || intl.formatMessage(messages.title)}
</h2>
<div className="rating-container mb-0">
<RTRating
name="satisfaction"
value={satisfaction}
// Qui l'implementazione di design react kit sta su con gli stecchini, rifatta
inputs={[
{
name: 'star1b',
value: 1,
},
{
name: 'star2b',
value: 2,
},
{
name: 'star3b',
value: 3,
},
{
name: 'star4b',
value: 4,
},
{
name: 'star5b',
value: 5,
},
]}
aria-controls={
satisfaction > threshold
? 'vf-more-positive'
: 'vf-more-negative'
}
className="volto-feedback-rating mb-0"
onChangeRating={changeSatisfaction}
legend={intl.formatMessage(messages.feedback)}
wrapperClassName={'rating'}
/>
</div>
<AnswersStep
updateFormData={updateFormData}
userFeedback={satisfaction}
intl={intl}
step={step}
totalSteps={numberOfSteps}
getFormFieldValue={getFormFieldValue}
/>
<CommentsStep
updateFormData={updateFormData}
userFeedback={satisfaction}
intl={intl}
step={step}
totalSteps={numberOfSteps}
getFormFieldValue={getFormFieldValue}
/>
<HoneypotWidget
updateFormData={updateFormData}
field={fieldHoney}
/>
<GoogleReCaptchaWidget
key={action}
onVerify={onVerifyCaptcha}
action={action}
/>
<div
className={cx(
'form-step-actions flex-nowrap w100 justify-content-center button-shadow',
{
'pt-4 d-flex': satisfaction,
'd-none': !satisfaction,
},
)}
aria-hidden={!satisfaction}
>
{/* Bug bottoni del kit. Disabled e' settato anche se compare la prop aria-disabled,
quando lo scopo sarebbe continuare a poter usufruire dei focus anche in screen reader.
Per i vedenti, la classe dimmed fa il suo lavoro e disabilita i click/input utente */}
<button
type="button"
onClick={prevStep}
disabled={false}
className={cx(
'me-4 fw-bold btn btn-outline-primary',
{
disabled: step === 0,
},
)}
aria-disabled={!(!invalidForm && step !== 0)}
>
{intl.formatMessage(messages.prev)}
</button>
{step !== numberOfSteps - 1 && (
<button
type="button"
onClick={nextStep}
disabled={false}
aria-disabled={invalidForm}
className={cx('fw-bold btn btn-primary', {
disabled: invalidForm,
})}
>
{intl.formatMessage(messages.next)}
</button>
)}
{step === numberOfSteps - 1 && (
<button
className={cx('fw-bold btn btn-primary', {
disabled: invalidForm,
})}
type="submit"
disabled={false}
aria-disabled={invalidForm}
onClick={sendFormData}
onKeyDown={(e) => {
if (e.key === 'Enter' && !invalidForm)
sendFormData();
}}
>
{intl.formatMessage(messages.next)}
</button>
)}
</div>
</>
)}
{submitResults?.loading && (
<div className="d-flex justify-content-center align-items-center">
<Spinner double active />
</div>
)}
{submitResults?.loaded && step === 2 && (
<CardHeader className="border-0 mb-0 px-0">
<h4
id="rating-feedback"
className="title-medium-2-semi-bold mb-0"
>
{intl.formatMessage(messages.thank_you)}
</h4>
</CardHeader>
)}
{step === 2 &&
!submitResults?.loaded &&
!submitResults.loading &&
submitResults.error?.response?.body?.message && (
<>
<CardHeader className="border-0 mb-0 px-0">
<h4
id="rating-feedback"
className="title-medium-2-semi-bold mb-0"
>
{intl.formatMessage(messages.error)}{' '}
{submitResults.error?.response.status}:{' '}
{submitResults.error?.response.statusText}
</h4>
</CardHeader>
<CardBody>
{submitResults.error?.response?.body?.message}
</CardBody>
</>
)}
</Card>
</div>
</Col>
</Row>
</Container>
</section>
);
};
FeedbackForm.propTypes = {
title: PropTypes.string,
pathname: PropTypes.string,
};
export default FeedbackForm;