mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Add Other Option to Multiple Choice Questions (#314)
* add other options to multiple choice question types --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com> Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Survey } from "@formbricks/types/surveys";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -31,11 +32,26 @@ export default function MultipleChoiceMultiForm({
|
||||
};
|
||||
|
||||
const addChoice = () => {
|
||||
const newChoices = !question.choices ? [] : question.choices;
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
newChoices.push({ id: createId(), label: "" });
|
||||
if (otherChoice) {
|
||||
newChoices.push(otherChoice);
|
||||
}
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
};
|
||||
|
||||
const addOther = () => {
|
||||
if (question.choices.filter((c) => c.id === "other").length === 0) {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
@@ -90,7 +106,8 @@ export default function MultipleChoiceMultiForm({
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
placeholder={`Option ${choiceIdx + 1}`}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
/>
|
||||
{question.choices && question.choices.length > 2 && (
|
||||
@@ -101,9 +118,19 @@ export default function MultipleChoiceMultiForm({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
<>
|
||||
<p>or</p>
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
|
||||
Add "Other" with specify
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Survey } from "@formbricks/types/surveys";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -31,11 +32,26 @@ export default function MultipleChoiceSingleForm({
|
||||
};
|
||||
|
||||
const addChoice = () => {
|
||||
const newChoices = !question.choices ? [] : question.choices;
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
newChoices.push({ id: createId(), label: "" });
|
||||
if (otherChoice) {
|
||||
newChoices.push(otherChoice);
|
||||
}
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
};
|
||||
|
||||
const addOther = () => {
|
||||
if (question.choices.filter((c) => c.id === "other").length === 0) {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
@@ -90,7 +106,8 @@ export default function MultipleChoiceSingleForm({
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
placeholder={`Option ${choiceIdx + 1}`}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
/>
|
||||
{question.choices && question.choices.length > 2 && (
|
||||
@@ -101,9 +118,19 @@ export default function MultipleChoiceSingleForm({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
<>
|
||||
<p>or</p>
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
|
||||
Add "Other" with specify
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,43 +1,93 @@
|
||||
import { MultipleChoiceMultiQuestion, MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
|
||||
import type { QuestionSummary } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui";
|
||||
import { PersonAvatar, ProgressBar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { truncate } from "@/lib/utils";
|
||||
|
||||
interface MultipleChoiceSummaryProps {
|
||||
questionSummary: QuestionSummary<MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion>;
|
||||
environmentId: string;
|
||||
surveyType: string;
|
||||
}
|
||||
|
||||
interface ChoiceResult {
|
||||
id: string;
|
||||
label: string;
|
||||
count: number;
|
||||
percentage?: number;
|
||||
otherValues?: {
|
||||
value: string;
|
||||
person: {
|
||||
id: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function MultipleChoiceSummary({ questionSummary }: MultipleChoiceSummaryProps) {
|
||||
export default function MultipleChoiceSummary({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
surveyType,
|
||||
}: MultipleChoiceSummaryProps) {
|
||||
const isSingleChoice = questionSummary.question.type === "multipleChoiceSingle";
|
||||
|
||||
const results: ChoiceResult[] = useMemo(() => {
|
||||
if (!("choices" in questionSummary.question)) return [];
|
||||
console.log(questionSummary.responses);
|
||||
// build a dictionary of choices
|
||||
const resultsDict: { [key: string]: ChoiceResult } = {};
|
||||
for (const choice of questionSummary.question.choices) {
|
||||
resultsDict[choice.label] = {
|
||||
count: 0,
|
||||
id: choice.id,
|
||||
label: choice.label,
|
||||
count: 0,
|
||||
percentage: 0,
|
||||
otherValues: [],
|
||||
};
|
||||
}
|
||||
|
||||
function findEmail(person) {
|
||||
const emailAttribute = person.attributes.find((attr) => attr.attributeClass.name === "email");
|
||||
return emailAttribute ? emailAttribute.value : null;
|
||||
}
|
||||
|
||||
const addOtherChoice = (response, value) => {
|
||||
for (const key in resultsDict) {
|
||||
if (resultsDict[key].id === "other" && value !== "") {
|
||||
const email = response.person && findEmail(response.person);
|
||||
const displayIdentifier = email || truncate(response.personId, 16);
|
||||
resultsDict[key].otherValues?.push({
|
||||
value,
|
||||
person: {
|
||||
id: response.personId,
|
||||
email: displayIdentifier,
|
||||
},
|
||||
});
|
||||
resultsDict[key].count += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// count the responses
|
||||
for (const response of questionSummary.responses) {
|
||||
// if single choice, only add responses that are in the choices
|
||||
if (isSingleChoice && response.value in resultsDict) {
|
||||
resultsDict[response.value].count += 1;
|
||||
} else if (isSingleChoice) {
|
||||
// if single choice and not in choices, add to other
|
||||
addOtherChoice(response, response.value);
|
||||
} else {
|
||||
// if multi choice add all responses
|
||||
for (const choice of response.value) {
|
||||
if (choice in resultsDict) {
|
||||
resultsDict[choice].count += 1;
|
||||
} else {
|
||||
// if multi choice and not in choices, add to other
|
||||
addOtherChoice(response, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +99,15 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
|
||||
resultsDict[key].percentage = resultsDict[key].count / total;
|
||||
}
|
||||
}
|
||||
|
||||
// sort by count and transform to array
|
||||
const results = Object.values(resultsDict).sort((a: any, b: any) => b.count - a.count);
|
||||
const results = Object.values(resultsDict).sort((a: any, b: any) => {
|
||||
if (a.id === "other") return 1; // Always put a after b if a's id is 'other'
|
||||
if (b.id === "other") return -1; // Always put b after a if b's id is 'other'
|
||||
|
||||
// If neither id is 'other', compare counts
|
||||
return b.count - a.count;
|
||||
});
|
||||
return results;
|
||||
}, [questionSummary, isSingleChoice]);
|
||||
|
||||
@@ -103,6 +160,45 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand" progress={result.percentage} />
|
||||
{result.otherValues.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6 ">Specified "Other" answers</div>
|
||||
<div className="col-span-1 pl-6 ">{surveyType === "web" && "User"}</div>
|
||||
</div>
|
||||
{result.otherValues
|
||||
.filter((otherValue) => otherValue !== "")
|
||||
.map((otherValue, idx) => (
|
||||
<div key={idx}>
|
||||
{surveyType === "link" && (
|
||||
<div
|
||||
key={idx}
|
||||
className="ph-no-capture col-span-1 m-2 flex h-10 items-center rounded-lg pl-4 text-sm font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
)}
|
||||
{surveyType === "web" && (
|
||||
<Link
|
||||
href={
|
||||
otherValue.person.id
|
||||
? `/environments/${environmentId}/people/${otherValue.person.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
key={idx}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
<div className="ph-no-capture col-span-1 pl-4 font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.person.id && <PersonAvatar personId={otherValue.person.id} />}
|
||||
<span>{otherValue.person.email}</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,8 @@ export default function SummaryList({ environmentId, surveyId }) {
|
||||
MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion
|
||||
>
|
||||
}
|
||||
environmentId={environmentId}
|
||||
surveyType={survey.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
question: MultipleChoiceMultiQuestion;
|
||||
@@ -20,16 +21,22 @@ export default function MultipleChoiceMultiQuestion({
|
||||
}: MultipleChoiceMultiProps) {
|
||||
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
|
||||
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
|
||||
const [showOther, setShowOther] = useState(false);
|
||||
const [otherSpecified, setOtherSpecified] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setIsAtLeastOneChecked(selectedChoices.length > 0);
|
||||
}, [selectedChoices]);
|
||||
setIsAtLeastOneChecked(selectedChoices.length > 0 || otherSpecified.length > 0);
|
||||
}, [selectedChoices, otherSpecified]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (otherSpecified.length > 0 && showOther) {
|
||||
selectedChoices.push(otherSpecified);
|
||||
}
|
||||
|
||||
if (question.required && selectedChoices.length <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -37,8 +44,12 @@ export default function MultipleChoiceMultiQuestion({
|
||||
const data = {
|
||||
[question.id]: selectedChoices,
|
||||
};
|
||||
|
||||
onSubmit(data);
|
||||
// console.log(data);
|
||||
setSelectedChoices([]); // reset value
|
||||
setShowOther(false);
|
||||
setOtherSpecified("");
|
||||
}}>
|
||||
<Headline headline={question.headline} questionId={question.id} />
|
||||
<Subheader subheader={question.subheader} questionId={question.id} />
|
||||
@@ -48,39 +59,64 @@ export default function MultipleChoiceMultiQuestion({
|
||||
<div className="relative space-y-2 rounded-md bg-white">
|
||||
{question.choices &&
|
||||
question.choices.map((choice) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
className={cn(
|
||||
selectedChoices.includes(choice.label)
|
||||
? "z-10 border-slate-400 bg-slate-50"
|
||||
: "border-gray-200",
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
|
||||
)}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={choice.label}
|
||||
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
checked={selectedChoices.includes(choice.label)}
|
||||
onChange={(e) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
|
||||
} else {
|
||||
setSelectedChoices(
|
||||
selectedChoices.filter((label) => label !== e.currentTarget.value)
|
||||
);
|
||||
}
|
||||
}}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
<>
|
||||
<label
|
||||
key={choice.id}
|
||||
className={cn(
|
||||
selectedChoices.includes(choice.label) || (choice.id === "other" && showOther)
|
||||
? "z-10 border-slate-400 bg-slate-50"
|
||||
: "border-gray-200",
|
||||
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
|
||||
)}>
|
||||
<span className="flex flex-col text-sm">
|
||||
<span className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={choice.label}
|
||||
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
checked={
|
||||
selectedChoices.includes(choice.label) || (choice.id === "other" && showOther)
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (choice.id === "other") {
|
||||
setShowOther(e.currentTarget.checked);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
|
||||
} else {
|
||||
setSelectedChoices(
|
||||
selectedChoices.filter((label) => label !== e.currentTarget.value)
|
||||
);
|
||||
}
|
||||
}}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
</span>
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<Input
|
||||
type="text"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
className="mt-2 bg-white"
|
||||
placeholder="Please specify"
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
required={question.required}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState } from "react";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
question: MultipleChoiceSingleQuestion;
|
||||
@@ -19,13 +21,18 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const value = otherSpecify.current?.value || e.currentTarget[question.id].value;
|
||||
const data = {
|
||||
[question.id]: e.currentTarget[question.id].value,
|
||||
[question.id]: value,
|
||||
};
|
||||
// console.log(data);
|
||||
|
||||
onSubmit(data);
|
||||
setSelectedChoice(null); // reset form
|
||||
@@ -52,10 +59,8 @@ export default function MultipleChoiceSingleQuestion({
|
||||
value={choice.label}
|
||||
className="h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
}}
|
||||
checked={selectedChoice === choice.label}
|
||||
onChange={() => setSelectedChoice(choice.id)}
|
||||
checked={selectedChoice === choice.id}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
required={question.required && idx === 0}
|
||||
/>
|
||||
@@ -63,6 +68,17 @@ export default function MultipleChoiceSingleQuestion({
|
||||
{choice.label}
|
||||
</span>
|
||||
</span>
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<Input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
placeholder="Please specify"
|
||||
className="mt-3 bg-white"
|
||||
required={question.required}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -20,16 +20,25 @@ export default function MultipleChoiceMultiQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceMultiProps) {
|
||||
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
|
||||
const [showOther, setShowOther] = useState(false);
|
||||
const [otherSpecified, setOtherSpecified] = useState("");
|
||||
|
||||
const isAtLeastOneChecked = () => {
|
||||
return selectedChoices.length > 0;
|
||||
return selectedChoices.length > 0 || otherSpecified.length > 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!isAtLeastOneChecked() && question.required) return;
|
||||
|
||||
if (otherSpecified.length > 0 && showOther) {
|
||||
selectedChoices.push(otherSpecified);
|
||||
}
|
||||
|
||||
if (question.required && selectedChoices.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
[question.id]: selectedChoices,
|
||||
@@ -37,6 +46,8 @@ export default function MultipleChoiceMultiQuestion({
|
||||
|
||||
onSubmit(data);
|
||||
setSelectedChoices([]); // reset value
|
||||
setShowOther(false);
|
||||
setOtherSpecified("");
|
||||
}}>
|
||||
<Headline headline={question.headline} questionId={question.id} />
|
||||
<Subheader subheader={question.subheader} questionId={question.id} />
|
||||
@@ -52,7 +63,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
selectedChoices.includes(choice.label)
|
||||
? "fb-z-10 fb-border-slate-400 fb-bg-slate-50"
|
||||
: "fb-border-gray-200",
|
||||
"fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-rounded-md fb-border fb-p-4 hover:fb-bg-slate-50 focus:fb-outline-none"
|
||||
"fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-space-y-3 fb-rounded-md fb-border fb-p-4 hover:fb-bg-slate-50 focus:fb-outline-none"
|
||||
)}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
@@ -63,6 +74,12 @@ export default function MultipleChoiceMultiQuestion({
|
||||
className="fb-h-4 fb-w-4 fb-border fb-border-slate-300 focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
if (choice.id === "other") {
|
||||
setShowOther(e.currentTarget.checked);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
|
||||
} else {
|
||||
@@ -71,13 +88,25 @@ export default function MultipleChoiceMultiQuestion({
|
||||
);
|
||||
}
|
||||
}}
|
||||
checked={selectedChoices.includes(choice.label)}
|
||||
checked={selectedChoices.includes(choice.label) || (choice.id === "other" && showOther)}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-font-medium">
|
||||
{choice.label}
|
||||
</span>
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<input
|
||||
type="text"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { MultipleChoiceSingleQuestion } from "../../../types/questions";
|
||||
import Headline from "./Headline";
|
||||
@@ -20,13 +20,18 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const value = otherSpecify.current?.value || e.currentTarget[question.id].value;
|
||||
const data = {
|
||||
[question.id]: e.currentTarget[question.id].value,
|
||||
[question.id]: value,
|
||||
};
|
||||
|
||||
onSubmit(data);
|
||||
setSelectedChoice(null); // reset form
|
||||
}}>
|
||||
@@ -55,9 +60,9 @@ export default function MultipleChoiceSingleQuestion({
|
||||
className="fb-h-4 fb-w-4 fb-border fb-border-slate-300 focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
setSelectedChoice(choice.id);
|
||||
}}
|
||||
checked={selectedChoice === choice.label}
|
||||
checked={selectedChoice === choice.id}
|
||||
style={{ borderColor: brandColor, color: brandColor }}
|
||||
required={question.required && idx === 0}
|
||||
/>
|
||||
@@ -65,6 +70,16 @@ export default function MultipleChoiceSingleQuestion({
|
||||
{choice.label}
|
||||
</span>
|
||||
</span>
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
required={question.required}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user