mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
Compare commits
47 Commits
chore/basi
...
survey-hei
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d302dffe | ||
|
|
e9c45daf71 | ||
|
|
259f219eea | ||
|
|
828ce86764 | ||
|
|
f4fe8674fd | ||
|
|
25e4575172 | ||
|
|
acb6db4939 | ||
|
|
19221a65d7 | ||
|
|
a65cd855dc | ||
|
|
dabd5d361f | ||
|
|
dc47586813 | ||
|
|
be1a6f01c3 | ||
|
|
8dbad6f5ef | ||
|
|
f75e5bc7b4 | ||
|
|
9930f9af03 | ||
|
|
3ff20d04e0 | ||
|
|
8bac86638c | ||
|
|
100b15ac88 | ||
|
|
ddb1de95c4 | ||
|
|
913a6b5135 | ||
|
|
8b56786be5 | ||
|
|
a8a8cf6c88 | ||
|
|
dc0cc5e526 | ||
|
|
00e0307c81 | ||
|
|
84d4c59532 | ||
|
|
fba455c47f | ||
|
|
fd777ca227 | ||
|
|
512e9fb0a7 | ||
|
|
d9be37a134 | ||
|
|
187e509b41 | ||
|
|
9499e6265b | ||
|
|
3564faa638 | ||
|
|
5356ce4ed2 | ||
|
|
03ddf3d09a | ||
|
|
31496ee092 | ||
|
|
0f2b5e1709 | ||
|
|
b5a0b165ed | ||
|
|
25f8b2d07f | ||
|
|
0b88e58dcb | ||
|
|
89985d4f4f | ||
|
|
39e5518f2c | ||
|
|
1954c5ca61 | ||
|
|
b7679aa336 | ||
|
|
9e7c1d5245 | ||
|
|
4f9f064e76 | ||
|
|
6a9833dbeb | ||
|
|
94070774ad |
@@ -41,7 +41,7 @@ describe("Survey Builder", () => {
|
|||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
shuffleOption: "none",
|
shuffleOption: "none",
|
||||||
required: true,
|
required: false,
|
||||||
});
|
});
|
||||||
expect(question.choices.length).toBe(3);
|
expect(question.choices.length).toBe(3);
|
||||||
expect(question.id).toBeDefined();
|
expect(question.id).toBeDefined();
|
||||||
@@ -141,7 +141,7 @@ describe("Survey Builder", () => {
|
|||||||
inputType: "text",
|
inputType: "text",
|
||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
required: true,
|
required: false,
|
||||||
charLimit: {
|
charLimit: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
@@ -204,7 +204,7 @@ describe("Survey Builder", () => {
|
|||||||
range: 5,
|
range: 5,
|
||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
required: true,
|
required: false,
|
||||||
isColorCodingEnabled: false,
|
isColorCodingEnabled: false,
|
||||||
});
|
});
|
||||||
expect(question.id).toBeDefined();
|
expect(question.id).toBeDefined();
|
||||||
@@ -265,7 +265,7 @@ describe("Survey Builder", () => {
|
|||||||
headline: { default: "NPS Question" },
|
headline: { default: "NPS Question" },
|
||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
required: true,
|
required: false,
|
||||||
isColorCodingEnabled: false,
|
isColorCodingEnabled: false,
|
||||||
});
|
});
|
||||||
expect(question.id).toBeDefined();
|
expect(question.id).toBeDefined();
|
||||||
@@ -324,7 +324,7 @@ describe("Survey Builder", () => {
|
|||||||
label: { default: "I agree to terms" },
|
label: { default: "I agree to terms" },
|
||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
required: true,
|
required: false,
|
||||||
});
|
});
|
||||||
expect(question.id).toBeDefined();
|
expect(question.id).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -377,7 +377,7 @@ describe("Survey Builder", () => {
|
|||||||
headline: { default: "CTA Question" },
|
headline: { default: "CTA Question" },
|
||||||
buttonLabel: { default: "common.next" },
|
buttonLabel: { default: "common.next" },
|
||||||
backButtonLabel: { default: "common.back" },
|
backButtonLabel: { default: "common.back" },
|
||||||
required: true,
|
required: false,
|
||||||
buttonExternal: false,
|
buttonExternal: false,
|
||||||
});
|
});
|
||||||
expect(question.id).toBeDefined();
|
expect(question.id).toBeDefined();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const buildMultipleChoiceQuestion = ({
|
|||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
shuffleOption: shuffleOption || "none",
|
shuffleOption: shuffleOption || "none",
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
logic,
|
logic,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -105,7 +105,7 @@ export const buildOpenTextQuestion = ({
|
|||||||
headline: { default: headline },
|
headline: { default: headline },
|
||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
longAnswer,
|
longAnswer,
|
||||||
logic,
|
logic,
|
||||||
charLimit: {
|
charLimit: {
|
||||||
@@ -153,7 +153,7 @@ export const buildRatingQuestion = ({
|
|||||||
range,
|
range,
|
||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
isColorCodingEnabled,
|
isColorCodingEnabled,
|
||||||
lowerLabel: lowerLabel ? { default: lowerLabel } : undefined,
|
lowerLabel: lowerLabel ? { default: lowerLabel } : undefined,
|
||||||
upperLabel: upperLabel ? { default: upperLabel } : undefined,
|
upperLabel: upperLabel ? { default: upperLabel } : undefined,
|
||||||
@@ -194,7 +194,7 @@ export const buildNPSQuestion = ({
|
|||||||
headline: { default: headline },
|
headline: { default: headline },
|
||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
isColorCodingEnabled,
|
isColorCodingEnabled,
|
||||||
lowerLabel: lowerLabel ? { default: lowerLabel } : undefined,
|
lowerLabel: lowerLabel ? { default: lowerLabel } : undefined,
|
||||||
upperLabel: upperLabel ? { default: upperLabel } : undefined,
|
upperLabel: upperLabel ? { default: upperLabel } : undefined,
|
||||||
@@ -230,7 +230,7 @@ export const buildConsentQuestion = ({
|
|||||||
headline: { default: headline },
|
headline: { default: headline },
|
||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
label: { default: label },
|
label: { default: label },
|
||||||
logic,
|
logic,
|
||||||
};
|
};
|
||||||
@@ -269,7 +269,7 @@ export const buildCTAQuestion = ({
|
|||||||
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
buttonLabel: { default: buttonLabel || t(defaultButtonLabel) },
|
||||||
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) },
|
||||||
dismissButtonLabel: dismissButtonLabel ? { default: dismissButtonLabel } : undefined,
|
dismissButtonLabel: dismissButtonLabel ? { default: dismissButtonLabel } : undefined,
|
||||||
required: required ?? true,
|
required: required ?? false,
|
||||||
buttonExternal,
|
buttonExternal,
|
||||||
buttonUrl,
|
buttonUrl,
|
||||||
logic,
|
logic,
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ export const env = createEnv({
|
|||||||
PROMETHEUS_EXPORTER_PORT: z.string().optional(),
|
PROMETHEUS_EXPORTER_PORT: z.string().optional(),
|
||||||
PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(),
|
PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(),
|
||||||
USER_MANAGEMENT_MINIMUM_ROLE: z.enum(["owner", "manager", "disabled"]).optional(),
|
USER_MANAGEMENT_MINIMUM_ROLE: z.enum(["owner", "manager", "disabled"]).optional(),
|
||||||
SESSION_MAX_AGE: z.string().transform((val) => parseInt(val)).optional(),
|
SESSION_MAX_AGE: z
|
||||||
|
.string()
|
||||||
|
.transform((val) => parseInt(val))
|
||||||
|
.optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "Login mit SAML SSO",
|
"continue_with_saml": "Login mit SAML SSO",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "Bitte bestätige dein Passwort, bevor du deine E-Mail-Adresse änderst",
|
"confirm_password_description": "Bitte bestätige dein Passwort, bevor du deine E-Mail-Adresse änderst",
|
||||||
"email_already_exists": "Diese E-Mail wird bereits verwendet",
|
|
||||||
"email_change_success": "E-Mail erfolgreich geändert",
|
"email_change_success": "E-Mail erfolgreich geändert",
|
||||||
"email_change_success_description": "Du hast deine E-Mail-Adresse erfolgreich geändert. Bitte logge dich mit deiner neuen E-Mail-Adresse ein.",
|
"email_change_success_description": "Du hast deine E-Mail-Adresse erfolgreich geändert. Bitte logge dich mit deiner neuen E-Mail-Adresse ein.",
|
||||||
"email_verification_failed": "E-Mail-Bestätigung fehlgeschlagen",
|
"email_verification_failed": "E-Mail-Bestätigung fehlgeschlagen",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
|
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
|
||||||
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",
|
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",
|
||||||
"lost_access": "Zugriff verloren",
|
"lost_access": "Zugriff verloren",
|
||||||
"new_email_update_success": "Deine Anfrage zur Änderung der E-Mail wurde erhalten.",
|
|
||||||
"or_enter_the_following_code_manually": "Oder gib den folgenden Code manuell ein:",
|
"or_enter_the_following_code_manually": "Oder gib den folgenden Code manuell ein:",
|
||||||
"organization_identification": "Hilf deiner Organisation, Dich auf Formbricks zu identifizieren",
|
"organization_identification": "Hilf deiner Organisation, Dich auf Formbricks zu identifizieren",
|
||||||
"organizations_delete_message": "Du bist der einzige Besitzer dieser Organisationen, also werden sie <b>auch gelöscht.</b>",
|
"organizations_delete_message": "Du bist der einzige Besitzer dieser Organisationen, also werden sie <b>auch gelöscht.</b>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "Continue with SAML SSO",
|
"continue_with_saml": "Continue with SAML SSO",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "Please confirm your password before changing your email address",
|
"confirm_password_description": "Please confirm your password before changing your email address",
|
||||||
"email_already_exists": "This email is already in use",
|
|
||||||
"email_change_success": "Email changed successfully",
|
"email_change_success": "Email changed successfully",
|
||||||
"email_change_success_description": "You have successfully changed your email address. Please log in with your new email address.",
|
"email_change_success_description": "You have successfully changed your email address. Please log in with your new email address.",
|
||||||
"email_verification_failed": "Email verification failed",
|
"email_verification_failed": "Email verification failed",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
|
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
|
||||||
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",
|
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",
|
||||||
"lost_access": "Lost access",
|
"lost_access": "Lost access",
|
||||||
"new_email_update_success": "Your email change request was received.",
|
|
||||||
"or_enter_the_following_code_manually": "Or enter the following code manually:",
|
"or_enter_the_following_code_manually": "Or enter the following code manually:",
|
||||||
"organization_identification": "Assist your organization in identifying you on Formbricks",
|
"organization_identification": "Assist your organization in identifying you on Formbricks",
|
||||||
"organizations_delete_message": "You are the only owner of these organizations, so they <b>will be deleted as well.</b>",
|
"organizations_delete_message": "You are the only owner of these organizations, so they <b>will be deleted as well.</b>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "Continuer avec SAML SSO",
|
"continue_with_saml": "Continuer avec SAML SSO",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "Veuillez confirmer votre mot de passe avant de changer votre adresse e-mail",
|
"confirm_password_description": "Veuillez confirmer votre mot de passe avant de changer votre adresse e-mail",
|
||||||
"email_already_exists": "Cet e-mail est déjà utilisé",
|
|
||||||
"email_change_success": "E-mail changé avec succès",
|
"email_change_success": "E-mail changé avec succès",
|
||||||
"email_change_success_description": "Vous avez changé votre adresse e-mail avec succès. Veuillez vous connecter avec votre nouvelle adresse e-mail.",
|
"email_change_success_description": "Vous avez changé votre adresse e-mail avec succès. Veuillez vous connecter avec votre nouvelle adresse e-mail.",
|
||||||
"email_verification_failed": "Échec de la vérification de l'email",
|
"email_verification_failed": "Échec de la vérification de l'email",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "La taille du fichier doit être inférieure à 10 Mo.",
|
"file_size_must_be_less_than_10mb": "La taille du fichier doit être inférieure à 10 Mo.",
|
||||||
"invalid_file_type": "Type de fichier invalide. Seuls les fichiers JPEG, PNG et WEBP sont autorisés.",
|
"invalid_file_type": "Type de fichier invalide. Seuls les fichiers JPEG, PNG et WEBP sont autorisés.",
|
||||||
"lost_access": "Accès perdu",
|
"lost_access": "Accès perdu",
|
||||||
"new_email_update_success": "Votre demande de changement d'email a été reçue.",
|
|
||||||
"or_enter_the_following_code_manually": "Ou entrez le code suivant manuellement :",
|
"or_enter_the_following_code_manually": "Ou entrez le code suivant manuellement :",
|
||||||
"organization_identification": "Aidez votre organisation à vous identifier sur Formbricks",
|
"organization_identification": "Aidez votre organisation à vous identifier sur Formbricks",
|
||||||
"organizations_delete_message": "Tu es le seul propriétaire de ces organisations, elles <b>seront aussi supprimées.</b>",
|
"organizations_delete_message": "Tu es le seul propriétaire de ces organisations, elles <b>seront aussi supprimées.</b>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "Continuar com SAML SSO",
|
"continue_with_saml": "Continuar com SAML SSO",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "Por favor, confirme sua senha antes de mudar seu endereço de e-mail",
|
"confirm_password_description": "Por favor, confirme sua senha antes de mudar seu endereço de e-mail",
|
||||||
"email_already_exists": "Este e-mail já está em uso",
|
|
||||||
"email_change_success": "E-mail alterado com sucesso",
|
"email_change_success": "E-mail alterado com sucesso",
|
||||||
"email_change_success_description": "Você alterou seu endereço de e-mail com sucesso. Por favor, faça login com seu novo endereço de e-mail.",
|
"email_change_success_description": "Você alterou seu endereço de e-mail com sucesso. Por favor, faça login com seu novo endereço de e-mail.",
|
||||||
"email_verification_failed": "Falha na verificação do e-mail",
|
"email_verification_failed": "Falha na verificação do e-mail",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
|
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
|
||||||
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",
|
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",
|
||||||
"lost_access": "Perdi o acesso",
|
"lost_access": "Perdi o acesso",
|
||||||
"new_email_update_success": "Sua solicitação de alteração de e-mail foi recebida.",
|
|
||||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||||
"organization_identification": "Ajude sua organização a te identificar no Formbricks",
|
"organization_identification": "Ajude sua organização a te identificar no Formbricks",
|
||||||
"organizations_delete_message": "Você é o único dono dessas organizações, então elas <b>também serão apagadas.</b>",
|
"organizations_delete_message": "Você é o único dono dessas organizações, então elas <b>também serão apagadas.</b>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "Continuar com SAML SSO",
|
"continue_with_saml": "Continuar com SAML SSO",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "Por favor, confirme a sua palavra-passe antes de alterar o seu endereço de email",
|
"confirm_password_description": "Por favor, confirme a sua palavra-passe antes de alterar o seu endereço de email",
|
||||||
"email_already_exists": "Este email já está a ser utilizado",
|
|
||||||
"email_change_success": "Email alterado com sucesso",
|
"email_change_success": "Email alterado com sucesso",
|
||||||
"email_change_success_description": "Alterou com sucesso o seu endereço de email. Por favor, inicie sessão com o seu novo endereço de email.",
|
"email_change_success_description": "Alterou com sucesso o seu endereço de email. Por favor, inicie sessão com o seu novo endereço de email.",
|
||||||
"email_verification_failed": "Falha na verificação do email",
|
"email_verification_failed": "Falha na verificação do email",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "O tamanho do ficheiro deve ser inferior a 10MB.",
|
"file_size_must_be_less_than_10mb": "O tamanho do ficheiro deve ser inferior a 10MB.",
|
||||||
"invalid_file_type": "Tipo de ficheiro inválido. Apenas são permitidos ficheiros JPEG, PNG e WEBP.",
|
"invalid_file_type": "Tipo de ficheiro inválido. Apenas são permitidos ficheiros JPEG, PNG e WEBP.",
|
||||||
"lost_access": "Perdeu o acesso",
|
"lost_access": "Perdeu o acesso",
|
||||||
"new_email_update_success": "O seu pedido de alteração de email foi recebido.",
|
|
||||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||||
"organization_identification": "Ajude a sua organização a identificá-lo no Formbricks",
|
"organization_identification": "Ajude a sua organização a identificá-lo no Formbricks",
|
||||||
"organizations_delete_message": "É o único proprietário destas organizações, por isso <b>também serão eliminadas.</b>",
|
"organizations_delete_message": "É o único proprietário destas organizações, por isso <b>também serão eliminadas.</b>",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"continue_with_saml": "使用 SAML SSO 繼續",
|
"continue_with_saml": "使用 SAML SSO 繼續",
|
||||||
"email-change": {
|
"email-change": {
|
||||||
"confirm_password_description": "在更改您的電子郵件地址之前,請確認您的密碼",
|
"confirm_password_description": "在更改您的電子郵件地址之前,請確認您的密碼",
|
||||||
"email_already_exists": "此電子郵件地址已被使用",
|
|
||||||
"email_change_success": "電子郵件已成功更改",
|
"email_change_success": "電子郵件已成功更改",
|
||||||
"email_change_success_description": "您已成功更改電子郵件地址。請使用您的新電子郵件地址登入。",
|
"email_change_success_description": "您已成功更改電子郵件地址。請使用您的新電子郵件地址登入。",
|
||||||
"email_verification_failed": "電子郵件驗證失敗",
|
"email_verification_failed": "電子郵件驗證失敗",
|
||||||
@@ -1158,7 +1157,6 @@
|
|||||||
"file_size_must_be_less_than_10mb": "檔案大小必須小於 10MB。",
|
"file_size_must_be_less_than_10mb": "檔案大小必須小於 10MB。",
|
||||||
"invalid_file_type": "無效的檔案類型。僅允許 JPEG、PNG 和 WEBP 檔案。",
|
"invalid_file_type": "無效的檔案類型。僅允許 JPEG、PNG 和 WEBP 檔案。",
|
||||||
"lost_access": "無法存取",
|
"lost_access": "無法存取",
|
||||||
"new_email_update_success": "您的 email 更改請求已收到。",
|
|
||||||
"or_enter_the_following_code_manually": "或手動輸入下列程式碼:",
|
"or_enter_the_following_code_manually": "或手動輸入下列程式碼:",
|
||||||
"organization_identification": "協助您的組織在 Formbricks 上識別您",
|
"organization_identification": "協助您的組織在 Formbricks 上識別您",
|
||||||
"organizations_delete_message": "您是這些組織的唯一擁有者,因此它們也 <b>將被刪除。</b>",
|
"organizations_delete_message": "您是這些組織的唯一擁有者,因此它們也 <b>將被刪除。</b>",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const EditPublicSurveyAlertDialog = ({
|
|||||||
label: secondaryButtonText,
|
label: secondaryButtonText,
|
||||||
onClick: secondaryButtonAction,
|
onClick: secondaryButtonAction,
|
||||||
disabled: isLoading,
|
disabled: isLoading,
|
||||||
variant: "outline",
|
variant: "secondary",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (primaryButtonAction) {
|
if (primaryButtonAction) {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-8 mt-6 space-y-6">
|
<div className="mt-6 space-y-6">
|
||||||
<AdvancedOptionToggle
|
<AdvancedOptionToggle
|
||||||
isChecked={question.allowMultipleFiles}
|
isChecked={question.allowMultipleFiles}
|
||||||
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export const getCXQuestionNameMap = (t: TFnType) =>
|
|||||||
) as Record<TSurveyQuestionTypeEnum, string>;
|
) as Record<TSurveyQuestionTypeEnum, string>;
|
||||||
|
|
||||||
export const universalQuestionPresets = {
|
export const universalQuestionPresets = {
|
||||||
required: true,
|
required: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQuestionDefaults = (id: string, project: any, t: TFnType) => {
|
export const getQuestionDefaults = (id: string, project: any, t: TFnType) => {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const LinkSurveyWrapper = ({
|
|||||||
surveyType={surveyType}
|
surveyType={surveyType}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
onBackgroundLoaded={handleBackgroundLoaded}>
|
onBackgroundLoaded={handleBackgroundLoaded}>
|
||||||
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
|
<div className="flex max-h-dvh min-h-dvh items-start justify-center overflow-clip pt-[16dvh]">
|
||||||
{!styling.isLogoHidden && project.logo?.url && <ClientLogo projectLogo={project.logo} />}
|
{!styling.isLogoHidden && project.logo?.url && <ClientLogo projectLogo={project.logo} />}
|
||||||
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
||||||
{isPreview && (
|
{isPreview && (
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ContentRef}
|
ref={ContentRef}
|
||||||
|
id="mobile-preview"
|
||||||
className={`relative h-[90%] max-h-[42rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
|
className={`relative h-[90%] max-h-[42rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
|
||||||
{/* below element is use to create notch for the mobile device mockup */}
|
{/* below element is use to create notch for the mobile device mockup */}
|
||||||
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
|
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
|
||||||
@@ -175,10 +176,10 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
|||||||
);
|
);
|
||||||
} else if (isEditorView) {
|
} else if (isEditorView) {
|
||||||
return (
|
return (
|
||||||
<div ref={ContentRef} className="overflow-hiddem flex flex-grow flex-col rounded-b-lg">
|
<div ref={ContentRef} className="flex flex-grow flex-col overflow-hidden rounded-b-lg">
|
||||||
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
|
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
|
||||||
{renderBackground()}
|
{renderBackground()}
|
||||||
<div className="flex h-full w-full items-center justify-center">{children}</div>
|
<div className="flex h-full w-full items-start justify-center pt-[10dvh]">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export const PreviewSurvey = ({
|
|||||||
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="z-10 w-full max-w-md rounded-lg border border-transparent">
|
<div className="z-10 w-full rounded-lg border border-transparent">
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
isPreviewMode={true}
|
isPreviewMode={true}
|
||||||
survey={{ ...survey, type: "link" }}
|
survey={{ ...survey, type: "link" }}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
|||||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).not.toBeVisible();
|
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
await page.locator("path").nth(3).click();
|
await page.locator("path").nth(3).click();
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
|
|
||||||
for (let i = 0; i < 11; i++) {
|
for (let i = 0; i < 11; i++) {
|
||||||
@@ -135,7 +135,7 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
|||||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
await page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
await page.getByLabel(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||||
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// Picture Select Question
|
// Picture Select Question
|
||||||
@@ -760,7 +760,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
|||||||
page.locator("#questionCard-4").getByText(surveys.createWithLogicAndSubmit.ratingQuestion.highLabel)
|
page.locator("#questionCard-4").getByText(surveys.createWithLogicAndSubmit.ratingQuestion.highLabel)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
await page.getByRole("group", { name: "Choices" }).locator("path").nth(3).click();
|
await page.getByRole("group", { name: "Choices" }).locator("path").nth(3).click();
|
||||||
|
|
||||||
@@ -772,7 +772,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.locator("#questionCard-5").getByText(surveys.createWithLogicAndSubmit.npsQuestion.highLabel)
|
page.locator("#questionCard-5").getByText(surveys.createWithLogicAndSubmit.npsQuestion.highLabel)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Next" })).not.toBeVisible();
|
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-5").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
|
|
||||||
for (let i = 0; i < 11; i++) {
|
for (let i = 0; i < 11; i++) {
|
||||||
@@ -831,7 +831,7 @@ test.describe("Testing Survey with advanced logic", async () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).toBeVisible();
|
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).toBeVisible();
|
||||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
||||||
await page.getByText(surveys.createWithLogicAndSubmit.consentQuestion.checkboxLabel).check();
|
await page.getByLabel(surveys.createWithLogicAndSubmit.consentQuestion.checkboxLabel).check();
|
||||||
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// File Upload Question
|
// File Upload Question
|
||||||
|
|||||||
@@ -418,7 +418,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
await page.getByPlaceholder("Option 1").fill(params.singleSelectQuestion.options[0]);
|
await page.getByPlaceholder("Option 1").fill(params.singleSelectQuestion.options[0]);
|
||||||
await page.getByPlaceholder("Option 2").fill(params.singleSelectQuestion.options[1]);
|
await page.getByPlaceholder("Option 2").fill(params.singleSelectQuestion.options[1]);
|
||||||
await page.getByRole("button", { name: 'Add "Other"', exact: true }).click();
|
await page.getByRole("button", { name: 'Add "Other"', exact: true }).click();
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Multi Select Question
|
// Multi Select Question
|
||||||
await page
|
await page
|
||||||
@@ -463,8 +462,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Rating Question
|
// Rating Question
|
||||||
await page
|
await page
|
||||||
.locator("div")
|
.locator("div")
|
||||||
@@ -510,7 +507,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
await page.getByRole("button", { name: "Add option" }).click();
|
await page.getByRole("button", { name: "Add option" }).click();
|
||||||
await page.getByPlaceholder("Option 5").click();
|
await page.getByPlaceholder("Option 5").click();
|
||||||
await page.getByPlaceholder("Option 5").fill(params.ranking.choices[4]);
|
await page.getByPlaceholder("Option 5").fill(params.ranking.choices[4]);
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Matrix Question
|
// Matrix Question
|
||||||
await page
|
await page
|
||||||
@@ -549,7 +545,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
await page.getByRole("button", { name: "Statement (Call to Action)" }).click();
|
await page.getByRole("button", { name: "Statement (Call to Action)" }).click();
|
||||||
await page.getByPlaceholder("Your question here. Recall").fill(params.ctaQuestion.question);
|
await page.getByPlaceholder("Your question here. Recall").fill(params.ctaQuestion.question);
|
||||||
await page.getByPlaceholder("Finish").fill(params.ctaQuestion.buttonLabel);
|
await page.getByPlaceholder("Finish").fill(params.ctaQuestion.buttonLabel);
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Consent Question
|
// Consent Question
|
||||||
await page
|
await page
|
||||||
@@ -578,7 +573,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
.click();
|
.click();
|
||||||
await page.getByRole("button", { name: "Date" }).click();
|
await page.getByRole("button", { name: "Date" }).click();
|
||||||
await page.getByLabel("Question*").fill(params.date.question);
|
await page.getByLabel("Question*").fill(params.date.question);
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Cal Question
|
// Cal Question
|
||||||
await page
|
await page
|
||||||
@@ -588,7 +582,6 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
.click();
|
.click();
|
||||||
await page.getByRole("button", { name: "Schedule a meeting" }).click();
|
await page.getByRole("button", { name: "Schedule a meeting" }).click();
|
||||||
await page.getByLabel("Question*").fill(params.cal.question);
|
await page.getByLabel("Question*").fill(params.cal.question);
|
||||||
await page.getByLabel("Required").click();
|
|
||||||
|
|
||||||
// Fill Address Question
|
// Fill Address Question
|
||||||
await page
|
await page
|
||||||
@@ -633,8 +626,8 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
|
|||||||
await page.getByRole("option", { name: "secret" }).click();
|
await page.getByRole("option", { name: "secret" }).click();
|
||||||
await page.locator("#action-2-operator").click();
|
await page.locator("#action-2-operator").click();
|
||||||
await page.getByRole("option", { name: "Assign =" }).click();
|
await page.getByRole("option", { name: "Assign =" }).click();
|
||||||
await page.getByRole("textbox", { name: "Value" }).click();
|
await page.locator("#action-2-value-input").click();
|
||||||
await page.getByRole("textbox", { name: "Value" }).fill("This ");
|
await page.locator("#action-2-value-input").fill("1");
|
||||||
|
|
||||||
// Single Select Question
|
// Single Select Question
|
||||||
await page.getByRole("heading", { name: params.singleSelectQuestion.question }).click();
|
await page.getByRole("heading", { name: params.singleSelectQuestion.question }).click();
|
||||||
|
|||||||
@@ -258,7 +258,9 @@ export const setup = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||||
logger.debug(`${surveyNames.length.toString()} surveys could be shown to current user on trigger: ${surveyNames.join(", ")}`);
|
logger.debug(
|
||||||
|
`${surveyNames.length.toString()} surveys could be shown to current user on trigger: ${surveyNames.join(", ")}`
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
logger.debug("Error during sync. Please try again.");
|
logger.debug("Error during sync. Please try again.");
|
||||||
}
|
}
|
||||||
@@ -314,9 +316,11 @@ export const setup = async (
|
|||||||
environment: environmentState,
|
environment: environmentState,
|
||||||
filteredSurveys,
|
filteredSurveys,
|
||||||
});
|
});
|
||||||
|
|
||||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||||
logger.debug(`${surveyNames.length.toString()} surveys could be shown to current user on trigger: ${surveyNames.join(", ")}`);
|
logger.debug(
|
||||||
|
`${surveyNames.length.toString()} surveys could be shown to current user on trigger: ${surveyNames.join(", ")}`
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await handleErrorOnFirstSetup(e as { code: string; responseMessage: string });
|
await handleErrorOnFirstSetup(e as { code: string; responseMessage: string });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,8 +109,10 @@ export function EndingCard({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this effect when isCurrent changes
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this effect when isCurrent changes
|
||||||
}, [isCurrent]);
|
}, [isCurrent]);
|
||||||
|
|
||||||
|
const marginPreservingHeight = survey.type === "app" ? "fb-my-[37px]" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableContainer>
|
<ScrollableContainer className={marginPreservingHeight}>
|
||||||
<div className="fb-text-center">
|
<div className="fb-text-center">
|
||||||
{isResponseSendingFinished ? (
|
{isResponseSendingFinished ? (
|
||||||
<>
|
<>
|
||||||
@@ -136,7 +138,7 @@ export function EndingCard({
|
|||||||
questionId="EndingCard"
|
questionId="EndingCard"
|
||||||
/>
|
/>
|
||||||
{endingCard.buttonLabel ? (
|
{endingCard.buttonLabel ? (
|
||||||
<div className="fb-mt-6 fb-flex fb-w-full fb-flex-col fb-items-center fb-justify-center fb-space-y-4">
|
<div className="fb-mt-4 fb-flex fb-w-full fb-flex-col fb-items-center fb-justify-center fb-space-y-4">
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
buttonLabel={replaceRecallInfo(
|
buttonLabel={replaceRecallInfo(
|
||||||
getLocalizedValue(endingCard.buttonLabel, languageCode),
|
getLocalizedValue(endingCard.buttonLabel, languageCode),
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export function FileInput({
|
|||||||
{showUploader ? (
|
{showUploader ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-6 hover:fb-cursor-pointer w-full"
|
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-10 hover:fb-cursor-pointer w-full"
|
||||||
aria-label="Upload files by clicking or dragging them here"
|
aria-label="Upload files by clicking or dragging them here"
|
||||||
onClick={() => document.getElementById(uniqueHtmlFor)?.click()}>
|
onClick={() => document.getElementById(uniqueHtmlFor)?.click()}>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload";
|
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fb-group/image fb-relative fb-mb-4 fb-block fb-min-h-40 fb-rounded-md">
|
<div className="fb-group/image fb-relative fb-mb-6 fb-block fb-min-h-40 fb-rounded-md">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="fb-absolute fb-inset-auto fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
<div className="fb-absolute fb-inset-auto fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -35,7 +36,10 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
|||||||
key={imgUrl}
|
key={imgUrl}
|
||||||
src={imgUrl}
|
src={imgUrl}
|
||||||
alt={altText}
|
alt={altText}
|
||||||
className="fb-rounded-custom"
|
className={cn(
|
||||||
|
"fb-rounded-custom fb-max-h-[40dvh] fb-mx-auto fb-object-contain",
|
||||||
|
isLoading ? "fb-opacity-0" : ""
|
||||||
|
)}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
@@ -48,7 +52,7 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
|||||||
src={videoUrlWithParams}
|
src={videoUrlWithParams}
|
||||||
title="Question Video"
|
title="Question Video"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
className="fb-rounded-custom fb-aspect-video fb-w-full"
|
className={cn("fb-rounded-custom fb-aspect-video fb-w-full", isLoading ? "fb-opacity-0" : "")}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { render } from "@testing-library/preact";
|
import { render } from "@testing-library/preact";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
// Ensure screen is imported
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
// Use test consistently
|
||||||
import { RenderSurvey } from "./render-survey";
|
import { RenderSurvey } from "./render-survey";
|
||||||
|
|
||||||
// Stub SurveyContainer to render children and capture props
|
// Stub SurveyContainer to render children and capture props
|
||||||
@@ -21,17 +23,33 @@ vi.mock("./survey", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock ResizeObserver
|
||||||
|
let resizeCallback: Function | undefined;
|
||||||
|
const ResizeObserverMock = vi.fn((callback) => {
|
||||||
|
resizeCallback = callback;
|
||||||
|
return {
|
||||||
|
observe: vi.fn(),
|
||||||
|
unobserve: vi.fn(),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
global.ResizeObserver = ResizeObserverMock as any;
|
||||||
|
|
||||||
describe("RenderSurvey", () => {
|
describe("RenderSurvey", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
surveySpy.mockClear();
|
surveySpy.mockClear();
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
// Reset styles on documentElement before each test
|
||||||
|
document.documentElement.style.removeProperty("--fb-survey-card-max-height");
|
||||||
|
document.documentElement.style.removeProperty("--fb-survey-card-min-height");
|
||||||
|
resizeCallback = undefined; // Reset callback for each test
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with default props and handles close", () => {
|
test("renders with default props and handles close", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onFinished = vi.fn();
|
const onFinished = vi.fn();
|
||||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||||
@@ -63,7 +81,7 @@ describe("RenderSurvey", () => {
|
|||||||
expect(onClose).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("onFinished skips close if redirectToUrl", () => {
|
test("onFinished skips close if redirectToUrl", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onFinished = vi.fn();
|
const onFinished = vi.fn();
|
||||||
const survey = { endings: [{ id: "e1", type: "redirectToUrl" }] } as any;
|
const survey = { endings: [{ id: "e1", type: "redirectToUrl" }] } as any;
|
||||||
@@ -88,7 +106,7 @@ describe("RenderSurvey", () => {
|
|||||||
expect(onClose).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("onFinished closes after delay for non-redirect endings", () => {
|
test("onFinished closes after delay for non-redirect endings", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onFinished = vi.fn();
|
const onFinished = vi.fn();
|
||||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||||
@@ -115,7 +133,7 @@ describe("RenderSurvey", () => {
|
|||||||
expect(onClose).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("onFinished does not auto-close when inline mode", () => {
|
test("onFinished does not auto-close when inline mode", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onFinished = vi.fn();
|
const onFinished = vi.fn();
|
||||||
const survey = { endings: [] } as any;
|
const survey = { endings: [] } as any;
|
||||||
@@ -139,4 +157,49 @@ describe("RenderSurvey", () => {
|
|||||||
vi.advanceTimersByTime(5000);
|
vi.advanceTimersByTime(5000);
|
||||||
expect(onClose).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// New tests for surveyTypeStyles
|
||||||
|
test("should apply correct styles for link surveys", () => {
|
||||||
|
const propsForLinkSurvey = {
|
||||||
|
survey: { type: "link", endings: [] },
|
||||||
|
styling: {},
|
||||||
|
isBrandingEnabled: false,
|
||||||
|
languageCode: "en",
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onFinished: vi.fn(),
|
||||||
|
placement: "bottomRight",
|
||||||
|
mode: "modal",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
render(<RenderSurvey {...propsForLinkSurvey} />);
|
||||||
|
// Manually trigger the ResizeObserver callback if it was captured
|
||||||
|
if (resizeCallback) {
|
||||||
|
resizeCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--fb-survey-card-max-height")).toBe("56dvh");
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--fb-survey-card-min-height")).toBe("0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should apply correct styles for app (non-link) surveys", () => {
|
||||||
|
const propsForAppSurvey = {
|
||||||
|
survey: { type: "app", endings: [] },
|
||||||
|
styling: {},
|
||||||
|
isBrandingEnabled: false,
|
||||||
|
languageCode: "en",
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onFinished: vi.fn(),
|
||||||
|
placement: "bottomRight",
|
||||||
|
mode: "modal",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
render(<RenderSurvey {...propsForAppSurvey} />);
|
||||||
|
// Manually trigger the ResizeObserver callback if it was captured
|
||||||
|
if (resizeCallback) {
|
||||||
|
resizeCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--fb-survey-card-max-height")).toBe("40dvh");
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--fb-survey-card-min-height")).toBe("40dvh");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||||
import { SurveyContainer } from "../wrappers/survey-container";
|
import { SurveyContainer } from "../wrappers/survey-container";
|
||||||
import { Survey } from "./survey";
|
import { Survey } from "./survey";
|
||||||
|
|
||||||
export function RenderSurvey(props: SurveyContainerProps) {
|
export function RenderSurvey(props: Readonly<SurveyContainerProps>) {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
// Check viewport width on mount and resize
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
const isDesktop = window.innerWidth > 768;
|
||||||
|
|
||||||
|
if (props.survey.type === "link") {
|
||||||
|
root.style.setProperty("--fb-survey-card-max-height", isDesktop ? "56dvh" : "60dvh");
|
||||||
|
root.style.setProperty("--fb-survey-card-min-height", isDesktop ? "0" : "42dvh");
|
||||||
|
} else {
|
||||||
|
root.style.setProperty("--fb-survey-card-max-height", "40dvh");
|
||||||
|
root.style.setProperty("--fb-survey-card-min-height", "40dvh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(document.body);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
root.style.removeProperty("--fb-survey-card-max-height");
|
||||||
|
root.style.removeProperty("--fb-survey-card-min-height");
|
||||||
|
};
|
||||||
|
}, [props.survey.type]);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function WelcomeCard({
|
|||||||
{fileUrl ? (
|
{fileUrl ? (
|
||||||
<img
|
<img
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
className="fb-mb-8 fb-max-h-96 fb-w-1/4 fb-rounded-lg fb-object-contain"
|
className="fb-mb-8 fb-max-h-80 fb-w-1/4 fb-rounded-lg fb-object-contain"
|
||||||
alt="Company Logo"
|
alt="Company Logo"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -156,9 +156,36 @@ export function WelcomeCard({
|
|||||||
htmlString={replaceRecallInfo(getLocalizedValue(html, languageCode), responseData, variablesData)}
|
htmlString={replaceRecallInfo(getLocalizedValue(html, languageCode), responseData, variablesData)}
|
||||||
questionId="welcomeCard"
|
questionId="welcomeCard"
|
||||||
/>
|
/>
|
||||||
|
{timeToFinish && !showResponseCount ? (
|
||||||
|
<div className="fb-items-center fb-text-subheading fb-my-4 fb-flex">
|
||||||
|
<TimerIcon />
|
||||||
|
<p className="fb-pt-1 fb-text-xs">
|
||||||
|
<span> Takes {calculateTimeToComplete()} </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||||
|
<div className="fb-items-center fb-text-subheading fb-my-4 b-flex">
|
||||||
|
<UsersIcon />
|
||||||
|
<p className="fb-pt-1 fb-text-xs">
|
||||||
|
<span>{`${responseCount.toString()} people responded`}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{timeToFinish && showResponseCount ? (
|
||||||
|
<div className="fb-items-center fb-text-subheading fb-my-4 fb-flex">
|
||||||
|
<TimerIcon />
|
||||||
|
<p className="fb-pt-1 fb-text-xs">
|
||||||
|
<span> Takes {calculateTimeToComplete()} </span>
|
||||||
|
<span>
|
||||||
|
{responseCount && responseCount > 3 ? `⋅ ${responseCount.toString()} people responded` : ""}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
<div className="fb-mx-6 fb-mt-4 fb-flex fb-gap-4 fb-py-4">
|
<div className="fb-px-6 fb-py-4 fb-flex fb-gap-4">
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||||
isLastQuestion={false}
|
isLastQuestion={false}
|
||||||
@@ -173,33 +200,6 @@ export function WelcomeCard({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{timeToFinish && !showResponseCount ? (
|
|
||||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
|
||||||
<TimerIcon />
|
|
||||||
<p className="fb-pt-1 fb-text-xs">
|
|
||||||
<span> Takes {calculateTimeToComplete()} </span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
|
||||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
|
||||||
<UsersIcon />
|
|
||||||
<p className="fb-pt-1 fb-text-xs">
|
|
||||||
<span>{`${responseCount.toString()} people responded`}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{timeToFinish && showResponseCount ? (
|
|
||||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
|
||||||
<TimerIcon />
|
|
||||||
<p className="fb-pt-1 fb-text-xs">
|
|
||||||
<span> Takes {calculateTimeToComplete()} </span>
|
|
||||||
<span>
|
|
||||||
{responseCount && responseCount > 3 ? `⋅ ${responseCount.toString()} people responded` : ""}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function AddressQuestion({
|
|||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: AddressQuestionProps) {
|
}: Readonly<AddressQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
@@ -136,7 +136,7 @@ export function AddressQuestion({
|
|||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
<div className="fb-mt-4 fb-w-full fb-grid fb-grid-cols-1 fb-gap-3">
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const isFieldRequired = () => {
|
const isFieldRequired = () => {
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function CalQuestion({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: CalQuestionProps) {
|
}: Readonly<CalQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function ConsentQuestion({
|
|||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: ConsentQuestionProps) {
|
}: Readonly<ConsentQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const isCurrent = question.id === currentQuestionId;
|
const isCurrent = question.id === currentQuestionId;
|
||||||
@@ -48,8 +48,7 @@ export function ConsentQuestion({
|
|||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||||
|
|
||||||
const consentRef = useCallback(
|
const consentRef = useCallback(
|
||||||
(currentElement: HTMLLabelElement | null) => {
|
(currentElement: HTMLButtonElement | null) => {
|
||||||
// will focus on current element when the question ID matches the current question
|
|
||||||
if (question.id && currentElement && autoFocusEnabled && question.id === currentQuestionId) {
|
if (question.id && currentElement && autoFocusEnabled && question.id === currentQuestionId) {
|
||||||
currentElement.focus();
|
currentElement.focus();
|
||||||
}
|
}
|
||||||
@@ -80,14 +79,14 @@ export function ConsentQuestion({
|
|||||||
htmlString={getLocalizedValue(question.html, languageCode) || ""}
|
htmlString={getLocalizedValue(question.html, languageCode) || ""}
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
/>
|
/>
|
||||||
<div className="fb-bg-survey-bg fb-sticky -fb-bottom-2 fb-z-10 fb-w-full fb-px-1 fb-py-1">
|
<div className="fb-bg-survey-bg fb-sticky -fb-bottom-2 fb-z-10 fb-w-full fb-py-2">
|
||||||
<label
|
<button
|
||||||
|
type="button"
|
||||||
ref={consentRef}
|
ref={consentRef}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
id={`${question.id}-label`}
|
id={`${question.id}-label`}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Accessibility: if spacebar was pressed pass this down to the input
|
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
document.getElementById(question.id)?.click();
|
document.getElementById(question.id)?.click();
|
||||||
@@ -95,28 +94,30 @@ export function ConsentQuestion({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="fb-border-border fb-bg-input-bg fb-text-heading hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected focus:fb-ring-brand fb-rounded-custom fb-relative fb-z-10 fb-my-2 fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-border fb-p-4 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
className="fb-border-border fb-bg-input-bg fb-text-heading hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected focus:fb-ring-brand fb-rounded-custom fb-relative fb-z-10 fb-my-2 fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-border fb-p-4 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||||
<input
|
<label className="fb-flex fb-w-full fb-cursor-pointer fb-items-center">
|
||||||
tabIndex={-1}
|
<input
|
||||||
type="checkbox"
|
tabIndex={-1}
|
||||||
id={question.id}
|
type="checkbox"
|
||||||
name={question.id}
|
id={question.id}
|
||||||
value={getLocalizedValue(question.label, languageCode)}
|
name={question.id}
|
||||||
onChange={(e) => {
|
value={getLocalizedValue(question.label, languageCode)}
|
||||||
if (e.target instanceof HTMLInputElement && e.target.checked) {
|
onChange={(e) => {
|
||||||
onChange({ [question.id]: "accepted" });
|
if (e.target instanceof HTMLInputElement && e.target.checked) {
|
||||||
} else {
|
onChange({ [question.id]: "accepted" });
|
||||||
onChange({ [question.id]: "" });
|
} else {
|
||||||
}
|
onChange({ [question.id]: "" });
|
||||||
}}
|
}
|
||||||
checked={value === "accepted"}
|
}}
|
||||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
checked={value === "accepted"}
|
||||||
aria-labelledby={`${question.id}-label`}
|
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||||
required={question.required}
|
aria-labelledby={`${question.id}-label`}
|
||||||
/>
|
required={question.required}
|
||||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
|
/>
|
||||||
{getLocalizedValue(question.label, languageCode)}
|
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
|
||||||
</span>
|
{getLocalizedValue(question.label, languageCode)}
|
||||||
</label>
|
</span>
|
||||||
|
</label>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function ContactInfoQuestion({
|
|||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: ContactInfoQuestionProps) {
|
}: Readonly<ContactInfoQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
@@ -131,7 +131,7 @@ export function ContactInfoQuestion({
|
|||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
<div className="fb-mt-4 fb-w-full fb-grid fb-grid-cols-1 fb-gap-3">
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const isFieldRequired = () => {
|
const isFieldRequired = () => {
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function CTAQuestion({
|
|||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
onOpenExternalURL,
|
onOpenExternalURL,
|
||||||
}: CTAQuestionProps) {
|
}: Readonly<CTAQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const isCurrent = question.id === currentQuestionId;
|
const isCurrent = question.id === currentQuestionId;
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import { getMonthName, getOrdinalDate } from "@/lib/date-time";
|
|||||||
import { getLocalizedValue } from "@/lib/i18n";
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import DatePicker from "react-date-picker";
|
import DatePicker, { DatePickerProps } from "react-date-picker";
|
||||||
import { DatePickerProps } from "react-date-picker";
|
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||||
import "../../styles/date-picker.css";
|
import "../../styles/date-picker.css";
|
||||||
@@ -23,13 +22,12 @@ interface DateQuestionProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
isFirstQuestion: boolean;
|
isFirstQuestion: boolean;
|
||||||
isLastQuestion: boolean;
|
isLastQuestion: boolean;
|
||||||
autoFocus?: boolean;
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
ttc: TResponseTtc;
|
ttc: TResponseTtc;
|
||||||
setTtc: (ttc: TResponseTtc) => void;
|
setTtc: (ttc: TResponseTtc) => void;
|
||||||
autoFocusEnabled: boolean;
|
|
||||||
currentQuestionId: TSurveyQuestionId;
|
currentQuestionId: TSurveyQuestionId;
|
||||||
isBackButtonHidden: boolean;
|
isBackButtonHidden: boolean;
|
||||||
|
autoFocusEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarIcon() {
|
function CalendarIcon() {
|
||||||
@@ -94,7 +92,8 @@ export function DateQuestion({
|
|||||||
ttc,
|
ttc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: DateQuestionProps) {
|
autoFocusEnabled,
|
||||||
|
}: Readonly<DateQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
@@ -104,6 +103,15 @@ export function DateQuestion({
|
|||||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
||||||
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
|
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
|
||||||
|
|
||||||
|
const datePickerRef = useCallback(
|
||||||
|
(currentElement: HTMLButtonElement | null) => {
|
||||||
|
if (currentElement && autoFocusEnabled && isCurrent) {
|
||||||
|
currentElement.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[autoFocusEnabled, isCurrent]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (datePickerOpen) {
|
if (datePickerOpen) {
|
||||||
if (!selectedDate) setSelectedDate(new Date());
|
if (!selectedDate) setSelectedDate(new Date());
|
||||||
@@ -170,6 +178,7 @@ export function DateQuestion({
|
|||||||
<div className="fb-relative">
|
<div className="fb-relative">
|
||||||
{!datePickerOpen && (
|
{!datePickerOpen && (
|
||||||
<button
|
<button
|
||||||
|
ref={datePickerRef}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDatePickerOpen(true);
|
setDatePickerOpen(true);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,21 +14,21 @@ import { FileInput } from "../general/file-input";
|
|||||||
import { Subheader } from "../general/subheader";
|
import { Subheader } from "../general/subheader";
|
||||||
|
|
||||||
interface FileUploadQuestionProps {
|
interface FileUploadQuestionProps {
|
||||||
readonly question: TSurveyFileUploadQuestion;
|
question: TSurveyFileUploadQuestion;
|
||||||
readonly value: string[];
|
value: string[];
|
||||||
readonly onChange: (responseData: TResponseData) => void;
|
onChange: (responseData: TResponseData) => void;
|
||||||
readonly onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
||||||
readonly onBack: () => void;
|
onBack: () => void;
|
||||||
readonly onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||||
readonly isFirstQuestion: boolean;
|
isFirstQuestion: boolean;
|
||||||
readonly isLastQuestion: boolean;
|
isLastQuestion: boolean;
|
||||||
readonly surveyId: string;
|
surveyId: string;
|
||||||
readonly languageCode: string;
|
languageCode: string;
|
||||||
readonly ttc: TResponseTtc;
|
ttc: TResponseTtc;
|
||||||
readonly setTtc: (ttc: TResponseTtc) => void;
|
setTtc: (ttc: TResponseTtc) => void;
|
||||||
readonly autoFocusEnabled: boolean;
|
autoFocusEnabled: boolean;
|
||||||
readonly currentQuestionId: TSurveyQuestionId;
|
currentQuestionId: TSurveyQuestionId;
|
||||||
readonly isBackButtonHidden: boolean;
|
isBackButtonHidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileUploadQuestion({
|
export function FileUploadQuestion({
|
||||||
@@ -46,7 +46,7 @@ export function FileUploadQuestion({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: FileUploadQuestionProps) {
|
}: Readonly<FileUploadQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function MatrixQuestion({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: MatrixQuestionProps) {
|
}: Readonly<MatrixQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function MultipleChoiceMultiQuestion({
|
|||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: MultipleChoiceMultiProps) {
|
}: Readonly<MultipleChoiceMultiProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function MultipleChoiceSingleQuestion({
|
|||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: MultipleChoiceSingleProps) {
|
}: Readonly<MultipleChoiceSingleProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [otherSelected, setOtherSelected] = useState(false);
|
const [otherSelected, setOtherSelected] = useState(false);
|
||||||
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
||||||
|
|||||||
@@ -167,19 +167,6 @@ describe("NPSQuestion", () => {
|
|||||||
expect(getUpdatedTtc).toHaveBeenCalled();
|
expect(getUpdatedTtc).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("updates hover state when mouse moves over options", () => {
|
|
||||||
render(<NPSQuestion {...mockProps} />);
|
|
||||||
|
|
||||||
const option = screen.getByText("5").closest("label");
|
|
||||||
expect(option).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.mouseOver(option!);
|
|
||||||
expect(option).toHaveClass("fb-bg-accent-bg");
|
|
||||||
|
|
||||||
fireEvent.mouseLeave(option!);
|
|
||||||
expect(option).not.toHaveClass("fb-bg-accent-bg");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("supports keyboard navigation", () => {
|
test("supports keyboard navigation", () => {
|
||||||
render(<NPSQuestion {...mockProps} />);
|
render(<NPSQuestion {...mockProps} />);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function NPSQuestion({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: NPSQuestionProps) {
|
}: Readonly<NPSQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
@@ -96,17 +96,15 @@ export function NPSQuestion({
|
|||||||
<div className="fb-flex">
|
<div className="fb-flex">
|
||||||
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<button
|
||||||
|
type="button"
|
||||||
key={number}
|
key={number}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
onMouseOver={() => {
|
onMouseOver={() => setHoveredNumber(number)}
|
||||||
setHoveredNumber(number);
|
onMouseLeave={() => setHoveredNumber(-1)}
|
||||||
}}
|
onFocus={() => setHoveredNumber(number)}
|
||||||
onMouseLeave={() => {
|
onBlur={() => setHoveredNumber(-1)}
|
||||||
setHoveredNumber(-1);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Accessibility: if spacebar was pressed pass this down to the input
|
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
document.getElementById(number.toString())?.click();
|
document.getElementById(number.toString())?.click();
|
||||||
@@ -120,29 +118,29 @@ export function NPSQuestion({
|
|||||||
"fb-text-heading first:fb-rounded-l-custom last:fb-rounded-r-custom focus:fb-border-brand fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-overflow-hidden fb-border-b fb-border-l fb-border-t fb-text-center fb-text-sm last:fb-border-r focus:fb-border-2 focus:fb-outline-none",
|
"fb-text-heading first:fb-rounded-l-custom last:fb-rounded-r-custom focus:fb-border-brand fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-overflow-hidden fb-border-b fb-border-l fb-border-t fb-text-center fb-text-sm last:fb-border-r focus:fb-border-2 focus:fb-outline-none",
|
||||||
question.isColorCodingEnabled
|
question.isColorCodingEnabled
|
||||||
? "fb-h-[46px] fb-leading-[3.5em]"
|
? "fb-h-[46px] fb-leading-[3.5em]"
|
||||||
: "fb-h fb-leading-10",
|
: "fb-h-[41px] fb-leading-10",
|
||||||
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
||||||
)}>
|
)}>
|
||||||
{question.isColorCodingEnabled ? (
|
<label className="fb-w-full fb-h-full fb-flex fb-items-center fb-justify-center">
|
||||||
<div
|
{question.isColorCodingEnabled ? (
|
||||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
|
<div
|
||||||
|
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={number.toString()}
|
||||||
|
name="nps"
|
||||||
|
value={number}
|
||||||
|
checked={value === number}
|
||||||
|
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||||
|
onClick={() => handleClick(number)}
|
||||||
|
required={question.required}
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
) : null}
|
{number}
|
||||||
<input
|
</label>
|
||||||
type="radio"
|
</button>
|
||||||
id={number.toString()}
|
|
||||||
name="nps"
|
|
||||||
value={number}
|
|
||||||
checked={value === number}
|
|
||||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
|
||||||
onClick={() => {
|
|
||||||
handleClick(number);
|
|
||||||
}}
|
|
||||||
required={question.required}
|
|
||||||
tabIndex={-1}
|
|
||||||
/>
|
|
||||||
{number}
|
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ describe("OpenTextQuestion", () => {
|
|||||||
test("renders textarea for long answers", () => {
|
test("renders textarea for long answers", () => {
|
||||||
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, longAnswer: true }} />);
|
render(<OpenTextQuestion {...defaultProps} question={{ ...defaultQuestion, longAnswer: true }} />);
|
||||||
|
|
||||||
expect(screen.getByRole("textbox")).toHaveAttribute("rows", "3");
|
expect(screen.getByRole("textbox")).toHaveAttribute("rows", "5");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("displays character limit when configured", () => {
|
test("displays character limit when configured", () => {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ interface OpenTextQuestionProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
isFirstQuestion: boolean;
|
isFirstQuestion: boolean;
|
||||||
isLastQuestion: boolean;
|
isLastQuestion: boolean;
|
||||||
autoFocus?: boolean;
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
ttc: TResponseTtc;
|
ttc: TResponseTtc;
|
||||||
setTtc: (ttc: TResponseTtc) => void;
|
setTtc: (ttc: TResponseTtc) => void;
|
||||||
@@ -43,7 +42,7 @@ export function OpenTextQuestion({
|
|||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: OpenTextQuestionProps) {
|
}: Readonly<OpenTextQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [currentLength, setCurrentLength] = useState(value.length || 0);
|
const [currentLength, setCurrentLength] = useState(value.length || 0);
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
@@ -103,7 +102,7 @@ export function OpenTextQuestion({
|
|||||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
/>
|
/>
|
||||||
<div className="fb-mt-4">
|
<div className="fb-mt-4 fb-text-md">
|
||||||
{question.longAnswer === false ? (
|
{question.longAnswer === false ? (
|
||||||
<input
|
<input
|
||||||
ref={inputRef as RefObject<HTMLInputElement>}
|
ref={inputRef as RefObject<HTMLInputElement>}
|
||||||
@@ -120,7 +119,7 @@ export function OpenTextQuestion({
|
|||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
handleInputChange(e.currentTarget.value);
|
handleInputChange(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm"
|
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0"
|
||||||
pattern={question.inputType === "phone" ? "^[0-9+][0-9+\\- ]*[0-9]$" : ".*"}
|
pattern={question.inputType === "phone" ? "^[0-9+][0-9+\\- ]*[0-9]$" : ".*"}
|
||||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||||
@@ -135,7 +134,7 @@ export function OpenTextQuestion({
|
|||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef as RefObject<HTMLTextAreaElement>}
|
ref={inputRef as RefObject<HTMLTextAreaElement>}
|
||||||
rows={3}
|
rows={5}
|
||||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||||
name={question.id}
|
name={question.id}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
@@ -148,7 +147,7 @@ export function OpenTextQuestion({
|
|||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
handleInputChange(e.currentTarget.value);
|
handleInputChange(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0 sm:fb-text-sm"
|
className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0"
|
||||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||||
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
minLength={question.inputType === "text" ? question.charLimit?.min : undefined}
|
||||||
maxLength={question.inputType === "text" ? question.charLimit?.max : undefined}
|
maxLength={question.inputType === "text" ? question.charLimit?.max : undefined}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ describe("PictureSelectionQuestion", () => {
|
|||||||
render(<PictureSelectionQuestion {...mockProps} />);
|
render(<PictureSelectionQuestion {...mockProps} />);
|
||||||
|
|
||||||
const images = screen.getAllByRole("img");
|
const images = screen.getAllByRole("img");
|
||||||
const label = images[0].closest("label");
|
const label = images[0].closest("button");
|
||||||
|
|
||||||
fireEvent.keyDown(label!, { key: " " });
|
fireEvent.keyDown(label!, { key: " " });
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,15 @@ export function PictureSelectionQuestion({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: PictureSelectionProps) {
|
}: Readonly<PictureSelectionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
|
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(() => {
|
||||||
|
const initialLoadingState: Record<string, boolean> = {};
|
||||||
|
question.choices.forEach((choice) => {
|
||||||
|
initialLoadingState[choice.id] = true;
|
||||||
|
});
|
||||||
|
return initialLoadingState;
|
||||||
|
});
|
||||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||||
const isCurrent = question.id === currentQuestionId;
|
const isCurrent = question.id === currentQuestionId;
|
||||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
@@ -115,35 +122,72 @@ export function PictureSelectionQuestion({
|
|||||||
<div className="fb-mt-4">
|
<div className="fb-mt-4">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend className="fb-sr-only">Options</legend>
|
<legend className="fb-sr-only">Options</legend>
|
||||||
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-2 fb-gap-4">
|
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-1 sm:fb-grid-cols-2 fb-gap-4">
|
||||||
{questionChoices.map((choice) => (
|
{questionChoices.map((choice) => (
|
||||||
<label
|
<div className="fb-relative" key={choice.id}>
|
||||||
key={choice.id}
|
<button
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
type="button"
|
||||||
htmlFor={choice.id}
|
tabIndex={isCurrent ? 0 : -1}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Accessibility: if spacebar was pressed pass this down to the input
|
// Accessibility: if spacebar was pressed pass this down to the input
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
document.getElementById(choice.id)?.click();
|
document.getElementById(choice.id)?.click();
|
||||||
document.getElementById(choice.id)?.focus();
|
document.getElementById(choice.id)?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleChange(choice.id);
|
handleChange(choice.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus:fb-outline-none fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] focus:fb-border-brand focus:fb-border-4 group/image",
|
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus-visible:fb-outline-none focus-visible:fb-ring-2 focus-visible:fb-ring-brand focus-visible:fb-ring-offset-2 fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] group/image",
|
||||||
Array.isArray(value) && value.includes(choice.id)
|
Array.isArray(value) && value.includes(choice.id)
|
||||||
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
||||||
: ""
|
: ""
|
||||||
)}>
|
)}>
|
||||||
<img
|
{loadingImages[choice.id] && (
|
||||||
src={choice.imageUrl}
|
<div className="fb-absolute fb-inset-0 fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||||
id={choice.id}
|
)}
|
||||||
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
<img
|
||||||
className="fb-h-full fb-w-full fb-object-cover"
|
src={choice.imageUrl}
|
||||||
/>
|
id={choice.id}
|
||||||
|
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
||||||
|
className={cn(
|
||||||
|
"fb-h-full fb-w-full fb-object-cover",
|
||||||
|
loadingImages[choice.id] ? "fb-opacity-0" : ""
|
||||||
|
)}
|
||||||
|
onLoad={() => {
|
||||||
|
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{question.allowMulti ? (
|
||||||
|
<input
|
||||||
|
id={`${choice.id}-checked`}
|
||||||
|
name={`${choice.id}-checkbox`}
|
||||||
|
type="checkbox"
|
||||||
|
tabIndex={-1}
|
||||||
|
checked={value.includes(choice.id)}
|
||||||
|
className={cn(
|
||||||
|
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||||
|
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||||
|
)}
|
||||||
|
required={question.required && value.length ? false : question.required}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
id={`${choice.id}-radio`}
|
||||||
|
name={`${choice.id}-radio`}
|
||||||
|
type="radio"
|
||||||
|
tabIndex={-1}
|
||||||
|
checked={value.includes(choice.id)}
|
||||||
|
className={cn(
|
||||||
|
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
||||||
|
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||||
|
)}
|
||||||
|
required={question.required && value.length ? false : question.required}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href={choice.imageUrl}
|
href={choice.imageUrl}
|
||||||
@@ -153,52 +197,25 @@ export function PictureSelectionQuestion({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
|
className="fb-absolute fb-bottom-4 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100 fb-z-20">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="24"
|
||||||
height="16"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="lucide lucide-expand">
|
className="lucide lucide-image-down-icon lucide-image-down">
|
||||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
<path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" />
|
||||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
<path d="m14 19 3 3v-5.5" />
|
||||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
<path d="m17 22 3-3" />
|
||||||
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
|
<circle cx="9" cy="9" r="2" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{question.allowMulti ? (
|
</div>
|
||||||
<input
|
|
||||||
id={`${choice.id}-checked`}
|
|
||||||
name={`${choice.id}-checkbox`}
|
|
||||||
type="checkbox"
|
|
||||||
tabIndex={-1}
|
|
||||||
checked={value.includes(choice.id)}
|
|
||||||
className={cn(
|
|
||||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
|
||||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
|
||||||
)}
|
|
||||||
required={question.required && value.length ? false : question.required}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
id={`${choice.id}-radio`}
|
|
||||||
name={`${choice.id}-radio`}
|
|
||||||
type="radio"
|
|
||||||
tabIndex={-1}
|
|
||||||
checked={value.includes(choice.id)}
|
|
||||||
className={cn(
|
|
||||||
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
|
||||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
|
||||||
)}
|
|
||||||
required={question.required && value.length ? false : question.required}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function RankingQuestion({
|
|||||||
autoFocusEnabled,
|
autoFocusEnabled,
|
||||||
currentQuestionId,
|
currentQuestionId,
|
||||||
isBackButtonHidden,
|
isBackButtonHidden,
|
||||||
}: RankingQuestionProps) {
|
}: Readonly<RankingQuestionProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = question.id === currentQuestionId;
|
const isCurrent = question.id === currentQuestionId;
|
||||||
const shuffledChoicesIds = useMemo(() => {
|
const shuffledChoicesIds = useMemo(() => {
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export function AutoCloseWrapper({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (survey.autoClose) {
|
||||||
|
// Reset interaction state when auto-close is enabled
|
||||||
|
setHasInteracted(false);
|
||||||
|
setCountDownActive(true);
|
||||||
|
}
|
||||||
startCountdown();
|
startCountdown();
|
||||||
return stopCountdown;
|
return stopCountdown;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to run this effect on every render
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to run this effect on every render
|
||||||
|
|||||||
@@ -124,28 +124,6 @@ describe("ScrollableContainer", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses 60dvh maxHeight by default when not in survey preview", () => {
|
|
||||||
vi.spyOn(document, "getElementById").mockReturnValue(null);
|
|
||||||
const { container } = render(
|
|
||||||
<ScrollableContainer>
|
|
||||||
<div>Content</div>
|
|
||||||
</ScrollableContainer>
|
|
||||||
);
|
|
||||||
const scrollableDiv = container.querySelector<HTMLElement>(".fb-overflow-auto");
|
|
||||||
expect(scrollableDiv).toHaveStyle({ maxHeight: "60dvh" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses 42dvh maxHeight when isSurveyPreview is true", () => {
|
|
||||||
vi.spyOn(document, "getElementById").mockReturnValue(document.createElement("div")); // Simulate survey-preview element exists
|
|
||||||
const { container } = render(
|
|
||||||
<ScrollableContainer>
|
|
||||||
<div>Content</div>
|
|
||||||
</ScrollableContainer>
|
|
||||||
);
|
|
||||||
const scrollableDiv = container.querySelector<HTMLElement>(".fb-overflow-auto");
|
|
||||||
expect(scrollableDiv).toHaveStyle({ maxHeight: "42dvh" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("cleans up scroll event listener on unmount", () => {
|
test("cleans up scroll event listener on unmount", () => {
|
||||||
const { unmount, container } = render(
|
const { unmount, container } = render(
|
||||||
<ScrollableContainer>
|
<ScrollableContainer>
|
||||||
@@ -213,4 +191,52 @@ describe("ScrollableContainer", () => {
|
|||||||
);
|
);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("applies reduced height when isSurveyPreview is true", () => {
|
||||||
|
// Create a survey-preview element to make isSurveyPreview true
|
||||||
|
const previewElement = document.createElement("div");
|
||||||
|
previewElement.id = "survey-preview";
|
||||||
|
document.body.appendChild(previewElement);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<ScrollableContainer>
|
||||||
|
<div>Preview Content</div>
|
||||||
|
</ScrollableContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollableDiv = container.querySelector<HTMLElement>("#scrollable-container");
|
||||||
|
expect(scrollableDiv).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (scrollableDiv) {
|
||||||
|
const computedStyle = scrollableDiv.style;
|
||||||
|
expect(computedStyle.maxHeight).toBe("calc(var(--fb-survey-card-max-height, 42dvh) * 0.66)");
|
||||||
|
expect(computedStyle.minHeight).toBe("calc(var(--fb-survey-card-min-height, 42dvh) * 0.66)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(previewElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies normal height when isSurveyPreview is false", () => {
|
||||||
|
// Ensure no survey-preview element exists
|
||||||
|
const existingPreview = document.getElementById("survey-preview");
|
||||||
|
if (existingPreview) {
|
||||||
|
document.body.removeChild(existingPreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<ScrollableContainer>
|
||||||
|
<div>Regular Content</div>
|
||||||
|
</ScrollableContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollableDiv = container.querySelector<HTMLElement>("#scrollable-container");
|
||||||
|
expect(scrollableDiv).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (scrollableDiv) {
|
||||||
|
const computedStyle = scrollableDiv.style;
|
||||||
|
expect(computedStyle.maxHeight).toBe("calc(var(--fb-survey-card-max-height, 42dvh) * 1)");
|
||||||
|
expect(computedStyle.minHeight).toBe("calc(var(--fb-survey-card-min-height, 42dvh) * 1)");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
|||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
interface ScrollableContainerProps {
|
interface ScrollableContainerProps {
|
||||||
|
className?: string;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScrollableContainer({ children }: ScrollableContainerProps) {
|
export function ScrollableContainer({ className, children }: Readonly<ScrollableContainerProps>) {
|
||||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||||
const [isAtTop, setIsAtTop] = useState(false);
|
const [isAtTop, setIsAtTop] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isSurveyPreview = Boolean(document.getElementById("survey-preview"));
|
const isSurveyPreview = Boolean(document.getElementById("survey-preview"));
|
||||||
|
const isMobilePreview = isSurveyPreview ? Boolean(document.getElementById("mobile-preview")) : false;
|
||||||
|
const previewScaleCoifficient = isSurveyPreview ? 0.66 : 1;
|
||||||
|
|
||||||
const checkScroll = () => {
|
const checkScroll = () => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||||
|
|
||||||
setIsAtBottom(Math.round(scrollTop) + clientHeight >= scrollHeight);
|
setIsAtBottom(Math.round(scrollTop) + clientHeight >= scrollHeight);
|
||||||
|
|
||||||
setIsAtTop(scrollTop === 0);
|
setIsAtTop(scrollTop === 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,12 +47,18 @@ export function ScrollableContainer({ children }: ScrollableContainerProps) {
|
|||||||
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-6 fb-bg-gradient-to-b fb-to-transparent" />
|
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-6 fb-bg-gradient-to-b fb-to-transparent" />
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
id="scrollable-container"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{
|
style={{
|
||||||
scrollbarGutter: "stable both-edges",
|
scrollbarGutter: "stable both-edges",
|
||||||
maxHeight: isSurveyPreview ? "42dvh" : "60dvh",
|
maxHeight: isMobilePreview
|
||||||
|
? "30dvh"
|
||||||
|
: `calc(var(--fb-survey-card-max-height, 42dvh) * ${previewScaleCoifficient})`,
|
||||||
|
minHeight: isMobilePreview
|
||||||
|
? "30dvh"
|
||||||
|
: `calc(var(--fb-survey-card-min-height, 42dvh) * ${previewScaleCoifficient})`,
|
||||||
}}
|
}}
|
||||||
className={cn("fb-overflow-auto fb-px-4 fb-pb-4 fb-bg-survey-bg")}>
|
className={cn(`fb-overflow-auto fb-px-4 fb-pb-4 fb-bg-survey-bg ${className}`)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{!isAtBottom && (
|
{!isAtBottom && (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MutableRef } from "preact/hooks";
|
import { MutableRef, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
||||||
import { JSX } from "preact/jsx-runtime";
|
import { JSX } from "preact/jsx-runtime";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
@@ -32,10 +31,10 @@ export const StackedCard = ({
|
|||||||
hovered,
|
hovered,
|
||||||
cardArrangement,
|
cardArrangement,
|
||||||
}: StackedCardProps) => {
|
}: StackedCardProps) => {
|
||||||
const isHidden = offset < 0;
|
|
||||||
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
|
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
|
||||||
const [contentOpacity, setContentOpacity] = useState<number>(0);
|
const [contentOpacity, setContentOpacity] = useState<number>(0);
|
||||||
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
|
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
|
||||||
|
const isHidden = offset < 0 || offset > 2;
|
||||||
|
|
||||||
const getBottomStyles = () => {
|
const getBottomStyles = () => {
|
||||||
if (survey.type !== "link")
|
if (survey.type !== "link")
|
||||||
@@ -50,28 +49,26 @@ export const StackedCard = ({
|
|||||||
|
|
||||||
const calculateCardTransform = useMemo(() => {
|
const calculateCardTransform = useMemo(() => {
|
||||||
let rotationCoefficient = 3;
|
let rotationCoefficient = 3;
|
||||||
|
|
||||||
if (cardWidth >= 1000) {
|
if (cardWidth >= 1000) {
|
||||||
rotationCoefficient = 1.5;
|
rotationCoefficient = 1.5;
|
||||||
} else if (cardWidth > 650) {
|
} else if (cardWidth > 650) {
|
||||||
rotationCoefficient = 2;
|
rotationCoefficient = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rotationValue = ((hovered ? rotationCoefficient : rotationCoefficient - 0.5) * offset).toString();
|
||||||
|
let translateValue = ((hovered ? 12 : 10) * offset).toString();
|
||||||
|
|
||||||
return (offset: number) => {
|
return (offset: number) => {
|
||||||
switch (cardArrangement) {
|
switch (cardArrangement) {
|
||||||
case "casual":
|
case "casual":
|
||||||
return offset < 0
|
return offset < 0 ? `translateX(35%) scale(0.97)` : `translateX(0) rotate(-${rotationValue}deg)`;
|
||||||
? `translateX(33%)`
|
|
||||||
: `translateX(0) rotate(-${((hovered ? rotationCoefficient : rotationCoefficient - 0.5) * offset).toString()}deg)`;
|
|
||||||
case "straight":
|
case "straight":
|
||||||
return offset < 0
|
return offset < 0 ? `translateY(35%) scale(0.97)` : `translateY(-${translateValue}px)`;
|
||||||
? `translateY(25%)`
|
|
||||||
: `translateY(-${((hovered ? 12 : 10) * offset).toString()}px)`;
|
|
||||||
default:
|
default:
|
||||||
return offset < 0 ? `translateX(0)` : `translateX(0)`;
|
return `translateX(0)`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [cardArrangement, hovered, cardWidth]);
|
}, [cardArrangement, hovered, cardWidth, offset]);
|
||||||
|
|
||||||
const straightCardArrangementStyles =
|
const straightCardArrangementStyles =
|
||||||
cardArrangement === "straight"
|
cardArrangement === "straight"
|
||||||
@@ -105,7 +102,9 @@ export const StackedCard = ({
|
|||||||
transform: calculateCardTransform(offset),
|
transform: calculateCardTransform(offset),
|
||||||
opacity: isHidden ? 0 : (100 - 20 * offset) / 100,
|
opacity: isHidden ? 0 : (100 - 20 * offset) / 100,
|
||||||
height: fullSizeCards ? "100%" : currentCardHeight,
|
height: fullSizeCards ? "100%" : currentCardHeight,
|
||||||
transitionDuration: "600ms",
|
transitionProperty: "transform, opacity, margin, width",
|
||||||
|
transitionDuration: "500ms",
|
||||||
|
transitionBehavior: "ease-in-out",
|
||||||
pointerEvents: offset === 0 ? "auto" : "none",
|
pointerEvents: offset === 0 ? "auto" : "none",
|
||||||
...borderStyles,
|
...borderStyles,
|
||||||
...straightCardArrangementStyles,
|
...straightCardArrangementStyles,
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ describe("StackedCardsContainer", () => {
|
|||||||
test("renders stacked arrangement correctly", () => {
|
test("renders stacked arrangement correctly", () => {
|
||||||
render(<StackedCardsContainer {...defaultProps} cardArrangement="casual" />);
|
render(<StackedCardsContainer {...defaultProps} cardArrangement="casual" />);
|
||||||
// q1 is index 0. currentQuestionIdx = 0. prev = -1, next = 1, next+1 = 2
|
// q1 is index 0. currentQuestionIdx = 0. prev = -1, next = 1, next+1 = 2
|
||||||
expect(mockStackedCardFn).toHaveBeenCalledTimes(4); // prev, current, next, next+1
|
expect(mockStackedCardFn).toHaveBeenCalledTimes(5); // prev, current, next, next+1, next+2
|
||||||
expect(screen.getByTestId("stacked-card-0")).toBeInTheDocument(); // current
|
expect(screen.getByTestId("stacked-card-0")).toBeInTheDocument(); // current
|
||||||
expect(screen.getByTestId("stacked-card-1")).toBeInTheDocument(); // next
|
expect(screen.getByTestId("stacked-card-1")).toBeInTheDocument(); // next
|
||||||
// Check that getCardContent is called for the current card (offset 0) via the mock
|
// Check that getCardContent is called for the current card (offset 0) via the mock
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function StackedCardsContainer({
|
|||||||
setQuestionId,
|
setQuestionId,
|
||||||
shouldResetQuestionId = true,
|
shouldResetQuestionId = true,
|
||||||
fullSizeCards = false,
|
fullSizeCards = false,
|
||||||
}: StackedCardsContainerProps) {
|
}: Readonly<StackedCardsContainerProps>) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
||||||
? survey.styling?.highlightBorderColor?.light
|
? survey.styling?.highlightBorderColor?.light
|
||||||
@@ -40,7 +40,7 @@ export function StackedCardsContainer({
|
|||||||
: styling.cardBorderColor?.light;
|
: styling.cardBorderColor?.light;
|
||||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
const resizeObserver = useRef<ResizeObserver | null>(null);
|
const resizeObserver = useRef<ResizeObserver | null>(null);
|
||||||
const [cardHeight, setCardHeight] = useState("auto");
|
const [cardHeight, setCardHeight] = useState("inital");
|
||||||
const [cardWidth, setCardWidth] = useState<number>(0);
|
const [cardWidth, setCardWidth] = useState<number>(0);
|
||||||
|
|
||||||
const questionIdxTemp = useMemo(() => {
|
const questionIdxTemp = useMemo(() => {
|
||||||
@@ -130,7 +130,7 @@ export function StackedCardsContainer({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="stacked-cards-container"
|
data-testid="stacked-cards-container"
|
||||||
className="fb-relative fb-flex fb-h-full fb-items-end fb-justify-center md:fb-items-center"
|
className="fb-relative fb-flex fb-h-full fb-items-end fb-justify-center md:fb-items-start"
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setHovered(true);
|
setHovered(true);
|
||||||
}}
|
}}
|
||||||
@@ -148,7 +148,8 @@ export function StackedCardsContainer({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
questionIdxTemp !== undefined &&
|
questionIdxTemp !== undefined &&
|
||||||
[prevQuestionIdx, currentQuestionIdx, nextQuestionIdx, nextQuestionIdx + 1].map(
|
[prevQuestionIdx, currentQuestionIdx, nextQuestionIdx, nextQuestionIdx + 1, nextQuestionIdx + 2].map(
|
||||||
|
// [prevQuestionIdx, currentQuestionIdx, nextQuestionIdx, nextQuestionIdx + 1].map(
|
||||||
(dynamicQuestionIndex, index) => {
|
(dynamicQuestionIndex, index) => {
|
||||||
const hasEndingCard = survey.endings.length > 0;
|
const hasEndingCard = survey.endings.length > 0;
|
||||||
// Check for hiding extra card
|
// Check for hiding extra card
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface SurveyContainerProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
clickOutside?: boolean;
|
clickOutside?: boolean;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SurveyContainer({
|
export function SurveyContainer({
|
||||||
@@ -20,7 +21,8 @@ export function SurveyContainer({
|
|||||||
onClose,
|
onClose,
|
||||||
clickOutside,
|
clickOutside,
|
||||||
isOpen = true,
|
isOpen = true,
|
||||||
}: SurveyContainerProps) {
|
style,
|
||||||
|
}: Readonly<SurveyContainerProps>) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -73,7 +75,7 @@ export function SurveyContainer({
|
|||||||
|
|
||||||
if (!isModal) {
|
if (!isModal) {
|
||||||
return (
|
return (
|
||||||
<div id="fbjs" className="fb-formbricks-form" style={{ height: "100%", width: "100%" }}>
|
<div id="fbjs" className="fb-formbricks-form" style={{ ...style, height: "100%", width: "100%" }}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user