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:
Dhruwang Jariwala
2024-04-25 13:11:27 +05:30
committed by GitHub
parent 4e57807a52
commit 7fdee99974
29 changed files with 994 additions and 865 deletions
@@ -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
)}>
@@ -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}
-6
View File
@@ -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>
+1 -1
View File
@@ -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 -12
View File
@@ -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 {