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 ( +
+ +
+ ); +};