Skip to content

Commit a19dd5f

Browse files
committed
fix: adjust margins and padding for improved column header alignment in datagrid
1 parent e083ab3 commit a19dd5f

24 files changed

+686
-543
lines changed

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

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,49 +22,16 @@ $root: ".widget-datagrid";
2222
background-color: var(--bg-color-secondary, $bg-color-secondary);
2323
border-width: 0;
2424
border-color: var(--grid-border-color, $grid-border-color);
25-
padding: 0 var(--spacing-medium, $spacing-medium);
25+
padding: var(--spacing-medium, $spacing-medium);
2626
top: 0;
2727
min-width: 0;
2828
position: relative;
2929
}
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) */
@@ -78,6 +45,8 @@ $root: ".widget-datagrid";
7845
align-self: stretch;
7946
cursor: col-resize;
8047
margin-right: -12px;
48+
margin-top: calc(0px - var(--spacing-medium, 16px));
49+
margin-bottom: calc(0px - var(--spacing-medium, 16px));
8150

8251
&:hover .column-resizer-bar {
8352
background-color: var(--brand-primary, $brand-primary);
@@ -97,19 +66,17 @@ $root: ".widget-datagrid";
9766
cursor: grab;
9867
pointer-events: auto;
9968
position: relative;
100-
padding: 4px;
101-
// margin-inline-end: 4px;
69+
padding: var(--spacing-smaller, $spacing-smaller);
10270
flex-grow: 0;
10371
flex-shrink: 0;
10472
display: flex;
10573
justify-content: center;
106-
align-self: normal;
74+
align-self: flex-start;
10775
z-index: 1;
10876
opacity: 0;
10977
transition: opacity 0.15s ease;
11078

11179
&:hover {
112-
// background-color: var(--brand-primary-50, $brand-light);
11380
svg {
11481
color: var(--brand-primary, $brand-primary);
11582
}
@@ -126,6 +93,14 @@ $root: ".widget-datagrid";
12693
}
12794
}
12895

96+
&.locked-drag-active {
97+
z-index: 2;
98+
}
99+
100+
&.dragging-over-self {
101+
opacity: 0.25;
102+
}
103+
129104
&:hover .drag-handle,
130105
&:focus-within .drag-handle {
131106
opacity: 1;
@@ -136,6 +111,21 @@ $root: ".widget-datagrid";
136111
background-color: var(--brand-primary-50, $brand-light);
137112
}
138113

114+
/* Drag preview (dnd-kit) should look like hovered header */
115+
&.drag-preview {
116+
background-color: var(--brand-primary-50, $brand-light);
117+
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
118+
border: 1px solid var(--gray-light, $gray-light);
119+
120+
.drag-handle {
121+
opacity: 1;
122+
123+
svg {
124+
color: var(--brand-primary, $brand-primary);
125+
}
126+
}
127+
}
128+
139129
/* Remove left padding when drag handle is present */
140130
&:has(.drag-handle) {
141131
padding-left: 0;
@@ -148,7 +138,6 @@ $root: ".widget-datagrid";
148138
flex-grow: 1;
149139
align-self: stretch;
150140
min-width: 0;
151-
padding: var(--spacing-small, $spacing-small) 0;
152141

153142
&:not(:has(.filter)) {
154143
.column-header {
@@ -193,7 +182,9 @@ $root: ".widget-datagrid";
193182
/* Header filter */
194183
.filter {
195184
display: flex;
196-
margin-top: 4px;
185+
> * {
186+
margin-top: 4px;
187+
}
197188
> .form-group {
198189
margin-bottom: 0;
199190
}

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/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Breaking changes
10+
11+
- The DOM structure is rewritten, which may break existing CSS styling. We recommend checking the custom styling if there is any in your project.
12+
913
### Fixed
1014

1115
- We added missing Dutch translations for Datagrid 2.

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: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,53 @@
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 style = vm.getHeaderCellStyle(columnId, { transform, transition });
29+
const isLocked = !column.canDrag;
2130

2231
return (
2332
<div
2433
aria-sort={getAriaSort(canSort, sortDir)}
2534
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
35+
"dragging-over-self": isDragging,
36+
"locked-drag-active": isLocked && vm.isDragging
2937
})}
3038
role="columnheader"
31-
style={!canSort ? { cursor: "unset" } : undefined}
39+
style={style}
3240
title={caption}
41+
ref={setNodeRef}
3342
data-column-id={columnId}
34-
onDrop={vm.isDraggable ? vm.handleOnDrop : undefined}
35-
onDragEnter={vm.isDraggable ? vm.handleDragEnter : undefined}
36-
onDragOver={vm.isDraggable ? vm.handleDragOver : undefined}
3743
>
38-
{vm.isDraggable && (
39-
<DragHandle draggable={vm.isDraggable} onDragStart={vm.handleDragStart} onDragEnd={vm.handleDragEnd} />
44+
{isDndEnabled && (
45+
<DragHandle setActivatorNodeRef={setActivatorNodeRef} listeners={listeners} attributes={attributes} />
4046
)}
4147
<div className={classNames("column-container")} id={`${gridId}-column${columnId}`}>
4248
<ColumnHeader />
4349
{columnsFilterable && (
44-
<div className="filter" style={{ pointerEvents: vm.dragging ? "none" : undefined }}>
50+
<div className="filter" style={{ pointerEvents: vm.isDragging ? "none" : undefined }}>
4551
{columnFilters[columnIndex]?.renderFilterWidgets()}
4652
</div>
4753
)}

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)