mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
fix: surveys package rtl (#6379)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
@@ -7,13 +7,13 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
ref={ref} // Forward the ref to the input element
|
||||
className={cn(
|
||||
"focus:fb-border-brand fb-bg-input-bg fb-flex fb-w-full fb-border fb-border-border fb-rounded-custom fb-px-3 fb-py-2 fb-text-sm fb-text-subheading placeholder:fb-text-placeholder focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300",
|
||||
className ?? ""
|
||||
)}
|
||||
dir="auto"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/preact";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { LanguageSwitch } from "./language-switch";
|
||||
|
||||
@@ -50,6 +51,20 @@ describe("LanguageSwitch", () => {
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
const mockSurvey = {
|
||||
id: "1",
|
||||
name: "Test Survey",
|
||||
description: "Test Description",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
headline: {
|
||||
en: "Test Welcome Card",
|
||||
},
|
||||
},
|
||||
questions: [],
|
||||
} as unknown as TJsEnvironmentStateSurvey;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -65,6 +80,7 @@ describe("LanguageSwitch", () => {
|
||||
surveyLanguages={surveyLanguages}
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
setFirstRender={mockSetFirstRender}
|
||||
survey={mockSurvey}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -89,6 +105,7 @@ describe("LanguageSwitch", () => {
|
||||
surveyLanguages={surveyLanguages}
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
setFirstRender={mockSetFirstRender}
|
||||
survey={mockSurvey}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { LanguageIcon } from "@/components/icons/language-icon";
|
||||
import { mixColor } from "@/lib/color";
|
||||
import { useClickOutside } from "@/lib/use-click-outside-hook";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { checkIfSurveyIsRTL, cn } from "@/lib/utils";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface LanguageSwitchProps {
|
||||
survey: TJsEnvironmentStateSurvey;
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
setFirstRender?: (firstRender: boolean) => void;
|
||||
hoverColor?: string;
|
||||
borderRadius?: number;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
setDir?: (dir: "ltr" | "rtl" | "auto") => void;
|
||||
}
|
||||
|
||||
export function LanguageSwitch({
|
||||
survey,
|
||||
surveyLanguages,
|
||||
setSelectedLanguageCode,
|
||||
setFirstRender,
|
||||
hoverColor,
|
||||
borderRadius,
|
||||
dir = "auto",
|
||||
setDir,
|
||||
}: LanguageSwitchProps) {
|
||||
const hoverColorWithOpacity = hoverColor ?? mixColor("#000000", "#ffffff", 0.8);
|
||||
|
||||
@@ -34,11 +42,14 @@ export function LanguageSwitch({
|
||||
})?.language.code;
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
if (languageCode === defaultLanguageCode) {
|
||||
setSelectedLanguageCode("default");
|
||||
} else {
|
||||
setSelectedLanguageCode(languageCode);
|
||||
const calculatedLanguageCode = languageCode === defaultLanguageCode ? "default" : languageCode;
|
||||
setSelectedLanguageCode(calculatedLanguageCode);
|
||||
|
||||
if (setDir) {
|
||||
const calculateDir = checkIfSurveyIsRTL(survey, calculatedLanguageCode) ? "rtl" : "auto";
|
||||
setDir?.(calculateDir);
|
||||
}
|
||||
|
||||
if (setFirstRender) {
|
||||
//for lexical editor
|
||||
setFirstRender(true);
|
||||
@@ -74,7 +85,10 @@ export function LanguageSwitch({
|
||||
</button>
|
||||
{showLanguageDropdown ? (
|
||||
<div
|
||||
className="fb-bg-brand fb-text-on-brand fb-absolute fb-right-8 fb-top-10 fb-space-y-2 fb-rounded-md fb-p-2 fb-text-xs"
|
||||
className={cn(
|
||||
"fb-bg-brand fb-text-on-brand fb-absolute fb-top-10 fb-space-y-2 fb-rounded-md fb-p-2 fb-text-xs",
|
||||
dir === "rtl" ? "fb-left-8" : "fb-right-8"
|
||||
)}
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map((surveyLanguage) => {
|
||||
if (!surveyLanguage.enabled) return;
|
||||
|
||||
@@ -44,6 +44,7 @@ interface QuestionConditionalProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
onOpenExternalURL?: (url: string) => void | Promise<void>;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function QuestionConditional({
|
||||
@@ -65,6 +66,7 @@ export function QuestionConditional({
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
onOpenExternalURL,
|
||||
dir,
|
||||
}: QuestionConditionalProps) {
|
||||
const getResponseValueForRankingQuestion = (
|
||||
value: string[],
|
||||
@@ -102,6 +104,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceSingleQuestion
|
||||
@@ -119,6 +122,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceMultiQuestion
|
||||
@@ -136,6 +140,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.NPS ? (
|
||||
<NPSQuestion
|
||||
@@ -153,6 +158,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.CTA ? (
|
||||
<CTAQuestion
|
||||
@@ -188,6 +194,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Consent ? (
|
||||
<ConsentQuestion
|
||||
@@ -205,6 +212,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Date ? (
|
||||
<DateQuestion
|
||||
@@ -239,6 +247,7 @@ export function QuestionConditional({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentQuestionId}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
|
||||
<FileUploadQuestion
|
||||
@@ -306,6 +315,7 @@ export function QuestionConditional({
|
||||
currentQuestionId={currentQuestionId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
|
||||
<RankingQuestion
|
||||
@@ -338,6 +348,7 @@ export function QuestionConditional({
|
||||
currentQuestionId={currentQuestionId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
isBackButtonHidden={isBackButtonHidden}
|
||||
dir={dir}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ describe("RenderSurvey", () => {
|
||||
test("renders with default props and handles close", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "question" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
render(
|
||||
(
|
||||
@@ -66,7 +69,10 @@ describe("RenderSurvey", () => {
|
||||
test("onFinished skips close if redirectToUrl", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "redirectToUrl" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "redirectToUrl" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
render(
|
||||
(
|
||||
@@ -91,7 +97,10 @@ describe("RenderSurvey", () => {
|
||||
test("onFinished closes after delay for non-redirect endings", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "question" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
render(
|
||||
(
|
||||
@@ -118,7 +127,10 @@ describe("RenderSurvey", () => {
|
||||
test("onFinished does not auto-close when inline mode", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [] } as any;
|
||||
const survey = {
|
||||
endings: [],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
render(
|
||||
(
|
||||
@@ -143,7 +155,10 @@ describe("RenderSurvey", () => {
|
||||
test("close clears any pending onFinished timeout", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "question" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
const { unmount } = render(
|
||||
(
|
||||
<RenderSurvey
|
||||
@@ -178,7 +193,10 @@ describe("RenderSurvey", () => {
|
||||
test("double close only schedules one onClose", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "question" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
render(
|
||||
(
|
||||
@@ -212,7 +230,11 @@ describe("RenderSurvey", () => {
|
||||
test("cleanup on unmount clears pending timers (useEffect)", () => {
|
||||
const onClose = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const survey = { endings: [{ id: "e1", type: "question" }] } as any;
|
||||
const survey = {
|
||||
endings: [{ id: "e1", type: "question" }],
|
||||
welcomeCard: { enabled: true, headline: { en: "Welcome" } },
|
||||
} as any;
|
||||
|
||||
const { unmount } = render(
|
||||
(
|
||||
<RenderSurvey
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { checkIfSurveyIsRTL } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { SurveyContainer } from "../wrappers/survey-container";
|
||||
@@ -8,6 +9,14 @@ export function RenderSurvey(props: SurveyContainerProps) {
|
||||
const onFinishedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const isRTL = checkIfSurveyIsRTL(props.survey, props.languageCode);
|
||||
const [dir, setDir] = useState<"ltr" | "rtl" | "auto">(isRTL ? "rtl" : "auto");
|
||||
|
||||
useEffect(() => {
|
||||
const isRTL = checkIfSurveyIsRTL(props.survey, props.languageCode);
|
||||
setDir(isRTL ? "rtl" : "auto");
|
||||
}, [props.languageCode, props.survey]);
|
||||
|
||||
const close = () => {
|
||||
if (onFinishedTimeoutRef.current) {
|
||||
clearTimeout(onFinishedTimeoutRef.current);
|
||||
@@ -51,7 +60,8 @@ export function RenderSurvey(props: SurveyContainerProps) {
|
||||
darkOverlay={props.darkOverlay}
|
||||
clickOutside={props.clickOutside}
|
||||
onClose={close}
|
||||
isOpen={isOpen}>
|
||||
isOpen={isOpen}
|
||||
dir={dir}>
|
||||
{/* @ts-expect-error -- TODO: fix this */}
|
||||
<Survey
|
||||
{...props}
|
||||
@@ -72,6 +82,8 @@ export function RenderSurvey(props: SurveyContainerProps) {
|
||||
);
|
||||
}
|
||||
}}
|
||||
dir={dir}
|
||||
setDir={setDir}
|
||||
/>
|
||||
</SurveyContainer>
|
||||
);
|
||||
|
||||
@@ -75,6 +75,8 @@ export function Survey({
|
||||
isWebEnvironment = true,
|
||||
getRecaptchaToken,
|
||||
isSpamProtectionEnabled,
|
||||
dir = "auto",
|
||||
setDir,
|
||||
}: SurveyContainerProps) {
|
||||
let apiClient: ApiClient | null = null;
|
||||
|
||||
@@ -172,7 +174,7 @@ export function Survey({
|
||||
!getSetIsResponseSendingFinished
|
||||
);
|
||||
const [isSurveyFinished, setIsSurveyFinished] = useState(false);
|
||||
const [selectedLanguage, setselectedLanguage] = useState(languageCode);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languageCode);
|
||||
const [loadingElement, setLoadingElement] = useState(false);
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [responseData, setResponseData] = useState<TResponseData>(hiddenFieldsRecord ?? {});
|
||||
@@ -319,7 +321,7 @@ export function Survey({
|
||||
}, [getSetIsResponseSendingFinished]);
|
||||
|
||||
useEffect(() => {
|
||||
setselectedLanguage(languageCode);
|
||||
setSelectedLanguage(languageCode);
|
||||
}, [languageCode]);
|
||||
|
||||
const onChange = (responseDataUpdate: TResponseData) => {
|
||||
@@ -730,6 +732,7 @@ export function Survey({
|
||||
currentQuestionId={questionId}
|
||||
isBackButtonHidden={localSurvey.isBackButtonHidden}
|
||||
onOpenExternalURL={onOpenExternalURL}
|
||||
dir={dir}
|
||||
/>
|
||||
)
|
||||
);
|
||||
@@ -760,13 +763,16 @@ export function Survey({
|
||||
"fb-relative fb-w-full",
|
||||
isCloseButtonVisible || isLanguageSwitchVisible ? "fb-h-8" : "fb-h-5"
|
||||
)}>
|
||||
<div className="fb-flex fb-items-center fb-justify-end fb-absolute fb-top-0 fb-right-0">
|
||||
<div className={cn("fb-flex fb-items-center fb-justify-end fb-w-full")}>
|
||||
{isLanguageSwitchVisible && (
|
||||
<LanguageSwitch
|
||||
survey={localSurvey}
|
||||
surveyLanguages={localSurvey.languages}
|
||||
setSelectedLanguageCode={setselectedLanguage}
|
||||
setSelectedLanguageCode={setSelectedLanguage}
|
||||
hoverColor={styling.inputColor?.light ?? "#f8fafc"}
|
||||
borderRadius={styling.roundness ?? 8}
|
||||
setDir={setDir}
|
||||
dir={dir}
|
||||
/>
|
||||
)}
|
||||
{isLanguageSwitchVisible && isCloseButtonVisible && (
|
||||
|
||||
@@ -27,6 +27,7 @@ interface AddressQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
autoFocusEnabled: boolean;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function AddressQuestion({
|
||||
@@ -43,6 +44,7 @@ export function AddressQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<AddressQuestionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -170,6 +172,7 @@ export function AddressQuestion({
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
dir={!safeValue[index] ? dir : "auto"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ interface ConsentQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function ConsentQuestion({
|
||||
@@ -40,6 +41,7 @@ export function ConsentQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<ConsentQuestionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -76,7 +78,6 @@ export function ConsentQuestion({
|
||||
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode) || ""} />
|
||||
<label
|
||||
ref={consentRef}
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
id={`${question.id}-label`}
|
||||
onKeyDown={(e) => {
|
||||
@@ -91,6 +92,7 @@ export function ConsentQuestion({
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="checkbox"
|
||||
dir={dir}
|
||||
id={question.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(question.label, languageCode)}
|
||||
@@ -106,7 +108,7 @@ export function ConsentQuestion({
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
|
||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium fb-flex-1" dir="auto">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -27,6 +27,7 @@ interface ContactInfoQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
autoFocusEnabled: boolean;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function ContactInfoQuestion({
|
||||
@@ -43,6 +44,7 @@ export function ContactInfoQuestion({
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<ContactInfoQuestionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -169,6 +171,7 @@ export function ContactInfoQuestion({
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
dir={!safeValue[index] ? dir : "auto"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds, isRTL } from "@/lib/utils";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
@@ -25,6 +25,7 @@ interface MultipleChoiceMultiProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function MultipleChoiceMultiQuestion({
|
||||
@@ -41,6 +42,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<MultipleChoiceMultiProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -140,11 +142,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
: question.required;
|
||||
};
|
||||
|
||||
const otherOptionDir = useMemo(() => {
|
||||
const placeholder = getLocalizedValue(question.otherOptionPlaceholder, languageCode);
|
||||
if (!otherValue) return isRTL(placeholder) ? "rtl" : "ltr";
|
||||
return "auto";
|
||||
}, [languageCode, question.otherOptionPlaceholder, otherValue]);
|
||||
const otherOptionInputDir = !otherValue ? dir : "auto";
|
||||
|
||||
return (
|
||||
<ScrollableContainer>
|
||||
@@ -197,9 +195,10 @@ export function MultipleChoiceMultiQuestion({
|
||||
}
|
||||
}}
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
dir={dir}
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
tabIndex={-1}
|
||||
@@ -219,7 +218,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
}
|
||||
required={getIsRequired()}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
<span id={`${choice.id}-label`} className="fb-mx-3 fb-grow fb-font-medium" dir="auto">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -241,9 +240,10 @@ export function MultipleChoiceMultiQuestion({
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
dir={dir}
|
||||
tabIndex={-1}
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
@@ -263,14 +263,17 @@ export function MultipleChoiceMultiQuestion({
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
<span
|
||||
id={`${otherOption.id}-label`}
|
||||
className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium"
|
||||
dir="auto">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir={otherOptionDir}
|
||||
dir={otherOptionInputDir}
|
||||
id={`${otherOption.id}-label`}
|
||||
maxLength={250}
|
||||
name={question.id}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds, isRTL } from "@/lib/utils";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
@@ -25,6 +25,7 @@ interface MultipleChoiceSingleProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function MultipleChoiceSingleQuestion({
|
||||
@@ -41,6 +42,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<MultipleChoiceSingleProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [otherSelected, setOtherSelected] = useState(false);
|
||||
@@ -100,11 +102,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
}
|
||||
}, [otherSelected]);
|
||||
|
||||
const otherOptionDir = useMemo(() => {
|
||||
const placeholder = getLocalizedValue(question.otherOptionPlaceholder, languageCode);
|
||||
if (!value) return isRTL(placeholder) ? "rtl" : "ltr";
|
||||
return "auto";
|
||||
}, [languageCode, question.otherOptionPlaceholder, value]);
|
||||
const otherOptionInputDir = !value ? dir : "auto";
|
||||
|
||||
return (
|
||||
<ScrollableContainer>
|
||||
@@ -139,7 +137,6 @@ export function MultipleChoiceSingleQuestion({
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
dir="auto"
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
@@ -164,7 +161,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
dir="auto"
|
||||
dir={dir}
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-flex-shrink-0 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={() => {
|
||||
@@ -174,7 +171,10 @@ export function MultipleChoiceSingleQuestion({
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required ? idx === 0 : undefined}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
<span
|
||||
id={`${choice.id}-label`}
|
||||
className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium"
|
||||
dir="auto">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -183,7 +183,6 @@ export function MultipleChoiceSingleQuestion({
|
||||
})}
|
||||
{otherOption ? (
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
@@ -199,10 +198,10 @@ export function MultipleChoiceSingleQuestion({
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
|
||||
<span className="fb-flex fb-items-center fb-text-sm">
|
||||
<input
|
||||
tabIndex={-1}
|
||||
dir="auto"
|
||||
dir={dir}
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
@@ -215,7 +214,10 @@ export function MultipleChoiceSingleQuestion({
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
<span
|
||||
id={`${otherOption.id}-label`}
|
||||
className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium"
|
||||
dir="auto">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -223,7 +225,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
dir={otherOptionDir}
|
||||
dir={otherOptionInputDir}
|
||||
name={question.id}
|
||||
pattern=".*\S+.*"
|
||||
value={value}
|
||||
|
||||
@@ -25,6 +25,7 @@ interface NPSQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function NPSQuestion({
|
||||
@@ -40,6 +41,7 @@ export function NPSQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<NPSQuestionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||
@@ -120,9 +122,12 @@ export function NPSQuestion({
|
||||
value === number
|
||||
? "fb-border-border-highlight fb-bg-accent-selected-bg fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
"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 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 focus:fb-border-2 focus:fb-outline-none",
|
||||
question.isColorCodingEnabled ? "fb-h-[46px] fb-leading-[3.5em]" : "fb-h fb-leading-10",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : "",
|
||||
dir === "rtl"
|
||||
? "first:fb-rounded-r-custom first:fb-border-r last:fb-rounded-l-custom last:fb-border-l"
|
||||
: "first:fb-rounded-l-custom first:fb-border-l last:fb-rounded-r-custom last:fb-border-r"
|
||||
)}>
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
@@ -147,9 +152,13 @@ export function NPSQuestion({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-2 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p dir="auto">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p dir="auto">{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
<div className="fb-text-subheading fb-mt-2 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-gap-8">
|
||||
<p dir="auto" className="fb-max-w-[50%]">
|
||||
{getLocalizedValue(question.lowerLabel, languageCode)}
|
||||
</p>
|
||||
<p dir="auto" className="fb-max-w-[50%]">
|
||||
{getLocalizedValue(question.upperLabel, languageCode)}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,8 @@ import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { isRTL } from "@/lib/utils";
|
||||
import { type RefObject } from "preact";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -27,6 +26,7 @@ interface OpenTextQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function OpenTextQuestion({
|
||||
@@ -43,6 +43,7 @@ export function OpenTextQuestion({
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<OpenTextQuestionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [currentLength, setCurrentLength] = useState(value.length || 0);
|
||||
@@ -81,11 +82,7 @@ export function OpenTextQuestion({
|
||||
onSubmit({ [question.id]: value }, updatedTtc);
|
||||
};
|
||||
|
||||
const dir = useMemo(() => {
|
||||
const placeholder = getLocalizedValue(question.placeholder, languageCode);
|
||||
if (!value) return isRTL(placeholder) ? "rtl" : "ltr";
|
||||
return "auto";
|
||||
}, [value, languageCode, question.placeholder]);
|
||||
const computedDir = !value ? dir : "auto";
|
||||
|
||||
return (
|
||||
<ScrollableContainer>
|
||||
@@ -109,7 +106,7 @@ export function OpenTextQuestion({
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir={dir}
|
||||
dir={computedDir}
|
||||
step="any"
|
||||
required={question.required}
|
||||
value={value ? value : ""}
|
||||
|
||||
@@ -27,6 +27,7 @@ interface PictureSelectionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function PictureSelectionQuestion({
|
||||
@@ -42,6 +43,7 @@ export function PictureSelectionQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: Readonly<PictureSelectionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(() => {
|
||||
@@ -169,8 +171,9 @@ export function PictureSelectionQuestion({
|
||||
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" : ""
|
||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : "",
|
||||
dir === "rtl" ? "fb-left-2" : "fb-right-2"
|
||||
)}
|
||||
required={question.required && value.length === 0}
|
||||
/>
|
||||
@@ -182,8 +185,9 @@ export function PictureSelectionQuestion({
|
||||
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" : ""
|
||||
"fb-border-border fb-pointer-events-none fb-absolute 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" : "",
|
||||
dir === "rtl" ? "fb-left-2" : "fb-right-2"
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
@@ -198,7 +202,10 @@ export function PictureSelectionQuestion({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
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">
|
||||
className={cn(
|
||||
"fb-absolute fb-bottom-4 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",
|
||||
dir === "rtl" ? "fb-left-2" : "fb-right-2"
|
||||
)}>
|
||||
<span className="fb-sr-only">Open in new tab</span>
|
||||
<ImageDownIcon />
|
||||
</a>
|
||||
|
||||
@@ -38,6 +38,7 @@ interface RatingQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
isBackButtonHidden: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function RatingQuestion({
|
||||
@@ -53,6 +54,7 @@ export function RatingQuestion({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
isBackButtonHidden,
|
||||
dir = "auto",
|
||||
}: RatingQuestionProps) {
|
||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
@@ -160,8 +162,16 @@ export function RatingQuestion({
|
||||
value === number
|
||||
? "fb-bg-accent-selected-bg fb-border-border-highlight fb-z-10 fb-border"
|
||||
: "fb-border-border",
|
||||
a.length === number ? "fb-rounded-r-custom fb-border-r" : "",
|
||||
number === 1 ? "fb-rounded-l-custom" : "",
|
||||
a.length === number
|
||||
? dir === "rtl"
|
||||
? "fb-rounded-l-custom fb-border-l"
|
||||
: "fb-rounded-r-custom fb-border-r"
|
||||
: "",
|
||||
number === 1
|
||||
? dir === "rtl"
|
||||
? "fb-rounded-r-custom fb-border-r"
|
||||
: "fb-rounded-l-custom fb-border-l"
|
||||
: "",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : "",
|
||||
question.isColorCodingEnabled ? "fb-min-h-[47px]" : "fb-min-h-[41px]",
|
||||
"fb-text-heading focus:fb-border-brand fb-relative fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-justify-center fb-overflow-hidden fb-border-b fb-border-l fb-border-t focus:fb-border-2 focus:fb-outline-none"
|
||||
@@ -245,11 +255,11 @@ export function RatingQuestion({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-space-x-8">
|
||||
<p className="fb-w-1/2 fb-text-left" dir="auto">
|
||||
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6 fb-gap-8">
|
||||
<p className="fb-max-w-[50%]" dir="auto">
|
||||
{getLocalizedValue(question.lowerLabel, languageCode)}
|
||||
</p>
|
||||
<p className="fb-w-1/2 fb-text-right" dir="auto">
|
||||
<p className="fb-max-w-[50%]" dir="auto">
|
||||
{getLocalizedValue(question.upperLabel, languageCode)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ interface SurveyContainerProps {
|
||||
onClose?: () => void;
|
||||
clickOutside?: boolean;
|
||||
isOpen?: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function SurveyContainer({
|
||||
@@ -20,6 +21,7 @@ export function SurveyContainer({
|
||||
onClose,
|
||||
clickOutside,
|
||||
isOpen = true,
|
||||
dir = "auto",
|
||||
}: Readonly<SurveyContainerProps>) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const isCenter = placement === "center";
|
||||
@@ -67,14 +69,14 @@ export function SurveyContainer({
|
||||
|
||||
if (!isModal) {
|
||||
return (
|
||||
<div id="fbjs" className="fb-formbricks-form" style={{ height: "100%", width: "100%" }}>
|
||||
<div id="fbjs" className="fb-formbricks-form" style={{ height: "100%", width: "100%" }} dir={dir}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="fbjs" className="fb-formbricks-form">
|
||||
<div id="fbjs" className="fb-formbricks-form" dir={dir}>
|
||||
<div
|
||||
aria-live="assertive"
|
||||
className={cn(
|
||||
|
||||
@@ -34,13 +34,23 @@ export const renderSurvey = (props: SurveyContainerProps) => {
|
||||
|
||||
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||
|
||||
render(h(RenderSurvey, surveyInlineProps), element);
|
||||
render(
|
||||
h(RenderSurvey, {
|
||||
...surveyInlineProps,
|
||||
}),
|
||||
element
|
||||
);
|
||||
} else {
|
||||
const modalContainer = document.createElement("div");
|
||||
modalContainer.id = "formbricks-modal-container";
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
render(h(RenderSurvey, props), modalContainer);
|
||||
render(
|
||||
h(RenderSurvey, {
|
||||
...props,
|
||||
}),
|
||||
modalContainer
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -177,3 +177,23 @@ export function isRTL(text: string): boolean {
|
||||
const rtlCharRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||
return rtlCharRegex.test(text);
|
||||
}
|
||||
|
||||
export const checkIfSurveyIsRTL = (survey: TJsEnvironmentStateSurvey, languageCode: string): boolean => {
|
||||
if (survey.welcomeCard.enabled) {
|
||||
const welcomeCardHeadline = survey.welcomeCard.headline?.[languageCode];
|
||||
if (welcomeCardHeadline) {
|
||||
return isRTL(welcomeCardHeadline);
|
||||
}
|
||||
}
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const questionHeadline = question.headline[languageCode];
|
||||
|
||||
// the first non-empty question headline is the survey direction
|
||||
if (questionHeadline) {
|
||||
return isRTL(questionHeadline);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface SurveyBaseProps {
|
||||
prefillResponseData?: TResponseData;
|
||||
skipPrefilled?: boolean;
|
||||
languageCode: string;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
setDir?: (dir: "ltr" | "rtl" | "auto") => void;
|
||||
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
responseCount?: number;
|
||||
isCardBorderVisible?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user