Compare commits

...

7 Commits

Author SHA1 Message Date
Piyush Gupta
57e7485564 fix: reliability issues (#5781) 2025-05-13 16:40:16 +00:00
Matti Nannt
42a38a6f47 chore: fix environment surveys filter not working properly (#5793) 2025-05-13 13:34:27 +02:00
Dhruwang Jariwala
34bb9c2127 fix: multi select question (#5792)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-05-13 13:12:22 +02:00
Dhruwang Jariwala
6442b5e4aa fix: Vietnamese char interpretation (#5747) 2025-05-13 11:54:30 +02:00
Piyush Gupta
dde5a55446 fix: CTA and consent question breaking the survey editor (#5745) 2025-05-13 04:18:34 +00:00
Piyush Gupta
13e615a798 fix: duplicate switch cases (#5752)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-05-12 15:00:55 +00:00
victorvhs017
9c81961b0b chore: remove redundant code (#5751) 2025-05-12 12:23:05 +00:00
14 changed files with 221 additions and 141 deletions

View File

@@ -109,7 +109,7 @@ export const MainNavigation = ({
useEffect(() => {
const toggleTextOpacity = () => {
setIsTextVisible(isCollapsed ? true : false);
setIsTextVisible(isCollapsed);
};
const timeoutId = setTimeout(toggleTextOpacity, 150);
return () => clearTimeout(timeoutId);
@@ -170,7 +170,7 @@ export const MainNavigation = ({
name: t("common.actions"),
href: `/environments/${environment.id}/actions`,
icon: MousePointerClick,
isActive: pathname?.includes("/actions") || pathname?.includes("/actions"),
isActive: pathname?.includes("/actions"),
},
{
name: t("common.integrations"),

View File

@@ -1,7 +1,14 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { OptionsType, QuestionOption, QuestionOptions, QuestionsComboBox } from "./QuestionsComboBox";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import {
OptionsType,
QuestionOption,
QuestionOptions,
QuestionsComboBox,
SelectedCommandItem,
} from "./QuestionsComboBox";
describe("QuestionsComboBox", () => {
afterEach(() => {
@@ -53,3 +60,67 @@ describe("QuestionsComboBox", () => {
expect(screen.getByText("Q1")).toBeInTheDocument(); // Verify the selected item is now displayed
});
});
describe("SelectedCommandItem", () => {
test("renders question icon and color for QUESTIONS with questionType", () => {
const { container } = render(
<SelectedCommandItem
label="Q1"
type={OptionsType.QUESTIONS}
questionType={TSurveyQuestionTypeEnum.OpenText}
/>
);
expect(container.querySelector(".bg-brand-dark")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("Q1");
});
test("renders attribute icon and color for ATTRIBUTES", () => {
const { container } = render(<SelectedCommandItem label="Attr" type={OptionsType.ATTRIBUTES} />);
expect(container.querySelector(".bg-indigo-500")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("Attr");
});
test("renders hidden field icon and color for HIDDEN_FIELDS", () => {
const { container } = render(<SelectedCommandItem label="Hidden" type={OptionsType.HIDDEN_FIELDS} />);
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("Hidden");
});
test("renders meta icon and color for META with label", () => {
const { container } = render(<SelectedCommandItem label="device" type={OptionsType.META} />);
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("device");
});
test("renders other icon and color for OTHERS with label", () => {
const { container } = render(<SelectedCommandItem label="Language" type={OptionsType.OTHERS} />);
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("Language");
});
test("renders tag icon and color for TAGS", () => {
const { container } = render(<SelectedCommandItem label="Tag1" type={OptionsType.TAGS} />);
expect(container.querySelector(".bg-indigo-500")).toBeInTheDocument();
expect(container.querySelector("svg")).toBeInTheDocument();
expect(container.textContent).toContain("Tag1");
});
test("renders fallback color and no icon for unknown type", () => {
const { container } = render(<SelectedCommandItem label="Unknown" type={"UNKNOWN"} />);
expect(container.querySelector(".bg-amber-500")).toBeInTheDocument();
expect(container.querySelector("svg")).not.toBeInTheDocument();
expect(container.textContent).toContain("Unknown");
});
test("renders fallback for non-string label", () => {
const { container } = render(
<SelectedCommandItem label={{ default: "NonString" }} type={OptionsType.QUESTIONS} />
);
expect(container.textContent).toContain("NonString");
});
});

View File

@@ -18,11 +18,12 @@ import {
CheckIcon,
ChevronDown,
ChevronUp,
ContactIcon,
EyeOff,
GlobeIcon,
GridIcon,
HashIcon,
HelpCircleIcon,
HomeIcon,
ImageIcon,
LanguagesIcon,
ListIcon,
@@ -63,59 +64,60 @@ interface QuestionComboBoxProps {
onChangeValue: (option: QuestionOption) => void;
}
const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
const getIconType = () => {
switch (type) {
case OptionsType.QUESTIONS:
switch (questionType) {
case TSurveyQuestionTypeEnum.OpenText:
return <MessageSquareTextIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Rating:
return <StarIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.CTA:
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.OpenText:
return <HelpCircleIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
return <ListIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
return <Rows3Icon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.NPS:
return <NetPromoterScoreIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Consent:
return <CheckIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.PictureSelection:
return <ImageIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Matrix:
return <GridIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Ranking:
return <ListOrderedIcon width={18} height={18} className="text-white" />;
}
case OptionsType.ATTRIBUTES:
return <User width={18} height={18} className="text-white" />;
const questionIcons = {
// questions
[TSurveyQuestionTypeEnum.OpenText]: MessageSquareTextIcon,
[TSurveyQuestionTypeEnum.Rating]: StarIcon,
[TSurveyQuestionTypeEnum.CTA]: MousePointerClickIcon,
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: ListIcon,
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: Rows3Icon,
[TSurveyQuestionTypeEnum.NPS]: NetPromoterScoreIcon,
[TSurveyQuestionTypeEnum.Consent]: CheckIcon,
[TSurveyQuestionTypeEnum.PictureSelection]: ImageIcon,
[TSurveyQuestionTypeEnum.Matrix]: GridIcon,
[TSurveyQuestionTypeEnum.Ranking]: ListOrderedIcon,
[TSurveyQuestionTypeEnum.Address]: HomeIcon,
[TSurveyQuestionTypeEnum.ContactInfo]: ContactIcon,
case OptionsType.HIDDEN_FIELDS:
return <EyeOff width={18} height={18} className="text-white" />;
case OptionsType.META:
switch (label) {
case "device":
return <SmartphoneIcon width={18} height={18} className="text-white" />;
case "os":
return <AirplayIcon width={18} height={18} className="text-white" />;
case "browser":
return <GlobeIcon width={18} height={18} className="text-white" />;
case "source":
return <GlobeIcon width={18} height={18} className="text-white" />;
case "action":
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
}
case OptionsType.OTHERS:
switch (label) {
case "Language":
return <LanguagesIcon width={18} height={18} className="text-white" />;
}
case OptionsType.TAGS:
return <HashIcon width={18} height={18} className="text-white" />;
// attributes
[OptionsType.ATTRIBUTES]: User,
// hidden fields
[OptionsType.HIDDEN_FIELDS]: EyeOff,
// meta
device: SmartphoneIcon,
os: AirplayIcon,
browser: GlobeIcon,
source: GlobeIcon,
action: MousePointerClickIcon,
// others
Language: LanguagesIcon,
// tags
[OptionsType.TAGS]: HashIcon,
};
const getIcon = (type: string) => {
const IconComponent = questionIcons[type];
return IconComponent ? <IconComponent width={18} height={18} className="text-white" /> : null;
};
export const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
const getIconType = () => {
if (type) {
if (type === OptionsType.QUESTIONS && questionType) {
return getIcon(questionType);
} else if (type === OptionsType.ATTRIBUTES) {
return getIcon(OptionsType.ATTRIBUTES);
} else if (type === OptionsType.HIDDEN_FIELDS) {
return getIcon(OptionsType.HIDDEN_FIELDS);
} else if ([OptionsType.META, OptionsType.OTHERS].includes(type) && label) {
return getIcon(label);
} else if (type === OptionsType.TAGS) {
return getIcon(OptionsType.TAGS);
}
}
};
@@ -164,7 +166,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
value={inputValue}
onValueChange={setInputValue}
placeholder={t("common.search") + "..."}
className="h-5 border-none border-transparent p-0 shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0"
className="h-5 border-none border-transparent p-0 shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
/>
)}
<div>

View File

@@ -254,7 +254,7 @@ describe("getEnvironmentState", () => {
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
vi.mocked(getProjectForEnvironmentState).mockResolvedValue(mockProject);
vi.mocked(getSurveysForEnvironmentState).mockResolvedValue(mockSurveys);
vi.mocked(getSurveysForEnvironmentState).mockResolvedValue([mockSurveys[0]]); // Only return the app, inProgress survey
vi.mocked(getActionClassesForEnvironmentState).mockResolvedValue(mockActionClasses);
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50); // Default below limit
});

View File

@@ -99,12 +99,8 @@ export const getEnvironmentState = async (
getActionClassesForEnvironmentState(environmentId),
]);
const filteredSurveys = surveys.filter(
(survey) => survey.type === "app" && survey.status === "inProgress"
);
const data: TJsEnvironmentState["data"] = {
surveys: !isMonthlyResponsesLimitReached ? filteredSurveys : [],
surveys: !isMonthlyResponsesLimitReached ? surveys : [],
actionClasses,
project: project,
...(IS_RECAPTCHA_CONFIGURED ? { recaptchaSiteKey: RECAPTCHA_SITE_KEY } : {}),

View File

@@ -100,7 +100,11 @@ describe("getSurveysForEnvironmentState", () => {
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
expect(prisma.survey.findMany).toHaveBeenCalledWith({
where: { environmentId },
where: {
environmentId,
type: "app",
status: "inProgress",
},
select: expect.any(Object), // Check if select is called, specific fields are in the original code
orderBy: { createdAt: "desc" },
take: 30,
@@ -116,7 +120,11 @@ describe("getSurveysForEnvironmentState", () => {
const result = await getSurveysForEnvironmentState(environmentId);
expect(prisma.survey.findMany).toHaveBeenCalledWith({
where: { environmentId },
where: {
environmentId,
type: "app",
status: "inProgress",
},
select: expect.any(Object),
orderBy: { createdAt: "desc" },
take: 30,

View File

@@ -20,6 +20,8 @@ export const getSurveysForEnvironmentState = reactCache(
const surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
type: "app",
status: "inProgress",
},
orderBy: {
createdAt: "desc",

View File

@@ -1,12 +1,6 @@
import structuredClonePolyfill from "@ungap/structured-clone";
let structuredCloneExport: typeof structuredClonePolyfill;
if (typeof structuredClone === "undefined") {
structuredCloneExport = structuredClonePolyfill;
} else {
// @ts-expect-error
structuredCloneExport = structuredClone;
}
const structuredCloneExport =
typeof structuredClone === "undefined" ? structuredClonePolyfill : structuredClone;
export { structuredCloneExport as structuredClone };

View File

@@ -71,16 +71,18 @@ export function LocalizedEditor({
key={`${questionIdx}-${selectedLanguageCode}`}
setFirstRender={setFirstRender}
setText={(v: string) => {
const translatedHtml = {
...value,
[selectedLanguageCode]: v,
};
if (questionIdx === -1) {
// welcome card
updateQuestion({ html: translatedHtml });
return;
if (localSurvey.questions[questionIdx] || questionIdx === -1) {
const translatedHtml = {
...value,
[selectedLanguageCode]: v,
};
if (questionIdx === -1) {
// welcome card
updateQuestion({ html: translatedHtml });
return;
}
updateQuestion(questionIdx, { html: translatedHtml });
}
updateQuestion(questionIdx, { html: translatedHtml });
}}
/>
{localSurvey.languages.length > 1 && (

View File

@@ -99,7 +99,7 @@ export const FileUploadQuestionForm = ({
const removeExtension = (event, index: number) => {
event.preventDefault();
if (question.allowedFileExtensions) {
const updatedExtensions = [...question?.allowedFileExtensions];
const updatedExtensions = [...(question.allowedFileExtensions || [])];
updatedExtensions.splice(index, 1);
// Ensure array is set to undefined if empty, matching toggle behavior
updateQuestion(questionIdx, {
@@ -178,7 +178,7 @@ export const FileUploadQuestionForm = ({
</Button>
)}
</div>
<div className="mt-6 mb-8 space-y-6">
<div className="mb-8 mt-6 space-y-6">
<AdvancedOptionToggle
isChecked={question.allowMultipleFiles}
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
@@ -218,7 +218,7 @@ export const FileUploadQuestionForm = ({
updateQuestion(questionIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
}}
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
MB
</p>

View File

@@ -35,7 +35,6 @@ export const ResponseOptionsCard = ({
const autoComplete = localSurvey.autoComplete !== null;
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
useState;
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
const [verifyEmailToggle, setVerifyEmailToggle] = useState(localSurvey.isVerifyEmailEnabled);
const [recaptchaToggle, setRecaptchaToggle] = useState(localSurvey.recaptcha?.enabled ?? false);
@@ -318,7 +317,7 @@ export const ResponseOptionsCard = ({
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pr-5 pl-2">
<div className="flex items-center pl-2 pr-5">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -356,7 +355,7 @@ export const ResponseOptionsCard = ({
value={localSurvey.autoComplete?.toString()}
onChange={handleInputResponse}
onBlur={handleInputResponseBlur}
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
{t("environments.surveys.edit.completed_responses")}
</p>
@@ -451,7 +450,7 @@ export const ResponseOptionsCard = ({
<Input
autoFocus
id="heading"
className="mt-2 mb-4 bg-white"
className="mb-4 mt-2 bg-white"
name="heading"
defaultValue={surveyClosedMessage.heading}
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
@@ -506,7 +505,7 @@ export const ResponseOptionsCard = ({
<Input
autoFocus
id="heading"
className="mt-2 mb-4 bg-white"
className="mb-4 mt-2 bg-white"
name="heading"
value={singleUseMessage.heading}
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
@@ -514,7 +513,7 @@ export const ResponseOptionsCard = ({
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
<Input
className="mt-2 mb-4 bg-white"
className="mb-4 mt-2 bg-white"
id="subheading"
name="subheading"
value={singleUseMessage.subheading}

View File

@@ -135,25 +135,6 @@ describe("MultipleChoiceMultiQuestion", () => {
expect(onChange3).toHaveBeenCalledWith({ q1: ["Option 2"] });
});
test("handles 'Other' option correctly", async () => {
const onChange = vi.fn();
render(<MultipleChoiceMultiQuestion {...defaultProps} onChange={onChange} />);
// When clicking Other, it calls onChange with an empty string first
await userEvent.click(screen.getByLabelText("Other"));
expect(screen.getByPlaceholderText("Please specify")).toBeInTheDocument();
expect(onChange).toHaveBeenCalledWith({ q1: [""] });
// Clear the mock to focus on typing behavior
onChange.mockClear();
// Enter text in the field and use fireEvent directly which doesn't trigger onChange for each character
const otherInput = screen.getByPlaceholderText("Please specify");
fireEvent.change(otherInput, { target: { value: "Custom response" } });
expect(onChange).toHaveBeenCalledWith({ q1: ["Custom response"] });
});
test("handles form submission", async () => {
const onSubmit = vi.fn();
const { container } = render(
@@ -168,6 +149,46 @@ describe("MultipleChoiceMultiQuestion", () => {
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
});
test("filters out invalid values during submission", async () => {
const onSubmit = vi.fn();
const { container } = render(
<MultipleChoiceMultiQuestion
{...defaultProps}
// Add an invalid value that should be filtered out
value={["Option 1", "Invalid Option"]}
onSubmit={onSubmit}
/>
);
// Submit the form
const form = container.querySelector("form");
fireEvent.submit(form!);
// Check that onSubmit was called with only valid values
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
});
test("calls onChange with updated values during submission", async () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
const { container } = render(
<MultipleChoiceMultiQuestion
{...defaultProps}
value={["Option 1", "Invalid Option"]}
onChange={onChange}
onSubmit={onSubmit}
/>
);
// Submit the form
const form = container.querySelector("form");
fireEvent.submit(form!);
// Check that onChange was called with filtered values
expect(onChange).toHaveBeenCalledWith({ q1: ["Option 1"] });
expect(onSubmit).toHaveBeenCalledWith({ q1: ["Option 1"] }, { questionId: "ttc-value" });
});
test("calls onBack when back button is clicked", async () => {
const onBack = vi.fn();
render(<MultipleChoiceMultiQuestion {...defaultProps} onBack={onBack} />);

View File

@@ -64,20 +64,6 @@ export function MultipleChoiceMultiQuestion({
const [otherSelected, setOtherSelected] = useState<boolean>(false);
const [otherValue, setOtherValue] = useState("");
useEffect(() => {
setOtherSelected(
Boolean(value) &&
(Array.isArray(value) ? value : [value]).some((item) => {
return !getChoicesWithoutOtherLabels().includes(item);
})
);
setOtherValue(
(Array.isArray(value) &&
value.filter((v) => !question.choices.find((c) => c.label[languageCode] === v))[0]) ||
""
);
}, [question.id, getChoicesWithoutOtherLabels, question.choices, value, languageCode]);
const questionChoices = useMemo(() => {
if (!question.choices) {
return [];
@@ -135,6 +121,16 @@ export function MultipleChoiceMultiQuestion({
onChange({ [question.id]: [] }); // if not array, make it an array
};
const getIsRequired = () => {
const responseValues = [...value];
if (otherSelected && otherValue) {
responseValues.push(otherValue);
}
return question.required && Array.isArray(responseValues) && responseValues.length
? false
: question.required;
};
return (
<form
key={question.id}
@@ -143,10 +139,11 @@ export function MultipleChoiceMultiQuestion({
const newValue = value.filter((item) => {
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
if (otherValue && otherSelected && !newValue.includes(otherValue)) newValue.push(otherValue);
onChange({ [question.id]: newValue });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
onSubmit({ [question.id]: newValue }, updatedTtcObj);
}}
className="fb-w-full">
<ScrollableContainer>
@@ -208,11 +205,7 @@ export function MultipleChoiceMultiQuestion({
Array.isArray(value) &&
value.includes(getLocalizedValue(choice.label, languageCode))
}
required={
question.required && Array.isArray(value) && value.length
? false
: question.required
}
required={getIsRequired()}
/>
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
{getLocalizedValue(choice.label, languageCode)}
@@ -225,9 +218,7 @@ export function MultipleChoiceMultiQuestion({
<label
tabIndex={isCurrent ? 0 : -1}
className={cn(
value.includes(getLocalizedValue(otherOption.label, languageCode))
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
: "fb-border-border",
otherSelected ? "fb-border-brand fb-bg-input-bg-selected fb-z-10" : "fb-border-border",
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
)}
onKeyDown={(e) => {
@@ -249,11 +240,6 @@ export function MultipleChoiceMultiQuestion({
aria-labelledby={`${otherOption.id}-label`}
onChange={() => {
setOtherSelected(!otherSelected);
if (!value.includes(otherValue)) {
addItem(otherValue);
} else {
removeItem(otherValue);
}
}}
checked={otherSelected}
/>
@@ -270,9 +256,9 @@ export function MultipleChoiceMultiQuestion({
name={question.id}
tabIndex={isCurrent ? 0 : -1}
value={otherValue}
pattern=".*\S+.*"
onChange={(e) => {
setOtherValue(e.currentTarget.value);
addItem(e.currentTarget.value);
}}
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
placeholder={
@@ -280,7 +266,6 @@ export function MultipleChoiceMultiQuestion({
}
required={question.required}
aria-labelledby={`${otherOption.id}-label`}
pattern=".*\S+.*"
/>
) : null}
</label>

View File

@@ -225,6 +225,7 @@ export function MultipleChoiceSingleQuestion({
id={`${otherOption.id}-label`}
dir="auto"
name={question.id}
pattern=".*\S+.*"
value={value}
onChange={(e) => {
onChange({ [question.id]: e.currentTarget.value });
@@ -238,7 +239,6 @@ export function MultipleChoiceSingleQuestion({
required={question.required}
aria-labelledby={`${otherOption.id}-label`}
maxLength={250}
pattern=".*\S+.*"
/>
) : null}
</label>