From ca976afa1011fd98cf083dbf0bf534e5672877c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Wed, 17 Dec 2025 14:41:37 +0100
Subject: [PATCH] Fix drag and drop in dropdown (#16622)
## Bug description
Due to the dropdown using floating ui, there was an offset on the
draggable item when the drag and drop was implemented inside a
scrollable dropdown.
## Video QA
### Before
https://github.com/user-attachments/assets/fe4b4c36-39ae-4d26-9e19-85d5ffd23b30
### After
https://github.com/user-attachments/assets/1f00b9b0-c231-49cb-a232-75ca42a98e8c
---
.../ObjectOptionsDropdownContent.stories.tsx | 33 +++++++++++--------
.../components/DraggableItem.tsx | 9 ++++-
.../layout/dropdown/components/Dropdown.tsx | 9 ++++-
.../dropdownYPositionComponentState.ts | 10 ++++++
.../ViewFieldsVisibleDropdownSection.tsx | 7 ++++
5 files changed, 52 insertions(+), 16 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/states/internal/dropdownYPositionComponentState.ts
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
index 200b735b0f8..527988e4c0e 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
@@ -10,6 +10,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewType } from '@/views/types/ViewType';
import { useEffect } from 'react';
@@ -105,21 +106,25 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
recordFieldByFieldMetadataItemId,
}}
>
- {},
- resetContent: () => {},
- dropdownId: OBJECT_OPTIONS_DROPDOWN_ID,
- }}
+
-
-
-
-
+ {},
+ resetContent: () => {},
+ dropdownId: OBJECT_OPTIONS_DROPDOWN_ID,
+ }}
+ >
+
+
+
+
+
);
},
diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableItem.tsx b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableItem.tsx
index 00234c9551a..8b79e3f8462 100644
--- a/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableItem.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/DraggableItem.tsx
@@ -1,6 +1,7 @@
import { useTheme } from '@emotion/react';
import { Draggable } from '@hello-pangea/dnd';
import { isFunction } from '@sniptt/guards';
+import { isDefined } from 'twenty-shared/utils';
type DraggableItemProps = {
draggableId: string;
@@ -12,6 +13,7 @@ type DraggableItemProps = {
isInsideScrollableContainer?: boolean;
draggableComponentStyles?: React.CSSProperties;
disableDraggingBackground?: boolean;
+ containerOffsetY?: number;
};
export const DraggableItem = ({
@@ -22,6 +24,7 @@ export const DraggableItem = ({
isInsideScrollableContainer,
draggableComponentStyles,
disableDraggingBackground,
+ containerOffsetY,
}: DraggableItemProps) => {
const theme = useTheme();
@@ -47,11 +50,15 @@ export const DraggableItem = ({
...draggableComponentStyles,
...draggableStyle,
left: 'auto',
- ...(isInsideScrollableContainer ? {} : { top: 'auto' }),
transform: draggableStyle?.transform?.replace(
/\(-?\d+px,/,
'(0,',
),
+ ...(isInsideScrollableContainer
+ ? {
+ top: `${(isDefined(draggableStyle) && 'top' in draggableStyle ? draggableStyle.top : 0) - (containerOffsetY ?? 0)}px`,
+ }
+ : { top: 'auto' }),
background:
!disableDraggingBackground && isDragging
? theme.background.transparent.light
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx
index 057707a3d2a..a64aed1ab79 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx
@@ -9,6 +9,7 @@ import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
+import { dropdownYPositionComponentState } from '@/ui/layout/dropdown/states/internal/dropdownYPositionComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { type GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
@@ -117,6 +118,11 @@ export const Dropdown = ({
dropdownId,
);
+ const setDropdownYPosition = useSetRecoilComponentState(
+ dropdownYPositionComponentState,
+ dropdownId,
+ );
+
const isMobile = useIsMobile();
const bottomAutoresizePadding = isMobile
? (middlewareBoundaryPadding.bottomMobile ??
@@ -143,7 +149,7 @@ export const Dropdown = ({
...boundaryOptions,
}),
size({
- apply: ({ availableHeight, availableWidth }) => {
+ apply: ({ availableHeight, availableWidth, y: floatingY }) => {
flushSync(() => {
const maxHeightToApply =
availableHeight < DROPDOWN_RESIZE_MIN_HEIGHT
@@ -157,6 +163,7 @@ export const Dropdown = ({
setDropdownMaxHeight(maxHeightToApply);
setDropdownMaxWidth(maxWidthToApply);
+ setDropdownYPosition(floatingY);
});
},
...boundaryOptions,
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/internal/dropdownYPositionComponentState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/internal/dropdownYPositionComponentState.ts
new file mode 100644
index 00000000000..ef3a5626e1c
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/internal/dropdownYPositionComponentState.ts
@@ -0,0 +1,10 @@
+import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
+import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
+
+export const dropdownYPositionComponentState = createComponentState<
+ number | undefined
+>({
+ key: 'dropdownYPositionComponentState',
+ componentInstanceContext: DropdownComponentInstanceContext,
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx
index 072c4b582cf..aaaed9fc77e 100644
--- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx
@@ -10,6 +10,7 @@ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-fie
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { dropdownYPositionComponentState } from '@/ui/layout/dropdown/states/internal/dropdownYPositionComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { ViewType } from '@/views/types/ViewType';
import { useContext } from 'react';
@@ -76,6 +77,10 @@ export const ViewFieldsVisibleDropdownSection = () => {
)
.toSorted(sortByProperty('position'));
+ const dropdownYPosition = useRecoilComponentValue(
+ dropdownYPositionComponentState,
+ );
+
return (
<>
@@ -107,6 +112,8 @@ export const ViewFieldsVisibleDropdownSection = () => {
key={recordField.fieldMetadataItemId}
draggableId={recordField.fieldMetadataItemId}
index={fieldIndex + 1}
+ isInsideScrollableContainer
+ containerOffsetY={dropdownYPosition}
itemComponent={