Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e083d4b
refactor: migrate event controllers from DI to React Context for simp…
iobuhov Nov 20, 2025
f004128
refactor: migrate event handlers to container
iobuhov Nov 20, 2025
ae84d71
refactor: extract drag & drop state and logic to mobx
yordan-st Nov 20, 2025
77b819c
refactor: update components to use new state management
yordan-st Nov 20, 2025
97446d7
refactor: remove obsolete tests and snapshots, create for new component
yordan-st Nov 20, 2025
905e2a3
refactor: rewrite columnreszier to use injection hooks
yordan-st Nov 20, 2025
cb926ba
refactor: enhance ColumnResizer test structure and update snapshot
yordan-st Nov 20, 2025
7010ee1
refactor: fix failing test
yordan-st Nov 21, 2025
fb17581
feat: enhance drag-and-drop functionality with DragHandle component
yordan-st Nov 21, 2025
fce4136
refactor: fix lint errors
yordan-st Nov 21, 2025
7550f39
refactor: standardize use of brandi
yordan-st Nov 27, 2025
488cebc
refactor: improve naming, consistency and clean up
yordan-st Nov 28, 2025
9fb13a9
refactor: ensure consistent naming, prop destructuring, update tests
yordan-st Dec 1, 2025
a4084c2
fix: update SelectActionHandler initialization to use null instead of…
yordan-st Dec 2, 2025
0117fbd
fix: restore sort icon state, add dragndropdesign mode and react icon
yordan-st Dec 8, 2025
33c777c
chore: update unit test snapshot
yordan-st Dec 9, 2025
6e8a03c
fix: restore individual column reorder and reflect in preview, revert…
yordan-st Dec 11, 2025
28335e2
feat: enhance drag-and-drop functionality with new drag handle and st…
yordan-st Dec 18, 2025
07a7060
refactor: simplify boolean expressions
yordan-st Dec 23, 2025
a79017b
fix: update sorting test to use the correct sort button locator
yordan-st Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,43 +30,6 @@ $root: ".widget-datagrid";
}

.th {
&.dragging {
opacity: 0.5;
&.dragging-over-self {
opacity: 0.8;
}
}

&.drop-after:after,
&.drop-before:after {
content: "";
position: absolute;
top: 0;
height: 100%;
width: var(--spacing-smaller, $spacing-smaller);
background-color: $dragging-color-effect;

z-index: 1;
}

&.drop-before {
&:after {
left: 0;
}
&:not(:first-child):after {
transform: translateX(-50%);
}
}

&.drop-after {
&:after {
right: 0;
}
&:not(:last-child):after {
transform: translateX(50%);
}
}

/* Clickable column header (Sortable) */
.clickable {
cursor: pointer;
Expand All @@ -78,6 +41,8 @@ $root: ".widget-datagrid";
align-self: stretch;
cursor: col-resize;
margin-right: -12px;
margin-top: calc(0px - var(--spacing-medium, 16px));
margin-bottom: calc(0px - var(--spacing-medium, 16px));

&:hover .column-resizer-bar {
background-color: var(--brand-primary, $brand-primary);
Expand All @@ -92,6 +57,70 @@ $root: ".widget-datagrid";
}
}

&.locked-drag-active {
z-index: 2;
}

&.dragging-over-self {
opacity: 0.25;
}

/* Drag handle */
.drag-handle {
cursor: grab;
pointer-events: auto;
position: relative;
width: var(--spacing-medium, $spacing-medium);
padding: 0;
flex-grow: 0;
flex-shrink: 0;
display: flex;
justify-content: center;
align-self: center;
z-index: 1;
opacity: 0;
transition: opacity 0.15s ease;

&:hover {
svg {
color: var(--brand-primary, $brand-primary);
}
}
&:active {
cursor: grabbing;
}
&:focus-visible {
opacity: 1;
}
> svg {
margin: 0;
}
}

&:hover .drag-handle {
opacity: 1;
}

/* Parent background change on drag handle hover */
&:has(.drag-handle:hover) {
background-color: var(--brand-primary-50, $brand-light);
}

/* Drag preview (dnd-kit) should look like hovered header */
&.drag-preview {
background-color: var(--brand-primary-50, $brand-light);
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
border: 1px solid var(--gray-light, $gray-light);

.drag-handle {
opacity: 1;

svg {
color: var(--brand-primary, $brand-primary);
}
}
}

/* Content of the column header */
.column-container {
display: flex;
Expand All @@ -114,7 +143,12 @@ $root: ".widget-datagrid";
align-items: baseline;
font-weight: 600;

span {
.column-caption {
user-select: text;
cursor: text;
}

span:not(.drag-handle) {
min-width: 0;
flex-grow: 1;
text-overflow: ellipsis;
Expand All @@ -124,26 +158,56 @@ $root: ".widget-datagrid";
}

svg {
margin-left: 8px;
margin-inline-start: 8px;
flex: 0 0 var(--btn-font-size, $btn-font-size);
color: var(--gray-dark, $gray-dark);
height: var(--btn-font-size, $btn-font-size);
align-self: center;
}

.sort-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
font-size: inherit;
color: inherit;
height: 100%;

&:focus:not(:focus-visible) {
outline: none;
}

&:focus-visible {
outline: 1px solid var(--brand-primary, $brand-primary);
}
}

&:focus:not(:focus-visible) {
outline: none;
}

&:focus-visible {
outline: 1px solid var(--brand-primary, $brand-primary);
}

&:has(.drag-handle) {
margin-inline-start: calc(var(--spacing-medium, 16px) * -1 + 1px);
.drag-handle svg {
flex: none;
margin: 0;
}
}
}

/* Header filter */
.filter {
display: flex;
margin-top: 4px;
> * {
margin-top: 4px;
}
> .form-group {
margin-bottom: 0;
}
Expand Down Expand Up @@ -301,6 +365,12 @@ $root: ".widget-datagrid";
}
}

