mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-28 01:20:24 -05:00
fix: rtl tweaks (#7136)
This commit is contained in:
committed by
GitHub
parent
8f6d27c1ef
commit
8d47ab9709
@@ -24,6 +24,7 @@ interface LinkSurveyWrapperProps {
|
||||
IS_FORMBRICKS_CLOUD: boolean;
|
||||
publicDomain: string;
|
||||
isBrandingEnabled: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export const LinkSurveyWrapper = ({
|
||||
@@ -41,6 +42,7 @@ export const LinkSurveyWrapper = ({
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
publicDomain,
|
||||
isBrandingEnabled,
|
||||
dir = "auto",
|
||||
}: LinkSurveyWrapperProps) => {
|
||||
//for embedded survey strip away all surrounding css
|
||||
const [isBackgroundLoaded, setIsBackgroundLoaded] = useState(false);
|
||||
@@ -80,11 +82,11 @@ export const LinkSurveyWrapper = ({
|
||||
onBackgroundLoaded={handleBackgroundLoaded}>
|
||||
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
|
||||
{!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && (
|
||||
<ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} />
|
||||
<ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} dir={dir} />
|
||||
)}
|
||||
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
||||
{isPreview && (
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div className="fixed top-0 left-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div />
|
||||
Survey Preview 👀
|
||||
<ResetProgressButton onClick={handleResetSurvey} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { CustomScriptsInjector } from "@/modules/survey/link/components/custom-scripts-injector";
|
||||
import { LinkSurveyWrapper } from "@/modules/survey/link/components/link-survey-wrapper";
|
||||
import { getPrefillValue } from "@/modules/survey/link/lib/prefill";
|
||||
import { isRTLLanguage } from "@/modules/survey/link/lib/utils";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
|
||||
interface SurveyClientWrapperProps {
|
||||
@@ -116,6 +117,11 @@ export const SurveyClientWrapper = ({
|
||||
}
|
||||
setResponseData({});
|
||||
};
|
||||
// Determine text direction based on language code for logo positioning only
|
||||
// which checks both language code and survey content. This is only for logo UI positioning.
|
||||
const logoDir = useMemo(() => {
|
||||
return isRTLLanguage(survey, languageCode) ? "rtl" : "auto";
|
||||
}, [languageCode, survey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -140,7 +146,8 @@ export const SurveyClientWrapper = ({
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
dir={logoDir}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
environmentId={survey.environmentId}
|
||||
|
||||
@@ -1,85 +1,188 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getWebAppLocale } from "./utils";
|
||||
import { getElementsFromSurveyBlocks, getWebAppLocale, isRTL, isRTLLanguage } from "./utils";
|
||||
|
||||
const createMockSurvey = (languages: TSurvey["languages"] = []): TSurvey =>
|
||||
({
|
||||
id: "survey-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test",
|
||||
type: "link",
|
||||
environmentId: "env-1",
|
||||
createdBy: null,
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [],
|
||||
blocks: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
languages,
|
||||
displayPercentage: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
followUps: [],
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
showLanguageSwitch: null,
|
||||
recaptcha: null,
|
||||
isBackButtonHidden: false,
|
||||
isCaptureIpEnabled: false,
|
||||
slug: null,
|
||||
metadata: {},
|
||||
}) as TSurvey;
|
||||
|
||||
describe("getWebAppLocale", () => {
|
||||
const createMockSurvey = (languages: TSurvey["languages"] = []): TSurvey => {
|
||||
return {
|
||||
id: "survey-1",
|
||||
test("maps language codes and handles defaults", () => {
|
||||
expect(getWebAppLocale("en", createMockSurvey())).toBe("en-US");
|
||||
expect(getWebAppLocale("de", createMockSurvey())).toBe("de-DE");
|
||||
const surveyWithLang = createMockSurvey([
|
||||
{
|
||||
language: {
|
||||
id: "l1",
|
||||
code: "de",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "p1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(getWebAppLocale("default", surveyWithLang)).toBe("de-DE");
|
||||
expect(getWebAppLocale("xx", createMockSurvey())).toBe("en-US");
|
||||
});
|
||||
|
||||
test("returns en-US when default requested but no default language", () => {
|
||||
const surveyNoDefault = createMockSurvey([
|
||||
{
|
||||
language: { id: "l1", code: "de", alias: null, createdAt: new Date(), updatedAt: new Date(), projectId: "p1" },
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(getWebAppLocale("default", surveyNoDefault)).toBe("en-US");
|
||||
});
|
||||
|
||||
test("matches base language code for variants", () => {
|
||||
expect(getWebAppLocale("pt-PT", createMockSurvey())).toBe("pt-PT");
|
||||
expect(getWebAppLocale("es-MX", createMockSurvey())).toBe("es-ES");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTL", () => {
|
||||
test("detects RTL characters", () => {
|
||||
expect(isRTL("مرحبا")).toBe(true);
|
||||
expect(isRTL("שלום")).toBe(true);
|
||||
expect(isRTL("Hello")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTLLanguage", () => {
|
||||
const createJsSurvey = (
|
||||
languages: TJsEnvironmentStateSurvey["languages"] = [],
|
||||
blocks: TSurveyBlock[] = []
|
||||
): TJsEnvironmentStateSurvey =>
|
||||
({
|
||||
id: "s1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Survey",
|
||||
name: "Test",
|
||||
type: "link",
|
||||
environmentId: "env-1",
|
||||
createdBy: null,
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [],
|
||||
blocks: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
blocks,
|
||||
languages,
|
||||
displayPercentage: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
followUps: [],
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
showLanguageSwitch: null,
|
||||
recaptcha: null,
|
||||
isBackButtonHidden: false,
|
||||
isCaptureIpEnabled: false,
|
||||
slug: null,
|
||||
metadata: {},
|
||||
} as TSurvey;
|
||||
};
|
||||
}) as unknown as TJsEnvironmentStateSurvey;
|
||||
|
||||
test("maps language codes to web app locales", () => {
|
||||
const survey = createMockSurvey();
|
||||
expect(getWebAppLocale("en", survey)).toBe("en-US");
|
||||
expect(getWebAppLocale("de", survey)).toBe("de-DE");
|
||||
expect(getWebAppLocale("pt-BR", survey)).toBe("pt-BR");
|
||||
});
|
||||
|
||||
test("handles 'default' languageCode by finding default language in survey", () => {
|
||||
const survey = createMockSurvey([
|
||||
test("checks language codes when multi-language enabled", () => {
|
||||
const survey = createJsSurvey([
|
||||
{
|
||||
language: {
|
||||
id: "lang1",
|
||||
code: "de",
|
||||
id: "l1",
|
||||
code: "ar",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "proj1",
|
||||
projectId: "p1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(getWebAppLocale("default", survey)).toBe("de-DE");
|
||||
expect(isRTLLanguage(survey, "ar")).toBe(true);
|
||||
expect(isRTLLanguage(survey, "en")).toBe(false);
|
||||
});
|
||||
|
||||
test("falls back to en-US when language is not supported", () => {
|
||||
const survey = createMockSurvey();
|
||||
expect(getWebAppLocale("default", survey)).toBe("en-US");
|
||||
expect(getWebAppLocale("xx", survey)).toBe("en-US");
|
||||
test("checks content when no languages configured", () => {
|
||||
const element = {
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "مرحبا" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const block = { id: "b1", name: "Block", elements: [element] } as TSurveyBlock;
|
||||
expect(isRTLLanguage(createJsSurvey([], [block]), "default")).toBe(true);
|
||||
});
|
||||
|
||||
test("checks welcomeCard headline when enabled and no languages", () => {
|
||||
const survey = {
|
||||
...createJsSurvey([], []),
|
||||
welcomeCard: { enabled: true, headline: { default: "مرحبا" } },
|
||||
} as unknown as TJsEnvironmentStateSurvey;
|
||||
expect(isRTLLanguage(survey, "default")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when no languages and no headlines found", () => {
|
||||
const element = { id: "q1", type: TSurveyElementTypeEnum.OpenText, headline: {}, required: false };
|
||||
const block = { id: "b1", name: "Block", elements: [element] } as TSurveyBlock;
|
||||
expect(isRTLLanguage(createJsSurvey([], [block]), "default")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getElementsFromSurveyBlocks", () => {
|
||||
test("extracts elements from blocks", () => {
|
||||
const el1 = {
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const el2 = {
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q2" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const block = { id: "b1", name: "Block", elements: [el1, el2] } as TSurveyBlock;
|
||||
const result = getElementsFromSurveyBlocks([block]);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe("q1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export function isRTL(text: string): boolean {
|
||||
const rtlCharRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||
return rtlCharRegex.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of RTL language codes
|
||||
*/
|
||||
const RTL_LANGUAGES = ["ar", "ar-SA", "ar-EG", "ar-AE", "ar-MA", "he", "fa", "ur"];
|
||||
|
||||
/**
|
||||
* Returns true if the language code represents an RTL language.
|
||||
* @param survey The survey to test
|
||||
* @param languageCode The language code to test (e.g., "ar", "ar-SA", "he")
|
||||
*/
|
||||
export function isRTLLanguage(survey: TJsEnvironmentStateSurvey, languageCode: string): boolean {
|
||||
if (survey.languages.length === 0) {
|
||||
if (survey.welcomeCard.enabled) {
|
||||
const welcomeCardHeadline = survey.welcomeCard.headline?.[languageCode];
|
||||
if (welcomeCardHeadline) {
|
||||
return isRTL(welcomeCardHeadline);
|
||||
}
|
||||
}
|
||||
|
||||
const questions = getElementsFromSurveyBlocks(survey.blocks);
|
||||
for (const question of questions) {
|
||||
const questionHeadline = question.headline[languageCode];
|
||||
|
||||
// the first non-empty question headline is the survey direction
|
||||
if (questionHeadline) {
|
||||
return isRTL(questionHeadline);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const code =
|
||||
languageCode === "default"
|
||||
? survey.languages.find((language) => language.default)?.language.code
|
||||
: languageCode;
|
||||
const baseCode = code?.split("-")[0].toLowerCase() ?? "en";
|
||||
return RTL_LANGUAGES.some((rtl) => rtl.toLowerCase().startsWith(baseCode));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a flat array of elements from the survey's blocks structure.
|
||||
* @param blocks The blocks array
|
||||
* @returns An array of TSurveyElement (pure elements without block-level properties)
|
||||
*/
|
||||
export const getElementsFromSurveyBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] =>
|
||||
blocks.flatMap((block) => block.elements);
|
||||
|
||||
/**
|
||||
* Maps survey language codes to web app locale codes.
|
||||
* Falls back to "en-US" if the language is not available in web app locales.
|
||||
|
||||
@@ -13,6 +13,7 @@ interface ClientLogoProps {
|
||||
projectLogo: Project["logo"] | null;
|
||||
surveyLogo?: TLogo | null;
|
||||
previewSurvey?: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export const ClientLogo = ({
|
||||
@@ -20,13 +21,23 @@ export const ClientLogo = ({
|
||||
projectLogo,
|
||||
surveyLogo,
|
||||
previewSurvey = false,
|
||||
dir = "auto",
|
||||
}: ClientLogoProps) => {
|
||||
const { t } = useTranslation();
|
||||
const logoToUse = surveyLogo?.url ? surveyLogo : projectLogo;
|
||||
|
||||
let positionClasses = "";
|
||||
if (!previewSurvey) {
|
||||
if (dir === "rtl") {
|
||||
positionClasses = "top-3 right-3 md:top-7 md:right-7";
|
||||
} else {
|
||||
positionClasses = "top-3 left-3 md:top-7 md:left-7";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(previewSurvey ? "" : "top-3 left-3 md:top-7 md:left-7", "group absolute z-0 rounded-lg")}
|
||||
className={cn(positionClasses, "group absolute z-0 rounded-lg")}
|
||||
style={{ backgroundColor: logoToUse?.bgColor }}>
|
||||
{previewSurvey && environmentId && (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user