mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 02:52:50 -05:00
fix: Max height (#2477)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
committed by
GitHub
parent
4e57807a52
commit
7fdee99974
@@ -103,11 +103,10 @@ export default function Modal({
|
||||
}, [clickOutsideClose, scalingClasses.transformOrigin]);
|
||||
|
||||
const highlightBorderColorStyle = useMemo(() => {
|
||||
if (!highlightBorderColor) return { overflow: "auto" };
|
||||
if (!highlightBorderColor) return;
|
||||
|
||||
return {
|
||||
border: `2px solid ${highlightBorderColor}`,
|
||||
overflow: "auto",
|
||||
};
|
||||
}, [highlightBorderColor]);
|
||||
|
||||
@@ -155,7 +154,7 @@ export default function Modal({
|
||||
}),
|
||||
}}
|
||||
className={cn(
|
||||
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto bg-white shadow-lg transition-all duration-500 ease-in-out ",
|
||||
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm bg-white shadow-lg transition-all duration-500 ease-in-out ",
|
||||
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",
|
||||
slidingAnimationClass
|
||||
)}>
|
||||
|
||||
+3
-3
@@ -208,7 +208,7 @@ export const PreviewSurvey = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-items-center">
|
||||
<div className="flex h-full w-full flex-col items-center justify-items-center" id="survey-preview">
|
||||
<motion.div
|
||||
variants={previewParentContainerVariant}
|
||||
className="fixed hidden h-[95%] w-5/6"
|
||||
@@ -260,13 +260,13 @@ export const PreviewSurvey = ({
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col justify-end">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && product.logo?.url && (
|
||||
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-full items-end">
|
||||
<div className="no-scrollbar z-10 w-full border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
|
||||
@@ -56,12 +56,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
|
||||
@@ -153,9 +153,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center">{children}</div>
|
||||
);
|
||||
|
||||
if (isMobilePreview) {
|
||||
@@ -171,7 +169,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
);
|
||||
} else if (isEditorView) {
|
||||
return (
|
||||
<div ref={ContentRef} className="flex flex-grow flex-col overflow-y-auto rounded-b-lg">
|
||||
<div ref={ContentRef} className="flex flex-grow flex-col rounded-b-lg">
|
||||
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
|
||||
{renderBackground()}
|
||||
<div className="flex h-full w-full items-center justify-center">{children}</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ test.describe("JS Package Test", async () => {
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByLabel("").fill("Much higher response rates!");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByLabel("Please be as specific as").fill("Make this end to end test pass!");
|
||||
await page.getByLabel("How can we improve My Product").fill("Make this end to end test pass!");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
await page.getByText("Thank you!").click();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const FormbricksBranding = () => {
|
||||
href="https://formbricks.com?utm_source=survey_branding"
|
||||
target="_blank"
|
||||
tabIndex={-1}
|
||||
className="mb-5 mt-2 flex justify-center">
|
||||
className="my-2 flex justify-center">
|
||||
<p className="text-signature text-xs">
|
||||
Powered by{" "}
|
||||
<b>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function HtmlBody({ htmlString, questionId }: HtmlBodyProps) {
|
||||
return (
|
||||
<label
|
||||
htmlFor={questionId}
|
||||
className="fb-htmlbody" // styles are in global.css
|
||||
className="fb-htmlbody break-words" // styles are in global.css
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml }}></label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function Subheader({ subheader, questionId }: { subheader?: string; questionId: string }) {
|
||||
return (
|
||||
<label htmlFor={questionId} className="text-subheading block text-sm font-normal leading-5">
|
||||
<p htmlFor={questionId} className="text-subheading block break-words text-sm font-normal leading-5">
|
||||
{subheader}
|
||||
</label>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ export const Survey = ({
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [responseData, setResponseData] = useState<TResponseData>({});
|
||||
const [ttc, setTtc] = useState<TResponseTtc>({});
|
||||
|
||||
const currentQuestionIndex = survey.questions.findIndex((q) => q.id === questionId);
|
||||
const currentQuestion = useMemo(() => {
|
||||
if (questionId === "end" && !survey.thankYouCard.enabled) {
|
||||
@@ -333,7 +332,7 @@ export const Survey = ({
|
||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
||||
<div
|
||||
className={cn(
|
||||
"no-scrollbar md:rounded-custom bg-survey-bg rounded-t-custom flex h-full w-full flex-col justify-between rounded-b-none px-6 py-6 md:pb-3 md:pt-6",
|
||||
"no-scrollbar md:rounded-custom bg-survey-bg rounded-t-custom flex h-full w-full flex-col justify-between",
|
||||
isCardBorderVisible ? "border-survey-border border" : "",
|
||||
survey.type === "link" ? "fb-survey-shadow" : ""
|
||||
)}>
|
||||
@@ -345,7 +344,7 @@ export const Survey = ({
|
||||
getCardContent()
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mx-6 mb-6 mt-2 space-y-3 md:mt-6">
|
||||
{isBrandingEnabled && <FormbricksBranding />}
|
||||
{showProgressBar && <ProgressBar survey={survey} questionId={questionId} />}
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SurveyInline(props: SurveyInlineProps) {
|
||||
<div id="fbjs" className="formbricks-form h-full w-full">
|
||||
{isMobile ? (
|
||||
<div className="flex h-screen w-full flex-col justify-end overflow-hidden">
|
||||
<div className="max-h-[90vh] overflow-auto">
|
||||
<div className="overflow-auto pt-[10vh]">
|
||||
<Survey {...props} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Button from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { LoadingSpinner } from "@/components/general/LoadingSpinner";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import RedirectCountDown from "@/components/general/RedirectCountdown";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TI18nString } from "@formbricks/types/surveys";
|
||||
@@ -37,31 +37,33 @@ export const ThankYouCard = ({
|
||||
isResponseSendingFinished,
|
||||
isInIframe,
|
||||
}: ThankYouCardProps) => {
|
||||
const media = imageUrl || videoUrl ? <QuestionMedia imgUrl={imageUrl} videoUrl={videoUrl} /> : null;
|
||||
const checkmark = (
|
||||
<div className="text-brand flex flex-col items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-24 w-24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="bg-brand mb-[10px] inline-block h-1 w-16 rounded-[100%]"></span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
{isResponseSendingFinished ? (
|
||||
<>
|
||||
{media || checkmark}
|
||||
<ScrollableContainer>
|
||||
<div className="text-center">
|
||||
{imageUrl || videoUrl ? (
|
||||
<QuestionMedia imgUrl={imageUrl} videoUrl={videoUrl} />
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-brand flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-24 w-24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="bg-brand mb-[10px] inline-block h-1 w-16 rounded-[100%]"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Headline
|
||||
alignTextCenter={true}
|
||||
headline={replaceRecallInfo(getLocalizedValue(headline, languageCode))}
|
||||
@@ -72,7 +74,7 @@ export const ThankYouCard = ({
|
||||
questionId="thankYouCard"
|
||||
/>
|
||||
<RedirectCountDown redirectUrl={redirectUrl} isRedirectDisabled={isRedirectDisabled} />
|
||||
{buttonLabel && (
|
||||
{buttonLabel && isResponseSendingFinished && (
|
||||
<div className="mt-6 flex w-full flex-col items-center justify-center space-y-4">
|
||||
<Button
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
@@ -83,18 +85,11 @@ export const ThankYouCard = ({
|
||||
window.location.replace(buttonLink);
|
||||
}}
|
||||
/>
|
||||
<p className="text-subheading text-xs">Press Enter ↵</p>
|
||||
<p class="text-subheading text-xs">Press Enter ↵</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="my-3">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
<h1 className="text-brand">Sending responses...</h1>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
@@ -101,15 +102,19 @@ export const WelcomeCard = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fileUrl && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={fileUrl} className="mb-8 max-h-96 w-1/3 rounded-lg object-contain" alt="Company Logo" />
|
||||
)}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{fileUrl && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={fileUrl} className="mb-8 max-h-96 w-1/3 rounded-lg object-contain" alt="Company Logo" />
|
||||
)}
|
||||
|
||||
<Headline headline={getLocalizedValue(headline, languageCode)} questionId="welcomeCard" />
|
||||
<HtmlBody htmlString={getLocalizedValue(html, languageCode)} questionId="welcomeCard" />
|
||||
<Headline headline={getLocalizedValue(headline, languageCode)} questionId="welcomeCard" />
|
||||
<HtmlBody htmlString={getLocalizedValue(html, languageCode)} questionId="welcomeCard" />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="mt-10 flex w-full justify-between">
|
||||
<div className="mx-6 mt-4 flex w-full justify-between">
|
||||
<div className="flex w-full justify-start gap-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -129,35 +130,39 @@ export const AddressQuestion = ({
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="w-full" ref={formRef}>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4 space-y-2">
|
||||
{inputConfig.map(({ name, placeholder, required }, index) => (
|
||||
<input
|
||||
ref={index === 0 ? addressTextRef : null}
|
||||
key={index}
|
||||
name={name}
|
||||
autoComplete={name}
|
||||
id={`${question.id}-${index}`}
|
||||
placeholder={placeholder}
|
||||
tabIndex={index + 1}
|
||||
required={required}
|
||||
value={safeValue[index] || ""}
|
||||
onInput={(e) => handleInputChange(e.currentTarget.value, index)}
|
||||
autoFocus={!isInIframe && index === 0}
|
||||
className="border-border placeholder:text-placeholder text-subheading focus:border-border-highlight bg-input-bg rounded-custom block w-full border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="mt-4 space-y-2">
|
||||
{inputConfig.map(({ name, placeholder, required }, index) => (
|
||||
<input
|
||||
ref={index === 0 ? addressTextRef : null}
|
||||
key={index}
|
||||
name={name}
|
||||
autoComplete={name}
|
||||
id={`${question.id}-${index}`}
|
||||
placeholder={placeholder}
|
||||
tabIndex={index + 1}
|
||||
required={required}
|
||||
value={safeValue[index] || ""}
|
||||
onInput={(e) => handleInputChange(e.currentTarget.value, index)}
|
||||
autoFocus={!isInIframe && index === 0}
|
||||
className="border-border placeholder:text-placeholder text-subheading focus:border-border-highlight bg-input-bg rounded-custom block w-full border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={8}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import HtmlBody from "@/components/general/HtmlBody";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -44,14 +45,18 @@ export const CTAQuestion = ({
|
||||
|
||||
return (
|
||||
<div key={question.id}>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode)} questionId={question.id} />
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode)} questionId={question.id} />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import CalEmbed from "@/components/general/CalEmbed";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
|
||||
@@ -67,22 +68,25 @@ export const CalQuestion = ({
|
||||
onSubmit({ [question.id]: value }, updatedttc);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<>
|
||||
{errorMessage && <span className="text-red-500">{errorMessage}</span>}
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
</>
|
||||
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<>
|
||||
{errorMessage && <span className="text-red-500">{errorMessage}</span>}
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
</>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import HtmlBody from "@/components/general/HtmlBody";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
@@ -42,76 +43,82 @@ export const ConsentQuestion = ({
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
|
||||
return (
|
||||
<div key={question.id}>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode) || ""} questionId={question.id} />
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}>
|
||||
<label
|
||||
tabIndex={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="border-border bg-input-bg text-heading hover:bg-input-bg-selected focus:bg-input-bg-selected focus:ring-brand rounded-custom relative z-10 mt-4 flex w-full cursor-pointer items-center border p-4 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
<input
|
||||
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]: "dismissed" });
|
||||
}
|
||||
}}
|
||||
checked={value === "accepted"}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
<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} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={3}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div />
|
||||
<SubmitButton
|
||||
tabIndex={2}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
<HtmlBody
|
||||
htmlString={getLocalizedValue(question.html, languageCode) || ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
|
||||
<label
|
||||
tabIndex={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="border-border bg-input-bg text-heading hover:bg-input-bg-selected focus:bg-input-bg-selected focus:ring-brand rounded-custom relative z-10 mt-4 flex w-full cursor-pointer items-center border p-4 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
<input
|
||||
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]: "dismissed" });
|
||||
}
|
||||
}}
|
||||
checked={value === "accepted"}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={3}
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div />
|
||||
<SubmitButton
|
||||
tabIndex={2}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
@@ -95,29 +96,35 @@ export const DateQuestion = ({
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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={"text-red-600"}>
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
<div className={cn("my-4", errorMessage && "rounded-lg border-2 border-red-500")} id="date-picker-root">
|
||||
{loading && (
|
||||
<div className="bg-survey-bg border-border text-placeholder relative flex h-12 w-full cursor-pointer appearance-none items-center justify-center rounded-lg border text-left text-base font-normal focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
||||
<span
|
||||
className="h-6 w-6 animate-spin rounded-full border-b-2 border-neutral-900"
|
||||
style={{ borderTopColor: "transparent" }}></span>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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={"text-red-600"}>
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<div
|
||||
className={cn("mt-4", errorMessage && "rounded-lg border-2 border-red-500")}
|
||||
id="date-picker-root">
|
||||
{loading && (
|
||||
<div className="bg-survey-bg border-border text-placeholder relative flex h-12 w-full cursor-pointer appearance-none items-center justify-center rounded-lg border text-left text-base font-normal focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
||||
<span
|
||||
className="h-6 w-6 animate-spin rounded-full border-b-2 border-neutral-900"
|
||||
style={{ borderTopColor: "transparent" }}></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
<div>
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
@@ -70,34 +71,38 @@ export const FileUploadQuestion = ({
|
||||
}
|
||||
}}
|
||||
className="w-full ">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<FileInput
|
||||
surveyId={surveyId}
|
||||
onFileUpload={onFileUpload}
|
||||
onUploadCallback={(urls: string[]) => {
|
||||
if (urls) {
|
||||
onChange({ [question.id]: urls });
|
||||
} else {
|
||||
onChange({ [question.id]: "skipped" });
|
||||
}
|
||||
}}
|
||||
fileUrls={value as string[]}
|
||||
allowMultipleFiles={question.allowMultipleFiles}
|
||||
{...(!!question.allowedFileExtensions
|
||||
? { allowedFileExtensions: question.allowedFileExtensions }
|
||||
: {})}
|
||||
{...(!!question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
/>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<FileInput
|
||||
surveyId={surveyId}
|
||||
onFileUpload={onFileUpload}
|
||||
onUploadCallback={(urls: string[]) => {
|
||||
if (urls) {
|
||||
onChange({ [question.id]: urls });
|
||||
} else {
|
||||
onChange({ [question.id]: "skipped" });
|
||||
}
|
||||
}}
|
||||
fileUrls={value as string[]}
|
||||
allowMultipleFiles={question.allowMultipleFiles}
|
||||
{...(!!question.allowedFileExtensions
|
||||
? { allowedFileExtensions: question.allowedFileExtensions }
|
||||
: {})}
|
||||
{...(!!question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { JSX } from "preact";
|
||||
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||
@@ -91,64 +92,69 @@ export const MatrixQuestion = ({
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader subheader={getLocalizedValue(question.subheader, languageCode)} questionId={question.id} />
|
||||
<div className="mt-4 max-h-[33vh] overflow-auto">
|
||||
<div className="min-w-full table-auto overflow-auto">
|
||||
<table className="min-w-full table-auto border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2"></th>
|
||||
{columnsHeaders}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{question.rows.map((row, rowIndex) => (
|
||||
// Table rows
|
||||
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
|
||||
<td className="text-heading rounded-l-custom max-w-40 break-words px-4 py-2">
|
||||
{getLocalizedValue(row, languageCode)}
|
||||
</td>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={columnIndex}
|
||||
className={`px-4 py-2 text-gray-800 ${columnIndex === question.columns.length - 1 ? "rounded-r-custom" : ""}`}
|
||||
onClick={() =>
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center p-2">
|
||||
{/* radio input */}
|
||||
<input
|
||||
type="radio"
|
||||
id={`${row}-${column}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
checked={
|
||||
typeof value === "object" && !Array.isArray(value)
|
||||
? value[getLocalizedValue(row, languageCode)] ===
|
||||
getLocalizedValue(column, languageCode)
|
||||
: false
|
||||
}
|
||||
className="border-brand text-brand h-5 w-5 border focus:ring-0 focus:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={getLocalizedValue(question.subheader, languageCode)}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="no-scrollbar min-w-full table-auto border-collapse text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2"></th>
|
||||
{columnsHeaders}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{question.rows.map((row, rowIndex) => (
|
||||
// Table rows
|
||||
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
|
||||
<td className="text-heading rounded-l-custom max-w-40 break-words px-4 py-2">
|
||||
{getLocalizedValue(row, languageCode)}
|
||||
</td>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={columnIndex}
|
||||
className={`px-4 py-2 text-gray-800 ${columnIndex === question.columns.length - 1 ? "rounded-r-custom" : ""}`}
|
||||
onClick={() =>
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center p-2">
|
||||
{/* radio input */}
|
||||
<input
|
||||
type="radio"
|
||||
id={`${row}-${column}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
checked={
|
||||
typeof value === "object" && !Array.isArray(value)
|
||||
? value[getLocalizedValue(row, languageCode)] ===
|
||||
getLocalizedValue(column, languageCode)
|
||||
: false
|
||||
}
|
||||
className="border-brand text-brand h-5 w-5 border focus:ring-0 focus:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, shuffleQuestions } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
@@ -134,134 +135,140 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div
|
||||
className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto py-0.5 pr-2"
|
||||
ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
className={cn(
|
||||
value === choice.label ? "border-border bg-input-selected-bg z-10" : "border-border",
|
||||
"text-heading bg-input-bg focus-within:border-brand hover:bg-input-bg-selected focus:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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 && !isInIframe}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
tabIndex={-1}
|
||||
value={choice.label}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus: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));
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="bg-survey-bg relative space-y-2" ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
className={cn(
|
||||
value === choice.label ? "border-border bg-input-selected-bg z-10" : "border-border",
|
||||
"text-heading bg-input-bg focus-within:border-brand hover:bg-input-bg-selected focus:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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();
|
||||
}
|
||||
}}
|
||||
checked={
|
||||
Array.isArray(value) && value.includes(getLocalizedValue(choice.label, languageCode))
|
||||
}
|
||||
required={
|
||||
question.required && Array.isArray(value) && value.length ? false : question.required
|
||||
}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{otherOption && (
|
||||
<label
|
||||
tabIndex={questionChoices.length + 1}
|
||||
className={cn(
|
||||
value.includes(getLocalizedValue(otherOption.label, languageCode))
|
||||
? "border-border bg-input-selected-bg z-10"
|
||||
: "border-border",
|
||||
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={(e) => {
|
||||
setOtherSelected(!otherSelected);
|
||||
if ((e.target as HTMLInputElement)?.checked) {
|
||||
if (!otherValue) return;
|
||||
addItem(otherValue);
|
||||
} else {
|
||||
removeItem(otherValue);
|
||||
}
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
name={question.id}
|
||||
autoFocus={idx === 0 && !isInIframe}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
tabIndex={-1}
|
||||
value={choice.label}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus: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={
|
||||
question.required && Array.isArray(value) && value.length
|
||||
? false
|
||||
: question.required
|
||||
}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{otherOption && (
|
||||
<label
|
||||
tabIndex={questionChoices.length + 1}
|
||||
value={otherValue}
|
||||
onChange={(e) => {
|
||||
setOtherValue(e.currentTarget.value);
|
||||
addItem(e.currentTarget.value);
|
||||
}}
|
||||
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
className={cn(
|
||||
value.includes(getLocalizedValue(otherOption.label, languageCode))
|
||||
? "border-border bg-input-selected-bg z-10"
|
||||
: "border-border",
|
||||
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={(e) => {
|
||||
setOtherSelected(!otherSelected);
|
||||
if ((e.target as HTMLInputElement)?.checked) {
|
||||
if (!otherValue) return;
|
||||
addItem(otherValue);
|
||||
} else {
|
||||
removeItem(otherValue);
|
||||
}
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
name={question.id}
|
||||
tabIndex={questionChoices.length + 1}
|
||||
value={otherValue}
|
||||
onChange={(e) => {
|
||||
setOtherValue(e.currentTarget.value);
|
||||
addItem(e.currentTarget.value);
|
||||
}}
|
||||
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={questionChoices.length + 3}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, shuffleQuestions } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
@@ -96,123 +97,124 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
|
||||
<div
|
||||
className="bg-survey-bg relative max-h-[27vh] space-y-2 overflow-y-auto py-0.5 pr-2"
|
||||
role="radiogroup"
|
||||
ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
tabIndex={idx + 1}
|
||||
key={choice.id}
|
||||
className={cn(
|
||||
value === choice.label ? "border-brand z-10" : "border-border",
|
||||
"text-heading bg-input-bg focus-within:border-brand focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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 && !isInIframe}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="radio"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={choice.label}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(false);
|
||||
onChange({ [question.id]: getLocalizedValue(choice.label, languageCode) });
|
||||
<div className="bg-survey-bg relative space-y-2" role="radiogroup" ref={choicesContainerRef}>
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
tabIndex={idx + 1}
|
||||
key={choice.id}
|
||||
className={cn(
|
||||
value === choice.label ? "border-brand z-10" : "border-border",
|
||||
"text-heading bg-input-bg focus-within:border-brand focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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();
|
||||
}
|
||||
}}
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required && idx === 0}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{otherOption && (
|
||||
<label
|
||||
tabIndex={questionChoices.length + 1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
? "border-border bg-input-bg-selected z-10"
|
||||
: "border-border",
|
||||
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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="flex items-center text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
tabIndex={-1}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
autoFocus={idx === 0 && !isInIframe}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="radio"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={choice.label}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus: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}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{otherOption && (
|
||||
<label
|
||||
tabIndex={questionChoices.length + 1}
|
||||
id={`${otherOption.id}-label`}
|
||||
name={question.id}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
? "border-border bg-input-bg-selected z-10"
|
||||
: "border-border",
|
||||
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus: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="flex items-center text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
tabIndex={-1}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
onChange={() => {
|
||||
setOtherSelected(!otherSelected);
|
||||
onChange({ [question.id]: "" });
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
tabIndex={questionChoices.length + 1}
|
||||
id={`${otherOption.id}-label`}
|
||||
name={question.id}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "preact/hooks";
|
||||
@@ -52,74 +53,82 @@ export const NPSQuestion = ({
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="my-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
||||
return (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={idx + 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
|
||||
? "border-border-highlight bg-accent-selected-bg z-10 border"
|
||||
: "border-border",
|
||||
"text-heading first:rounded-l-custom last:rounded-r-custom focus:border-brand relative h-10 flex-1 cursor-pointer border-b border-l border-t text-center text-sm leading-10 last:border-r focus:border-2 focus:outline-none",
|
||||
hoveredNumber === number ? "bg-accent-bg" : ""
|
||||
)}>
|
||||
<input
|
||||
type="radio"
|
||||
id={number.toString()}
|
||||
name="nps"
|
||||
value={number}
|
||||
checked={value === number}
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit(
|
||||
{
|
||||
[question.id]: number,
|
||||
},
|
||||
updatedTtcObj
|
||||
);
|
||||
onChange({ [question.id]: number });
|
||||
}}
|
||||
required={question.required}
|
||||
/>
|
||||
{number}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="my-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
||||
return (
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={idx + 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
|
||||
? "border-border-highlight bg-accent-selected-bg z-10 border"
|
||||
: "border-border",
|
||||
"text-heading first:rounded-l-custom last:rounded-r-custom focus:border-brand relative h-10 flex-1 cursor-pointer border-b border-l border-t text-center text-sm leading-10 last:border-r focus:border-2 focus:outline-none",
|
||||
hoveredNumber === number ? "bg-accent-bg" : ""
|
||||
)}>
|
||||
<input
|
||||
type="radio"
|
||||
id={number.toString()}
|
||||
name="nps"
|
||||
value={number}
|
||||
checked={value === number}
|
||||
className="absolute left-0 h-full w-full cursor-pointer opacity-0"
|
||||
onClick={() => {
|
||||
const updatedTtcObj = getUpdatedTtc(
|
||||
ttc,
|
||||
question.id,
|
||||
performance.now() - startTime
|
||||
);
|
||||
setTtc(updatedTtcObj);
|
||||
onSubmit(
|
||||
{
|
||||
[question.id]: number,
|
||||
},
|
||||
updatedTtcObj
|
||||
);
|
||||
onChange({ [question.id]: number });
|
||||
}}
|
||||
required={question.required}
|
||||
/>
|
||||
{number}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="text-subheading mt-2 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p>{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p>{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="text-subheading mt-2 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p>{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p>{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={isLastQuestion ? 12 : 13}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useCallback } from "react";
|
||||
@@ -42,7 +43,6 @@ export const OpenTextQuestion = ({
|
||||
}: OpenTextQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
|
||||
const handleInputChange = (inputValue: string) => {
|
||||
@@ -77,57 +77,61 @@ export const OpenTextQuestion = ({
|
||||
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={openTextRef}
|
||||
tabIndex={1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
step={"any"}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
required={question.required}
|
||||
value={value ? (value as string) : ""}
|
||||
type={question.inputType}
|
||||
onInput={(e) => handleInputChange(e.currentTarget.value)}
|
||||
autoFocus={!isInIframe}
|
||||
className="border-border placeholder:text-placeholder text-subheading focus:border-border-highlight bg-input-bg rounded-custom block w-full border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
ref={openTextRef}
|
||||
rows={3}
|
||||
name={question.id}
|
||||
tabIndex={1}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
required={question.required}
|
||||
value={value as string}
|
||||
type={question.inputType}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
handleInputResize(e);
|
||||
}}
|
||||
autoFocus={!isInIframe}
|
||||
className="border-border placeholder:text-placeholder bg-input-bg text-subheading focus:border-border-highlight rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
<Subheader
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
<div className="mt-4">
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={openTextRef}
|
||||
tabIndex={1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
step={"any"}
|
||||
required={question.required}
|
||||
value={value ? (value as string) : ""}
|
||||
type={question.inputType}
|
||||
onInput={(e) => handleInputChange(e.currentTarget.value)}
|
||||
autoFocus={!isInIframe}
|
||||
className="border-border placeholder:text-placeholder text-subheading focus:border-border-highlight bg-input-bg rounded-custom block w-full border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
ref={openTextRef}
|
||||
rows={3}
|
||||
name={question.id}
|
||||
tabIndex={1}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
required={question.required}
|
||||
value={value as string}
|
||||
type={question.inputType}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
handleInputResize(e);
|
||||
}}
|
||||
autoFocus={!isInIframe}
|
||||
className="border-border placeholder:text-placeholder bg-input-bg text-subheading focus:border-border-highlight rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
@@ -93,105 +94,109 @@ export const PictureSelectionQuestion = ({
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="rounded-m bg-survey-bg relative grid max-h-[33vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
htmlFor={choice.id}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
onClick={() => handleChange(choice.id)}
|
||||
className={cn(
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? `border-brand text-brand z-10 border-4 shadow-xl`
|
||||
: "",
|
||||
"focus:border-brand group/image relative inline-block h-28 w-full cursor-pointer overflow-hidden rounded-xl border focus:border-4 focus:outline-none"
|
||||
)}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={choice.imageUrl.split("/").pop()}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={choice.imageUrl}
|
||||
target="_blank"
|
||||
title="Open in new tab"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute bottom-2 right-2 flex items-center gap-2 whitespace-nowrap rounded-md bg-gray-800 bg-opacity-40 p-1.5 text-white opacity-0 backdrop-blur-lg transition duration-300 ease-in-out hover:bg-opacity-65 group-hover/image:opacity-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="rounded-m bg-survey-bg relative grid max-h-[33vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={idx + 1}
|
||||
htmlFor={choice.id}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
onClick={() => handleChange(choice.id)}
|
||||
className={cn(
|
||||
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded border",
|
||||
value.includes(choice.id) ? "border-brand text-brand" : ""
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? `border-brand text-brand z-10 border-4 shadow-xl`
|
||||
: "",
|
||||
"focus:border-brand group/image relative inline-block h-28 w-full cursor-pointer overflow-hidden rounded-xl border focus:border-4 focus:outline-none"
|
||||
)}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={choice.imageUrl.split("/").pop()}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={choice.imageUrl}
|
||||
target="_blank"
|
||||
title="Open in new tab"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute bottom-2 right-2 flex items-center gap-2 whitespace-nowrap rounded-md bg-gray-800 bg-opacity-40 p-1.5 text-white opacity-0 backdrop-blur-lg transition duration-300 ease-in-out hover:bg-opacity-65 group-hover/image:opacity-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded border",
|
||||
value.includes(choice.id) ? "border-brand text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${choice.id}-radio`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded-full border",
|
||||
value.includes(choice.id) ? "border-brand text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${choice.id}-radio`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded-full border",
|
||||
value.includes(choice.id) ? "border-brand text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={questionChoices.length + 3}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/BackButton";
|
||||
import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
@@ -95,118 +96,122 @@ export const RatingQuestion = ({
|
||||
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
|
||||
}}
|
||||
className="w-full">
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mb-4 mt-6 flex items-center justify-center">
|
||||
<fieldset className="w-full">
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className="flex w-full">
|
||||
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
|
||||
<span
|
||||
key={number}
|
||||
onMouseOver={() => setHoveredNumber(number)}
|
||||
onMouseLeave={() => setHoveredNumber(0)}
|
||||
className="bg-survey-bg flex-1 text-center text-sm">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
tabIndex={i + 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
|
||||
? "bg-accent-selected-bg border-border-highlight z-10 border"
|
||||
: "border-border",
|
||||
a.length === number ? "rounded-r-custom border-r" : "",
|
||||
number === 1 ? "rounded-l-custom" : "",
|
||||
hoveredNumber === number ? "bg-accent-bg " : "",
|
||||
"text-heading focus:border-brand relative flex min-h-[41px] w-full cursor-pointer items-center justify-center border-b border-l border-t focus:border-2 focus:outline-none"
|
||||
)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
{number}
|
||||
</label>
|
||||
) : question.scale === "star" ? (
|
||||
<label
|
||||
tabIndex={i + 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 as number)
|
||||
? "text-amber-400"
|
||||
: "text-input-bg-selected",
|
||||
hoveredNumber === number ? "text-amber-400 " : "",
|
||||
"relative flex max-h-16 min-h-9 cursor-pointer justify-center focus:outline-none"
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
<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="mb-4 mt-6 flex items-center justify-center">
|
||||
<fieldset className="w-full">
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className="flex w-full">
|
||||
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
|
||||
<span
|
||||
key={number}
|
||||
onMouseOver={() => setHoveredNumber(number)}
|
||||
onMouseLeave={() => setHoveredNumber(0)}
|
||||
className="bg-survey-bg flex-1 text-center text-sm">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
tabIndex={i + 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
|
||||
? "bg-accent-selected-bg border-border-highlight z-10 border"
|
||||
: "border-border",
|
||||
a.length === number ? "rounded-r-custom border-r" : "",
|
||||
number === 1 ? "rounded-l-custom" : "",
|
||||
hoveredNumber === number ? "bg-accent-bg " : "",
|
||||
"text-heading focus:border-brand relative flex min-h-[41px] w-full cursor-pointer items-center justify-center border-b border-l border-t focus:border-2 focus:outline-none"
|
||||
)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
{number}
|
||||
</label>
|
||||
) : question.scale === "star" ? (
|
||||
<label
|
||||
tabIndex={i + 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 as number)
|
||||
? "text-amber-400"
|
||||
: "text-input-bg-selected",
|
||||
hoveredNumber === number ? "text-amber-400 " : "",
|
||||
"relative flex max-h-16 min-h-9 cursor-pointer justify-center focus:outline-none"
|
||||
)}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="h-full w-full max-w-[74px] 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
|
||||
className={cn(
|
||||
"relative flex max-h-16 min-h-9 w-full cursor-pointer justify-center",
|
||||
value === number || hoveredNumber === number
|
||||
? "stroke-rating-selected text-rating-selected"
|
||||
: "stroke-heading text-heading focus:border-accent-bg focus:border-2 focus:outline-none"
|
||||
)}
|
||||
tabIndex={i + 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();
|
||||
}
|
||||
}}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="h-full w-full max-w-[74px] object-contain">
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
idx={i}
|
||||
range={question.range}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="h-full w-full max-w-[74px] 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
|
||||
className={cn(
|
||||
"relative flex max-h-16 min-h-9 w-full cursor-pointer justify-center",
|
||||
value === number || hoveredNumber === number
|
||||
? "stroke-rating-selected text-rating-selected"
|
||||
: "stroke-heading text-heading focus:border-accent-bg focus:border-2 focus:outline-none"
|
||||
)}
|
||||
tabIndex={i + 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();
|
||||
}
|
||||
}}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="h-full w-full max-w-[74px] object-contain">
|
||||
<RatingSmiley
|
||||
active={value === number || hoveredNumber === number}
|
||||
idx={i}
|
||||
range={question.range}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-subheading mt-4 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p className="w-1/2 text-left">{getLocalizedValue(question.lowerLabel, "default")}</p>
|
||||
<p className="w-1/2 text-right">{getLocalizedValue(question.upperLabel, "default")}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="text-subheading mt-4 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p className="w-1/2 text-left">{getLocalizedValue(question.lowerLabel, "default")}</p>
|
||||
<p className="w-1/2 text-right">{getLocalizedValue(question.upperLabel, "default")}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="flex w-full justify-between px-6 py-4">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={!question.required || value ? question.range + 2 : question.range + 1}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
interface ScrollableContainerProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
const [isOverflowHidden, setIsOverflowHidden] = useState(true);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
const [isAtTop, setIsAtTop] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isSurveyPreview = !!document.getElementById("survey-preview");
|
||||
|
||||
const checkScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||
|
||||
setIsAtBottom(Math.round(scrollTop) + clientHeight >= scrollHeight);
|
||||
|
||||
setIsAtTop(scrollTop === 0);
|
||||
};
|
||||
|
||||
const toggleOverflow = (hide: boolean) => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
if (hide) {
|
||||
timeoutRef.current = setTimeout(() => setIsOverflowHidden(true), 1500);
|
||||
} else {
|
||||
setIsOverflowHidden(false);
|
||||
checkScroll();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const handleScroll = () => checkScroll();
|
||||
element.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkScroll();
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="relative pt-6">
|
||||
{!isAtTop && (
|
||||
<div className="from-survey-bg absolute left-0 right-2 top-6 z-10 h-4 bg-gradient-to-b to-transparent"></div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
scrollbarGutter: "stable",
|
||||
maxHeight: isSurveyPreview ? "40vh" : "60vh",
|
||||
}}
|
||||
className={`overflow-${isOverflowHidden ? "hidden" : "auto"} pb-1 pl-6 pr-4`}
|
||||
onMouseEnter={() => toggleOverflow(false)}
|
||||
onTouchStart={() => toggleOverflow(false)}
|
||||
onTouchEnd={() => toggleOverflow(true)}
|
||||
onMouseLeave={() => toggleOverflow(true)}>
|
||||
{children}
|
||||
</div>
|
||||
{!isAtBottom && (
|
||||
<div className="from-survey-bg absolute -bottom-2 left-0 right-2 h-8 bg-gradient-to-t to-transparent"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -122,7 +122,7 @@ export default function Question({ defaultDate, format }: { defaultDate?: Date;
|
||||
className={`dp-input-root rounded-custom ${!datePickerOpen ? "wrapper-hide" : ""}
|
||||
${hideInvalid ? "hide-invalid" : ""}
|
||||
`}
|
||||
calendarClassName="calendar-root w-80 rounded-lg border border-[#e5e7eb] p-3 shadow-md"
|
||||
calendarClassName="calendar-root w-80 rounded-lg border border-[#e5e7eb] p-3 shadow-md h-40 overflow-auto"
|
||||
clearIcon={null}
|
||||
onCalendarOpen={() => {
|
||||
setDatePickerOpen(true);
|
||||
|
||||
@@ -11,7 +11,6 @@ declare global {
|
||||
selectedDate: Date;
|
||||
}
|
||||
}
|
||||
|
||||
const addStylesToDom = () => {
|
||||
if (document.getElementById("formbricks__question_date_css") === null) {
|
||||
const styleElement = document.createElement("style");
|
||||
|
||||
@@ -8,29 +8,24 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
#fbjs * {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
#fbjs *::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
border-radius: 99px;
|
||||
width: 8px ;
|
||||
background: transparent ;
|
||||
}
|
||||
|
||||
#fbjs *::-webkit-scrollbar-track {
|
||||
background: #e2e8f0;
|
||||
border-radius: 99px;
|
||||
background: transparent
|
||||
}
|
||||
|
||||
#fbjs *::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border: 3px solid #cbd5e1;
|
||||
border-radius: 99px;
|
||||
background-color: var(--fb-brand-color) ;
|
||||
border: none;
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
|
||||
/* this is for styling the HtmlBody component */
|
||||
.fb-htmlbody {
|
||||
@apply block text-sm font-normal leading-6;
|
||||
@@ -40,6 +35,7 @@
|
||||
/* without this, it wont override the color */
|
||||
p.fb-editor-paragraph {
|
||||
color: var(--fb-subheading-color) !important;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.fb-survey-shadow {
|
||||
|
||||
Reference in New Issue
Block a user