diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index bf39f22bab..dd7f0c1848 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -279,7 +279,6 @@ "no_result_found": "Kein Ergebnis gefunden", "no_results": "Keine Ergebnisse", "no_surveys_found": "Keine Umfragen gefunden.", - "none_of_the_above": "Keine der oben genannten Optionen", "not_authenticated": "Du bist nicht authentifiziert, um diese Aktion durchzuführen.", "not_authorized": "Nicht berechtigt", "not_connected": "Nicht verbunden", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Rahmen hinzufügen", "add_highlight_border_description": "Füge deiner Umfragekarte einen äußeren Rahmen hinzu.", "add_logic": "Logik hinzufügen", - "add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu", "add_option": "Option hinzufügen", "add_other": "Anderes hinzufügen", "add_photo_or_video": "Foto oder Video hinzufügen", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index c4da0adaa6..bdd443ef65 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -279,7 +279,6 @@ "no_result_found": "No result found", "no_results": "No results", "no_surveys_found": "No surveys found.", - "none_of_the_above": "None of the above", "not_authenticated": "You are not authenticated to perform this action.", "not_authorized": "Not authorized", "not_connected": "Not Connected", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Add highlight border", "add_highlight_border_description": "Add an outer border to your survey card.", "add_logic": "Add logic", - "add_none_of_the_above": "Add \"None of the Above\"", "add_option": "Add option", "add_other": "Add \"Other\"", "add_photo_or_video": "Add photo or video", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index bbee4c313b..e8f5b045e0 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -279,7 +279,6 @@ "no_result_found": "Aucun résultat trouvé", "no_results": "Aucun résultat", "no_surveys_found": "Aucun sondage trouvé.", - "none_of_the_above": "Aucun des éléments ci-dessus", "not_authenticated": "Vous n'êtes pas authentifié pour effectuer cette action.", "not_authorized": "Non autorisé", "not_connected": "Non connecté", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Ajouter une bordure de surlignage", "add_highlight_border_description": "Ajoutez une bordure extérieure à votre carte d'enquête.", "add_logic": "Ajouter de la logique", - "add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"", "add_option": "Ajouter une option", "add_other": "Ajouter \"Autre", "add_photo_or_video": "Ajouter une photo ou une vidéo", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index 6de7841c4d..ab61fb2ef8 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -279,7 +279,6 @@ "no_result_found": "結果が見つかりません", "no_results": "結果なし", "no_surveys_found": "フォームが見つかりません。", - "none_of_the_above": "いずれも該当しません", "not_authenticated": "このアクションを実行するための認証がされていません。", "not_authorized": "権限がありません", "not_connected": "未接続", @@ -1210,7 +1209,6 @@ "add_highlight_border": "ハイライトボーダーを追加", "add_highlight_border_description": "フォームカードに外側のボーダーを追加します。", "add_logic": "ロジックを追加", - "add_none_of_the_above": "\"いずれも該当しません\" を追加", "add_option": "オプションを追加", "add_other": "「その他」を追加", "add_photo_or_video": "写真または動画を追加", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 4fe20187d1..00cc4facea 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -279,7 +279,6 @@ "no_result_found": "Nenhum resultado encontrado", "no_results": "Nenhum resultado", "no_surveys_found": "Não foram encontradas pesquisas.", - "none_of_the_above": "Nenhuma das opções acima", "not_authenticated": "Você não está autenticado para realizar essa ação.", "not_authorized": "Não autorizado", "not_connected": "Desconectado", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Adicionar borda de destaque", "add_highlight_border_description": "Adicione uma borda externa ao seu cartão de pesquisa.", "add_logic": "Adicionar lógica", - "add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"", "add_option": "Adicionar opção", "add_other": "Adicionar \"Outro", "add_photo_or_video": "Adicionar foto ou video", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 770d8b4019..db78e52c47 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -279,7 +279,6 @@ "no_result_found": "Nenhum resultado encontrado", "no_results": "Nenhum resultado", "no_surveys_found": "Nenhum inquérito encontrado.", - "none_of_the_above": "Nenhuma das opções acima", "not_authenticated": "Não está autenticado para realizar esta ação.", "not_authorized": "Não autorizado", "not_connected": "Não Conectado", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Adicionar borda de destaque", "add_highlight_border_description": "Adicione uma borda externa ao seu cartão de inquérito.", "add_logic": "Adicionar lógica", - "add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"", "add_option": "Adicionar opção", "add_other": "Adicionar \"Outro\"", "add_photo_or_video": "Adicionar foto ou vídeo", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 46e31f31ea..74cd9b62fa 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -279,7 +279,6 @@ "no_result_found": "Niciun rezultat găsit", "no_results": "Nicio rezultat", "no_surveys_found": "Nu au fost găsite sondaje.", - "none_of_the_above": "Niciuna dintre cele de mai sus", "not_authenticated": "Nu sunteți autentificat pentru a efectua această acțiune.", "not_authorized": "Neautorizat", "not_connected": "Neconectat", @@ -1210,7 +1209,6 @@ "add_highlight_border": "Adaugă bordură evidențiată", "add_highlight_border_description": "Adaugă o margine exterioară cardului tău de sondaj.", "add_logic": "Adaugă logică", - "add_none_of_the_above": "Adăugați \"Niciuna dintre cele de mai sus\"", "add_option": "Adăugați opțiune", "add_other": "Adăugați \"Altele\"", "add_photo_or_video": "Adaugă fotografie sau video", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 455c902b87..3990b1dd8e 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -279,7 +279,6 @@ "no_result_found": "没有 结果", "no_results": "没有 结果", "no_surveys_found": "未找到 调查", - "none_of_the_above": "以上 都 不 是", "not_authenticated": "您 未 认证 以 执行 该 操作。", "not_authorized": "未授权", "not_connected": "未连接", @@ -1210,7 +1209,6 @@ "add_highlight_border": "添加 高亮 边框", "add_highlight_border_description": "在 你的 调查 卡片 添加 外 边框。", "add_logic": "添加逻辑", - "add_none_of_the_above": "添加 “以上 都 不 是”", "add_option": "添加 选项", "add_other": "添加 \"其他\"", "add_photo_or_video": "添加 照片 或 视频", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 9db2ba3cd1..2fa24a0c54 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -279,7 +279,6 @@ "no_result_found": "找不到結果", "no_results": "沒有結果", "no_surveys_found": "找不到問卷。", - "none_of_the_above": "以上皆非", "not_authenticated": "您未經授權執行此操作。", "not_authorized": "未授權", "not_connected": "未連線", @@ -1210,7 +1209,6 @@ "add_highlight_border": "新增醒目提示邊框", "add_highlight_border_description": "在您的問卷卡片新增外邊框。", "add_logic": "新增邏輯", - "add_none_of_the_above": "新增 \"以上皆非\"", "add_option": "新增選項", "add_other": "新增「其他」", "add_photo_or_video": "新增照片或影片", diff --git a/packages/surveys/src/components/questions/multiple-choice-single-question.test.tsx b/packages/surveys/src/components/questions/multiple-choice-single-question.test.tsx index dce77782a0..e57e9060e8 100644 --- a/packages/surveys/src/components/questions/multiple-choice-single-question.test.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-single-question.test.tsx @@ -272,4 +272,117 @@ describe("MultipleChoiceSingleQuestion", () => { const backButton = screen.getByTestId("back-button"); expect(backButton).toHaveAttribute("tabIndex", "0"); }); + + test("allows deselecting a choice when question is not required", async () => { + const user = userEvent.setup(); + const optionalQuestion = { ...mockQuestion, required: false }; + + render(); + + const choice1Radio = screen.getByLabelText("Choice 1"); + await user.click(choice1Radio); + + expect(mockOnChange).toHaveBeenCalledWith({ q1: undefined }); + }); + + test("does not deselect a choice when question is required", async () => { + const user = userEvent.setup(); + + render(); + + const choice1Radio = screen.getByLabelText("Choice 1"); + await user.click(choice1Radio); + + expect(mockOnChange).not.toHaveBeenCalledWith({ q1: undefined }); + expect(mockOnChange).toHaveBeenCalledWith({ q1: "Choice 1" }); + }); + + test("allows deselecting 'Other' option when question is not required", async () => { + const user = userEvent.setup(); + const optionalQuestion = { ...mockQuestion, required: false }; + + render(); + + const otherRadio = screen.getByRole("radio", { name: "Other" }); + await user.click(otherRadio); + + expect(mockOnChange).toHaveBeenCalledWith({ q1: undefined }); + }); + + test("does not deselect 'Other' option when question is required", async () => { + const user = userEvent.setup(); + + render(); + + const otherRadio = screen.getByRole("radio", { name: "Other" }); + await user.click(otherRadio); + + expect(mockOnChange).not.toHaveBeenCalledWith({ q1: undefined }); + }); + + test("clears otherSelected when selecting a regular choice after 'Other' was selected", async () => { + const user = userEvent.setup(); + + render(); + + const otherRadio = screen.getByRole("radio", { name: "Other" }); + await user.click(otherRadio); + + expect(screen.getByPlaceholderText("Please specify")).toBeInTheDocument(); + + mockOnChange.mockClear(); + + const choice1Radio = screen.getByLabelText("Choice 1"); + await user.click(choice1Radio); + + expect(mockOnChange).toHaveBeenCalledWith({ q1: "Choice 1" }); + }); + + test("handles spacebar key press on 'Other' option label when not selected", async () => { + const user = userEvent.setup(); + + render(); + + const otherLabel = screen.getByLabelText("Other").closest("label"); + + if (otherLabel) { + await user.type(otherLabel, " "); + } + + expect(mockOnChange).toHaveBeenCalledWith({ q1: "" }); + }); + + test("does not trigger click when spacebar is pressed on 'Other' option label and otherSelected is true", async () => { + const user = userEvent.setup(); + + render(); + + const otherRadio = screen.getByRole("radio", { name: "Other" }); + await user.click(otherRadio); + + mockOnChange.mockClear(); + + const otherLabel = screen.getByRole("radio", { name: "Other" }).closest("label"); + + if (otherLabel) { + await user.type(otherLabel, " "); + } + + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + test("displays custom other option placeholder when provided", async () => { + const user = userEvent.setup(); + const questionWithCustomPlaceholder = { + ...mockQuestion, + otherOptionPlaceholder: { default: "Custom placeholder text" }, + }; + + render(); + + const otherRadio = screen.getByRole("radio", { name: "Other" }); + await user.click(otherRadio); + + expect(screen.getByPlaceholderText("Custom placeholder text")).toBeInTheDocument(); + }); }); diff --git a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx index 24e7140bfa..429819171c 100644 --- a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx @@ -1,3 +1,6 @@ +import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; +import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { BackButton } from "@/components/buttons/back-button"; import { SubmitButton } from "@/components/buttons/submit-button"; import { Headline } from "@/components/general/headline"; @@ -7,9 +10,6 @@ import { ScrollableContainer } from "@/components/wrappers/scrollable-container" import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn, getShuffledChoicesIds } from "@/lib/utils"; -import { useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; -import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; interface MultipleChoiceSingleProps { question: TSurveyMultipleChoiceQuestion; @@ -164,9 +164,14 @@ export function MultipleChoiceSingleQuestion({ dir={dir} className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0" aria-labelledby={`${choice.id}-label`} - onChange={() => { - setOtherSelected(false); - onChange({ [question.id]: getLocalizedValue(choice.label, languageCode) }); + onClick={() => { + const choiceValue = getLocalizedValue(choice.label, languageCode); + if (!question.required && value === choiceValue) { + onChange({ [question.id]: undefined }); + } else { + setOtherSelected(false); + onChange({ [question.id]: choiceValue }); + } }} checked={value === getLocalizedValue(choice.label, languageCode)} required={question.required ? idx === 0 : undefined} @@ -209,10 +214,11 @@ export function MultipleChoiceSingleQuestion({ className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0" aria-labelledby={`${otherOption.id}-label`} onClick={() => { - if (otherSelected) { + if (otherSelected && !question.required) { onChange({ [question.id]: undefined }); - } else { - setOtherSelected(!otherSelected); + setOtherSelected(false); + } else if (!otherSelected) { + setOtherSelected(true); onChange({ [question.id]: "" }); } }}