diff --git a/docs/pages/api-docs/data-grid/grid-col-def.md b/docs/pages/api-docs/data-grid/grid-col-def.md
index e7f4b0737df0..6e64ffb5ae19 100644
--- a/docs/pages/api-docs/data-grid/grid-col-def.md
+++ b/docs/pages/api-docs/data-grid/grid-col-def.md
@@ -19,6 +19,7 @@ import { GridColDef } from '@material-ui/data-grid';
| description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. |
| disableColumnMenu? | boolean | false
| If `true`, the column menu is disabled for this column. |
| disableExport? | boolean | false
| If `true`, this column will not be included in exports. |
+| disableReorder? | boolean | false
| If `true`, this column cannot be reordered. |
| editable? | boolean | false
| If `true`, the cells of the column are editable. |
| field | string | | The column identifier. It's used to map with GridRowData values. |
| filterOperators? | GridFilterOperator[] | | Allows setting the filter operators for this column. |
diff --git a/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.js b/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.js
new file mode 100644
index 000000000000..9bb99def7e33
--- /dev/null
+++ b/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.js
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { XGrid } from '@material-ui/x-grid';
+
+const rows = [
+ {
+ id: 1,
+ username: '@MaterialUI',
+ age: 20,
+ },
+];
+
+export default function ColumnOrderingDisabledGrid() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.tsx b/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.tsx
new file mode 100644
index 000000000000..9bb99def7e33
--- /dev/null
+++ b/docs/src/pages/components/data-grid/columns/ColumnOrderingDisabledGrid.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { XGrid } from '@material-ui/x-grid';
+
+const rows = [
+ {
+ id: 1,
+ username: '@MaterialUI',
+ age: 20,
+ },
+];
+
+export default function ColumnOrderingDisabledGrid() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/data-grid/columns/columns.md b/docs/src/pages/components/data-grid/columns/columns.md
index c4312f0aa951..033835ed0ba6 100644
--- a/docs/src/pages/components/data-grid/columns/columns.md
+++ b/docs/src/pages/components/data-grid/columns/columns.md
@@ -286,7 +286,13 @@ To disable the column selector, set the prop `disableColumnSelector={true}`.
By default, `XGrid` allows all column reordering by dragging the header cells and moving them left or right.
-To disable column reordering, set the prop `disableColumnReorder={true}`.
+{{"demo": "pages/components/data-grid/columns/ColumnOrderingGrid.js", "disableAd": true, "bg": "inline"}}
+
+To disable reordering on all columns, set the prop `disableColumnReorder={true}`.
+
+To disable reordering in a specific column, set the `disableReorder` property to true in the `GridColDef` of the respective column.
+
+{{"demo": "pages/components/data-grid/columns/ColumnOrderingDisabledGrid.js", "disableAd": true, "bg": "inline"}}
In addition, column reordering emits the following events that can be imported:
@@ -295,8 +301,6 @@ In addition, column reordering emits the following events that can be imported:
- `columnHeaderDragOver`: emitted when dragging a header cell over another header cell.
- `columnHeaderDragEnd`: emitted when dragging of a header cell stops.
-{{"demo": "pages/components/data-grid/columns/ColumnOrderingGrid.js", "disableAd": true, "bg": "inline"}}
-
## 🚧 Column groups
> ⚠️ This feature isn't implemented yet. It's coming.
diff --git a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx
index 7b63a328d61f..94e1caf210fe 100644
--- a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx
+++ b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx
@@ -208,7 +208,7 @@ export function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
>
diff --git a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx
index 12903318cdab..856b77385bd0 100644
--- a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx
+++ b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx
@@ -42,7 +42,7 @@ export const useGridColumnReorder = (apiRef: GridApiRef): void => {
const logger = useLogger('useGridColumnReorder');
const [, setGridState, forceUpdate] = useGridState(apiRef);
- const dragCol = useGridSelector(apiRef, gridColumnReorderDragColSelector);
+ const dragColField = useGridSelector(apiRef, gridColumnReorderDragColSelector);
const options = useGridSelector(apiRef, optionsSelector);
const dragColNode = React.useRef
(null);
const cursorPosition = React.useRef({
@@ -60,7 +60,7 @@ export const useGridColumnReorder = (apiRef: GridApiRef): void => {
const handleColumnHeaderDragStart = React.useCallback(
(params: GridColumnHeaderParams, event: React.MouseEvent) => {
- if (options.disableColumnReorder) {
+ if (options.disableColumnReorder || params.colDef.disableReorder) {
return;
}
@@ -93,7 +93,7 @@ export const useGridColumnReorder = (apiRef: GridApiRef): void => {
const handleDragOver = React.useCallback(
(params: GridColumnHeaderParams | GridCellParams, event: React.DragEvent) => {
- if (!dragCol) {
+ if (!dragColField) {
return;
}
@@ -103,32 +103,38 @@ export const useGridColumnReorder = (apiRef: GridApiRef): void => {
const coordinates = { x: event.clientX, y: event.clientY };
if (
- params.field !== dragCol &&
+ params.field !== dragColField &&
hasCursorPositionChanged(cursorPosition.current, coordinates)
) {
const targetColIndex = apiRef.current.getColumnIndex(params.field, false);
- const dragColIndex = apiRef.current.getColumnIndex(dragCol, false);
-
- if (
- (getCursorMoveDirectionX(cursorPosition.current, coordinates) ===
- CURSOR_MOVE_DIRECTION_RIGHT &&
- dragColIndex < targetColIndex) ||
- (getCursorMoveDirectionX(cursorPosition.current, coordinates) ===
- CURSOR_MOVE_DIRECTION_LEFT &&
- targetColIndex < dragColIndex)
- ) {
- apiRef.current.setColumnIndex(dragCol, targetColIndex);
+ const targetColVisibleIndex = apiRef.current.getColumnIndex(params.field, true);
+ const targetCol = apiRef.current.getColumn(params.field);
+ const dragColIndex = apiRef.current.getColumnIndex(dragColField, false);
+ const visibleColumnAmount = apiRef.current.getVisibleColumns().length;
+
+ const canBeReordered =
+ !targetCol.disableReorder ||
+ (targetColVisibleIndex > 0 && targetColVisibleIndex < visibleColumnAmount - 1);
+
+ const cursorMoveDirectionX = getCursorMoveDirectionX(cursorPosition.current, coordinates);
+ const hasMovedLeft =
+ cursorMoveDirectionX === CURSOR_MOVE_DIRECTION_LEFT && targetColIndex < dragColIndex;
+ const hasMovedRight =
+ cursorMoveDirectionX === CURSOR_MOVE_DIRECTION_RIGHT && dragColIndex < targetColIndex;
+
+ if (canBeReordered && (hasMovedLeft || hasMovedRight)) {
+ apiRef.current.setColumnIndex(dragColField, targetColIndex);
}
cursorPosition.current = coordinates;
}
},
- [apiRef, dragCol, logger],
+ [apiRef, dragColField, logger],
);
const handleDragEnd = React.useCallback(
(params: GridColumnHeaderParams | GridCellParams, event: React.DragEvent): void => {
- if (options.disableColumnReorder) {
+ if (options.disableColumnReorder || !dragColField) {
return;
}
@@ -150,7 +156,7 @@ export const useGridColumnReorder = (apiRef: GridApiRef): void => {
}));
forceUpdate();
},
- [options.disableColumnReorder, logger, setGridState, forceUpdate, apiRef],
+ [options.disableColumnReorder, logger, setGridState, forceUpdate, apiRef, dragColField],
);
useGridApiEventHandler(apiRef, GRID_COLUMN_HEADER_DRAG_START, handleColumnHeaderDragStart);
diff --git a/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts b/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts
index aa9c50549512..705c27d862c7 100644
--- a/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts
+++ b/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts
@@ -149,8 +149,8 @@ export function useGridColumns(apiRef: GridApiRef, { columns }: { columns: GridC
const getVisibleColumns = React.useCallback(() => visibleColumns, [visibleColumns]);
const getColumnsMeta: () => GridColumnsMeta = React.useCallback(() => columnsMeta, [columnsMeta]);
- const getColumnIndex: (field: string, useVisibleColumns?: boolean) => number = React.useCallback(
- (field, useVisibleColumns = true) =>
+ const getColumnIndex = React.useCallback(
+ (field: string, useVisibleColumns: boolean = true): number =>
useVisibleColumns
? visibleColumns.findIndex((col) => col.field === field)
: allColumns.findIndex((col) => col.field === field),
diff --git a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
index 0e7005d0de0d..1df3c0524c2e 100644
--- a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
+++ b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
@@ -14,6 +14,7 @@ export const gridCheckboxSelectionColDef: GridColDef = {
sortable: false,
filterable: false,
disableColumnMenu: true,
+ disableReorder: true,
valueGetter: (params) => {
const selectionLookup = selectedIdsLookupSelector(params.api.getState());
return selectionLookup[params.id] !== undefined;
diff --git a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts
index 0839c0322cbf..d163cba2d1a1 100644
--- a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts
+++ b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts
@@ -143,6 +143,11 @@ export interface GridColDef {
* Allows setting the filter operators for this column.
*/
filterOperators?: GridFilterOperator[];
+ /**
+ * If `true`, this column cannot be reordered.
+ * @default false
+ */
+ disableReorder?: boolean;
/**
* If `true`, this column will not be included in exports.
* @default false
diff --git a/packages/grid/x-grid/src/tests/reorder.XGrid.test.tsx b/packages/grid/x-grid/src/tests/reorder.XGrid.test.tsx
index e26eb9f14df8..8ab9457fcdec 100644
--- a/packages/grid/x-grid/src/tests/reorder.XGrid.test.tsx
+++ b/packages/grid/x-grid/src/tests/reorder.XGrid.test.tsx
@@ -24,6 +24,25 @@ import {
const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+function createDragOverEvent(target: ChildNode) {
+ const dragOverEvent = createEvent.dragOver(target);
+ // Safari 13 doesn't have DragEvent.
+ // RTL fallbacks to Event which doesn't allow to set these fields during initialization.
+ Object.defineProperty(dragOverEvent, 'clientX', { value: 1 });
+ Object.defineProperty(dragOverEvent, 'clientY', { value: 1 });
+
+ return dragOverEvent;
+}
+
+function createDragEndEvent(target: ChildNode, isOutsideTheGrid: boolean = false) {
+ const dragEndEvent = createEvent.dragEnd(target);
+ Object.defineProperty(dragEndEvent, 'dataTransfer', {
+ value: { dropEffect: isOutsideTheGrid ? 'none' : 'copy' },
+ });
+
+ return dragEndEvent;
+}
+
describe(' - Reorder', () => {
// TODO v5: replace with createClientRender
const render = createClientRenderStrictMode();
@@ -109,21 +128,16 @@ describe(' - Reorder', () => {
render();
expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
- const dragCol = getColumnHeaderCell(0).firstChild;
+ const dragCol = getColumnHeaderCell(0).firstChild!;
+ const targetCell = getCell(0, 2)!;
- const targetCell = getCell(0, 2);
fireEvent.dragStart(dragCol);
fireEvent.dragEnter(targetCell);
- const dragOverEvent = createEvent.dragOver(targetCell);
- // Safari 13 doesn't have DragEvent.
- // RTL fallbacks to Event which doesn't allow to set these fields during initialization.
- Object.defineProperty(dragOverEvent, 'clientX', { value: 1 });
- Object.defineProperty(dragOverEvent, 'clientY', { value: 1 });
+ const dragOverEvent = createDragOverEvent(targetCell);
fireEvent(targetCell, dragOverEvent);
expect(getColumnHeadersTextContent()).to.deep.equal(['desc', 'type', 'brand']);
- const dragEndEvent = createEvent.dragEnd(dragCol);
- Object.defineProperty(dragEndEvent, 'dataTransfer', { value: { dropEffect: 'copy' } });
+ const dragEndEvent = createDragEndEvent(dragCol);
fireEvent(dragCol, dragEndEvent);
expect(getColumnHeadersTextContent()).to.deep.equal(['desc', 'type', 'brand']);
});
@@ -145,21 +159,16 @@ describe(' - Reorder', () => {
render();
expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
- const dragCol = getColumnHeaderCell(0).firstChild;
+ const dragCol = getColumnHeaderCell(0).firstChild!;
+ const targetCell = getCell(0, 2);
fireEvent.dragStart(dragCol);
- const targetCell = getCell(0, 2);
fireEvent.dragEnter(targetCell);
- const dragOverEvent = createEvent.dragOver(targetCell);
- // Safari 13 doesn't have DragEvent.
- // RTL fallbacks to Event which doesn't allow to set these fields during initialization.
- Object.defineProperty(dragOverEvent, 'clientX', { value: 1 });
- Object.defineProperty(dragOverEvent, 'clientY', { value: 1 });
+ const dragOverEvent = createDragOverEvent(targetCell);
fireEvent(targetCell, dragOverEvent);
expect(getColumnHeadersTextContent()).to.deep.equal(['desc', 'type', 'brand']);
- const dragEndEvent = createEvent.dragEnd(dragCol);
- Object.defineProperty(dragEndEvent, 'dataTransfer', { value: { dropEffect: 'none' } });
+ const dragEndEvent = createDragEndEvent(dragCol, true);
fireEvent(dragCol, dragEndEvent);
expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
});
@@ -206,10 +215,152 @@ describe(' - Reorder', () => {
render();
expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
- const dragCol = getColumnHeaderCell(2).firstChild;
- const dragEndEvent = createEvent.dragEnd(dragCol);
- Object.defineProperty(dragEndEvent, 'dataTransfer', { value: { dropEffect: 'none' } });
+ const dragCol = getColumnHeaderCell(2).firstChild!;
+ const dragEndEvent = createDragEndEvent(dragCol, true);
fireEvent(dragCol, dragEndEvent);
expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
});
+
+ describe('column disableReorder', () => {
+ it('should not allow to start dragging a column with disableReorder=true', () => {
+ let apiRef: GridApiRef;
+ const rows = [{ id: 0, brand: 'Nike' }];
+ const columns = [
+ { field: 'brand' },
+ { field: 'desc', disableReorder: true },
+ { field: 'type' },
+ ];
+
+ const Test = () => {
+ apiRef = useGridApiRef();
+
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ const dragCol = getColumnHeaderCell(1).firstChild! as HTMLElement;
+ const targetCol = getColumnHeaderCell(0).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+
+ expect(dragCol).to.have.attribute('draggable', 'false');
+ expect(dragCol).not.to.have.class(GRID_COLUMN_HEADER_DRAGGING_CSS_CLASS);
+
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ });
+
+ it('should not allow to drag left of first visible column if it has disableReorder=true', () => {
+ let apiRef: GridApiRef;
+ const rows = [{ id: 0, brand: 'Nike' }];
+ const columns = [
+ { field: 'brand', disableReorder: true },
+ { field: 'desc' },
+ { field: 'type' },
+ ];
+
+ const Test = () => {
+ apiRef = useGridApiRef();
+
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ const dragCol = getColumnHeaderCell(1).firstChild!;
+ const targetCol = getColumnHeaderCell(0).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ });
+
+ it('should not allow to drag right of last visible column if it has disableReorder=true', () => {
+ let apiRef: GridApiRef;
+ const rows = [{ id: 0, brand: 'Nike' }];
+ const columns = [
+ { field: 'brand' },
+ { field: 'desc' },
+ { field: 'type', disableReorder: true },
+ ];
+
+ const Test = () => {
+ apiRef = useGridApiRef();
+
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ const dragCol = getColumnHeaderCell(1).firstChild!;
+ const targetCol = getColumnHeaderCell(2).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ });
+
+ it('should allow to drag right of a column with disableReorder=true if it is not the last visible one', () => {
+ const rows = [{ id: 0, brand: 'Nike' }];
+ const columns = [
+ { field: 'brand' },
+ { field: 'desc', disableReorder: true },
+ { field: 'type' },
+ ];
+
+ const Test = () => {
+ return (
+
+
+
+ );
+ };
+
+ render();
+ expect(getColumnHeadersTextContent()).to.deep.equal(['brand', 'desc', 'type']);
+ const dragCol = getColumnHeaderCell(0).firstChild!;
+ const targetCol = getColumnHeaderCell(2).firstChild!;
+
+ fireEvent.dragStart(dragCol);
+ fireEvent.dragEnter(targetCol);
+ const dragOverEvent2 = createDragOverEvent(targetCol);
+ fireEvent(targetCol, dragOverEvent2);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['desc', 'type', 'brand']);
+
+ const dragEndEvent = createDragEndEvent(dragCol);
+ fireEvent(dragCol, dragEndEvent);
+ expect(getColumnHeadersTextContent()).to.deep.equal(['desc', 'type', 'brand']);
+ });
+ });
});
diff --git a/packages/storybook/src/stories/grid-reorder.stories.tsx b/packages/storybook/src/stories/grid-reorder.stories.tsx
index 635d5221c205..5572f0d17c7c 100644
--- a/packages/storybook/src/stories/grid-reorder.stories.tsx
+++ b/packages/storybook/src/stories/grid-reorder.stories.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-import { ElementSize, XGrid } from '@material-ui/x-grid';
+import { ElementSize, GridColDef, XGrid } from '@material-ui/x-grid';
+import { useDemoData } from '@material-ui/x-grid-data-generator';
import '../style/grid-stories.css';
import { useData } from '../hooks/useData';
@@ -13,6 +14,7 @@ export default {
},
},
};
+
export const ReorderSmallDataset = () => {
const size: ElementSize = { width: 800, height: 600 };
const data = useData(5, 4);
@@ -23,3 +25,27 @@ export const ReorderSmallDataset = () => {
);
};
+
+export const DisableReorderOnSomeColumn = () => {
+ const { data } = useDemoData({
+ dataSet: 'Commodity',
+ rowLength: 10,
+ maxColumns: 6,
+ });
+ const [columns, setColumns] = React.useState(data.columns);
+
+ React.useEffect(() => {
+ if (data.columns.length > 0) {
+ const newColumns: GridColDef[] = data.columns.map((col) =>
+ col.field === 'quantity' ? { ...col, disableReorder: true } : col,
+ );
+ setColumns(newColumns);
+ }
+ }, [data.columns]);
+
+ return (
+
+
+
+ );
+};