finish link survey tweaks

This commit is contained in:
Johannes
2025-05-17 12:02:05 +07:00
parent 31496ee092
commit 03ddf3d09a
21 changed files with 164 additions and 218 deletions

View File

@@ -178,7 +178,7 @@ export const authOptions: NextAuthOptions = {
...(ENTERPRISE_LICENSE_KEY ? getSSOProviders() : []),
],
session: {
maxAge: 3600,
maxAge: 604800, // 7 days
},
callbacks: {
async jwt({ token }) {

View File

@@ -78,7 +78,7 @@ export const LinkSurveyWrapper = ({
surveyType={surveyType}
styling={styling}
onBackgroundLoaded={handleBackgroundLoaded}>
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip md:items-start md:pt-[16dvh]">
<div className="flex max-h-dvh min-h-dvh items-start justify-center overflow-clip pt-[16dvh]">
{!styling.isLogoHidden && project.logo?.url && <ClientLogo projectLogo={project.logo} />}
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
{isPreview && (

View File

@@ -1,4 +1,4 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import toast from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest";

View File

@@ -342,7 +342,7 @@ export function FileInput({
{showUploader ? (
<button
type="button"
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-6 hover:fb-cursor-pointer w-full"
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-10 hover:fb-cursor-pointer w-full"
aria-label="Upload files by clicking or dragging them here"
onClick={() => document.getElementById(uniqueHtmlFor)?.click()}>
<svg

View File

@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload";
import { useState } from "preact/hooks";
@@ -35,7 +36,10 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
key={imgUrl}
src={imgUrl}
alt={altText}
className="fb-rounded-custom"
className={cn(
"fb-rounded-custom fb-max-h-[40dvh] fb-mx-auto fb-object-contain",
isLoading ? "fb-opacity-0" : ""
)}
onLoad={() => {
setIsLoading(false);
}}
@@ -48,7 +52,7 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
src={videoUrlWithParams}
title="Question Video"
frameBorder="0"
className="fb-rounded-custom fb-aspect-video fb-w-full"
className={cn("fb-rounded-custom fb-aspect-video fb-w-full", isLoading ? "fb-opacity-0" : "")}
onLoad={() => {
setIsLoading(false);
}}

View File

@@ -24,8 +24,8 @@ export function RenderSurvey(props: SurveyContainerProps) {
const surveyTypeStyles =
props.survey.type === "link"
? ({
"--fb-survey-card-max-height": isDesktop ? "56dvh" : "33dvh",
"--fb-survey-card-min-height": isDesktop ? `0dvh` : "33dvh",
"--fb-survey-card-max-height": isDesktop ? "56dvh" : "60dvh",
"--fb-survey-card-min-height": isDesktop ? "0" : "42dvh",
} as React.CSSProperties)
: ({
"--fb-survey-card-max-height": "25dvh",

View File

@@ -139,7 +139,7 @@ export function WelcomeCard({
{fileUrl ? (
<img
src={fileUrl}
className="fb-mb-8 fb-max-h-96 fb-w-1/4 fb-rounded-lg fb-object-contain"
className="fb-mb-8 fb-max-h-80 fb-w-1/4 fb-rounded-lg fb-object-contain"
alt="Company Logo"
/>
) : null}

View File

@@ -43,7 +43,7 @@ export function AddressQuestion({
currentQuestionId,
autoFocusEnabled,
isBackButtonHidden,
}: AddressQuestionProps) {
}: Readonly<AddressQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
const formRef = useRef<HTMLFormElement>(null);
@@ -136,7 +136,7 @@ export function AddressQuestion({
questionId={question.id}
/>
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
<div className="fb-mt-4 fb-w-full fb-grid fb-grid-cols-1 md:fb-grid-cols-2 fb-gap-4">
{fields.map((field, index) => {
const isFieldRequired = () => {
if (field.required) {

View File

@@ -40,7 +40,7 @@ export function CalQuestion({
setTtc,
currentQuestionId,
isBackButtonHidden,
}: CalQuestionProps) {
}: Readonly<CalQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
const [errorMessage, setErrorMessage] = useState("");

View File

@@ -40,7 +40,7 @@ export function ConsentQuestion({
currentQuestionId,
autoFocusEnabled,
isBackButtonHidden,
}: ConsentQuestionProps) {
}: Readonly<ConsentQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
const isCurrent = question.id === currentQuestionId;
@@ -48,8 +48,7 @@ export function ConsentQuestion({
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
const consentRef = useCallback(
(currentElement: HTMLLabelElement | null) => {
// will focus on current element when the question ID matches the current question
(currentElement: HTMLButtonElement | null) => {
if (question.id && currentElement && autoFocusEnabled && question.id === currentQuestionId) {
currentElement.focus();
}
@@ -80,14 +79,14 @@ export function ConsentQuestion({
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
<div className="fb-bg-survey-bg fb-sticky -fb-bottom-2 fb-z-10 fb-w-full fb-py-2">
<button
type="button"
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();
@@ -95,28 +94,30 @@ export function ConsentQuestion({
}
}}
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>
<label className="fb-flex fb-w-full fb-cursor-pointer fb-items-center">
<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>
</button>
</div>
</div>
</ScrollableContainer>

View File

@@ -43,7 +43,7 @@ export function ContactInfoQuestion({
currentQuestionId,
autoFocusEnabled,
isBackButtonHidden,
}: ContactInfoQuestionProps) {
}: Readonly<ContactInfoQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
const formRef = useRef<HTMLFormElement>(null);
@@ -131,7 +131,7 @@ export function ContactInfoQuestion({
questionId={question.id}
/>
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
<div className="fb-mt-4 fb-w-full fb-grid fb-grid-cols-1 md:fb-grid-cols-2 fb-gap-4">
{fields.map((field, index) => {
const isFieldRequired = () => {
if (field.required) {

View File

@@ -41,7 +41,7 @@ export function CTAQuestion({
currentQuestionId,
isBackButtonHidden,
onOpenExternalURL,
}: CTAQuestionProps) {
}: Readonly<CTAQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
const isCurrent = question.id === currentQuestionId;

View File

@@ -9,8 +9,7 @@ import { getLocalizedValue } from "@/lib/i18n";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { cn } from "@/lib/utils";
import { useEffect, useMemo, useState } from "preact/hooks";
import DatePicker from "react-date-picker";
import { DatePickerProps } from "react-date-picker";
import DatePicker, { DatePickerProps } from "react-date-picker";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import "../../styles/date-picker.css";
@@ -23,11 +22,9 @@ interface DateQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
autoFocus?: boolean;
languageCode: string;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
isBackButtonHidden: boolean;
}
@@ -94,7 +91,7 @@ export function DateQuestion({
ttc,
currentQuestionId,
isBackButtonHidden,
}: DateQuestionProps) {
}: Readonly<DateQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const [errorMessage, setErrorMessage] = useState("");
const isMediaAvailable = question.imageUrl || question.videoUrl;

View File

@@ -40,7 +40,7 @@ export function MatrixQuestion({
setTtc,
currentQuestionId,
isBackButtonHidden,
}: MatrixQuestionProps) {
}: Readonly<MatrixQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);

View File

@@ -41,7 +41,7 @@ export function MultipleChoiceMultiQuestion({
autoFocusEnabled,
currentQuestionId,
isBackButtonHidden,
}: MultipleChoiceMultiProps) {
}: Readonly<MultipleChoiceMultiProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);

View File

@@ -41,7 +41,7 @@ export function MultipleChoiceSingleQuestion({
autoFocusEnabled,
currentQuestionId,
isBackButtonHidden,
}: MultipleChoiceSingleProps) {
}: Readonly<MultipleChoiceSingleProps>) {
const [startTime, setStartTime] = useState(performance.now());
const [otherSelected, setOtherSelected] = useState(false);
const otherSpecify = useRef<HTMLInputElement | null>(null);

View File

@@ -40,7 +40,7 @@ export function NPSQuestion({
setTtc,
currentQuestionId,
isBackButtonHidden,
}: NPSQuestionProps) {
}: Readonly<NPSQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const [hoveredNumber, setHoveredNumber] = useState(-1);
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -96,17 +96,15 @@ export function NPSQuestion({
<div className="fb-flex">
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
return (
<label
<button
type="button"
key={number}
tabIndex={isCurrent ? 0 : -1}
onMouseOver={() => {
setHoveredNumber(number);
}}
onMouseLeave={() => {
setHoveredNumber(-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();
@@ -120,29 +118,29 @@ export function NPSQuestion({
"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",
: "fb-h-[41px] 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)}`}
<label className="fb-w-full fb-h-full fb-flex fb-items-center fb-justify-center">
{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}
/>
) : 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>
{number}
</label>
</button>
);
})}
</div>

View File

@@ -12,20 +12,19 @@ import { type TResponseData, type TResponseTtc } from "@formbricks/types/respons
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
interface OpenTextQuestionProps {
question: TSurveyOpenTextQuestion;
value: string;
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
autoFocus?: boolean;
languageCode: string;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
isBackButtonHidden: boolean;
readonly question: TSurveyOpenTextQuestion;
readonly value: string;
readonly onChange: (responseData: TResponseData) => void;
readonly onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
readonly onBack: () => void;
readonly isFirstQuestion: boolean;
readonly isLastQuestion: boolean;
readonly languageCode: string;
readonly ttc: TResponseTtc;
readonly setTtc: (ttc: TResponseTtc) => void;
readonly autoFocusEnabled: boolean;
readonly currentQuestionId: TSurveyQuestionId;
readonly isBackButtonHidden: boolean;
}
export function OpenTextQuestion({
@@ -42,7 +41,7 @@ export function OpenTextQuestion({
autoFocusEnabled,
currentQuestionId,
isBackButtonHidden,
}: OpenTextQuestionProps) {
}: Readonly<OpenTextQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const [currentLength, setCurrentLength] = useState(value.length || 0);
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -95,7 +94,7 @@ export function OpenTextQuestion({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="fb-mt-4">
<div className="fb-mt-4 fb-text-md">
{question.longAnswer === false ? (
<input
ref={inputRef as RefObject<HTMLInputElement>}
@@ -112,11 +111,11 @@ export function OpenTextQuestion({
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"
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"
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={
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
maxLength={
question.inputType === "text"
? question.charLimit?.max
: question.inputType === "phone"
@@ -127,7 +126,7 @@ export function OpenTextQuestion({
) : (
<textarea
ref={inputRef as RefObject<HTMLTextAreaElement>}
rows={3}
rows={5}
autoFocus={isCurrent ? autoFocusEnabled : undefined}
name={question.id}
tabIndex={isCurrent ? 0 : -1}
@@ -141,10 +140,10 @@ export function OpenTextQuestion({
handleInputChange(e.currentTarget.value);
handleInputResize(e);
}}
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"
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"
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}
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
maxLength={question.inputType === "text" ? question.charLimit?.max : undefined}
/>
)}
{question.inputType === "text" && question.charLimit?.max !== undefined && (

View File

@@ -41,8 +41,15 @@ export function PictureSelectionQuestion({
setTtc,
currentQuestionId,
isBackButtonHidden,
}: PictureSelectionProps) {
}: Readonly<PictureSelectionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(() => {
const initialLoadingState: Record<string, boolean> = {};
question.choices.forEach((choice) => {
initialLoadingState[choice.id] = true;
});
return initialLoadingState;
});
const isMediaAvailable = question.imageUrl || question.videoUrl;
const isCurrent = question.id === currentQuestionId;
useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent);
@@ -115,12 +122,12 @@ export function PictureSelectionQuestion({
<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-2 fb-gap-4">
<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) => (
<label
<button
key={choice.id}
type="button"
tabIndex={isCurrent ? 0 : -1}
htmlFor={choice.id}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
if (e.key === " ") {
@@ -133,16 +140,25 @@ export function PictureSelectionQuestion({
handleChange(choice.id);
}}
className={cn(
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus:fb-outline-none fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] focus:fb-border-brand focus:fb-border-4 group/image",
"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="fb-h-full fb-w-full fb-object-cover"
className={cn(
"fb-h-full fb-w-full fb-object-cover",
loadingImages[choice.id] ? "fb-opacity-0" : ""
)}
onLoad={() => {
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
}}
/>
<a
tabIndex={-1}
@@ -198,7 +214,7 @@ export function PictureSelectionQuestion({
required={question.required && value.length ? false : question.required}
/>
)}
</label>
</button>
))}
</div>
</fieldset>

View File

@@ -46,7 +46,7 @@ export function RankingQuestion({
autoFocusEnabled,
currentQuestionId,
isBackButtonHidden,
}: RankingQuestionProps) {
}: Readonly<RankingQuestionProps>) {
const [startTime, setStartTime] = useState(performance.now());
const isCurrent = question.id === currentQuestionId;
const shuffledChoicesIds = useMemo(() => {

View File

@@ -53,7 +53,7 @@ export function RatingQuestion({
setTtc,
currentQuestionId,
isBackButtonHidden,
}: RatingQuestionProps) {
}: Readonly<RatingQuestionProps>) {
const [hoveredNumber, setHoveredNumber] = useState(0);
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -139,113 +139,44 @@ export function RatingQuestion({
<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
<div
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"
/>
</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>
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-[41px] fb-leading-10",
hoveredNumber === number ? "fb-bg-accent-bg" : ""
)}>
<input
type="radio"
id={number.toString()}
name="nps"
value={number}
checked={value === number}
onChange={() => handleSelect(number)}
onMouseOver={() => setHoveredNumber(number)}
onMouseLeave={() => setHoveredNumber(-1)}
onFocus={() => setHoveredNumber(number)}
onBlur={() => setHoveredNumber(-1)}
required={question.required}
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
tabIndex={isCurrent ? 0 : -1}
/>
<label
htmlFor={number.toString()}
className="fb-w-full fb-h-full fb-flex fb-items-center fb-justify-center">
{question.isColorCodingEnabled ? (
<div
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(question.range, number)}`}
/>
) : null}
{number}
</label>
</div>
))}
</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">