mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
style: scroll indicator update (#6310)
This commit is contained in:
@@ -4,7 +4,7 @@ export function FormbricksBranding() {
|
||||
href="https://formbricks.com?utm_source=survey_branding"
|
||||
target="_blank"
|
||||
tabIndex={-1}
|
||||
className="fb-my-2 fb-flex fb-justify-center"
|
||||
className="fb-flex fb-justify-center"
|
||||
rel="noopener">
|
||||
<p className="fb-text-signature fb-text-xs">
|
||||
Powered by{" "}
|
||||
|
||||
@@ -765,8 +765,8 @@ export function Survey({
|
||||
)}>
|
||||
{content()}
|
||||
</div>
|
||||
<div className="fb-space-y-4">
|
||||
<div className="fb-px-4 space-y-2">
|
||||
<div className="fb-gap-y-2 fb-min-h-8 fb-flex fb-flex-col fb-justify-end">
|
||||
<div className="fb-px-4 fb-space-y-2">
|
||||
{isBrandingEnabled ? <FormbricksBranding /> : null}
|
||||
{isSpamProtectionEnabled ? <RecaptchaBranding /> : null}
|
||||
</div>
|
||||
|
||||
@@ -133,73 +133,67 @@ export function WelcomeCard({
|
||||
}, [isCurrent]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{fileUrl ? (
|
||||
<img
|
||||
src={fileUrl}
|
||||
className="fb-mb-8 fb-max-h-96 fb-w-1/4 fb-rounded-lg fb-object-contain"
|
||||
alt="Company Logo"
|
||||
/>
|
||||
) : null}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{fileUrl ? (
|
||||
<img
|
||||
src={fileUrl}
|
||||
className="fb-mb-8 fb-max-h-96 fb-w-1/4 fb-rounded-lg fb-object-contain"
|
||||
alt="Company Logo"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Headline
|
||||
headline={replaceRecallInfo(
|
||||
getLocalizedValue(headline, languageCode),
|
||||
responseData,
|
||||
variablesData
|
||||
)}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
<HtmlBody
|
||||
htmlString={replaceRecallInfo(getLocalizedValue(html, languageCode), responseData, variablesData)}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-mx-6 fb-mt-4 fb-flex fb-gap-4 fb-py-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
isLastQuestion={false}
|
||||
focus={isCurrent ? autoFocusEnabled : false}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
<Headline
|
||||
headline={replaceRecallInfo(getLocalizedValue(headline, languageCode), responseData, variablesData)}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
<HtmlBody
|
||||
htmlString={replaceRecallInfo(getLocalizedValue(html, languageCode), responseData, variablesData)}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
<div className="fb-mt-4 fb-flex fb-gap-4 fb-pt-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
isLastQuestion={false}
|
||||
focus={isCurrent ? autoFocusEnabled : false}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{timeToFinish && !showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-flex">
|
||||
<TimerIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-flex">
|
||||
<UsersIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span>{`${responseCount.toString()} people responded`}</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{timeToFinish && showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-flex">
|
||||
<TimerIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
<span>
|
||||
{responseCount && responseCount > 3 ? `⋅ ${responseCount.toString()} people responded` : ""}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{timeToFinish && !showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<TimerIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<UsersIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span>{`${responseCount.toString()} people responded`}</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{timeToFinish && showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<TimerIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
<span>
|
||||
{responseCount && responseCount > 3 ? `⋅ ${responseCount.toString()} people responded` : ""}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ export function AddressQuestion({
|
||||
);
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<ScrollableContainer>
|
||||
<ScrollableContainer>
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
@@ -176,27 +176,27 @@ export function AddressQuestion({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,29 +58,29 @@ export function CalQuestion({
|
||||
}, [onChange, onSubmit, question.id, setTtc, startTime, ttc]);
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
setErrorMessage("Please book an appointment");
|
||||
// Scroll to bottom to show the error message
|
||||
setTimeout(() => {
|
||||
if (scrollableRef.current?.scrollToBottom) {
|
||||
scrollableRef.current.scrollToBottom();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
<ScrollableContainer ref={scrollableRef}>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
setErrorMessage("Please book an appointment");
|
||||
// Scroll to bottom to show the error message
|
||||
setTimeout(() => {
|
||||
if (scrollableRef.current?.scrollToBottom) {
|
||||
scrollableRef.current.scrollToBottom();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
|
||||
onChange({ [question.id]: value });
|
||||
onSubmit({ [question.id]: value }, updatedttc);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer ref={scrollableRef}>
|
||||
onChange({ [question.id]: value });
|
||||
onSubmit({ [question.id]: value }, updatedttc);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
@@ -97,25 +97,25 @@ export function CalQuestion({
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
{errorMessage ? <span className="fb-text-red-500">{errorMessage}</span> : null}
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
}}
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,88 +58,81 @@ export function ConsentQuestion({
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}>
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody
|
||||
htmlString={getLocalizedValue(question.html, languageCode) || ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<label
|
||||
ref={consentRef}
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
id={`${question.id}-label`}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(question.id)?.click();
|
||||
document.getElementById(`${question.id}-label`)?.focus();
|
||||
}
|
||||
}}
|
||||
className="fb-border-border fb-bg-input-bg fb-text-heading hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected focus:fb-ring-brand fb-rounded-custom fb-relative fb-z-10 fb-my-2 fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-border fb-p-4 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="checkbox"
|
||||
id={question.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(question.label, languageCode)}
|
||||
onChange={(e) => {
|
||||
if (e.target instanceof HTMLInputElement && e.target.checked) {
|
||||
onChange({ [question.id]: "accepted" });
|
||||
} else {
|
||||
onChange({ [question.id]: "" });
|
||||
}
|
||||
}}
|
||||
checked={value === "accepted"}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody
|
||||
htmlString={getLocalizedValue(question.html, languageCode) || ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-bg-survey-bg fb-sticky -fb-bottom-2 fb-z-10 fb-w-full fb-px-1 fb-py-1">
|
||||
<label
|
||||
ref={consentRef}
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
id={`${question.id}-label`}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(question.id)?.click();
|
||||
document.getElementById(`${question.id}-label`)?.focus();
|
||||
}
|
||||
}}
|
||||
className="fb-border-border fb-bg-input-bg fb-text-heading hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected focus:fb-ring-brand fb-rounded-custom fb-relative fb-z-10 fb-my-2 fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-border fb-p-4 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="checkbox"
|
||||
id={question.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(question.label, languageCode)}
|
||||
onChange={(e) => {
|
||||
if (e.target instanceof HTMLInputElement && e.target.checked) {
|
||||
onChange({ [question.id]: "accepted" });
|
||||
} else {
|
||||
onChange({ [question.id]: "" });
|
||||
}
|
||||
}}
|
||||
checked={value === "accepted"}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,90 +115,85 @@ export function ContactInfoQuestion({
|
||||
);
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<ScrollableContainer>
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
|
||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
||||
{fields.map((field, index) => {
|
||||
const isFieldRequired = () => {
|
||||
if (field.required) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
let inputType = "text";
|
||||
if (field.id === "email") {
|
||||
inputType = "email";
|
||||
} else if (field.id === "phone") {
|
||||
inputType = "number";
|
||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
||||
{fields.map((field, index) => {
|
||||
const isFieldRequired = () => {
|
||||
if (field.required) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<div className="fb-space-y-1">
|
||||
<Label htmlForId={field.id} text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
id={field.id}
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
return false;
|
||||
};
|
||||
|
||||
let inputType = "text";
|
||||
if (field.id === "email") {
|
||||
inputType = "email";
|
||||
} else if (field.id === "phone") {
|
||||
inputType = "number";
|
||||
}
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<div className="fb-space-y-1">
|
||||
<Label htmlForId={field.id} text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
id={field.id}
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,58 +60,58 @@ export function CTAQuestion({
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode)} questionId={question.id} />
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-start">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
focus={isCurrent ? autoFocusEnabled : false}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
if (question.buttonExternal && question.buttonUrl) {
|
||||
if (onOpenExternalURL) {
|
||||
onOpenExternalURL(question.buttonUrl);
|
||||
} else {
|
||||
window.open(question.buttonUrl, "_blank")?.focus();
|
||||
}
|
||||
}
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: "clicked" }, updatedTtcObj);
|
||||
onChange({ [question.id]: "clicked" });
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
{!question.required && (
|
||||
<button
|
||||
dir="auto"
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: "" }, updatedTtcObj);
|
||||
onChange({ [question.id]: "" });
|
||||
}}
|
||||
className="fb-text-heading focus:fb-ring-focus fb-mr-4 fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
{getLocalizedValue(question.dismissButtonLabel, languageCode) || "Skip"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-start">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
focus={isCurrent ? autoFocusEnabled : false}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
if (question.buttonExternal && question.buttonUrl) {
|
||||
if (onOpenExternalURL) {
|
||||
onOpenExternalURL(question.buttonUrl);
|
||||
} else {
|
||||
window.open(question.buttonUrl, "_blank")?.focus();
|
||||
}
|
||||
}
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: "clicked" }, updatedTtcObj);
|
||||
onChange({ [question.id]: "clicked" });
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
{!question.required && (
|
||||
<button
|
||||
dir="auto"
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: "" }, updatedTtcObj);
|
||||
onChange({ [question.id]: "" });
|
||||
}}
|
||||
className="fb-text-heading focus:fb-ring-focus fb-mr-4 fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
{getLocalizedValue(question.dismissButtonLabel, languageCode) || "Skip"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,159 +134,154 @@ export function DateQuestion({
|
||||
}, [selectedDate]);
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
setErrorMessage("Please select a date.");
|
||||
return;
|
||||
}
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div id="error-message" className="fb-text-red-600" aria-live="assertive">
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn("fb-mt-4 fb-w-full", errorMessage && "fb-rounded-lg fb-border-2 fb-border-red-500")}
|
||||
id="date-picker-root">
|
||||
<div className="fb-relative">
|
||||
{!datePickerOpen && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setDatePickerOpen(true);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") setDatePickerOpen(true);
|
||||
}}
|
||||
aria-label={selectedDate ? `You have selected ${formattedDate}` : "Select a date"}
|
||||
aria-describedby={errorMessage ? "error-message" : undefined}
|
||||
className="focus:fb-outline-brand fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-text-heading fb-rounded-custom fb-relative fb-flex fb-h-[12dvh] fb-w-full fb-cursor-pointer fb-appearance-none fb-items-center fb-justify-center fb-border fb-text-left fb-text-base fb-font-normal">
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
{selectedDate ? (
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
<CalendarCheckIcon /> <span>{formattedDate}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
<CalendarIcon /> <span>Select a date</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<DatePicker
|
||||
key={datePickerOpen}
|
||||
value={selectedDate}
|
||||
isOpen={datePickerOpen}
|
||||
onChange={(value) => {
|
||||
const date = value as Date;
|
||||
setSelectedDate(date);
|
||||
|
||||
// Get the timezone offset in minutes and convert it to milliseconds
|
||||
const timezoneOffset = date.getTimezoneOffset() * 60000;
|
||||
|
||||
// Adjust the date by subtracting the timezone offset
|
||||
const adjustedDate = new Date(date.getTime() - timezoneOffset);
|
||||
|
||||
// Format the date as YYYY-MM-DD
|
||||
const dateString = adjustedDate.toISOString().split("T")[0];
|
||||
|
||||
onChange({ [question.id]: dateString });
|
||||
}}
|
||||
minDate={
|
||||
new Date(new Date().getFullYear() - 100, new Date().getMonth(), new Date().getDate())
|
||||
}
|
||||
maxDate={new Date("3000-12-31")}
|
||||
dayPlaceholder="DD"
|
||||
monthPlaceholder="MM"
|
||||
yearPlaceholder="YYYY"
|
||||
format={question.format ?? "M-d-y"}
|
||||
className={`dp-input-root fb-rounded-custom wrapper-hide ${!datePickerOpen ? "" : "fb-h-[46dvh] sm:fb-h-[34dvh]"} ${hideInvalid ? "hide-invalid" : ""} `}
|
||||
calendarProps={{
|
||||
className:
|
||||
"calendar-root !fb-text-heading !fb-bg-input-bg fb-border fb-border-border fb-rounded-custom fb-p-3 fb-h-[46dvh] sm:fb-h-[33dvh] fb-overflow-auto",
|
||||
tileClassName: ({ date }: { date: Date }) => {
|
||||
const baseClass =
|
||||
"hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200";
|
||||
// active date class (check first to take precedence over today's date)
|
||||
if (
|
||||
selectedDate &&
|
||||
date.getDate() === selectedDate?.getDate() &&
|
||||
date.getMonth() === selectedDate.getMonth() &&
|
||||
date.getFullYear() === selectedDate.getFullYear()
|
||||
) {
|
||||
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile`;
|
||||
}
|
||||
// today's date class
|
||||
if (
|
||||
date.getDate() === new Date().getDate() &&
|
||||
date.getMonth() === new Date().getMonth() &&
|
||||
date.getFullYear() === new Date().getFullYear()
|
||||
) {
|
||||
return `${baseClass} !fb-bg-brand !fb-opacity-50 !fb-border-border-highlight !fb-text-calendar-tile focus:fb-ring-2 focus:fb-bg-slate-200`;
|
||||
}
|
||||
|
||||
return `${baseClass} !fb-text-heading`;
|
||||
},
|
||||
formatShortWeekday: (_: any, date: Date) => {
|
||||
return date.toLocaleDateString("en-US", { weekday: "short" }).slice(0, 2);
|
||||
},
|
||||
showNeighboringMonth: false,
|
||||
}}
|
||||
clearIcon={null}
|
||||
onCalendarOpen={() => {
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
setErrorMessage("Please select a date.");
|
||||
return;
|
||||
}
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div id="error-message" className="fb-text-red-600" aria-live="assertive">
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn("fb-mt-4 fb-w-full", errorMessage && "fb-rounded-lg fb-border-2 fb-border-red-500")}
|
||||
id="date-picker-root">
|
||||
<div className="fb-relative">
|
||||
{!datePickerOpen && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setDatePickerOpen(true);
|
||||
}}
|
||||
onCalendarClose={() => {
|
||||
// reset state
|
||||
setDatePickerOpen(false);
|
||||
setSelectedDate(selectedDate);
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
type="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") setDatePickerOpen(true);
|
||||
}}
|
||||
calendarIcon={(<CalendarIcon />) as DatePickerProps["calendarIcon"]}
|
||||
showLeadingZeros={false}
|
||||
/>
|
||||
</div>
|
||||
aria-label={selectedDate ? `You have selected ${formattedDate}` : "Select a date"}
|
||||
aria-describedby={errorMessage ? "error-message" : undefined}
|
||||
className="focus:fb-outline-brand fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-text-heading fb-rounded-custom fb-relative fb-flex fb-h-[12dvh] fb-w-full fb-cursor-pointer fb-appearance-none fb-items-center fb-justify-center fb-border fb-text-left fb-text-base fb-font-normal">
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
{selectedDate ? (
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
<CalendarCheckIcon /> <span>{formattedDate}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fb-flex fb-items-center fb-gap-2">
|
||||
<CalendarIcon /> <span>Select a date</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<DatePicker
|
||||
key={datePickerOpen}
|
||||
value={selectedDate}
|
||||
isOpen={datePickerOpen}
|
||||
onChange={(value) => {
|
||||
const date = value as Date;
|
||||
setSelectedDate(date);
|
||||
|
||||
// Get the timezone offset in minutes and convert it to milliseconds
|
||||
const timezoneOffset = date.getTimezoneOffset() * 60000;
|
||||
|
||||
// Adjust the date by subtracting the timezone offset
|
||||
const adjustedDate = new Date(date.getTime() - timezoneOffset);
|
||||
|
||||
// Format the date as YYYY-MM-DD
|
||||
const dateString = adjustedDate.toISOString().split("T")[0];
|
||||
|
||||
onChange({ [question.id]: dateString });
|
||||
}}
|
||||
minDate={new Date(new Date().getFullYear() - 100, new Date().getMonth(), new Date().getDate())}
|
||||
maxDate={new Date("3000-12-31")}
|
||||
dayPlaceholder="DD"
|
||||
monthPlaceholder="MM"
|
||||
yearPlaceholder="YYYY"
|
||||
format={question.format ?? "M-d-y"}
|
||||
className={`dp-input-root fb-rounded-custom wrapper-hide ${!datePickerOpen ? "" : "fb-h-[46dvh] sm:fb-h-[34dvh]"} ${hideInvalid ? "hide-invalid" : ""} `}
|
||||
calendarProps={{
|
||||
className:
|
||||
"calendar-root !fb-text-heading !fb-bg-input-bg fb-border fb-border-border fb-rounded-custom fb-p-3 fb-h-[46dvh] sm:fb-h-[33dvh] fb-overflow-auto",
|
||||
tileClassName: ({ date }: { date: Date }) => {
|
||||
const baseClass =
|
||||
"hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200";
|
||||
// active date class (check first to take precedence over today's date)
|
||||
if (
|
||||
selectedDate &&
|
||||
date.getDate() === selectedDate?.getDate() &&
|
||||
date.getMonth() === selectedDate.getMonth() &&
|
||||
date.getFullYear() === selectedDate.getFullYear()
|
||||
) {
|
||||
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile`;
|
||||
}
|
||||
// today's date class
|
||||
if (
|
||||
date.getDate() === new Date().getDate() &&
|
||||
date.getMonth() === new Date().getMonth() &&
|
||||
date.getFullYear() === new Date().getFullYear()
|
||||
) {
|
||||
return `${baseClass} !fb-bg-brand !fb-opacity-50 !fb-border-border-highlight !fb-text-calendar-tile focus:fb-ring-2 focus:fb-bg-slate-200`;
|
||||
}
|
||||
|
||||
return `${baseClass} !fb-text-heading`;
|
||||
},
|
||||
formatShortWeekday: (_: any, date: Date) => {
|
||||
return date.toLocaleDateString("en-US", { weekday: "short" }).slice(0, 2);
|
||||
},
|
||||
showNeighboringMonth: false,
|
||||
}}
|
||||
clearIcon={null}
|
||||
onCalendarOpen={() => {
|
||||
setDatePickerOpen(true);
|
||||
}}
|
||||
onCalendarClose={() => {
|
||||
// reset state
|
||||
setDatePickerOpen(false);
|
||||
setSelectedDate(selectedDate);
|
||||
}}
|
||||
calendarIcon={(<CalendarIcon />) as DatePickerProps["calendarIcon"]}
|
||||
showLeadingZeros={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
isLastQuestion={isLastQuestion}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
isLastQuestion={isLastQuestion}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,75 +53,71 @@ export function FileUploadQuestion({
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
if (question.required) {
|
||||
if (value && value.length > 0) {
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
if (question.required) {
|
||||
if (value && value.length > 0) {
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
} else {
|
||||
alert("Please upload a file");
|
||||
}
|
||||
} else if (value) {
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
} else {
|
||||
alert("Please upload a file");
|
||||
onSubmit({ [question.id]: "skipped" }, updatedTtcObj);
|
||||
}
|
||||
} else if (value) {
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
} else {
|
||||
onSubmit({ [question.id]: "skipped" }, updatedTtcObj);
|
||||
}
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<FileInput
|
||||
htmlFor={question.id}
|
||||
surveyId={surveyId}
|
||||
onFileUpload={onFileUpload}
|
||||
onUploadCallback={(urls: string[]) => {
|
||||
if (urls) {
|
||||
onChange({ [question.id]: urls });
|
||||
} else {
|
||||
onChange({ [question.id]: "skipped" });
|
||||
}
|
||||
}}
|
||||
fileUrls={value}
|
||||
allowMultipleFiles={question.allowMultipleFiles}
|
||||
{...(question.allowedFileExtensions
|
||||
? { allowedFileExtensions: question.allowedFileExtensions }
|
||||
: {})}
|
||||
{...(question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<FileInput
|
||||
htmlFor={question.id}
|
||||
surveyId={surveyId}
|
||||
onFileUpload={onFileUpload}
|
||||
onUploadCallback={(urls: string[]) => {
|
||||
if (urls) {
|
||||
onChange({ [question.id]: urls });
|
||||
} else {
|
||||
onChange({ [question.id]: "skipped" });
|
||||
}
|
||||
}}
|
||||
fileUrls={value}
|
||||
allowMultipleFiles={question.allowMultipleFiles}
|
||||
{...(question.allowedFileExtensions
|
||||
? { allowedFileExtensions: question.allowedFileExtensions }
|
||||
: {})}
|
||||
{...(question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
/>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,109 +119,100 @@ export function MatrixQuestion({
|
||||
);
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={getLocalizedValue(question.subheader, languageCode)}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-overflow-x-auto fb-py-4">
|
||||
<table className="fb-no-scrollbar fb-min-w-full fb-table-auto fb-border-collapse fb-text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="fb-px-4 fb-py-2" />
|
||||
{columnsHeaders}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{questionRows.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={`row-${rowIndex.toString()}`}
|
||||
className={rowIndex % 2 === 0 ? "fb-bg-input-bg" : ""}>
|
||||
<th
|
||||
scope="row"
|
||||
className="fb-text-heading fb-rounded-l-custom fb-max-w-40 fb-break-words fb-pr-4 fb-pl-2 fb-py-2 fb-text-left fb-min-w-[20%] fb-font-semibold"
|
||||
dir="auto">
|
||||
{getLocalizedValue(row, languageCode)}
|
||||
</th>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={`column-${columnIndex.toString()}`}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === question.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
||||
onClick={() => {
|
||||
<ScrollableContainer>
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader subheader={getLocalizedValue(question.subheader, languageCode)} questionId={question.id} />
|
||||
<div className="fb-overflow-x-auto fb-py-4">
|
||||
<table className="fb-no-scrollbar fb-min-w-full fb-table-auto fb-border-collapse fb-text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="fb-px-4 fb-py-2" />
|
||||
{columnsHeaders}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{questionRows.map((row, rowIndex) => (
|
||||
<tr key={`row-${rowIndex.toString()}`} className={rowIndex % 2 === 0 ? "fb-bg-input-bg" : ""}>
|
||||
<th
|
||||
scope="row"
|
||||
className="fb-text-heading fb-rounded-l-custom fb-max-w-40 fb-break-words fb-pr-4 fb-pl-2 fb-py-2 fb-text-left fb-min-w-[20%] fb-font-semibold"
|
||||
dir="auto">
|
||||
{getLocalizedValue(row, languageCode)}
|
||||
</th>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={`column-${columnIndex.toString()}`}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === question.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
||||
onClick={() => {
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
);
|
||||
}
|
||||
}}
|
||||
dir="auto">
|
||||
<div className="fb-flex fb-items-center fb-justify-center fb-p-2">
|
||||
<input
|
||||
dir="auto"
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
required={question.required}
|
||||
id={`row${rowIndex.toString()}-column${columnIndex.toString()}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
checked={
|
||||
typeof value === "object" && !Array.isArray(value)
|
||||
? value[getLocalizedValue(row, languageCode)] ===
|
||||
getLocalizedValue(column, languageCode)
|
||||
: false
|
||||
}
|
||||
}}
|
||||
dir="auto">
|
||||
<div className="fb-flex fb-items-center fb-justify-center fb-p-2">
|
||||
<input
|
||||
dir="auto"
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
required={question.required}
|
||||
id={`row${rowIndex.toString()}-column${columnIndex.toString()}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
checked={
|
||||
typeof value === "object" && !Array.isArray(value)
|
||||
? value[getLocalizedValue(row, languageCode)] ===
|
||||
getLocalizedValue(column, languageCode)
|
||||
: false
|
||||
}
|
||||
aria-label={`${getLocalizedValue(
|
||||
question.headline,
|
||||
languageCode
|
||||
)}: ${getLocalizedValue(row, languageCode)} – ${getLocalizedValue(
|
||||
column,
|
||||
languageCode
|
||||
)}`}
|
||||
className="fb-border-brand fb-text-brand fb-h-5 fb-w-5 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
aria-label={`${getLocalizedValue(
|
||||
question.headline,
|
||||
languageCode
|
||||
)}: ${getLocalizedValue(row, languageCode)} – ${getLocalizedValue(
|
||||
column,
|
||||
languageCode
|
||||
)}`}
|
||||
className="fb-border-brand fb-text-brand fb-h-5 fb-w-5 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={handleBackButtonClick}
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={handleBackButtonClick}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,187 +147,182 @@ export function MultipleChoiceMultiQuestion({
|
||||
}, [languageCode, question.otherOptionPlaceholder, otherValue]);
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
|
||||
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
|
||||
if (otherValue && otherSelected && !newValue.includes(otherValue)) newValue.push(otherValue);
|
||||
onChange({ [question.id]: newValue });
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: newValue }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-bg-survey-bg fb-relative fb-space-y-2" ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => {
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border fb-bg-input-bg",
|
||||
"fb-text-heading focus-within:fb-border-brand hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(choice.id)?.click();
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
tabIndex={-1}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
if ((e.target as HTMLInputElement).checked) {
|
||||
addItem(getLocalizedValue(choice.label, languageCode));
|
||||
} else {
|
||||
removeItem(getLocalizedValue(choice.label, languageCode));
|
||||
}
|
||||
}}
|
||||
checked={
|
||||
Array.isArray(value) &&
|
||||
value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
}
|
||||
required={getIsRequired()}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{otherOption ? (
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
|
||||
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
|
||||
if (otherValue && otherSelected && !newValue.includes(otherValue)) newValue.push(otherValue);
|
||||
onChange({ [question.id]: newValue });
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: newValue }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-bg-survey-bg fb-relative fb-space-y-2" ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => {
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
otherSelected ? "fb-border-brand fb-bg-input-bg-selected fb-z-10" : "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border fb-bg-input-bg",
|
||||
"fb-text-heading focus-within:fb-border-brand hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
if (otherSelected) return;
|
||||
document.getElementById(otherOption.id)?.click();
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
e.preventDefault();
|
||||
document.getElementById(choice.id)?.click();
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
tabIndex={-1}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
if (otherSelected) {
|
||||
setOtherValue("");
|
||||
onChange({
|
||||
[question.id]: value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item);
|
||||
}),
|
||||
});
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
if ((e.target as HTMLInputElement).checked) {
|
||||
addItem(getLocalizedValue(choice.label, languageCode));
|
||||
} else {
|
||||
removeItem(getLocalizedValue(choice.label, languageCode));
|
||||
}
|
||||
setOtherSelected(!otherSelected);
|
||||
}}
|
||||
checked={otherSelected}
|
||||
checked={
|
||||
Array.isArray(value) &&
|
||||
value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
}
|
||||
required={getIsRequired()}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir={otherOptionDir}
|
||||
id={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
value={otherValue}
|
||||
pattern=".*\S+.*"
|
||||
onChange={(e) => {
|
||||
setOtherValue(e.currentTarget.value);
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode).length > 0
|
||||
? getLocalizedValue(question.otherOptionPlaceholder, languageCode)
|
||||
: "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onBlur={() => {
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item);
|
||||
});
|
||||
if (otherValue && otherSelected) {
|
||||
newValue.push(otherValue);
|
||||
onChange({ [question.id]: newValue });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{otherOption ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
otherSelected ? "fb-border-brand fb-bg-input-bg-selected fb-z-10" : "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
if (otherSelected) return;
|
||||
document.getElementById(otherOption.id)?.click();
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
if (otherSelected) {
|
||||
setOtherValue("");
|
||||
onChange({
|
||||
[question.id]: value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item);
|
||||
}),
|
||||
});
|
||||
}
|
||||
setOtherSelected(!otherSelected);
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir={otherOptionDir}
|
||||
id={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
value={otherValue}
|
||||
pattern=".*\S+.*"
|
||||
onChange={(e) => {
|
||||
setOtherValue(e.currentTarget.value);
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode).length > 0
|
||||
? getLocalizedValue(question.otherOptionPlaceholder, languageCode)
|
||||
: "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onBlur={() => {
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item);
|
||||
});
|
||||
if (otherValue && otherSelected) {
|
||||
newValue.push(otherValue);
|
||||
onChange({ [question.id]: newValue });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,168 +107,164 @@ export function MultipleChoiceSingleQuestion({
|
||||
}, [languageCode, question.otherOptionPlaceholder, value]);
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
|
||||
<div
|
||||
className="fb-bg-survey-bg fb-relative fb-space-y-2"
|
||||
role="radiogroup"
|
||||
ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => {
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
dir="auto"
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(choice.label, languageCode)
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border",
|
||||
"fb-text-heading fb-bg-input-bg focus-within:fb-border-brand focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(choice.id)?.click();
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="radio"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
dir="auto"
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(false);
|
||||
onChange({ [question.id]: getLocalizedValue(choice.label, languageCode) });
|
||||
}}
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required ? idx === 0 : undefined}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{otherOption ? (
|
||||
<div
|
||||
className="fb-bg-survey-bg fb-relative fb-space-y-2"
|
||||
role="radiogroup"
|
||||
ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => {
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
dir="auto"
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
value === getLocalizedValue(choice.label, languageCode)
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
"fb-text-heading fb-bg-input-bg focus-within:fb-border-brand focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
if (otherSelected) return;
|
||||
document.getElementById(otherOption.id)?.click();
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
e.preventDefault();
|
||||
document.getElementById(choice.id)?.click();
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
}}
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
dir="auto"
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
dir="auto"
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
setOtherSelected(false);
|
||||
onChange({ [question.id]: getLocalizedValue(choice.label, languageCode) });
|
||||
}}
|
||||
checked={otherSelected}
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required ? idx === 0 : undefined}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
dir={otherOptionDir}
|
||||
name={question.id}
|
||||
pattern=".*\S+.*"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode).length > 0
|
||||
? getLocalizedValue(question.otherOptionPlaceholder, languageCode)
|
||||
: "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{otherOption ? (
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
if (otherSelected) return;
|
||||
document.getElementById(otherOption.id)?.click();
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
dir="auto"
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
dir={otherOptionDir}
|
||||
name={question.id}
|
||||
pattern=".*\S+.*"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode).length > 0
|
||||
? getLocalizedValue(question.otherOptionPlaceholder, languageCode)
|
||||
: "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
/>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,114 +68,114 @@ export function NPSQuestion({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-my-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-flex">
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
||||
return (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(-1);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === number
|
||||
? "fb-border-border-highlight fb-bg-accent-selected-bg fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
"fb-text-heading first:fb-rounded-l-custom last:fb-rounded-r-custom focus:fb-border-brand fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-overflow-hidden fb-border-b fb-border-l fb-border-t fb-text-center fb-text-sm last:fb-border-r focus:fb-border-2 focus:fb-outline-none",
|
||||
question.isColorCodingEnabled
|
||||
? "fb-h-[46px] fb-leading-[3.5em]"
|
||||
: "fb-h fb-leading-10",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
||||
)}>
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
|
||||
/>
|
||||
) : null}
|
||||
<input
|
||||
type="radio"
|
||||
id={number.toString()}
|
||||
name="nps"
|
||||
value={number}
|
||||
checked={value === number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleClick(number);
|
||||
}}
|
||||
required={question.required}
|
||||
tabIndex={-1}
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}>
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-my-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-flex">
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
||||
return (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(-1);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(-1);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === number
|
||||
? "fb-border-border-highlight fb-bg-accent-selected-bg fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
"fb-text-heading first:fb-rounded-l-custom last:fb-rounded-r-custom focus:fb-border-brand fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-overflow-hidden fb-border-b fb-border-l fb-border-t fb-text-center fb-text-sm last:fb-border-r focus:fb-border-2 focus:fb-outline-none",
|
||||
question.isColorCodingEnabled ? "fb-h-[46px] fb-leading-[3.5em]" : "fb-h fb-leading-10",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
||||
)}>
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
|
||||
/>
|
||||
{number}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-2 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p dir="auto">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p dir="auto">{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
) : null}
|
||||
<input
|
||||
type="radio"
|
||||
id={number.toString()}
|
||||
name="nps"
|
||||
value={number}
|
||||
checked={value === number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleClick(number);
|
||||
}}
|
||||
required={question.required}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
{number}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-2 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p dir="auto">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p dir="auto">{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
{question.required ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
{question.required ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,100 +88,96 @@ export function OpenTextQuestion({
|
||||
}, [value, languageCode, question.placeholder]);
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleOnSubmit} className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir={dir}
|
||||
step="any"
|
||||
required={question.required}
|
||||
value={value ? value : ""}
|
||||
type={question.inputType}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
}}
|
||||
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm"
|
||||
pattern={question.inputType === "phone" ? "^[0-9+][0-9+\\- ]*[0-9]$" : ".*"}
|
||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||
maxLength={
|
||||
question.inputType === "text"
|
||||
? question.charLimit?.max
|
||||
: question.inputType === "phone"
|
||||
? 30
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
ref={inputRef as RefObject<HTMLTextAreaElement>}
|
||||
rows={3}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label="textarea"
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir={dir}
|
||||
required={question.required}
|
||||
value={value}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
}}
|
||||
className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0 sm:fb-text-sm"
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||
maxLength={question.inputType === "text" ? question.charLimit?.max : undefined}
|
||||
/>
|
||||
)}
|
||||
{question.inputType === "text" && question.charLimit?.max !== undefined && (
|
||||
<span
|
||||
className={`fb-text-xs ${currentLength >= question.charLimit?.max ? "fb-text-red-500 font-semibold" : "text-neutral-400"}`}>
|
||||
{currentLength}/{question.charLimit?.max}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
<ScrollableContainer>
|
||||
<form key={question.id} onSubmit={handleOnSubmit} className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir={dir}
|
||||
step="any"
|
||||
required={question.required}
|
||||
value={value ? value : ""}
|
||||
type={question.inputType}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
}}
|
||||
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm"
|
||||
pattern={question.inputType === "phone" ? "^[0-9+][0-9+\\- ]*[0-9]$" : ".*"}
|
||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||
maxLength={
|
||||
question.inputType === "text"
|
||||
? question.charLimit?.max
|
||||
: question.inputType === "phone"
|
||||
? 30
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
ref={inputRef as RefObject<HTMLTextAreaElement>}
|
||||
rows={3}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label="textarea"
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir={dir}
|
||||
required={question.required}
|
||||
value={value}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
}}
|
||||
className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0 sm:fb-text-sm"
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||
maxLength={question.inputType === "text" ? question.charLimit?.max : undefined}
|
||||
/>
|
||||
)}
|
||||
{question.inputType === "text" && question.charLimit?.max !== undefined && (
|
||||
<span
|
||||
className={`fb-text-xs ${currentLength >= question.charLimit?.max ? "fb-text-red-500 font-semibold" : "text-neutral-400"}`}>
|
||||
{currentLength}/{question.charLimit?.max}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,153 +96,151 @@ export function PictureSelectionQuestion({
|
||||
const questionChoices = question.choices;
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-1 sm:fb-grid-cols-2 fb-gap-4">
|
||||
{questionChoices.map((choice) => (
|
||||
<div className="fb-relative" key={choice.id}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
e.currentTarget.focus();
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
handleChange(choice.id);
|
||||
}}
|
||||
className={cn(
|
||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus-visible:fb-outline-none focus-visible:fb-ring-2 focus-visible:fb-ring-brand focus-visible:fb-ring-offset-2 fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] group/image",
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
||||
: ""
|
||||
)}>
|
||||
{loadingImages[choice.id] && (
|
||||
<div className="fb-absolute fb-inset-0 fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||
)}
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
||||
className={cn(
|
||||
"fb-h-full fb-w-full fb-object-cover",
|
||||
loadingImages[choice.id] ? "fb-opacity-0" : ""
|
||||
)}
|
||||
onLoad={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
onError={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
/>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${question.id}`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={choice.imageUrl}
|
||||
target="_blank"
|
||||
title="Open in new tab"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="fb-absolute fb-bottom-4 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100 fb-z-20">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-image-down-icon lucide-image-down">
|
||||
<path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" />
|
||||
<path d="m14 19 3 3v-5.5" />
|
||||
<path d="m17 22 3-3" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-1 sm:fb-grid-cols-2 fb-gap-4">
|
||||
{questionChoices.map((choice) => (
|
||||
<div className="fb-relative" key={choice.id}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
e.currentTarget.focus();
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
handleChange(choice.id);
|
||||
}}
|
||||
className={cn(
|
||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus-visible:fb-outline-none focus-visible:fb-ring-2 focus-visible:fb-ring-brand focus-visible:fb-ring-offset-2 fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] group/image",
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
||||
: ""
|
||||
)}>
|
||||
{loadingImages[choice.id] && (
|
||||
<div className="fb-absolute fb-inset-0 fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||
)}
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
||||
className={cn(
|
||||
"fb-h-full fb-w-full fb-object-cover",
|
||||
loadingImages[choice.id] ? "fb-opacity-0" : ""
|
||||
)}
|
||||
onLoad={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
onError={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
/>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length === 0}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${question.id}`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={choice.imageUrl}
|
||||
target="_blank"
|
||||
title="Open in new tab"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="fb-absolute fb-bottom-4 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100 fb-z-20">
|
||||
<span className="fb-sr-only">Open in new tab</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-image-down-icon lucide-image-down">
|
||||
<path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" />
|
||||
<path d="m14 19 3 3v-5.5" />
|
||||
<path d="m17 22 3-3" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,153 +154,148 @@ export function RankingQuestion({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="fb-w-full">
|
||||
<ScrollableContainer ref={scrollableRef}>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Ranking Items</legend>
|
||||
<div className="fb-relative" ref={parent}>
|
||||
{[...sortedItems, ...unsortedItems].map((item, idx) => {
|
||||
if (!item) return null;
|
||||
const isSorted = sortedItems.includes(item);
|
||||
const isFirst = isSorted && idx === 0;
|
||||
const isLast = isSorted && idx === sortedItems.length - 1;
|
||||
<ScrollableContainer ref={scrollableRef}>
|
||||
<form onSubmit={handleSubmit} className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Ranking Items</legend>
|
||||
<div className="fb-relative" ref={parent}>
|
||||
{[...sortedItems, ...unsortedItems].map((item, idx) => {
|
||||
if (!item) return null;
|
||||
const isSorted = sortedItems.includes(item);
|
||||
const isFirst = isSorted && idx === 0;
|
||||
const isLast = isSorted && idx === sortedItems.length - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"fb-flex fb-h-12 fb-items-center fb-mb-2 fb-border fb-border-border fb-transition-all fb-text-heading hover:fb-bg-input-bg-selected focus-within:fb-border-brand focus-within:fb-shadow-outline focus-within:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-cursor-pointer w-full focus:outline-none",
|
||||
isSorted ? "fb-bg-input-bg-selected" : "fb-bg-input-bg"
|
||||
)}>
|
||||
<button
|
||||
autoFocus={idx === 0 && autoFocusEnabled}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleItemClick(item);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"fb-flex fb-h-12 fb-items-center fb-mb-2 fb-border fb-border-border fb-transition-all fb-text-heading hover:fb-bg-input-bg-selected focus-within:fb-border-brand focus-within:fb-shadow-outline focus-within:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-cursor-pointer w-full focus:outline-none",
|
||||
isSorted ? "fb-bg-input-bg-selected" : "fb-bg-input-bg"
|
||||
)}>
|
||||
<button
|
||||
autoFocus={idx === 0 && autoFocusEnabled}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleItemClick(item);
|
||||
}}
|
||||
type="button"
|
||||
aria-label={`Select ${getLocalizedValue(item.label, languageCode)} for ranking`}
|
||||
className="fb-flex fb-gap-x-4 fb-px-4 fb-items-center fb-grow fb-h-full group text-left focus:outline-none">
|
||||
<span
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleItemClick(item);
|
||||
}}
|
||||
type="button"
|
||||
aria-label={`Select ${getLocalizedValue(item.label, languageCode)} for ranking`}
|
||||
className="fb-flex fb-gap-x-4 fb-px-4 fb-items-center fb-grow fb-h-full group text-left focus:outline-none">
|
||||
<span
|
||||
className={cn(
|
||||
"fb-w-6 fb-grow-0 fb-h-6 fb-flex fb-items-center fb-justify-center fb-rounded-full fb-text-xs fb-font-semibold fb-border-brand fb-border",
|
||||
isSorted
|
||||
? "fb-bg-brand fb-text-white fb-border"
|
||||
: "fb-border-dashed group-hover:fb-bg-white fb-text-transparent group-hover:fb-text-heading"
|
||||
)}>
|
||||
{(idx + 1).toString()}
|
||||
</span>
|
||||
<div className="fb-grow fb-shrink fb-font-medium fb-text-sm fb-text-start" dir="auto">
|
||||
{getLocalizedValue(item.label, languageCode)}
|
||||
</div>
|
||||
</button>
|
||||
{isSorted ? (
|
||||
<div className="fb-flex fb-flex-col fb-h-full fb-grow-0 fb-border-l fb-border-border">
|
||||
<button
|
||||
tabIndex={isFirst ? -1 : 0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleMove(item.id, "up");
|
||||
}}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} up`}
|
||||
className={cn(
|
||||
"fb-w-6 fb-grow-0 fb-h-6 fb-flex fb-items-center fb-justify-center fb-rounded-full fb-text-xs fb-font-semibold fb-border-brand fb-border",
|
||||
isSorted
|
||||
? "fb-bg-brand fb-text-white fb-border"
|
||||
: "fb-border-dashed group-hover:fb-bg-white fb-text-transparent group-hover:fb-text-heading"
|
||||
)}>
|
||||
{(idx + 1).toString()}
|
||||
</span>
|
||||
<div className="fb-grow fb-shrink fb-font-medium fb-text-sm fb-text-start" dir="auto">
|
||||
{getLocalizedValue(item.label, languageCode)}
|
||||
</div>
|
||||
</button>
|
||||
{isSorted ? (
|
||||
<div className="fb-flex fb-flex-col fb-h-full fb-grow-0 fb-border-l fb-border-border">
|
||||
<button
|
||||
tabIndex={isFirst ? -1 : 0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleMove(item.id, "up");
|
||||
}}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} up`}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex fb-flex-1 fb-items-center fb-justify-center",
|
||||
isFirst
|
||||
? "fb-opacity-30 fb-cursor-not-allowed"
|
||||
: "hover:fb-bg-black/5 fb-rounded-tr-custom fb-transition-colors"
|
||||
)}
|
||||
disabled={isFirst}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-chevron-up">
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
tabIndex={isLast ? -1 : 0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleMove(item.id, "down");
|
||||
}}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex-1 fb-border-t fb-border-border fb-flex fb-items-center fb-justify-center",
|
||||
isLast
|
||||
? "fb-opacity-30 fb-cursor-not-allowed"
|
||||
: "hover:fb-bg-black/5 fb-rounded-br-custom fb-transition-colors"
|
||||
)}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} down`}
|
||||
disabled={isLast}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-chevron-down">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{error ? <div className="fb-text-red-500 fb-mt-2 fb-text-sm">{error}</div> : null}
|
||||
"fb-px-2 fb-flex fb-flex-1 fb-items-center fb-justify-center",
|
||||
isFirst
|
||||
? "fb-opacity-30 fb-cursor-not-allowed"
|
||||
: "hover:fb-bg-black/5 fb-rounded-tr-custom fb-transition-colors"
|
||||
)}
|
||||
disabled={isFirst}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-chevron-up">
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
tabIndex={isLast ? -1 : 0}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleMove(item.id, "down");
|
||||
}}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex-1 fb-border-t fb-border-border fb-flex fb-items-center fb-justify-center",
|
||||
isLast
|
||||
? "fb-opacity-30 fb-cursor-not-allowed"
|
||||
: "hover:fb-bg-black/5 fb-rounded-br-custom fb-transition-colors"
|
||||
)}
|
||||
aria-label={`Move ${getLocalizedValue(item.label, languageCode)} down`}
|
||||
disabled={isLast}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-chevron-down">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
{error ? <div className="fb-text-red-500 fb-mt-2 fb-text-sm">{error}</div> : null}
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={handleBack}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,179 +111,175 @@ export function RatingQuestion({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mb-4 fb-mt-6 fb-flex fb-items-center fb-justify-center">
|
||||
<fieldset className="fb-w-full">
|
||||
<legend className="fb-sr-only">Choices</legend>
|
||||
<div className="fb-flex fb-w-full">
|
||||
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
|
||||
<span
|
||||
key={number}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(0);
|
||||
}}
|
||||
className="fb-bg-survey-bg fb-flex-1 fb-text-center fb-text-sm">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === number
|
||||
? "fb-bg-accent-selected-bg fb-border-border-highlight fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
a.length === number ? "fb-rounded-r-custom fb-border-r" : "",
|
||||
number === 1 ? "fb-rounded-l-custom" : "",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : "",
|
||||
question.isColorCodingEnabled ? "fb-min-h-[47px]" : "fb-min-h-[41px]",
|
||||
"fb-text-heading focus:fb-border-brand fb-relative fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-justify-center fb-overflow-hidden fb-border-b fb-border-l fb-border-t focus:fb-border-2 focus:fb-outline-none"
|
||||
)}>
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(question.range, number)}`}
|
||||
<ScrollableContainer>
|
||||
<form
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="fb-w-full">
|
||||
{isMediaAvailable ? <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} /> : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="fb-mb-4 fb-mt-6 fb-flex fb-items-center fb-justify-center">
|
||||
<fieldset className="fb-w-full">
|
||||
<legend className="fb-sr-only">Choices</legend>
|
||||
<div className="fb-flex fb-w-full">
|
||||
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
|
||||
<span
|
||||
key={number}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(0);
|
||||
}}
|
||||
className="fb-bg-survey-bg fb-flex-1 fb-text-center fb-text-sm">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
value === number
|
||||
? "fb-bg-accent-selected-bg fb-border-border-highlight fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
a.length === number ? "fb-rounded-r-custom fb-border-r" : "",
|
||||
number === 1 ? "fb-rounded-l-custom" : "",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : "",
|
||||
question.isColorCodingEnabled ? "fb-min-h-[47px]" : "fb-min-h-[41px]",
|
||||
"fb-text-heading focus:fb-border-brand fb-relative fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-justify-center fb-overflow-hidden fb-border-b fb-border-l fb-border-t focus:fb-border-2 focus:fb-outline-none"
|
||||
)}>
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(question.range, number)}`}
|
||||
/>
|
||||
) : null}
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
{number}
|
||||
</label>
|
||||
) : question.scale === "star" ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
number <= hoveredNumber || number <= value!
|
||||
? "fb-text-amber-400"
|
||||
: "fb-text-[#8696AC]",
|
||||
hoveredNumber === number ? "fb-text-amber-400" : "",
|
||||
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-cursor-pointer fb-justify-center focus:fb-outline-none"
|
||||
)}
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
|
||||
/>
|
||||
) : null}
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
{number}
|
||||
</label>
|
||||
) : question.scale === "star" ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
number <= hoveredNumber || number <= value!
|
||||
? "fb-text-amber-400"
|
||||
: "fb-text-[#8696AC]",
|
||||
hoveredNumber === number ? "fb-text-amber-400" : "",
|
||||
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-cursor-pointer fb-justify-center focus:fb-outline-none"
|
||||
)}
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
) : (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-w-full fb-cursor-pointer fb-justify-center",
|
||||
value === number || hoveredNumber === number
|
||||
? "fb-stroke-rating-selected fb-text-rating-selected"
|
||||
: "fb-stroke-heading fb-text-heading focus:fb-border-accent-bg focus:fb-border-2 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className={cn("fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain")}>
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
idx={i}
|
||||
range={question.range}
|
||||
addColors={question.isColorCodingEnabled}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p className="fb-w-1/2 fb-text-left" dir="auto">
|
||||
{getLocalizedValue(question.lowerLabel, languageCode)}
|
||||
</p>
|
||||
<p className="fb-w-1/2 fb-text-right" dir="auto">
|
||||
{getLocalizedValue(question.upperLabel, languageCode)}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
) : (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-w-full fb-cursor-pointer fb-justify-center",
|
||||
value === number || hoveredNumber === number
|
||||
? "fb-stroke-rating-selected fb-text-rating-selected"
|
||||
: "fb-stroke-heading fb-text-heading focus:fb-border-accent-bg focus:fb-border-2 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(number.toString())?.click();
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className={cn("fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain")}>
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
idx={i}
|
||||
range={question.range}
|
||||
addColors={question.isColorCodingEnabled}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p className="fb-w-1/2 fb-text-left" dir="auto">
|
||||
{getLocalizedValue(question.lowerLabel, languageCode)}
|
||||
</p>
|
||||
<p className="fb-w-1/2 fb-text-right" dir="auto">
|
||||
{getLocalizedValue(question.upperLabel, languageCode)}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
{question.required ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-pt-4">
|
||||
{question.required ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<SubmitButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
<div />
|
||||
{!isFirstQuestion && !isBackButtonHidden && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,14 @@ export const ScrollableContainer = forwardRef<ScrollableContainerHandle, Scrolla
|
||||
if (!containerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||
|
||||
setIsAtBottom(Math.round(scrollTop) + clientHeight >= scrollHeight);
|
||||
// Use a small tolerance to account for zoom-related precision issues
|
||||
const tolerance = 1;
|
||||
|
||||
setIsAtTop(scrollTop === 0);
|
||||
// Check if at bottom with tolerance
|
||||
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - tolerance);
|
||||
|
||||
// Check if at top with tolerance
|
||||
setIsAtTop(scrollTop <= tolerance);
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -59,7 +64,7 @@ export const ScrollableContainer = forwardRef<ScrollableContainerHandle, Scrolla
|
||||
return (
|
||||
<div className="fb-relative">
|
||||
{!isAtTop && (
|
||||
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-6 fb-bg-gradient-to-b fb-to-transparent" />
|
||||
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-4 fb-bg-gradient-to-b fb-to-transparent" />
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -67,11 +72,11 @@ export const ScrollableContainer = forwardRef<ScrollableContainerHandle, Scrolla
|
||||
scrollbarGutter: "stable both-edges",
|
||||
maxHeight: isSurveyPreview ? "42dvh" : "60dvh",
|
||||
}}
|
||||
className={cn("fb-overflow-auto fb-px-4 fb-pb-4 fb-bg-survey-bg")}>
|
||||
className={cn("fb-overflow-auto fb-px-4 fb-pb-1 fb-bg-survey-bg")}>
|
||||
{children}
|
||||
</div>
|
||||
{!isAtBottom && (
|
||||
<div className="fb-from-survey-bg fb-absolute -fb-bottom-2 fb-left-0 fb-right-2 fb-h-8 fb-bg-gradient-to-t fb-to-transparent" />
|
||||
<div className="fb-from-survey-bg fb-absolute fb-bottom-0 fb-left-4 fb-right-4 fb-h-4 fb-bg-gradient-to-t fb-to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user