.table-compact .table .th:hover .column-header:has(.drag-handle),
.table-compact .table .th.drag-preview .column-header:has(.drag-handle) {
margin-inline-start: 0;
transition: margin-inline-start 0.2s ease;
}

:where(.table .th .filter input:not([type="checkbox"])) {
font-weight: normal;
flex-grow: 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ $brand-light: #e6eaff !default;
$grid-selected-row-background: $brand-light !default;

// Text and icon colors
$gray-light: #6c7180 !default;
$gray-dark: #606671 !default;
$gray-darker: #3b4251 !default;
$pagination-caption-color: #0a1325 !default;
Expand All @@ -33,7 +34,7 @@ $spacing-larger: 32px !default;
$gallery-gap: $spacing-small !default;

// Effects and animations
$dragging-color-effect: rgba(10, 19, 37, 0.8) !default;
$dragging-color-effect: $brand-primary !default;
$skeleton-background: linear-gradient(90deg, rgba(194, 194, 194, 0.2) 0%, #d2d2d2 100%) !default;

// Assets
Expand Down
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/datagrid-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Breaking changes

- The DOM structure is rewritten, which may break existing CSS styling. We recommend checking the custom styling if there is any in your project.

### Fixed

- We added missing Dutch translations for Datagrid 2.
Expand Down
36 changes: 16 additions & 20 deletions packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path";
import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";
import * as XLSX from "xlsx";
import AxeBuilder from "@axe-core/playwright";

Expand Down Expand Up @@ -49,39 +49,35 @@ test.describe("capabilities: sorting", () => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name");
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute(
"data-icon",
"arrows-alt-v"
);
await expect(
page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='arrows-alt-v']")
).toBeVisible();
await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12");
});

test("changes order of data to ASC when clicking sort option", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name");
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute(
"data-icon",
"arrows-alt-v"
);
await page.locator(".mx-name-datagrid1 .column-header").nth(1).click();
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute(
"data-icon",
"long-arrow-alt-up"
);
await expect(
page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='arrows-alt-v']")
).toBeVisible();
await page.locator(".mx-name-datagrid1 .column-header").nth(1).locator(".sort-button").click();
await expect(
page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='long-arrow-alt-up']")
).toBeVisible();
await expect(page.getByRole("gridcell", { name: "10" }).first()).toHaveText("10");
});

test("changes order of data to DESC when clicking sort option", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name");
await page.locator(".mx-name-datagrid1 .column-header").nth(1).click();
await page.locator(".mx-name-datagrid1 .column-header").nth(1).click();
await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute(
"data-icon",
"long-arrow-alt-down"
);
await page.locator(".mx-name-datagrid1 .column-header").nth(1).locator(".sort-button").click();
await page.locator(".mx-name-datagrid1 .column-header").nth(1).locator(".sort-button").click();
await expect(
page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='long-arrow-alt-down']")
).toBeVisible();
await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12");
});
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/datagrid-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"verify": "rui-verify-package-format"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@floating-ui/react": "^0.26.27",
"@mendix/widget-plugin-component-kit": "workspace:*",
"@mendix/widget-plugin-external-events": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ColumnsPreviewType, DatagridPreviewProps } from "typings/DatagridProps"
import { FaArrowsAltV } from "./components/icons/FaArrowsAltV";
import { FaEye } from "./components/icons/FaEye";
import { ColumnPreview } from "./helpers/ColumnPreview";

import "./ui/DatagridPreview.scss";

declare module "mendix/preview/Selectable" {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import classNames from "classnames";
import { ReactElement } from "react";
import { ColumnHeader } from "./ColumnHeader";
import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDndVM } from "../model/hooks/injection-hooks";
import { ColumnResizerProps } from "./ColumnResizer";
import { observer } from "mobx-react-lite";
import { useSortable } from "@dnd-kit/sortable";

export interface ColumnContainerProps {
isLast?: boolean;
resizer: ReactElement<ColumnResizerProps>;
}

export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement {
const { columnsFilterable, columnsResizable, columnsSortable, id: gridId } = useDatagridConfig();
const columnsStore = useColumnsStore();
const { columnFilters } = columnsStore;
const column = useColumn();
const { canSort, columnId, columnIndex, canResize, sortDir, header } = column;
const isSortable = columnsSortable && canSort;
const isResizable = columnsResizable && canResize;
const caption = header.trim();
const vm = useHeaderDndVM();
const { setNodeRef, transform, transition, isDragging } = useSortable({
id: columnId
});
const style = vm.getHeaderCellStyle(columnId, { transform, transition });
const isLocked = !column.canDrag;

return (
<div
aria-sort={getAriaSort(isSortable, sortDir)}
className={classNames("th", {
"dragging-over-self": isDragging,
"locked-drag-active": isLocked && vm.isDragging
})}
role="columnheader"
style={style}
title={caption}
ref={setNodeRef}
data-column-id={columnId}
>
<div className={classNames("column-container")} id={`${gridId}-column${columnId}`}>
<ColumnHeader />
{columnsFilterable && (
<div className="filter" style={{ pointerEvents: vm.isDragging ? "none" : undefined }}>
{columnFilters[columnIndex]?.renderFilterWidgets()}
</div>
)}
</div>
{isResizable ? props.resizer : null}
</div>
);
});

function getAriaSort(canSort: boolean, sortDir: string | undefined): "ascending" | "descending" | "none" | undefined {
if (!canSort) {
return undefined;
}

switch (sortDir) {
case "asc":
return "ascending";
case "desc":
return "descending";
default:
return "none";
}
}
Loading
Loading