feat: dropdown ui for multi select (#7191)

This commit is contained in:
Dhruwang Jariwala
2026-02-03 10:46:03 +05:30
committed by GitHub
parent c3ec5ddc3a
commit 009beba866
41 changed files with 175 additions and 46 deletions

View File

@@ -1200,6 +1200,7 @@ checksums:
environments/surveys/edit/disable_the_visibility_of_survey_progress: 2af631010114307ac2a91612559c9618
environments/surveys/edit/display_an_estimate_of_completion_time_for_survey: 03f0a816569399c1c61d08dbc913de06
environments/surveys/edit/display_number_of_responses_for_survey: 06294567ecba9aba2cce337c669577f6
environments/surveys/edit/display_type: 68c2deaca48289119f1a988ede39dbad
environments/surveys/edit/divide: ca443836e15d0a1cbde5f03cc8edba78
environments/surveys/edit/does_not_contain: d618eb0f854f7efa0d7c644e6628fa42
environments/surveys/edit/does_not_end_with: 885c4c1981b97a4bfa213e185b78b6c4
@@ -1207,6 +1208,7 @@ checksums:
environments/surveys/edit/does_not_include_all_of: c18c1a71e6d96c681a3e95c7bd6c9482
environments/surveys/edit/does_not_include_one_of: 91090d2e0667faf654f6a81d9857440f
environments/surveys/edit/does_not_start_with: 9395869b54cdfb353a51a7e0864f4fd7
environments/surveys/edit/dropdown: b4069d14e572b53ca4156d25b0a970c1
environments/surveys/edit/duplicate_block: d4ea4afb5fc5b18a81cbe0302fa05997
environments/surveys/edit/duplicate_question: 910751de01fdd327165968214717711b
environments/surveys/edit/edit_link: 40ba9e15beac77a46c5baf30be84ac54
@@ -1339,6 +1341,7 @@ checksums:
environments/surveys/edit/limit_the_maximum_file_size: 6ae5944fe490b9acdaaee92b30381ec0
environments/surveys/edit/limit_upload_file_size_to: 949c48d25ae45259cc19464e95752d29
environments/surveys/edit/link_survey_description: f45569b5e6b78be6bc02bc6a46da948b
environments/surveys/edit/list: 94f13e7ef909a4de9db7abaa1f9f0b61
environments/surveys/edit/load_segment: 5341d3de37ff10f7526152e38e25e3c5
environments/surveys/edit/logic_error_warning: 542fbb918ffdb29e6f9a4a6196ffb558
environments/surveys/edit/logic_error_warning_text: f2afad8852a95ed169a39959efbf592c

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.",
"display_an_estimate_of_completion_time_for_survey": "Zeige eine Schätzung der Fertigstellungszeit für die Umfrage an",
"display_number_of_responses_for_survey": "Anzahl der Antworten für Umfrage anzeigen",
"display_type": "Anzeigetyp",
"divide": "Teilen /",
"does_not_contain": "Enthält nicht",
"does_not_end_with": "Endet nicht mit",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Enthält nicht alle von",
"does_not_include_one_of": "Enthält nicht eines von",
"does_not_start_with": "Fängt nicht an mit",
"dropdown": "Dropdown",
"duplicate_block": "Block duplizieren",
"duplicate_question": "Frage duplizieren",
"edit_link": "Bearbeitungslink",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Begrenzen Sie die maximale Dateigröße für Uploads.",
"limit_upload_file_size_to": "Upload-Dateigröße begrenzen auf",
"link_survey_description": "Teile einen Link zu einer Umfrageseite oder bette ihn in eine Webseite oder E-Mail ein.",
"list": "Liste",
"load_segment": "Segment laden",
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
"logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage",

View File

@@ -1267,6 +1267,8 @@
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
"date_format": "Date format",
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
"display_type": "Display type",
"dropdown": "Dropdown",
"delete_anyways": "Delete anyways",
"delete_block": "Delete block",
"delete_choice": "Delete choice",
@@ -1416,6 +1418,7 @@
"logic_error_warning": "Changing will cause logic errors",
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
"logo_settings": "Logo settings",
"list": "List",
"long_answer": "Long answer",
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
"lower_label": "Lower Label",
@@ -3091,4 +3094,4 @@
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
"usability_score_name": "System Usability Score (SUS)"
}
}
}

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Desactivar la visibilidad del progreso de la encuesta.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar una estimación del tiempo de finalización de la encuesta",
"display_number_of_responses_for_survey": "Mostrar número de respuestas para la encuesta",
"display_type": "Tipo de visualización",
"divide": "Dividir /",
"does_not_contain": "No contiene",
"does_not_end_with": "No termina con",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "No incluye todos los",
"does_not_include_one_of": "No incluye uno de",
"does_not_start_with": "No comienza con",
"dropdown": "Desplegable",
"duplicate_block": "Duplicar bloque",
"duplicate_question": "Duplicar pregunta",
"edit_link": "Editar enlace",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Limita el tamaño máximo de archivo para las subidas.",
"limit_upload_file_size_to": "Limitar el tamaño de archivo de subida a",
"link_survey_description": "Comparte un enlace a una página de encuesta o incrústala en una página web o correo electrónico.",
"list": "Lista",
"load_segment": "Cargar segmento",
"logic_error_warning": "El cambio causará errores lógicos",
"logic_error_warning_text": "Cambiar el tipo de pregunta eliminará las condiciones lógicas de esta pregunta",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.",
"display_an_estimate_of_completion_time_for_survey": "Afficher une estimation du temps de complétion pour l'enquête.",
"display_number_of_responses_for_survey": "Afficher le nombre de réponses pour l'enquête",
"display_type": "Type d'affichage",
"divide": "Diviser /",
"does_not_contain": "Ne contient pas",
"does_not_end_with": "Ne se termine pas par",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "n'inclut pas tout",
"does_not_include_one_of": "n'inclut pas un de",
"does_not_start_with": "Ne commence pas par",
"dropdown": "Menu déroulant",
"duplicate_block": "Dupliquer le bloc",
"duplicate_question": "Dupliquer la question",
"edit_link": "Modifier le lien",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Limiter la taille maximale des fichiers pour les téléversements.",
"limit_upload_file_size_to": "Limiter la taille de téléversement des fichiers à",
"link_survey_description": "Partagez un lien vers une page d'enquête ou intégrez-le dans une page web ou un e-mail.",
"list": "Liste",
"load_segment": "Segment de chargement",
"logic_error_warning": "Changer causera des erreurs logiques",
"logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "フォームの進捗状況の表示を無効にする。",
"display_an_estimate_of_completion_time_for_survey": "フォームの完了時間の目安を表示",
"display_number_of_responses_for_survey": "フォームの回答数を表示",
"display_type": "表示タイプ",
"divide": "除算 /",
"does_not_contain": "を含まない",
"does_not_end_with": "で終わらない",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "のすべてを含まない",
"does_not_include_one_of": "のいずれも含まない",
"does_not_start_with": "で始まらない",
"dropdown": "ドロップダウン",
"duplicate_block": "ブロックを複製",
"duplicate_question": "質問を複製",
"edit_link": "編集 リンク",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "アップロードの最大ファイルサイズを制限します。",
"limit_upload_file_size_to": "アップロードファイルサイズの上限",
"link_survey_description": "フォームページへのリンクを共有するか、ウェブページやメールに埋め込みます。",
"list": "リスト",
"load_segment": "セグメントを読み込み",
"logic_error_warning": "変更するとロジックエラーが発生します",
"logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Schakel de zichtbaarheid van de voortgang van het onderzoek uit.",
"display_an_estimate_of_completion_time_for_survey": "Geef een schatting weer van de voltooiingstijd voor het onderzoek",
"display_number_of_responses_for_survey": "Weergave aantal reacties voor enquête",
"display_type": "Weergavetype",
"divide": "Verdeling /",
"does_not_contain": "Bevat niet",
"does_not_end_with": "Eindigt niet met",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Omvat niet alles",
"does_not_include_one_of": "Bevat niet een van",
"does_not_start_with": "Begint niet met",
"dropdown": "Dropdown",
"duplicate_block": "Blok dupliceren",
"duplicate_question": "Vraag dupliceren",
"edit_link": "Link bewerken",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Beperk de maximale bestandsgrootte voor uploads.",
"limit_upload_file_size_to": "Beperk uploadbestandsgrootte tot",
"link_survey_description": "Deel een link naar een enquêtepagina of sluit deze in op een webpagina of e-mail.",
"list": "Lijst",
"load_segment": "Laadsegment",
"logic_error_warning": "Wijzigen zal logische fouten veroorzaken",
"logic_error_warning_text": "Als u het vraagtype wijzigt, worden de logische voorwaarden van deze vraag verwijderd",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa de tempo de conclusão da pesquisa",
"display_number_of_responses_for_survey": "Mostrar número de respostas da pesquisa",
"display_type": "Tipo de exibição",
"divide": "Divida /",
"does_not_contain": "não contém",
"does_not_end_with": "Não termina com",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Não inclui todos de",
"does_not_include_one_of": "Não inclui um de",
"does_not_start_with": "Não começa com",
"dropdown": "Menu suspenso",
"duplicate_block": "Duplicar bloco",
"duplicate_question": "Duplicar pergunta",
"edit_link": "Editar link",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Limitar o tamanho máximo de arquivo para uploads.",
"limit_upload_file_size_to": "Limitar tamanho de arquivo de upload para",
"link_survey_description": "Compartilhe um link para a página da pesquisa ou incorpore-a em uma página da web ou e-mail.",
"list": "Lista",
"load_segment": "segmento de carga",
"logic_error_warning": "Mudar vai causar erros de lógica",
"logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa do tempo de conclusão do inquérito",
"display_number_of_responses_for_survey": "Mostrar número de respostas do inquérito",
"display_type": "Tipo de exibição",
"divide": "Dividir /",
"does_not_contain": "Não contém",
"does_not_end_with": "Não termina com",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Não inclui todos de",
"does_not_include_one_of": "Não inclui um de",
"does_not_start_with": "Não começa com",
"dropdown": "Menu suspenso",
"duplicate_block": "Duplicar bloco",
"duplicate_question": "Duplicar pergunta",
"edit_link": "Editar link",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Limitar o tamanho máximo de ficheiro para carregamentos.",
"limit_upload_file_size_to": "Limitar o tamanho de ficheiro de carregamento para",
"link_survey_description": "Partilhe um link para uma página de inquérito ou incorpore-o numa página web ou email.",
"list": "Lista",
"load_segment": "Carregar segmento",
"logic_error_warning": "A alteração causará erros de lógica",
"logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Dezactivați vizibilitatea progresului sondajului",
"display_an_estimate_of_completion_time_for_survey": "Afișează o estimare a timpului de finalizare pentru sondaj",
"display_number_of_responses_for_survey": "Afișează numărul de răspunsuri pentru sondaj",
"display_type": "Tip de afișare",
"divide": "Împarte /",
"does_not_contain": "Nu conține",
"does_not_end_with": "Nu se termină cu",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Nu include toate",
"does_not_include_one_of": "Nu include una dintre",
"does_not_start_with": "Nu începe cu",
"dropdown": "Dropdown",
"duplicate_block": "Duplicați blocul",
"duplicate_question": "Duplică întrebarea",
"edit_link": "Editare legătură",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Limitați dimensiunea maximă a fișierului pentru încărcări.",
"limit_upload_file_size_to": "Limitați dimensiunea fișierului încărcat la",
"link_survey_description": "Partajați un link către o pagină de chestionar sau încorporați-l într-o pagină web sau email.",
"list": "Listă",
"load_segment": "Încarcă segment",
"logic_error_warning": "Schimbarea va provoca erori de logică",
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Отключить отображение прогресса опроса.",
"display_an_estimate_of_completion_time_for_survey": "Показывать примерное время прохождения опроса",
"display_number_of_responses_for_survey": "Показывать количество ответов на опрос",
"display_type": "Тип отображения",
"divide": "Разделить /",
"does_not_contain": "Не содержит",
"does_not_end_with": "Не заканчивается на",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Не включает все из",
"does_not_include_one_of": "Не включает ни одного из",
"does_not_start_with": "Не начинается с",
"dropdown": "Выпадающий список",
"duplicate_block": "Дублировать блок",
"duplicate_question": "Дублировать вопрос",
"edit_link": "Редактировать ссылку",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Ограничьте максимальный размер загружаемых файлов.",
"limit_upload_file_size_to": "Ограничить размер загружаемого файла до",
"link_survey_description": "Поделитесь ссылкой на страницу опроса или вставьте её на веб-страницу или в электронное письмо.",
"list": "Список",
"load_segment": "Загрузить сегмент",
"logic_error_warning": "Изменение приведёт к логическим ошибкам",
"logic_error_warning_text": "Изменение типа вопроса удалит логические условия из этого вопроса",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "Inaktivera synligheten av enkätens framsteg.",
"display_an_estimate_of_completion_time_for_survey": "Visa en uppskattning av tid för att slutföra enkäten",
"display_number_of_responses_for_survey": "Visa antal svar för enkäten",
"display_type": "Visningstyp",
"divide": "Dividera /",
"does_not_contain": "Innehåller inte",
"does_not_end_with": "Slutar inte med",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "Inkluderar inte alla av",
"does_not_include_one_of": "Inkluderar inte en av",
"does_not_start_with": "Börjar inte med",
"dropdown": "Rullgardinsmeny",
"duplicate_block": "Duplicera block",
"duplicate_question": "Duplicera fråga",
"edit_link": "Redigera länk",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "Begränsa den maximala filstorleken för uppladdningar.",
"limit_upload_file_size_to": "Begränsa uppladdad filstorlek till",
"link_survey_description": "Dela en länk till en enkätsida eller bädda in den på en webbsida eller i e-post.",
"list": "Lista",
"load_segment": "Ladda segment",
"logic_error_warning": "Ändring kommer att orsaka logikfel",
"logic_error_warning_text": "Att ändra frågetypen kommer att ta bort logikvillkoren från denna fråga",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "禁用问卷 进度 的可见性。",
"display_an_estimate_of_completion_time_for_survey": "显示 调查 预计 完成 时间",
"display_number_of_responses_for_survey": "显示 调查 响应 数量",
"display_type": "显示类型",
"divide": "划分 /",
"does_not_contain": "不包含",
"does_not_end_with": "不 以 结尾",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "不包括所有 ",
"does_not_include_one_of": "不包括一 个",
"does_not_start_with": "不 以 开头",
"dropdown": "下拉菜单",
"duplicate_block": "复制区块",
"duplicate_question": "复制问题",
"edit_link": "编辑 链接",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "限制上传文件的最大大小。",
"limit_upload_file_size_to": "将上传文件大小限制为",
"link_survey_description": "分享 问卷 页面 链接 或 将其 嵌入 网页 或 电子邮件 中。",
"list": "列表",
"load_segment": "载入 段落",
"logic_error_warning": "更改 将 导致 逻辑 错误",
"logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件",

