mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 19:21:15 -05:00
finish link survey tweaks
This commit is contained in:
@@ -178,7 +178,7 @@ export const authOptions: NextAuthOptions = {
|
||||
...(ENTERPRISE_LICENSE_KEY ? getSSOProviders() : []),
|
||||
],
|
||||
session: {
|
||||
maxAge: 3600,
|
||||
maxAge: 604800, // 7 days
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token }) {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user