Skip to content

Commit 91ab06e

Browse files
committed
refactor: enhance and customize DnDrop interaction for column reordering using dnd-kit library
1 parent c7217d9 commit 91ab06e

23 files changed

+710
-553
lines changed

packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,41 +30,8 @@ $root: ".widget-datagrid";
3030
}
3131

3232
.th {
33-
&.dragging {
34-
opacity: 0.5;
35-
&.dragging-over-self {
36-
opacity: 0.8;
37-
}
38-
}
39-
40-
&.drop-after:after,
41-
&.drop-before:after {
42-
content: "";
43-
position: absolute;
44-
top: 0;
45-
height: 100%;
46-
width: var(--spacing-smaller, $spacing-smaller);
47-
background-color: var(--brand-primary, $dragging-color-effect);
48-
49-
z-index: 1;
50-
}
51-
52-
&.drop-before {
53-
&:after {
54-
left: 0;
55-
}
56-
&:not(:first-child):after {
57-
transform: translateX(-50%);
58-
}
59-
}
60-
61-
&.drop-after {
62-
&:after {
63-
right: 0;
64-
}
65-
&:not(:last-child):after {
66-
transform: translateX(50%);
67-
}
33+
&.dragging-over-self {
34+
opacity: 0.8;
6835
}
6936

7037
/* Clickable column header (Sortable) */
@@ -97,19 +64,17 @@ $root: ".widget-datagrid";
9764
cursor: grab;
9865
pointer-events: auto;
9966
position: relative;
100-
padding: 4px;
101-
// margin-inline-end: 4px;
67+
padding: 12px 4px 4px 4px;
10268
flex-grow: 0;
10369
flex-shrink: 0;
10470
display: flex;
10571
justify-content: center;
106-
align-self: normal;
72+
align-self: flex-start;
10773
z-index: 1;
10874
opacity: 0;
10975
transition: opacity 0.15s ease;
11076

11177
&:hover {
112-
// background-color: var(--brand-primary-50, $brand-light);
11378
svg {
11479
color: var(--brand-primary, $brand-primary);
11580
}
@@ -126,6 +91,14 @@ $root: ".widget-datagrid";
12691
}
12792
}
12893

94+
&.locked-drag-active {
95+
z-index: 2;
96+
}
97+
98+
&.dragging-over-self {
99+
opacity: 0.25;
100+
}
101+
129102
&:hover .drag-handle,
130103
&:focus-within .drag-handle {
131104
opacity: 1;
@@ -136,6 +109,21 @@ $root: ".widget-datagrid";
136109
background-color: var(--brand-primary-50, $brand-light);
137110
}
138111

112+
/* Drag preview (dnd-kit) should look like hovered header */
113+
&.drag-preview {
114+
background-color: var(--brand-primary-50, $brand-light);
115+
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
116+
border: 1px solid var(--gray-light, $gray-light);
117+
118+
.drag-handle {
119+
opacity: 1;
120+
121+
svg {
122+
color: var(--brand-primary, $brand-primary);
123+
}
124+
}
125+
}
126+
139127
/* Remove left padding when drag handle is present */
140128
&:has(.drag-handle) {
141129
padding-left: 0;
@@ -193,7 +181,9 @@ $root: ".widget-datagrid";
193181
/* Header filter */
194182
.filter {
195183
display: flex;
196-
margin-top: 4px;
184+
> * {
185+
margin-top: 4px;
186+
}
197187
> .form-group {
198188
margin-bottom: 0;
199189
}

packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ $brand-light: #e6eaff !default;
1313
$grid-selected-row-background: $brand-light !default;
1414

1515
// Text and icon colors
16+
$gray-light: #6c7180 !default;
1617
$gray-dark: #606671 !default;
1718
$gray-darker: #3b4251 !default;
1819
$pagination-caption-color: #0a1325 !default;

packages/pluggableWidgets/datagrid-web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
"verify": "rui-verify-package-format"
4242
},
4343
"dependencies": {
44+
"@dnd-kit/core": "^6.3.1",
45+
"@dnd-kit/sortable": "^10.0.0",
46+
"@dnd-kit/utilities": "^3.2.2",
4447
"@floating-ui/react": "^0.26.27",
4548
"@mendix/widget-plugin-component-kit": "workspace:*",
4649
"@mendix/widget-plugin-external-events": "workspace:*",

packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { GUID, ObjectItem } from "mendix";
44
import { Selectable } from "mendix/preview/Selectable";
55
import { createContext, CSSProperties, PropsWithChildren, ReactElement, ReactNode, useContext } from "react";
66
import { ColumnsPreviewType, DatagridPreviewProps } from "typings/DatagridProps";
7-
import { DragHandle } from "./components/DragHandle";
87
import { FaArrowsAltV } from "./components/icons/FaArrowsAltV";
98
import { FaEye } from "./components/icons/FaEye";
109
import { ColumnPreview } from "./helpers/ColumnPreview";
@@ -159,7 +158,7 @@ function GridHeader(): ReactNode {
159158
}
160159

161160
function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode {
162-
const { columnsFilterable, columnsSortable, columnsHidable, columnsDraggable } = useProps();
161+
const { columnsFilterable, columnsSortable, columnsHidable } = useProps();
163162
const columnPreview = new ColumnPreview(column, 0);
164163
const caption = columnPreview.header;
165164
const canSort = columnsSortable && columnPreview.canSort;
@@ -174,9 +173,6 @@ function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode {
174173
>
175174
<div className="column-container">
176175
<div className="column-header">
177-
{columnsDraggable && columnPreview.canDrag && (
178-
<DragHandle draggable={false} onDragStart={() => {}} onDragEnd={() => {}} />
179-
)}
180176
<span>{caption.length > 0 ? caption : "\u00a0"}</span>
181177
{canSort && <FaArrowsAltV />}
182178
</div>

packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,57 @@
11
import classNames from "classnames";
22
import { ReactElement } from "react";
33
import { ColumnHeader } from "./ColumnHeader";
4-
import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDragnDropVM } from "../model/hooks/injection-hooks";
4+
import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDndVM } from "../model/hooks/injection-hooks";
55
import { ColumnResizerProps } from "./ColumnResizer";
66
import { observer } from "mobx-react-lite";
77
import { DragHandle } from "./DragHandle";
8+
import { useSortable } from "@dnd-kit/sortable";
89

910
export interface ColumnContainerProps {
1011
isLast?: boolean;
1112
resizer: ReactElement<ColumnResizerProps>;
1213
}
1314

1415
export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement {
15-
const { columnsFilterable, id: gridId } = useDatagridConfig();
16-
const { columnFilters } = useColumnsStore();
16+
const { columnsFilterable, columnsDraggable, id: gridId } = useDatagridConfig();
17+
const columnsStore = useColumnsStore();
18+
const { columnFilters } = columnsStore;
1719
const column = useColumn();
1820
const { canSort, columnId, columnIndex, canResize, sortDir, header } = column;
19-
const vm = useHeaderDragnDropVM();
2021
const caption = header.trim();
22+
const vm = useHeaderDndVM();
23+
const isDndEnabled = Boolean(columnsDraggable && column.canDrag);
24+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
25+
id: columnId,
26+
disabled: !isDndEnabled
27+
});
28+
const setHeaderRef = (ref: HTMLDivElement | null): void => {
29+
column.setHeaderElementRef(ref);
30+
setNodeRef(ref);
31+
};
32+
const style = vm.getHeaderCellStyle(columnId, { transform, transition });
33+
const isLocked = !column.canDrag;
2134

2235
return (
2336
<div
2437
aria-sort={getAriaSort(canSort, sortDir)}
2538
className={classNames("th", {
26-
[`drop-${vm.dropTarget?.[1]}`]: columnId === vm.dropTarget?.[0],
27-
dragging: columnId === vm.dragging?.[1],
28-
"dragging-over-self": columnId === vm.dragging?.[1] && !vm.dropTarget
39+
"dragging-over-self": isDragging,
40+
"locked-drag-active": isLocked && vm.isDragging
2941
})}
3042
role="columnheader"
31-
style={!canSort ? { cursor: "unset" } : undefined}
43+
style={style}
3244
title={caption}
33-
ref={ref => column.setHeaderElementRef(ref)}
45+
ref={setHeaderRef}
3446
data-column-id={columnId}
35-
onDrop={vm.isDraggable ? vm.handleOnDrop : undefined}
36-
onDragEnter={vm.isDraggable ? vm.handleDragEnter : undefined}
37-
onDragOver={vm.isDraggable ? vm.handleDragOver : undefined}
3847
>
39-
{vm.isDraggable && (
40-
<DragHandle draggable={vm.isDraggable} onDragStart={vm.handleDragStart} onDragEnd={vm.handleDragEnd} />
48+
{isDndEnabled && (
49+
<DragHandle setActivatorNodeRef={setActivatorNodeRef} listeners={listeners} attributes={attributes} />
4150
)}
4251
<div className={classNames("column-container")} id={`${gridId}-column${columnId}`}>
4352
<ColumnHeader />
4453
{columnsFilterable && (
45-
<div className="filter" style={{ pointerEvents: vm.dragging ? "none" : undefined }}>
54+
<div className="filter" style={{ pointerEvents: vm.isDragging ? "none" : undefined }}>
4655
{columnFilters[columnIndex]?.renderFilterWidgets()}
4756
</div>
4857
)}

packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode } from "react";
33
import { FaArrowsAltV } from "./icons/FaArrowsAltV";
44
import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown";
55
import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp";
6-
import { useColumn, useHeaderDragnDropVM } from "../model/hooks/injection-hooks";
6+
import { useColumn, useHeaderDndVM } from "../model/hooks/injection-hooks";
77
import { observer } from "mobx-react-lite";
88
import { SortDirection } from "../typings/sorting";
99

@@ -16,12 +16,12 @@ export const ColumnHeader = observer(function ColumnHeader(): ReactElement {
1616
const { header, canSort, alignment } = column;
1717
const caption = header.trim();
1818
const sortProps = canSort ? getSortProps(() => column.toggleSort()) : null;
19-
const vm = useHeaderDragnDropVM();
19+
const vm = useHeaderDndVM();
2020

2121
return (
2222
<div
2323
className={classNames("column-header", { clickable: canSort }, `align-column-${alignment}`)}
24-
style={{ pointerEvents: vm.dragging ? "none" : undefined }}
24+
style={{ pointerEvents: vm.isDragging ? "none" : undefined }}
2525
{...sortProps}
2626
aria-label={canSort ? "sort " + caption : caption}
2727
>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import classNames from "classnames";
2+
import { ReactElement } from "react";
3+
import { useColumn, useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks";
4+
import { ColumnHeader } from "./ColumnHeader";
5+
import { DragHandleIcon } from "./DragHandleIcon";
6+
7+
/**
8+
* Drag preview content for column header reordering.
9+
*
10+
* Rendered by @dnd-kit DragOverlay in a portal, so we provide the same selector context
11+
* used by the datagrid SCSS to make it look like a real header.
12+
*/
13+
export function ColumnHeaderDragPreview(): ReactElement {
14+
const { columnsFilterable, id: gridId } = useDatagridConfig();
15+
const { columnFilters } = useColumnsStore();
16+
const column = useColumn();
17+
const { columnId, columnIndex, header, size } = column;
18+
const caption = header.trim();
19+
20+
return (
21+
<div className="widget-datagrid">
22+
<div className="widget-datagrid-grid table">
23+
<div
24+
className={classNames("th", "drag-preview")}
25+
role="presentation"
26+
title={caption}
27+
style={size ? { width: `${size}px` } : undefined}
28+
>
29+
<DragHandleIcon />
30+
<div className={classNames("column-container")} id={`${gridId}-column${columnId}`}>
31+
<ColumnHeader />
32+
{columnsFilterable && (
33+
<div className="filter">{columnFilters[columnIndex]?.renderFilterWidgets()}</div>
34+
)}
35+
</div>
36+
</div>
37+
</div>
38+
</div>
39+
);
40+
}

packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,15 @@
1-
import { DragEvent, DragEventHandler, MouseEvent, ReactElement } from "react";
1+
import { ReactElement } from "react";
2+
import { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core";
23
import { FaGripVertical } from "./icons/FaGripVertical";
34

45
interface DragHandleProps {
5-
draggable: boolean;
6-
onDragStart?: DragEventHandler<HTMLSpanElement>;
7-
onDragEnd?: DragEventHandler<HTMLSpanElement>;
6+
setActivatorNodeRef: (element: HTMLElement | null) => void;
7+
listeners?: DraggableSyntheticListeners;
8+
attributes?: DraggableAttributes;
89
}
9-
export function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement {
10-
const handleMouseDown = (e: MouseEvent<HTMLSpanElement>): void => {
11-
// Only stop propagation, don't prevent default - we need default for drag to work
12-
e.stopPropagation();
13-
};
14-
15-
const handleClick = (e: MouseEvent<HTMLSpanElement>): void => {
16-
// Stop click events from bubbling to prevent sorting
17-
e.stopPropagation();
18-
e.preventDefault();
19-
};
20-
21-
const handleDragStart = (e: DragEvent<HTMLSpanElement>): void => {
22-
// Don't stop propagation here - let the drag start properly
23-
if (onDragStart) {
24-
onDragStart(e);
25-
}
26-
};
27-
28-
const handleDragEnd = (e: DragEvent<HTMLSpanElement>): void => {
29-
if (onDragEnd) {
30-
onDragEnd(e);
31-
}
32-
};
33-
10+
export function DragHandle({ setActivatorNodeRef, listeners, attributes }: DragHandleProps): ReactElement {
3411
return (
35-
<span
36-
className="drag-handle"
37-
draggable={draggable}
38-
onDragStart={handleDragStart}
39-
onDragEnd={handleDragEnd}
40-
onMouseDown={handleMouseDown}
41-
onClick={handleClick}
42-
>
12+
<span className="drag-handle" ref={setActivatorNodeRef} {...attributes} {...listeners}>
4313
<FaGripVertical />
4414
</span>
4515
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ReactElement } from "react";
2+
import { FaGripVertical } from "./icons/FaGripVertical";
3+
4+
/**
5+
* Visual-only drag handle.
6+
*
7+
* For preview purposes only; does not implement drag-and-drop functionality.
8+
*/
9+
export function DragHandleIcon(): ReactElement {
10+
return (
11+
<span className="drag-handle" aria-hidden="true">
12+
<FaGripVertical />
13+
</span>
14+
);
15+
}

0 commit comments

Comments
 (0)