View File

@@ -1273,6 +1273,7 @@
"disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。",
"display_an_estimate_of_completion_time_for_survey": "顯示問卷的估計完成時間",
"display_number_of_responses_for_survey": "顯示問卷的回應數",
"display_type": "顯示類型",
"divide": "除 /",
"does_not_contain": "不包含",
"does_not_end_with": "不以...結尾",
@@ -1280,6 +1281,7 @@
"does_not_include_all_of": "不包含全部",
"does_not_include_one_of": "不包含其中之一",
"does_not_start_with": "不以...開頭",
"dropdown": "下拉選單",
"duplicate_block": "複製區塊",
"duplicate_question": "複製問題",
"edit_link": "編輯 連結",
@@ -1412,6 +1414,7 @@
"limit_the_maximum_file_size": "限制上傳檔案的最大大小。",
"limit_upload_file_size_to": "將上傳檔案大小限制為",
"link_survey_description": "分享問卷頁面的連結或將其嵌入網頁或電子郵件中。",
"list": "清單",
"load_segment": "載入區隔",
"logic_error_warning": "變更將導致邏輯錯誤",
"logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件",

View File

@@ -10,7 +10,7 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
import { TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
import { TMultipleChoiceOptionDisplayType, TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
@@ -21,6 +21,7 @@ import { ValidationRulesEditor } from "@/modules/survey/editor/components/valida
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
interface MultipleChoiceElementFormProps {
@@ -75,6 +76,11 @@ export const MultipleChoiceElementForm = ({
},
};
const multipleChoiceOptionDisplayTypeOptions = [
{ value: "list", label: t("environments.surveys.edit.list") },
{ value: "dropdown", label: t("environments.surveys.edit.dropdown") },
];
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
let newChoices: any[] = [];
if (element.choices) {
@@ -382,6 +388,20 @@ export const MultipleChoiceElementForm = ({
</div>
</div>
</div>
<div className="mt-3">
<Label>{t("environments.surveys.edit.display_type")}</Label>
<div className="mt-2">
<OptionsSwitch
options={multipleChoiceOptionDisplayTypeOptions}
currentOption={element.displayType ?? "list"}
handleOptionChange={(value: TMultipleChoiceOptionDisplayType) =>
updateElement(elementIdx, { displayType: value })
}
/>
</div>
</div>
<BulkEditOptionsModal
isOpen={isBulkEditOpen}
onClose={() => setIsBulkEditOpen(false)}

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -141,21 +141,23 @@ function DropdownVariant({
};
return (
<>
<div className="space-y-2">
<ElementError errorMessage={errorMessage} dir={dir} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
disabled={disabled}
className="rounded-input w-full justify-between"
className="rounded-input w-full justify-between bg-option-bg rounded-option border border-option-border my-0 h-input"
aria-invalid={Boolean(errorMessage)}
aria-label={headline}>
<span className="truncate">{displayText}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<span className="truncate font-input font-input-weight text-input-text">{displayText}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50 label-headline" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)]" align="start">
<DropdownMenuContent
className="bg-option-bg w-[var(--radix-dropdown-menu-trigger-width)] max-h-[300px] overflow-y-auto"
align="start">
{options
.filter((option) => option.id !== "none")
.map((option) => {
@@ -166,18 +168,23 @@ function DropdownVariant({
<DropdownMenuCheckboxItem
key={option.id}
id={optionId}
dir={dir}
checked={isChecked}
onCheckedChange={() => {
handleOptionToggle(option.id);
}}
onSelect={(e) => {
e.preventDefault();
}}
disabled={disabled}>
<span className={optionLabelClassName}>{option.label}</span>
<span className="font-input font-input-weight text-input-text">{option.label}</span>
</DropdownMenuCheckboxItem>
);
})}
{hasOtherOption && otherOptionId ? (
<DropdownMenuCheckboxItem
id={`${inputId}-${otherOptionId}`}
dir={dir}
checked={isOtherSelected}
onCheckedChange={() => {
if (isOtherSelected) {
@@ -186,8 +193,11 @@ function DropdownVariant({
handleOptionAdd(otherOptionId);
}
}}
onSelect={(e) => {
e.preventDefault();
}}
disabled={disabled}>
<span className={optionLabelClassName}>{otherOptionLabel}</span>
<span className="font-input font-input-weight text-input-text">{otherOptionLabel}</span>
</DropdownMenuCheckboxItem>
) : null}
{options
@@ -200,12 +210,16 @@ function DropdownVariant({
<DropdownMenuCheckboxItem
key={option.id}
id={optionId}
dir={dir}
checked={isChecked}
onCheckedChange={() => {
handleOptionToggle(option.id);
}}
onSelect={(e) => {
e.preventDefault();
}}
disabled={disabled}>
<span className={optionLabelClassName}>{option.label}</span>
<span className="font-input font-input-weight text-input-text">{option.label}</span>
</DropdownMenuCheckboxItem>
);
})}
@@ -224,7 +238,7 @@ function DropdownVariant({
className="w-full"
/>
) : null}
</>
</div>
);
}
@@ -463,10 +477,17 @@ function MultiSelect({
// Get selected option labels for dropdown display
const selectedLabels = options.filter((opt) => selectedValues.includes(opt.id)).map((opt) => opt.label);
// Handle "other" option label display
if (hasOtherOption && otherOptionId && selectedValues.includes(otherOptionId)) {
const otherLabel = otherValue || otherOptionLabel;
if (!selectedLabels.includes(otherLabel)) {
selectedLabels.push(otherLabel);
}
}
let displayText = placeholder;
if (selectedLabels.length > 0) {
displayText =
selectedLabels.length === 1 ? selectedLabels[0] : `${String(selectedLabels.length)} selected`;
displayText = selectedLabels.join(", ");
}
return (

View File

@@ -151,7 +151,7 @@ function SingleSelect({
/>
{/* Options */}
<div className="space-y-3">
<div className="space-y-2">
{variant === "dropdown" ? (
<>
<ElementError errorMessage={errorMessage} dir={dir} />
@@ -160,15 +160,15 @@ function SingleSelect({
<Button
variant="outline"
disabled={disabled}
className="rounded-input w-full justify-between"
className="rounded-input w-full justify-between bg-option-bg rounded-option border border-option-border my-0 h-input"
aria-invalid={Boolean(errorMessage)}
aria-label={headline}>
<span className="truncate">{displayText}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<span className="truncate font-input font-input-weight text-input-text">{displayText}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50 label-headline" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="bg-option-bg w-[var(--radix-dropdown-menu-trigger-width)]"
className="bg-option-bg w-[var(--radix-dropdown-menu-trigger-width)] max-h-[300px] overflow-y-auto"
align="start">
<DropdownMenuRadioGroup value={selectedValue} onValueChange={onChange}>
{options
@@ -181,8 +181,9 @@ function SingleSelect({
key={option.id}
value={option.id}
id={optionId}
dir={dir}
disabled={disabled}>
<span className={optionLabelClassName}>{option.label}</span>
<span className="font-input font-input-weight text-input-text">{option.label}</span>
</DropdownMenuRadioItem>
);
})}
@@ -190,8 +191,9 @@ function SingleSelect({
<DropdownMenuRadioItem
value={otherOptionId}
id={`${inputId}-${otherOptionId}`}
dir={dir}
disabled={disabled}>
<span className={optionLabelClassName}>{otherValue || otherOptionLabel}</span>
<span className="font-input font-input-weight text-input-text">{otherValue || otherOptionLabel}</span>
</DropdownMenuRadioItem>
) : null}
{options
@@ -204,8 +206,9 @@ function SingleSelect({
key={option.id}
value={option.id}
id={optionId}
dir={dir}
disabled={disabled}>
<span className={optionLabelClassName}>{option.label}</span>
<span className="font-input font-input-weight text-input-text">{option.label}</span>
</DropdownMenuRadioItem>
);
})}

View File

@@ -1,5 +1,5 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { CheckIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -24,16 +24,18 @@ function DropdownMenuContent({
}: Readonly<React.ComponentProps<typeof DropdownMenuPrimitive.Content>>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
<div id="fbjs">
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</div>
</DropdownMenuPrimitive.Portal >
);
}
@@ -74,12 +76,12 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_*]:cursor-pointer",
className
)}
checked={checked}
{...props}>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center label-headline">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
@@ -102,13 +104,13 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_*]:cursor-pointer",
className
)}
{...props}>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center label-headline">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -178,7 +180,7 @@ function DropdownMenuSubTrigger({
)}
{...props}>
{children}
<ChevronRightIcon className="ml-auto size-4" />
<ChevronRightIcon className="ml-auto label-headline size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}

View File

@@ -32,7 +32,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
"shadow-input",
// Placeholder styling
"placeholder:opacity-input-placeholder",
"placeholder:text-input-placeholder placeholder:text-sm",
"placeholder:text-input-placeholder",
// Selection styling
"selection:bg-primary selection:text-primary-foreground",

View File

@@ -13,7 +13,7 @@ function Textarea({ className, dir = "auto", ...props }: TextareaProps): React.J
style={{ fontSize: "var(--fb-input-font-size)" }}
dir={dir}
className={cn(
"w-input bg-input-bg border-input-border rounded-input font-input font-input-weight px-input-x py-input-y shadow-input placeholder:text-input-placeholder placeholder:opacity-input-placeholder focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 text-input text-input-text flex field-sizing-content min-h-16 border transition-[color,box-shadow] outline-none placeholder:text-sm focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"w-input bg-input-bg border-input-border rounded-input font-input font-input-weight px-input-x py-input-y shadow-input placeholder:text-input-placeholder placeholder:opacity-input-placeholder focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 text-input text-input-text flex field-sizing-content min-h-16 border transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}

View File

@@ -22,6 +22,8 @@ checksums:
common/respondents_will_not_see_this_card: 18c3dd44d6ff6ca2310ad196b84f30d3
common/retry: 6e44d18639560596569a1278f9c83676
common/retrying: 0cb623dbdcbf16d3680f0180ceac734c
common/select_option: d68a0fb9afd0817dc31b3e9cb11855cb
common/select_options: d5a80087e889848e0fed3f1be359366f
common/sending_responses: 184772f70cca69424eaf34f73520789f
common/takes_less_than_x_minutes: 1208ce0d4c0a679c11c7bd209b6ccc47
common/takes_x_minutes: 001d12366d07b406f50669e761d63e69

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "لن يرى المستجيبون هذه البطاقة",
"retry": "إعادة المحاولة",
"retrying": "إعادة المحاولة...",
"select_option": "اختر خيارًا",
"select_options": "اختر الخيارات",
"sending_responses": "جارٍ إرسال الردود...",
"takes_less_than_x_minutes": "{count, plural, zero {يستغرق أقل من دقيقة} one {يستغرق أقل من دقيقة واحدة} two {يستغرق أقل من دقيقتين} few {يستغرق أقل من {count} دقائق} many {يستغرق أقل من {count} دقيقة} other {يستغرق أقل من {count} دقيقة}}",
"takes_x_minutes": "{count, plural, zero {يستغرق صفر دقائق} one {يستغرق دقيقة واحدة} two {يستغرق دقيقتين} few {يستغرق {count} دقائق} many {يستغرق {count} دقيقة} other {يستغرق {count} دقيقة}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Befragte werden diese Karte nicht sehen",
"retry": "Wiederholen",
"retrying": "Erneuter Versuch...",
"select_option": "Wähle eine Option",
"select_options": "Wähle Optionen",
"sending_responses": "Antworten werden gesendet...",
"takes_less_than_x_minutes": "{count, plural, one {Dauert weniger als 1 Minute} other {Dauert weniger als {count} Minuten}}",
"takes_x_minutes": "{count, plural, one {Dauert 1 Minute} other {Dauert {count} Minuten}}",

View File

@@ -28,7 +28,9 @@
"terms_of_service": "Terms of Service",
"the_servers_cannot_be_reached_at_the_moment": "The servers cannot be reached at the moment.",
"they_will_be_redirected_immediately": "They will be redirected immediately",
"your_feedback_is_stuck": "Your feedback is stuck :("
"your_feedback_is_stuck": "Your feedback is stuck :(",
"select_option": "Select an option",
"select_options": "Select options"
},
"errors": {
"all_options_must_be_ranked": "Please rank all options",
@@ -76,4 +78,4 @@
"value_must_not_contain": "Value must not contain {value}",
"value_must_not_equal": "Value must not equal {value}"
}
}
}

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Los encuestados no verán esta tarjeta",
"retry": "Reintentar",
"retrying": "Reintentando...",
"select_option": "Selecciona una opción",
"select_options": "Selecciona opciones",
"sending_responses": "Enviando respuestas...",
"takes_less_than_x_minutes": "{count, plural, one {Toma menos de 1 minuto} other {Toma menos de {count} minutos}}",
"takes_x_minutes": "{count, plural, one {Toma 1 minuto} other {Toma {count} minutos}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Les répondants ne verront pas cette carte",
"retry": "Réessayer",
"retrying": "Nouvelle tentative...",
"select_option": "Sélectionner une option",
"select_options": "Sélectionner des options",
"sending_responses": "Envoi des réponses...",
"takes_less_than_x_minutes": "{count, plural, one {Prend moins d'une minute} other {Prend moins de {count} minutes}}",
"takes_x_minutes": "{count, plural, one {Prend 1 minute} other {Prend {count} minutes}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "उत्तरदाता इस कार्ड को नहीं देखेंगे",
"retry": "पुनः प्रयास करें",
"retrying": "पुनः प्रयास कर रहे हैं...",
"select_option": "एक विकल्प चुनें",
"select_options": "विकल्प चुनें",
"sending_responses": "प्रतिक्रियाएँ भेज रहे हैं...",
"takes_less_than_x_minutes": "{count, plural, one {1 मिनट से कम लगता है} other {{count} मिनट से कम लगता है}}",
"takes_x_minutes": "{count, plural, one {1 मिनट लगता है} other {{count} मिनट लगते हैं}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "I rispondenti non vedranno questa scheda",
"retry": "Riprova",
"retrying": "Riprovando...",
"select_option": "Seleziona un'opzione",
"select_options": "Seleziona opzioni",
"sending_responses": "Invio risposte in corso...",
"takes_less_than_x_minutes": "{count, plural, one {Richiede meno di 1 minuto} other {Richiede meno di {count} minuti}}",
"takes_x_minutes": "{count, plural, one {Richiede 1 minuto} other {Richiede {count} minuti}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "回答者はこのカードを見ることができません",
"retry": "再試行",
"retrying": "再試行中...",
"select_option": "オプションを選択",
"select_options": "オプションを選択",
"sending_responses": "回答を送信中...",
"takes_less_than_x_minutes": "{count, plural, one {1分未満} other {{count}分未満}}",
"takes_x_minutes": "{count, plural, one {1分} other {{count}分}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Respondenten zien deze kaart niet",
"retry": "Opnieuw proberen",
"retrying": "Opnieuw proberen...",
"select_option": "Selecteer een optie",
"select_options": "Selecteer opties",
"sending_responses": "Reacties verzenden...",
"takes_less_than_x_minutes": "{count, plural, one {Duurt minder dan 1 minuut} other {Duurt minder dan {count} minuten}}",
"takes_x_minutes": "{count, plural, one {Duurt 1 minuut} other {Duurt {count} minuten}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Os respondentes não verão este cartão",
"retry": "Tentar novamente",
"retrying": "Tentando novamente...",
"select_option": "Selecione uma opção",
"select_options": "Selecione opções",
"sending_responses": "Enviando respostas...",
"takes_less_than_x_minutes": "{count, plural, one {Leva menos de 1 minuto} other {Leva menos de {count} minutos}}",
"takes_x_minutes": "{count, plural, one {Leva 1 minuto} other {Leva {count} minutos}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Respondenții nu vor vedea acest card",
"retry": "Reîncearcă",
"retrying": "Se reîncearcă...",
"select_option": "Selectează o opțiune",
"select_options": "Selectează opțiuni",
"sending_responses": "Trimiterea răspunsurilor...",
"takes_less_than_x_minutes": "{count, plural, one {Durează mai puțin de 1 minut} few {Durează mai puțin de {count} minute} other {Durează mai puțin de {count} de minute}}",
"takes_x_minutes": "{count, plural, one {Durează 1 minut} few {Durează {count} minute} other {Durează {count} de minute}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Респонденты не увидят эту карточку",
"retry": "Повторить",
"retrying": "Повторная попытка...",
"select_option": "Выбери вариант",
"select_options": "Выбери варианты",
"sending_responses": "Отправка ответов...",
"takes_less_than_x_minutes": "{count, plural, one {Займёт меньше 1 минуты} few {Займёт меньше {count} минут} many {Займёт меньше {count} минут} other {Займёт меньше {count} минуты}}",
"takes_x_minutes": "{count, plural, one {Займёт 1 минуту} few {Займёт {count} минуты} many {Займёт {count} минут} other {Займёт {count} минуты}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Respondenter kommer inte att se detta kort",
"retry": "Försök igen",
"retrying": "Försöker igen...",
"select_option": "Välj ett alternativ",
"select_options": "Välj alternativ",
"sending_responses": "Skickar svar...",
"takes_less_than_x_minutes": "{count, plural, one {Tar mindre än 1 minut} other {Tar mindre än {count} minuter}}",
"takes_x_minutes": "{count, plural, one {Tar 1 minut} other {Tar {count} minuter}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "Javob beruvchilar ushbu kartani ko'rmaydi",
"retry": "Qayta urinib ko'ring",
"retrying": "Qayta urinilmoqda...",
"select_option": "Variantni tanla",
"select_options": "Variantlarni tanla",
"sending_responses": "Javoblar yuborilmoqda...",
"takes_less_than_x_minutes": "{count, plural, one {1 daqiqadan kam vaqt oladi} other {{count} daqiqadan kam vaqt oladi}}",
"takes_x_minutes": "{count, plural, one {1 daqiqa vaqt oladi} other {{count} daqiqa vaqt oladi}}",

View File

@@ -21,6 +21,8 @@
"respondents_will_not_see_this_card": "受访者将不会看到此卡片",
"retry": "重试",
"retrying": "重试中...",
"select_option": "请选择一个选项",
"select_options": "请选择多个选项",
"sending_responses": "正在发送响应...",
"takes_less_than_x_minutes": "{count, plural, one {少于 1 分钟} other {少于 {count} 分钟}}",
"takes_x_minutes": "{count, plural, one {1 分钟} other {{count} 分钟}}",

View File

@@ -30,11 +30,11 @@
}
},
"scripts": {
"dev": "vite build --watch --mode dev",
"dev": "concurrently -n \"ESM,UMD\" \"vite build --watch --mode dev\" \"BUILD_UMD=true vite build --watch --mode dev\"",
"build": "tsc && vite build && BUILD_UMD=true vite build",
"build:analyze": "tsc && ANALYZE=true vite build",
"build:dev": "tsc && vite build --mode dev",
"go": "vite build --watch --mode dev",
"go": "concurrently -n \"ESM,UMD\" \"vite build --watch --mode dev\" \"BUILD_UMD=true vite build --watch --mode dev\"",
"lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
"preview": "vite preview",
"clean": "rimraf .turbo node_modules dist",
@@ -70,4 +70,4 @@
"vite-plugin-dts": "4.5.3",
"vite-tsconfig-paths": "5.1.4"
}
}
}

View File

@@ -254,6 +254,8 @@ export function MultipleChoiceMultiElement({
requiredLabel={t("common.required")}
errorMessage={errorMessage}
dir={dir}
placeholder={t("common.select_options")}
variant={element.displayType ?? "list"}
otherOptionId={otherOption?.id}
otherOptionLabel={otherOption ? getLocalizedValue(otherOption.label, languageCode) : undefined}
otherOptionPlaceholder={

View File

@@ -174,6 +174,8 @@ export function MultipleChoiceSingleElement({
requiredLabel={t("common.required")}
errorMessage={errorMessage}
dir={dir}
placeholder={t("common.select_option")}
variant={element.displayType ?? "list"}
otherOptionId={otherOption?.id}
otherOptionLabel={otherOption ? getLocalizedValue(otherOption.label, languageCode) : undefined}
otherOptionPlaceholder={

View File

@@ -139,6 +139,9 @@ export type TSurveyElementChoice = z.infer<typeof ZSurveyElementChoice>;
export const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
export const ZMultipleChoiceOptionDisplayType = z.enum(["list", "dropdown"]);
export type TMultipleChoiceOptionDisplayType = z.infer<typeof ZMultipleChoiceOptionDisplayType>;
// Multiple Choice Single Element
export const ZSurveyMultipleChoiceSingleElement = ZSurveyElementBase.extend({
type: z.literal(TSurveyElementTypeEnum.MultipleChoiceSingle),
@@ -147,6 +150,7 @@ export const ZSurveyMultipleChoiceSingleElement = ZSurveyElementBase.extend({
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
shuffleOption: ZShuffleOption.optional(),
otherOptionPlaceholder: ZI18nString.optional(),
displayType: ZMultipleChoiceOptionDisplayType.optional(),
});
// Multiple Choice Multi Element
@@ -158,6 +162,7 @@ export const ZSurveyMultipleChoiceMultiElement = ZSurveyElementBase.extend({
shuffleOption: ZShuffleOption.optional(),
otherOptionPlaceholder: ZI18nString.optional(),
validation: ZValidation.optional(),
displayType: ZMultipleChoiceOptionDisplayType.optional(),
});
// Union type for Multiple Choice Elements