Standardize dragged element feedback (#23772)

## Summary

- make dragged table rows use a consistent background across sticky
cells
- apply one shared opacity treatment to table rows, Kanban cards, and
other dnd-kit feedback

## Before/After

<img width="1400" height="1980" alt="drag-feedback-before-after"
src="https://github.com/user-attachments/assets/ee1fe81f-a762-4343-8db6-c51cf63dbac8"
/>
This commit is contained in:
Thomas des Francs
2026-08-05 09:35:46 +02:00
committed by GitHub
parent 0f35b5895a
commit 8a856a4bce
11 changed files with 51 additions and 25 deletions
+11
View File
@@ -21,6 +21,17 @@ form {
display: contents;
}
:root {
--dnd-drag-feedback-opacity: 0.6;
--dnd-drag-source-opacity: 0.3;
}
[data-dnd-dragging='true'] {
/* dnd-kit can reuse the source node as its moving feedback. In that case,
feedback opacity must win over the source fade applied by the component. */
opacity: var(--dnd-drag-feedback-opacity) !important;
}
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
.grecaptcha-badge {
visibility: hidden !important;
@@ -41,6 +41,7 @@ export const RecordCalendarCardDraggableContainer = ({
type={RECORD_CALENDAR_CARD_DND_TYPE}
accept={RECORD_CALENDAR_CARD_DND_TYPE}
disabled={dragIsDisabled}
fadeSourceWhileDragging
>
<StyledDraggableContainer
id={`record-calendar-card-${recordId}-${calendarDay}`}
@@ -13,6 +13,7 @@ import { getRecordCalendarWeekTimedEventHeight } from '@/object-record/record-ca
import { RecordCard } from '@/object-record/record-card/components/RecordCard';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { DRAG_SOURCE_OPACITY } from '@/ui/utilities/drag-and-drop/constants/DragSourceOpacity';
import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useDraggable } from '@dnd-kit/react';
@@ -30,6 +31,7 @@ const RECORD_CALENDAR_WEEK_EVENT_EXPANDED_MIN_HEIGHT =
RECORD_CALENDAR_WEEK_EVENT_TIME_ROW_HEIGHT;
const StyledEventPositioner = styled.div<{
$isDragSourceFaded: boolean;
columnCount: number;
columnIndex: number;
heightInPixels: number;
@@ -47,6 +49,8 @@ const StyledEventPositioner = styled.div<{
columnIndex,
}).left};
min-width: 0;
opacity: ${({ $isDragSourceFaded }) =>
$isDragSourceFaded ? DRAG_SOURCE_OPACITY : 1};
overflow: hidden;
position: ${({ isAllDay }) => (isAllDay ? 'relative' : 'absolute')};
right: auto;
@@ -257,6 +261,7 @@ export const RecordCalendarWeekEvent = ({
return (
<StyledEventPositioner
ref={draggableRef}
$isDragSourceFaded={isDragSource}
columnCount={columnCount}
columnIndex={columnIndex}
heightInPixels={heightInPixels}
@@ -1,3 +1,4 @@
import { DRAG_SOURCE_OPACITY } from '@/ui/utilities/drag-and-drop/constants/DragSourceOpacity';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -11,7 +12,7 @@ const StyledBoardCard = styled.div<{
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.primary};
cursor: pointer;
opacity: ${({ isDragging }) => (isDragging ? '0.3' : '1')};
opacity: ${({ isDragging }) => (isDragging ? DRAG_SOURCE_OPACITY : 1)};
width: 100%;
@@ -7,6 +7,7 @@ import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidthClassName';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableCellStyleWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellStyleWrapper';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { isDefined } from 'twenty-shared/utils';
@@ -27,6 +28,7 @@ export const RecordTableCellCheckbox = () => {
const { t } = useLingui();
const { isSelected } = useRecordTableRowContextOrThrow();
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
const { setCurrentRowSelected } = useSetCurrentRowSelected();
@@ -43,6 +45,7 @@ export const RecordTableCellCheckbox = () => {
return (
<RecordTableCellStyleWrapper
isSelected={isSelected}
isDragging={isDragging}
hasRightBorder={false}
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
>
@@ -41,6 +41,7 @@ export const RecordTableCellDragAndDrop = () => {
return (
<RecordTableCellStyleWrapper
data-select-disable
isDragging={isDragging}
hasRightBorder={false}
hasBottomBorder={false}
widthClassName={RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME}
@@ -2,7 +2,6 @@ import { useSortable } from '@dnd-kit/react/sortable';
import { styled } from '@linaria/react';
import { type ReactNode, useContext, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { v4 } from 'uuid';
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
@@ -16,6 +15,7 @@ import { useIsTableRowSecondaryDragged } from '@/object-record/record-table/reco
import { type RecordTableRowDragData } from '@/object-record/record-table/types/RecordTableRowDragData';
import { DragDropItemDropLine } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropLine';
import { DND_KIT_PLUGINS_WITHOUT_OPTIMISTIC } from '@/ui/utilities/drag-and-drop/constants/DndKitPluginsWithoutOptimistic';
import { DRAG_SOURCE_OPACITY } from '@/ui/utilities/drag-and-drop/constants/DragSourceOpacity';
import { DragDropItemSortableHandleRefContext } from '@/ui/utilities/drag-and-drop/context/DragDropItemSortableHandleRefContext';
// The grip, checkbox and first field cells are sticky at
@@ -44,8 +44,6 @@ export const RecordTableDraggableTr = ({
onClick,
children,
}: RecordTableDraggableTrProps) => {
const { theme } = useContext(ThemeContext);
const { isSecondaryDragged } = useIsTableRowSecondaryDragged(recordId);
const { recordGroupId } = useContext(RecordGroupContext);
@@ -68,19 +66,18 @@ export const RecordTableDraggableTr = ({
// per-instance id avoids the collision; recordId travels in the drag data.
const [sortableId] = useState(() => v4());
const { handleRef, ref, isDragging, isDragSource, isDropTarget } =
useSortable({
id: sortableId,
index: draggableIndex,
group: droppableId,
type: RECORD_TABLE_ROW_DND_TYPE,
accept: RECORD_TABLE_ROW_DND_TYPE,
data: rowDragData,
disabled: isDragDisabled,
transition: null,
plugins: DND_KIT_PLUGINS_WITHOUT_OPTIMISTIC,
feedback: 'clone',
});
const { handleRef, ref, isDragSource, isDropTarget } = useSortable({
id: sortableId,
index: draggableIndex,
group: droppableId,
type: RECORD_TABLE_ROW_DND_TYPE,
accept: RECORD_TABLE_ROW_DND_TYPE,
data: rowDragData,
disabled: isDragDisabled,
transition: null,
plugins: DND_KIT_PLUGINS_WITHOUT_OPTIMISTIC,
feedback: 'clone',
});
return (
<RecordTableTr
@@ -89,19 +86,16 @@ export const RecordTableDraggableTr = ({
ref={ref}
className={className}
style={{
background: isDragging ? theme.background.transparent.light : undefined,
borderColor: isDragging
? `${theme.border.color.medium}`
: 'transparent',
opacity: isSecondaryDragged ? 0.3 : undefined,
opacity:
isDragSource || isSecondaryDragged ? DRAG_SOURCE_OPACITY : undefined,
}}
isDragging={isDragging}
isDragging={false}
data-testid={`row-id-${recordId}`}
data-selectable-id={recordId}
onClick={onClick}
>
<DragDropItemSortableHandleRefContext.Provider value={handleRef}>
<RecordTableRowDraggableContextProvider value={{ isDragging }}>
<RecordTableRowDraggableContextProvider value={{ isDragging: false }}>
{children}
<RecordTableRowMultiDragPreview />
</RecordTableRowDraggableContextProvider>
@@ -4,6 +4,8 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledTr = styled.div<{
isDragging: boolean;
}>`
border-bottom: ${({ isDragging }) =>
isDragging ? `1px solid ${themeCssVariables.border.color.medium}` : 'none'};
border-top: ${({ isDragging }) =>
isDragging ? `1px solid ${themeCssVariables.border.color.medium}` : 'none'};
@@ -177,7 +177,7 @@ export const RecordTableRowDragOverlayContent = ({
recordId={recordId}
focusIndex={sourceData.focusIndex}
style={{
background: themeCssVariables.background.transparent.light,
background: themeCssVariables.background.secondary,
borderColor: themeCssVariables.border.color.medium,
}}
isDragging
@@ -9,6 +9,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { DragDropItemDropLine } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropLine';
import { DND_KIT_PLUGINS_WITHOUT_OPTIMISTIC } from '@/ui/utilities/drag-and-drop/constants/DndKitPluginsWithoutOptimistic';
import { DRAG_SOURCE_OPACITY } from '@/ui/utilities/drag-and-drop/constants/DragSourceOpacity';
import { DragDropItemSortableHandleRefContext } from '@/ui/utilities/drag-and-drop/context/DragDropItemSortableHandleRefContext';
import { preventNativeDragStart } from '@/ui/utilities/drag-and-drop/utils/preventNativeDragStart';
@@ -23,6 +24,7 @@ const SORTABLE_TRANSITION = {
const StyledSortableRoot = styled.div<{
$disabled?: boolean;
$fill?: boolean;
$isDragSourceFaded?: boolean;
$isDraggingHighlighted?: boolean;
}>`
background: ${({ $isDraggingHighlighted }) =>
@@ -37,6 +39,8 @@ const StyledSortableRoot = styled.div<{
height: ${({ $fill }) => ($fill ? '100%' : 'auto')};
min-height: 0;
min-width: ${({ $fill }) => ($fill ? '0' : 'auto')};
opacity: ${({ $isDragSourceFaded }) =>
$isDragSourceFaded ? DRAG_SOURCE_OPACITY : 1};
outline: none;
position: relative;
transition: background 0.1s ease;
@@ -54,6 +58,7 @@ type DragDropItemSortableCellProps = {
children: ReactNode;
data?: Record<string, unknown>;
disabled?: boolean;
fadeSourceWhileDragging?: boolean;
fill?: boolean;
group: string;
hasTransition?: boolean;
@@ -70,6 +75,7 @@ export const DragDropItemSortableCell = ({
children,
data,
disabled = false,
fadeSourceWhileDragging = false,
fill = false,
group,
hasTransition = true,
@@ -116,6 +122,7 @@ export const DragDropItemSortableCell = ({
ref={ref}
$disabled={disabled}
$fill={fill}
$isDragSourceFaded={fadeSourceWhileDragging && isDragSource}
$isDraggingHighlighted={highlightWhileDragging && isDragging}
onDragStart={preventNativeDragStart}
>
@@ -0,0 +1 @@
export const DRAG_SOURCE_OPACITY = 'var(--dnd-drag-source-opacity)';