style: scroll indicator update (#6310)

This commit is contained in:
Jakob Schott
2025-07-30 14:27:15 +02:00
committed by GitHub
parent e29a67b1f6
commit 3b90223101
19 changed files with 1548 additions and 1603 deletions
@@ -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>
);