mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-16 11:41:41 -05:00
Compare commits
1 Commits
4.8.0
...
fix/1476-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6710f1d9a9 |
@@ -92,11 +92,7 @@ function Consent({
|
||||
disabled={disabled}
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
{/* need to use style here because tailwind is not able to use css variables for font size and weight */}
|
||||
<span
|
||||
className="font-input-weight text-input-text flex-1"
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||
dir={dir}>
|
||||
<span className="fb-text-input-size font-input-weight text-input-text flex-1" dir={dir}>
|
||||
{checkboxLabel}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -96,8 +96,7 @@ function UploadedFileItem({
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<UploadIcon />
|
||||
<p
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||
className="mt-1 w-full overflow-hidden px-2 text-center overflow-ellipsis whitespace-nowrap text-[var(--foreground)]"
|
||||
className="fb-text-input-size mt-1 w-full overflow-hidden px-2 text-center overflow-ellipsis whitespace-nowrap text-[var(--foreground)]"
|
||||
title={file.name}>
|
||||
{file.name}
|
||||
</p>
|
||||
@@ -188,10 +187,7 @@ function UploadArea({
|
||||
)}
|
||||
aria-label="Upload files by clicking or dragging them here">
|
||||
<Upload className="text-input-text h-6" aria-hidden="true" />
|
||||
<span
|
||||
className="text-input-text font-input-weight m-2"
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||
id={`${inputId}-label`}>
|
||||
<span className="fb-text-input-size text-input-text font-input-weight m-2" id={`${inputId}-label`}>
|
||||
{placeholderText}
|
||||
</span>
|
||||
<input
|
||||
@@ -306,11 +302,7 @@ function FileUpload({
|
||||
<div className="w-full">
|
||||
{isUploading ? (
|
||||
<div className="flex animate-pulse items-center justify-center rounded-lg py-4">
|
||||
<p
|
||||
className="text-muted-foreground font-medium"
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}>
|
||||
{uploadingText}
|
||||
</p>
|
||||
<p className="fb-text-input-size text-muted-foreground font-medium">{uploadingText}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -56,8 +56,7 @@ function ElementMedia({ imgUrl, videoUrl, altText = "Image" }: Readonly<ElementM
|
||||
<iframe
|
||||
src={videoUrlWithParams}
|
||||
title="Question video"
|
||||
style={{ border: 0 }}
|
||||
className={cn("aspect-video w-full rounded-md", isLoading ? "opacity-0" : "")}
|
||||
className={cn("aspect-video w-full rounded-md border-0", isLoading ? "opacity-0" : "")}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
|
||||
@@ -19,8 +19,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
type={type}
|
||||
dir={dir}
|
||||
data-slot="input"
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||
className={cn(
|
||||
"fb-text-input-size",
|
||||
// Layout and behavior
|
||||
"flex min-w-0 border transition-[color,box-shadow] outline-none",
|
||||
// Customizable styles via CSS variables (using Tailwind theme extensions)
|
||||
|
||||
@@ -8,6 +8,14 @@ export interface ProgressProps extends Omit<React.ComponentProps<"div">, "childr
|
||||
|
||||
function Progress({ className, value, ...props }: Readonly<ProgressProps>): React.JSX.Element {
|
||||
const progressValue: number = typeof value === "number" ? value : 0;
|
||||
const indicatorRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (indicatorRef.current) {
|
||||
indicatorRef.current.style.setProperty("transform", `translateX(-${String(100 - progressValue)}%)`);
|
||||
}
|
||||
}, [progressValue]);
|
||||
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
@@ -15,11 +23,9 @@ function Progress({ className, value, ...props }: Readonly<ProgressProps>): Reac
|
||||
className={cn("progress-track relative w-full overflow-hidden", className)}
|
||||
{...props}>
|
||||
<ProgressPrimitive.Indicator
|
||||
ref={indicatorRef}
|
||||
data-slot="progress-indicator"
|
||||
className="progress-indicator h-full w-full flex-1 transition-all"
|
||||
style={{
|
||||
transform: `translateX(-${String(100 - progressValue)}%)`,
|
||||
}}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
|
||||
@@ -10,9 +10,9 @@ function Textarea({ className, dir = "auto", ...props }: TextareaProps): React.J
|
||||
<div className="relative space-y-2">
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||
dir={dir}
|
||||
className={cn(
|
||||
"fb-text-input-size",
|
||||
"w-input bg-input-bg border-input-border rounded-input font-input font-input-weight px-input-x py-input-y shadow-input placeholder:text-input-placeholder placeholder:opacity-input-placeholder focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 text-input text-input-text flex field-sizing-content min-h-16 border transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -221,6 +221,13 @@
|
||||
Textarea Scrollbar Styling
|
||||
Smaller, more subtle scrollbars for textarea elements.
|
||||
--------------------------------------------------------------------------- */
|
||||
/* CSP-safe utility: font-size from --fb-input-font-size variable.
|
||||
Cannot use Tailwind `text-input` because the `input` token is ambiguous
|
||||
between fontSize.input and colors.input in this config. */
|
||||
#fbjs .fb-text-input-size {
|
||||
font-size: var(--fb-input-font-size);
|
||||
}
|
||||
|
||||
#fbjs textarea {
|
||||
/* Firefox */
|
||||
scrollbar-width: thin;
|
||||
|
||||
@@ -77,18 +77,6 @@ export function SubmitButton({
|
||||
"border-submit-button-border focus:ring-focus mb-1 flex items-center justify-center border leading-4 shadow-xs hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-hidden",
|
||||
"button-custom"
|
||||
)}
|
||||
style={{
|
||||
borderRadius: "var(--fb-button-border-radius)",
|
||||
backgroundColor: "var(--fb-button-bg-color)",
|
||||
color: "var(--fb-button-text-color)",
|
||||
height: "var(--fb-button-height)",
|
||||
fontSize: "var(--fb-button-font-size)",
|
||||
fontWeight: "var(--fb-button-font-weight)",
|
||||
paddingLeft: "var(--fb-button-padding-x)",
|
||||
paddingRight: "var(--fb-button-padding-x)",
|
||||
paddingTop: "var(--fb-button-padding-y)",
|
||||
paddingBottom: "var(--fb-button-padding-y)",
|
||||
}}
|
||||
onClick={onClick}
|
||||
disabled={disabled}>
|
||||
{buttonLabel || (isLastQuestion ? t("common.finish") : t("common.next"))}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
|
||||
interface AutoCloseProgressBarProps {
|
||||
autoCloseTimeout: number;
|
||||
}
|
||||
@@ -7,10 +9,12 @@ export function AutoCloseProgressBar({ autoCloseTimeout }: AutoCloseProgressBarP
|
||||
<div className="bg-accent-bg h-2 w-full overflow-hidden">
|
||||
<div
|
||||
key={autoCloseTimeout}
|
||||
className="bg-brand z-20 h-2"
|
||||
style={{
|
||||
animation: `shrink-width-to-zero ${autoCloseTimeout.toString()}s linear forwards`,
|
||||
width: "100%",
|
||||
className="bg-brand z-20 h-2 w-full"
|
||||
ref={(el) => {
|
||||
if (el)
|
||||
applyCsomStyles(el, {
|
||||
animation: `shrink-width-to-zero ${autoCloseTimeout.toString()}s linear forwards`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
@@ -8,6 +8,7 @@ import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import i18n from "@/lib/i18n.config";
|
||||
import { getLanguageDisplayName } from "@/lib/language-display-name";
|
||||
import { useClickOutside } from "@/lib/use-click-outside-hook";
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
import { cn, isRTLLanguage } from "@/lib/utils";
|
||||
|
||||
interface LanguageSwitchProps {
|
||||
@@ -35,6 +36,21 @@ export function LanguageSwitch({
|
||||
const hoverColorWithOpacity = hoverColor ?? mixColor("#000000", "#ffffff", 0.8);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const switchBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const applyHoverStyles = (el: HTMLButtonElement | null, hovered: boolean) => {
|
||||
if (!el) return;
|
||||
applyCsomStyles(el, {
|
||||
backgroundColor: hovered ? hoverColorWithOpacity : "transparent",
|
||||
transition: "background-color 0.2s ease",
|
||||
borderRadius: typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
|
||||
});
|
||||
};
|
||||
|
||||
// Keep styles in sync when props change
|
||||
useEffect(() => {
|
||||
applyHoverStyles(switchBtnRef.current, isHovered);
|
||||
}, [hoverColorWithOpacity, borderRadius]);
|
||||
|
||||
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
||||
const toggleDropdown = () => {
|
||||
@@ -79,21 +95,26 @@ export function LanguageSwitch({
|
||||
<button
|
||||
title={t("common.language_switch")}
|
||||
type="button"
|
||||
ref={(el) => {
|
||||
(switchBtnRef as { current: HTMLButtonElement | null }).current = el;
|
||||
applyHoverStyles(el, isHovered);
|
||||
}}
|
||||
className={cn(
|
||||
"text-heading relative flex h-8 w-8 items-center justify-center rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isHovered ? hoverColorWithOpacity : "transparent",
|
||||
transition: "background-color 0.2s ease",
|
||||
borderRadius: typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
|
||||
}}
|
||||
onClick={toggleDropdown}
|
||||
tabIndex={-1}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showLanguageDropdown}
|
||||
aria-label={t("common.language_switch")}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}>
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
applyHoverStyles(switchBtnRef.current, true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
applyHoverStyles(switchBtnRef.current, false);
|
||||
}}>
|
||||
<LanguageIcon />
|
||||
</button>
|
||||
{showLanguageDropdown ? (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
|
||||
export function Progress({ progress }: { progress: number }) {
|
||||
return (
|
||||
<div className="progress-track h-2 w-full overflow-hidden rounded-none">
|
||||
<div
|
||||
className="transition-width progress-indicator z-20 h-full duration-500"
|
||||
style={{
|
||||
width: `${Math.floor(progress * 100).toString()}%`,
|
||||
ref={(el) => {
|
||||
if (el) applyCsomStyles(el, { width: `${Math.floor(progress * 100).toString()}%` });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CloseIcon } from "@/components/icons/close-icon";
|
||||
import { mixColor } from "@/lib/color";
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SurveyCloseButtonProps {
|
||||
@@ -14,19 +15,34 @@ export function SurveyCloseButton({ onClose, hoverColor, borderRadius }: Readonl
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const hoverColorWithOpacity = hoverColor ?? mixColor("#000000", "#ffffff", 0.8);
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const applyStyles = (el: HTMLButtonElement | null, hovered: boolean) => {
|
||||
if (!el) return;
|
||||
applyCsomStyles(el, {
|
||||
backgroundColor: hovered ? hoverColorWithOpacity : "transparent",
|
||||
transition: "background-color 0.2s ease",
|
||||
borderRadius: typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-1001 flex w-fit items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
backgroundColor: isHovered ? hoverColorWithOpacity : "transparent",
|
||||
transition: "background-color 0.2s ease",
|
||||
borderRadius: typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
|
||||
ref={(el) => {
|
||||
(btnRef as { current: HTMLButtonElement | null }).current = el;
|
||||
applyStyles(el, isHovered);
|
||||
}}
|
||||
onClick={onClose}
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
applyStyles(btnRef.current, true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
applyStyles(btnRef.current, false);
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={cn(
|
||||
"text-heading relative flex h-8 w-8 items-center justify-center p-2 focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
|
||||
)}
|
||||
|
||||
@@ -72,17 +72,19 @@ export const ScrollableContainer = forwardRef<ScrollableContainerHandle, Scrolla
|
||||
maxHeight = "60dvh";
|
||||
}
|
||||
|
||||
// Apply maxHeight via CSSOM to avoid inline style attributes (CSP compliance)
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.style.setProperty("max-height", maxHeight);
|
||||
}
|
||||
}, [maxHeight]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{!isAtTop && (
|
||||
<div className="from-survey-bg absolute top-0 right-2 left-0 z-10 h-4 bg-linear-to-b to-transparent" />
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
className={cn("bg-survey-bg overflow-auto px-4")}>
|
||||
<div ref={containerRef} className={cn("bg-survey-bg overflow-auto px-4")}>
|
||||
{children}
|
||||
</div>
|
||||
{!isAtBottom && (
|
||||
@@ -91,8 +93,7 @@ export const ScrollableContainer = forwardRef<ScrollableContainerHandle, Scrolla
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToBottom}
|
||||
style={{ transform: "translateX(-50%)" }}
|
||||
className="bg-survey-bg hover:border-border focus:ring-brand absolute bottom-2 left-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full border border-transparent shadow-lg transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
|
||||
className="fb-translate-x-center bg-survey-bg hover:border-border focus:ring-brand absolute bottom-2 left-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full border border-transparent shadow-lg transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
|
||||
aria-label="Scroll to bottom">
|
||||
<ChevronDownIcon className="text-heading h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MutableRef } from "preact/hooks";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import React from "react";
|
||||
import { type TPlacement } from "@formbricks/types/common";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
|
||||
interface StackedCardProps {
|
||||
cardRefs: MutableRef<(HTMLDivElement | null)[]>;
|
||||
@@ -54,7 +55,14 @@ export const StackedCard = ({
|
||||
};
|
||||
|
||||
const getDummyCardContent = () => {
|
||||
return <div style={{ height: cardHeight }} className="w-full p-6"></div>;
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el) applyCsomStyles(el, { height: cardHeight });
|
||||
}}
|
||||
className="w-full p-6"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const calculateCardTransform = useMemo(() => {
|
||||
@@ -90,6 +98,45 @@ export const StackedCard = ({
|
||||
}
|
||||
: {};
|
||||
|
||||
// Collect all dynamic styles for the card element
|
||||
const cardStyles = useMemo(
|
||||
() => ({
|
||||
zIndex: `${(1000 - dynamicQuestionIndex).toString()}`,
|
||||
transform: calculateCardTransform(offset),
|
||||
opacity: `${isHidden ? 0 : (100 - 20 * offset) / 100}`,
|
||||
height: fullSizeCards ? "100%" : currentCardHeight,
|
||||
pointerEvents: offset === 0 ? "auto" : "none",
|
||||
...borderStyles,
|
||||
...straightCardArrangementStyles,
|
||||
...getTopBottomStyles(),
|
||||
}),
|
||||
[
|
||||
dynamicQuestionIndex,
|
||||
calculateCardTransform,
|
||||
offset,
|
||||
isHidden,
|
||||
fullSizeCards,
|
||||
currentCardHeight,
|
||||
borderStyles,
|
||||
straightCardArrangementStyles,
|
||||
]
|
||||
);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Apply card styles via CSSOM whenever they change
|
||||
useEffect(() => {
|
||||
const el = cardRefs.current[dynamicQuestionIndex];
|
||||
if (el) applyCsomStyles(el, cardStyles);
|
||||
}, [cardStyles, dynamicQuestionIndex]);
|
||||
|
||||
// Apply content opacity via CSSOM whenever it changes
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.setProperty("opacity", `${contentOpacity}`);
|
||||
}
|
||||
}, [contentOpacity]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDelayedOffset(offset);
|
||||
@@ -107,28 +154,13 @@ export const StackedCard = ({
|
||||
<div
|
||||
ref={(el) => {
|
||||
cardRefs.current[dynamicQuestionIndex] = el;
|
||||
if (el) applyCsomStyles(el, cardStyles);
|
||||
}}
|
||||
id={`questionCard-${dynamicQuestionIndex}`}
|
||||
data-testid={`questionCard-${dynamicQuestionIndex}`}
|
||||
key={dynamicQuestionIndex}
|
||||
style={{
|
||||
zIndex: 1000 - dynamicQuestionIndex,
|
||||
transform: calculateCardTransform(offset),
|
||||
opacity: isHidden ? 0 : (100 - 20 * offset) / 100,
|
||||
height: fullSizeCards ? "100%" : currentCardHeight,
|
||||
transition: "transform 600ms ease-in-out, opacity 600ms ease-in-out, width 600ms ease-in-out",
|
||||
pointerEvents: offset === 0 ? "auto" : "none",
|
||||
...borderStyles,
|
||||
...straightCardArrangementStyles,
|
||||
...getTopBottomStyles(),
|
||||
}}
|
||||
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 overflow-hidden">
|
||||
<div
|
||||
style={{
|
||||
opacity: contentOpacity,
|
||||
transition: "opacity 300ms ease-in-out",
|
||||
height: "100%",
|
||||
}}>
|
||||
className="pointer rounded-custom bg-survey-bg fb-card-transition absolute inset-x-0 overflow-hidden">
|
||||
<div className="fb-content-transition" ref={contentRef}>
|
||||
{delayedOffset === 0 ? getCardContent(dynamicQuestionIndex, offset) : getDummyCardContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { applyCsomStyles } from "@/lib/use-csom-style";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { StackedCard } from "./stacked-card";
|
||||
|
||||
@@ -42,6 +43,7 @@ export function StackedCardsContainer({
|
||||
? survey.styling?.cardBorderColor?.light
|
||||
: styling.cardBorderColor?.light;
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const spacerRef = useRef<HTMLDivElement>(null);
|
||||
const resizeObserver = useRef<ResizeObserver | null>(null);
|
||||
const [cardHeight, setCardHeight] = useState("auto");
|
||||
const [cardWidth, setCardWidth] = useState<number>(0);
|
||||
@@ -134,6 +136,13 @@ export function StackedCardsContainer({
|
||||
};
|
||||
}, [blockIdxTemp, cardArrangement, cardRefs]);
|
||||
|
||||
// Keep spacer height in sync via CSSOM (CSP compliance)
|
||||
useEffect(() => {
|
||||
if (spacerRef.current) {
|
||||
spacerRef.current.style.setProperty("height", cardHeight);
|
||||
}
|
||||
}, [cardHeight]);
|
||||
|
||||
// Reset block progress, when card arrangement changes
|
||||
useEffect(() => {
|
||||
if (shouldResetBlockId) {
|
||||
@@ -152,13 +161,15 @@ export function StackedCardsContainer({
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
}}>
|
||||
<div style={{ height: cardHeight }} />
|
||||
<div ref={spacerRef} />
|
||||
{cardArrangement === "simple" ? (
|
||||
<div
|
||||
id={`questionCard-${blockIdxTemp.toString()}`}
|
||||
data-testid={`questionCard-${blockIdxTemp.toString()}`}
|
||||
className={cn("bg-survey-bg w-full overflow-hidden", fullSizeCards ? "h-full" : "")}
|
||||
style={borderStyles}>
|
||||
ref={(el) => {
|
||||
if (el) applyCsomStyles(el, borderStyles);
|
||||
}}>
|
||||
{getCardContent(blockIdxTemp, 0)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -70,7 +70,7 @@ export function SurveyContainer({
|
||||
|
||||
if (!isModal) {
|
||||
return (
|
||||
<div id="fbjs" className="formbricks-form" style={{ height: "100%", width: "100%" }} dir={dir}>
|
||||
<div id="fbjs" className="formbricks-form fb-full-size" dir={dir}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
42
packages/surveys/src/lib/use-csom-style.ts
Normal file
42
packages/surveys/src/lib/use-csom-style.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from "preact/hooks";
|
||||
|
||||
/**
|
||||
* Returns a ref callback that applies style properties via CSSOM (element.style.setProperty)
|
||||
* instead of inline `style` attributes. This bypasses CSP `style-src` restrictions because
|
||||
* the browser treats CSSOM mutations as script-driven DOM manipulation, not inline styles.
|
||||
*
|
||||
* Usage:
|
||||
* const csomRef = useCsomStyle({ zIndex: "1000", transform: "translateY(25%)" });
|
||||
* <div ref={csomRef} />
|
||||
*/
|
||||
export function useCsomStyle(
|
||||
styles: Record<string, string | number | undefined | null>
|
||||
): (el: HTMLElement | null) => void {
|
||||
return useCallback(
|
||||
(el: HTMLElement | null) => {
|
||||
if (!el) return;
|
||||
for (const [prop, value] of Object.entries(styles)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
// Convert camelCase to kebab-case (e.g. zIndex → z-index, pointerEvents → pointer-events)
|
||||
const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
||||
el.style.setProperty(kebab, String(value));
|
||||
}
|
||||
},
|
||||
[JSON.stringify(styles)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies styles via CSSOM imperatively (for use in ref callbacks that also need
|
||||
* to store the element reference, e.g. cardRefs).
|
||||
*/
|
||||
export function applyCsomStyles(
|
||||
el: HTMLElement,
|
||||
styles: Record<string, string | number | undefined | null>
|
||||
): void {
|
||||
for (const [prop, value] of Object.entries(styles)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
||||
el.style.setProperty(kebab, String(value));
|
||||
}
|
||||
}
|
||||
@@ -173,4 +173,37 @@
|
||||
|
||||
#fbjs .grecaptcha-badge {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Submit button base styles using CSS variables (CSP-safe, no inline style needed) */
|
||||
#fbjs .button-custom {
|
||||
border-radius: var(--fb-button-border-radius, var(--fb-border-radius));
|
||||
background-color: var(--fb-button-bg-color, var(--fb-brand-color));
|
||||
color: var(--fb-button-text-color, var(--fb-brand-text-color));
|
||||
height: var(--fb-button-height, auto);
|
||||
font-size: var(--fb-button-font-size, inherit);
|
||||
font-weight: var(--fb-button-font-weight, inherit);
|
||||
padding-left: var(--fb-button-padding-x, 1.5rem);
|
||||
padding-right: var(--fb-button-padding-x, 1.5rem);
|
||||
padding-top: var(--fb-button-padding-y, 0.375rem);
|
||||
padding-bottom: var(--fb-button-padding-y, 0.375rem);
|
||||
}
|
||||
|
||||
/* CSP-safe utility classes (avoid inline style attributes) */
|
||||
#fbjs .fb-full-size {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#fbjs .fb-translate-x-center {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#fbjs .fb-content-transition {
|
||||
transition: opacity 300ms ease-in-out;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#fbjs .fb-card-transition {
|
||||
transition: transform 600ms ease-in-out, opacity 600ms ease-in-out, width 600ms ease-in-out;
|
||||
}
|
||||
Reference in New Issue
Block a user