Compare commits

...

2 Commits

Author SHA1 Message Date
Cursor Agent
3a0fb5c0b8 fix: Safari dnd-kit getBoundingClientRect undefined top property
Fixes FORMBRICKS-TR

Safari's getBoundingClientRect() can return objects with undefined
properties (particularly 'top'), causing dnd-kit collision detection
to throw TypeErrors when accessing these properties.

This fix implements Safari-safe collision detection wrappers that:
- Validate all DOMRect properties before use
- Filter out containers with invalid bounding rectangles
- Return empty array when no valid containers exist
- Fall through to original collision detection with valid containers

Applied to all dnd-kit usage across:
- ResponseTable (closestCenter)
- ContactsTable (closestCenter)
- AttributesTable (closestCenter)
- DataTableSettingsModal (closestCorners)
- ElementsView (closestCorners)
2026-03-18 14:01:26 +00:00
Dhruwang Jariwala
633bf18204 fix: auto-expand multi-language card when toggle is enabled (#7504)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:18:35 +00:00
7 changed files with 194 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import {
type CollisionDetection,
DndContext,
type DragEndEvent,
KeyboardSensor,
@@ -45,6 +46,42 @@ const SkeletonCell = () => (
</Skeleton>
);
/**
* Safari-safe collision detection that validates getBoundingClientRect() properties
* before using them. Safari can sometimes return DOMRect objects with undefined
* properties (particularly 'top'), which causes dnd-kit to throw TypeErrors.
*/
const safariSafeClosestCenter: CollisionDetection = (args) => {
const { droppableContainers } = args;
// Filter droppable containers to only include those with valid bounding rects
const validDroppableContainers = droppableContainers.filter((container) => {
const rect = container.rect.current;
if (!rect) return false;
// Check if all required properties exist and are numbers
return (
typeof rect.top === "number" &&
typeof rect.left === "number" &&
typeof rect.right === "number" &&
typeof rect.bottom === "number" &&
typeof rect.width === "number" &&
typeof rect.height === "number"
);
});
// If all containers were filtered out, return empty array (no collision)
if (validDroppableContainers.length === 0) {
return [];
}
// Call the original closestCenter with validated containers
return closestCenter({
...args,
droppableContainers: validDroppableContainers,
});
};
interface ResponseTableProps {
data: TResponseTableData[];
survey: TSurvey;
@@ -228,7 +265,7 @@ export const ResponseTable = ({
<div>
<DndContext
id="response-table"
collisionDetection={closestCenter}
collisionDetection={safariSafeClosestCenter}
modifiers={[restrictToHorizontalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}>

View File

@@ -1,6 +1,7 @@
"use client";
import {
type CollisionDetection,
DndContext,
type DragEndEvent,
KeyboardSensor,
@@ -33,6 +34,42 @@ import { deleteContactAttributeKeyAction } from "../actions";
import { generateAttributeTableColumns } from "./attribute-table-column";
import { EditAttributeModal } from "./edit-attribute-modal";
/**
* Safari-safe collision detection that validates getBoundingClientRect() properties
* before using them. Safari can sometimes return DOMRect objects with undefined
* properties (particularly 'top'), which causes dnd-kit to throw TypeErrors.
*/
const safariSafeClosestCenter: CollisionDetection = (args) => {
const { droppableContainers } = args;
// Filter droppable containers to only include those with valid bounding rects
const validDroppableContainers = droppableContainers.filter((container) => {
const rect = container.rect.current;
if (!rect) return false;
// Check if all required properties exist and are numbers
return (
typeof rect.top === "number" &&
typeof rect.left === "number" &&
typeof rect.right === "number" &&
typeof rect.bottom === "number" &&
typeof rect.width === "number" &&
typeof rect.height === "number"
);
});
// If all containers were filtered out, return empty array (no collision)
if (validDroppableContainers.length === 0) {
return [];
}
// Call the original closestCenter with validated containers
return closestCenter({
...args,
droppableContainers: validDroppableContainers,
});
};
interface AttributesTableProps {
contactAttributeKeys: TContactAttributeKey[];
isReadOnly: boolean;
@@ -230,7 +267,7 @@ export const AttributesTable = ({
return (
<div className="w-full">
<DndContext
collisionDetection={closestCenter}
collisionDetection={safariSafeClosestCenter}
modifiers={[restrictToHorizontalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}>

View File

@@ -1,6 +1,7 @@
"use client";
import {
type CollisionDetection,
DndContext,
type DragEndEvent,
KeyboardSensor,
@@ -32,6 +33,42 @@ import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/modules/ui
import { TContactTableData } from "../types/contact";
import { generateContactTableColumns } from "./contact-table-column";
/**
* Safari-safe collision detection that validates getBoundingClientRect() properties
* before using them. Safari can sometimes return DOMRect objects with undefined
* properties (particularly 'top'), which causes dnd-kit to throw TypeErrors.
*/
const safariSafeClosestCenter: CollisionDetection = (args) => {
const { droppableContainers } = args;
// Filter droppable containers to only include those with valid bounding rects
const validDroppableContainers = droppableContainers.filter((container) => {
const rect = container.rect.current;
if (!rect) return false;
// Check if all required properties exist and are numbers
return (
typeof rect.top === "number" &&
typeof rect.left === "number" &&
typeof rect.right === "number" &&
typeof rect.bottom === "number" &&
typeof rect.width === "number" &&
typeof rect.height === "number"
);
});
// If all containers were filtered out, return empty array (no collision)
if (validDroppableContainers.length === 0) {
return [];
}
// Call the original closestCenter with validated containers
return closestCenter({
...args,
droppableContainers: validDroppableContainers,
});
};
interface ContactsTableProps {
data: TContactTableData[];
fetchNextPage: () => void;
@@ -224,7 +261,7 @@ export const ContactsTable = ({
return (
<div className="w-full">
<DndContext
collisionDetection={closestCenter}
collisionDetection={safariSafeClosestCenter}
modifiers={[restrictToHorizontalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}>

View File

@@ -1,6 +1,7 @@
"use client";
import {
type CollisionDetection,
DndContext,
DragEndEvent,
PointerSensor,
@@ -56,6 +57,42 @@ import { MultiLanguageCard } from "@/modules/survey/multi-language-surveys/compo
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
import { isEndingCardValid, isWelcomeCardValid, validateElement } from "../lib/validation";
/**
* Safari-safe collision detection that validates getBoundingClientRect() properties
* before using them. Safari can sometimes return DOMRect objects with undefined
* properties (particularly 'top'), which causes dnd-kit to throw TypeErrors.
*/
const safariSafeClosestCorners: CollisionDetection = (args) => {
const { droppableContainers } = args;
// Filter droppable containers to only include those with valid bounding rects
const validDroppableContainers = droppableContainers.filter((container) => {
const rect = container.rect.current;
if (!rect) return false;
// Check if all required properties exist and are numbers
return (
typeof rect.top === "number" &&
typeof rect.left === "number" &&
typeof rect.right === "number" &&
typeof rect.bottom === "number" &&
typeof rect.width === "number" &&
typeof rect.height === "number"
);
});
// If all containers were filtered out, return empty array (no collision)
if (validDroppableContainers.length === 0) {
return [];
}
// Call the original closestCorners with validated containers
return closestCorners({
...args,
droppableContainers: validDroppableContainers,
});
};
interface ElementsViewProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
@@ -863,7 +900,7 @@ export const ElementsView = ({
id="blocks"
sensors={sensors}
onDragEnd={onBlockCardDragEnd}
collisionDetection={closestCorners}>
collisionDetection={safariSafeClosestCorners}>
<BlocksDroppable
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
@@ -903,7 +940,7 @@ export const ElementsView = ({
id="endings"
sensors={sensors}
onDragEnd={onEndingCardDragEnd}
collisionDetection={closestCorners}>
collisionDetection={safariSafeClosestCorners}>
<SortableContext items={localSurvey.endings} strategy={verticalListSortingStrategy}>
{localSurvey.endings.map((ending, index) => {
return (

View File

@@ -163,6 +163,9 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
}
} else {
setIsMultiLanguageActivated(true);
if (!open) {
setOpen(true);
}
}
};

View File

@@ -1,6 +1,7 @@
"use client";
import {
type CollisionDetection,
DndContext,
DragEndEvent,
PointerSensor,
@@ -24,6 +25,42 @@ import {
} from "@/modules/ui/components/dialog";
import { DataTableSettingsModalItem } from "./data-table-settings-modal-item";
/**
* Safari-safe collision detection that validates getBoundingClientRect() properties
* before using them. Safari can sometimes return DOMRect objects with undefined
* properties (particularly 'top'), which causes dnd-kit to throw TypeErrors.
*/
const safariSafeClosestCorners: CollisionDetection = (args) => {
const { droppableContainers } = args;
// Filter droppable containers to only include those with valid bounding rects
const validDroppableContainers = droppableContainers.filter((container) => {
const rect = container.rect.current;
if (!rect) return false;
// Check if all required properties exist and are numbers
return (
typeof rect.top === "number" &&
typeof rect.left === "number" &&
typeof rect.right === "number" &&
typeof rect.bottom === "number" &&
typeof rect.width === "number" &&
typeof rect.height === "number"
);
});
// If all containers were filtered out, return empty array (no collision)
if (validDroppableContainers.length === 0) {
return [];
}
// Call the original closestCorners with validated containers
return closestCorners({
...args,
droppableContainers: validDroppableContainers,
});
};
interface DataTableSettingsModalProps<T> {
open: boolean;
setOpen: (open: boolean) => void;
@@ -66,7 +103,7 @@ export const DataTableSettingsModal = <T,>({
id="table-settings"
sensors={sensors}
onDragEnd={handleDragEnd}
collisionDetection={closestCorners}>
collisionDetection={safariSafeClosestCorners}>
<SortableContext items={columnOrder} strategy={verticalListSortingStrategy}>
{columnOrder.map((columnId) => {
if (columnId === "select" || columnId === "createdAt") return;

View File

@@ -269,7 +269,6 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByText("Start from scratch").click();
await page.getByRole("button", { name: "Create survey", exact: true }).click();
await page.locator("#multi-lang-toggle").click();
await page.getByText("Multiple languages").click();
await page.getByRole("combobox").click();
await page.getByLabel("English (en)").click();
await page.getByRole("button", { name: "Confirm" }).click();