Skip to content

Commit

Permalink
[DataGrid] Add column reorder support (#165)
Browse files Browse the repository at this point in the history
* [useColumnReorder] Add the ability to reorder columns using drag and drop

* [useColumnReorder] Fix review comments

* fix broking functionality

* Fix issues related to pulling the latest changes

* {useColumnReorder] Refactor the feature hook and add drag scroll support for the ColumnsHeader

* [useColumnReorder] Fix lint and format issues

* [useColumnsReorder] Fix build and formatting

* [useColumnReorder] Fix lint errors

* Fix styles downgrade when dragging a column cell

* [useColumnReorder] Added option to disable the feature, added docs, added tests

* [useColumnReorder] Fix formatting and enable all the tests

* Remove unnessesary code from the mouse.test.ts file

* Fix formatting

* fix formatting

* Emit event when drag enters a column

* Working on PR comments

* Fix inifinte loop when dragging smaller cal over large col

* Rename drag handlers prefix

* Prep codebase for commit and push

* Run prettier

* Fix lint errors

* Rename file to match coding style

* Rename columnsHeaderRef to ref

* Fix PR comments

* Update docs/pages/api-docs/x-grid.md

Co-authored-by: Matt <[email protected]>

* Update docs/pages/api-docs/x-grid.md

Co-authored-by: Matt <[email protected]>

* Update docs/pages/api-docs/x-grid.md

Co-authored-by: Matt <[email protected]>

* Add relevant documentation about coumn reorder on the /components/data-grid/columns/ page

* Fix docs formatting

* Update docs/src/pages/components/data-grid/columns/columns.md

Co-authored-by: Olivier Tassinari <[email protected]>

* Update docs/src/pages/components/data-grid/columns/columns.md

Co-authored-by: Olivier Tassinari <[email protected]>

* Update docs/src/pages/components/data-grid/columns/columns.md

Co-authored-by: Olivier Tassinari <[email protected]>

* Update docs/src/pages/components/data-grid/columns/columns.md

Co-authored-by: Olivier Tassinari <[email protected]>

* Update code example

* Fix PR comments

* Fix the doc example code

* Update docs/src/pages/components/data-grid/columns/ColumnOrderingGrid.tsx

Co-authored-by: Olivier Tassinari <[email protected]>

* Fix column reordering issue while dragging over the same cell

* Make scrolling while dragging excelerate

* Format and lint the changes

* Fix scroll left issue

* Fix build

* fix scroll smoothness

* Fix Col header item alignment visuall regression

* Final polishing of the useColumnReorder hook

* Fix typings

* the logic never runs on Node.js

* clear timeout when unmounting, avoid edge-case leak

* use design token

* no shorthand

* Use CSS inherit

* simpler class name logic

* we have strong constraint that these value should be defined. It should fail if its not the case

* no shorthands

* the logic depends on the element to be the current target, not the target, remove potential confusion

* add visual clue about where the column is, outline shouldn't be visible

* Remove unnecessary checks related to disableColumnReorder flag

* Fix PR comments related

* remove raf

Co-authored-by: Matt <[email protected]>
Co-authored-by: Olivier Tassinari <[email protected]>
Co-authored-by: Danail Hadzhiatanasov <[email protected]>
  • Loading branch information
4 people authored Oct 15, 2020
1 parent 5fc0f76 commit 49237cd
Show file tree
Hide file tree
Showing 23 changed files with 609 additions and 85 deletions.
17 changes: 17 additions & 0 deletions docs/src/pages/components/data-grid/columns/ColumnOrderingGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { XGrid } from '@material-ui/x-grid';
import { useDemoData } from '@material-ui/x-grid-data-generator';

export default function ColumnOrderingGrid() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 20,
maxColumns: 20,
});

return (
<div style={{ height: 400, width: '100%' }}>
<XGrid {...data} />
</div>
);
}
17 changes: 17 additions & 0 deletions docs/src/pages/components/data-grid/columns/ColumnOrderingGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { XGrid } from '@material-ui/x-grid';
import { useDemoData } from '@material-ui/x-grid-data-generator';

export default function ColumnOrderingGrid() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 20,
maxColumns: 20,
});

return (
<div style={{ height: 400, width: '100%' }}>
<XGrid {...data} />
</div>
);
}
24 changes: 16 additions & 8 deletions docs/src/pages/components/data-grid/columns/columns.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,29 @@ const usdPrice: ColTypeDef = {

{{"demo": "pages/components/data-grid/columns/CustomColumnTypesGrid.js", "bg": "inline"}}

## 🚧 Column groups
## Column reorder [<span role="img" title="Enterprise">⚡️</span>](https://material-ui.com/store/items/material-ui-x/)

> ⚠️ This feature isn't implemented yet. It's coming.
>
> 👍 Upvote [issue #195](https://github.com/mui-org/material-ui-x/issues/195) if you want to see it land faster.
By default, `XGrid` allows all column reordering by dragging the header cells and moving them left or right.

Grouping columns allows you to have multiple levels of columns in your header and the ability, if needed, to 'open and close' column groups to show and hide additional columns.
To disable column reordering, set the prop `disableColumnReorder={true}`.

In addition, column reordering emits the following events that can be imported:

## 🚧 Column reorder [<span role="img" title="Enterprise">⚡️</span>](https://material-ui.com/store/items/material-ui-x/)
- `COL_REORDER_START`: emitted when dragging of a header cell starts.
- `COL_REORDER_DRAG_ENTER`: emitted when the cursor enters another header cell while dragging.
- `COL_REORDER_DRAG_OVER`: emitted when dragging a header cell over another header cell.
- `COL_REORDER_DRAG_OVER_HEADER`: emitted when dragging a header cell over the `ColumnsHeader` component.
- `COL_REORDER_STOP`: 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.
>
> 👍 Upvote [issue #194](https://github.com/mui-org/material-ui-x/issues/194) if you want to see it land faster.
> 👍 Upvote [issue #195](https://github.com/mui-org/material-ui-x/issues/195) if you want to see it land faster.
Column reordering enables reordering the columns by dragging the header cells.
Grouping columns allows you to have multiple levels of columns in your header and the ability, if needed, to 'open and close' column groups to show and hide additional columns.

## 🚧 Column pinning [<span role="img" title="Enterprise">⚡️</span>](https://material-ui.com/store/items/material-ui-x/)

Expand Down
6 changes: 6 additions & 0 deletions packages/grid/_modules_/grid/GridComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useForkRef } from '@material-ui/core/utils';
import { GridComponentProps } from './GridComponentProps';
import {
useApiRef,
useColumnReorder,
useColumnResize,
useComponents,
usePagination,
Expand Down Expand Up @@ -92,6 +93,7 @@ export const GridComponent = React.forwardRef<HTMLDivElement, GridComponentProps
apiRef,
);

const onColumnReorder = useColumnReorder(columnsHeaderRef, apiRef);
const onResizeColumn = useColumnResize(columnsHeaderRef, apiRef, internalOptions.headerHeight);
const paginationProps = usePagination(internalRows, internalColumns, internalOptions, apiRef);

Expand Down Expand Up @@ -221,6 +223,10 @@ export const GridComponent = React.forwardRef<HTMLDivElement, GridComponentProps
columns={internalColumns.visible || []}
hasScrollX={!!renderCtx?.hasScrollX}
onResizeColumn={onResizeColumn}
onColumnHeaderDragOver={onColumnReorder.handleColumnHeaderDragOver}
onColumnDragStart={onColumnReorder.handleDragStart}
onColumnDragEnter={onColumnReorder.handleDragEnter}
onColumnDragOver={onColumnReorder.handleDragOver}
renderCtx={renderCtx}
/>
</GridColumnsContainer>
Expand Down
4 changes: 2 additions & 2 deletions packages/grid/_modules_/grid/components/AutoSizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export const AutoSizer = React.forwardRef<HTMLDivElement, AutoSizerProps>(functi
width: defaultWidth,
});

const rootRef = React.useRef<HTMLDivElement>(null);
const parentElement = React.useRef(null) as React.MutableRefObject<HTMLElement | null>;
const rootRef = React.useRef<HTMLDivElement | null>(null);
const parentElement = React.useRef<HTMLElement | null>(null);

const handleResize = useEventCallback(() => {
// Guard against AutoSizer component being removed from the DOM immediately after being added.
Expand Down
77 changes: 77 additions & 0 deletions packages/grid/_modules_/grid/components/ScrollArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import { COL_REORDER_START, COL_REORDER_STOP, SCROLLING } from '../constants/eventsConstants';
import { ScrollParams, useApiEventHandler } from '../hooks';
import { ApiRef } from '../models';
import { classnames } from '../utils';
import { ApiContext } from './api-context';

const CLIFF = 1;
const SLOP = 1.5;

interface ScrollAreaProps {
scrollDirection: 'left' | 'right';
}

export const ScrollArea = React.memo(function ScrollArea(props: ScrollAreaProps) {
const { scrollDirection } = props;
const rootRef = React.useRef<HTMLDivElement>(null);
const api = React.useContext(ApiContext);
const timeout = React.useRef<number>();
const [dragging, setDragging] = React.useState<boolean>(false);
const scrollPosition = React.useRef<ScrollParams>({
left: 0,
top: 0,
});

const handleScrolling = React.useCallback((newScrollPosition) => {
scrollPosition.current = newScrollPosition;
}, []);

const handleDragOver = React.useCallback(
(event) => {
let offset;

if (scrollDirection === 'left') {
offset = event.clientX - rootRef.current!.getBoundingClientRect().right;
} else if (scrollDirection === 'right') {
offset = Math.max(1, event.clientX - rootRef.current!.getBoundingClientRect().left);
} else {
throw new Error('wrong dir');
}

offset = (offset - CLIFF) * SLOP + CLIFF;

clearTimeout(timeout.current);
// Avoid freeze and inertia.
timeout.current = setTimeout(() => {
api!.current.scroll({
left: scrollPosition.current.left + offset,
top: scrollPosition.current.top,
});
});
},
[scrollDirection, api],
);

React.useEffect(() => {
return () => {
clearTimeout(timeout.current);
};
}, []);

const toggleDragging = React.useCallback(() => {
setDragging((prevdragging) => !prevdragging);
}, []);

useApiEventHandler(api as ApiRef, SCROLLING, handleScrolling);
useApiEventHandler(api as ApiRef, COL_REORDER_START, toggleDragging);
useApiEventHandler(api as ApiRef, COL_REORDER_STOP, toggleDragging);

return dragging ? (
<div
ref={rootRef}
className={classnames('MuiDataGrid-scrollArea', `MuiDataGrid-scrollArea-${scrollDirection}`)}
onDragOver={handleDragOver}
/>
) : null;
});
102 changes: 60 additions & 42 deletions packages/grid/_modules_/grid/components/column-header-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,31 @@ import { ColumnHeaderSortIcon } from './column-header-sort-icon';
import { ColumnHeaderTitle } from './column-header-title';
import { ColumnHeaderSeparator } from './column-header-separator';
import { OptionsContext } from './options-context';
import { CursorCoordinates } from '../hooks/features/useColumnReorder';

interface ColumnHeaderItemProps {
column: ColDef;
colIndex: number;
onResizeColumn?: (c: any) => void;
onColumnDragStart?: (col: ColDef, currentTarget: HTMLElement) => void;
onColumnDragEnter?: (event: Event) => void;
onColumnDragOver?: (col: ColDef, coordinates: CursorCoordinates) => void;
}
const headerAlignPropToCss = {
center: 'MuiDataGrid-colCellCenter',
right: 'MuiDataGrid-colCellRight',
};

export const ColumnHeaderItem = React.memo(
({ column, colIndex, onResizeColumn }: ColumnHeaderItemProps) => {
({
column,
colIndex,
onResizeColumn,
onColumnDragStart,
onColumnDragEnter,
onColumnDragOver,
}: ColumnHeaderItemProps) => {
const api = React.useContext(ApiContext);
const { headerHeight, showColumnRightBorder, disableColumnResize } = React.useContext(
const { showColumnRightBorder, disableColumnResize, disableColumnReorder } = React.useContext(
OptionsContext,
);

const cssClass = classnames(
HEADER_CELL_CSS_CLASS,
showColumnRightBorder ? 'MuiDataGrid-withBorder' : '',
column.headerClassName,
column.headerAlign &&
column.headerAlign !== 'left' &&
headerAlignPropToCss[column.headerAlign],
{ 'MuiDataGrid-colCellSortable': column.sortable },
);

let headerComponent: React.ReactElement | null = null;
if (column.renderHeader) {
headerComponent = column.renderHeader({
Expand All @@ -44,8 +42,21 @@ export const ColumnHeaderItem = React.memo(
});
}

const handleResize = onResizeColumn ? () => onResizeColumn(column) : undefined;

const handleResize = onResizeColumn && (() => onResizeColumn(column));
const dragConfig = {
draggable:
!disableColumnReorder && !!onColumnDragStart && !!onColumnDragEnter && !!onColumnDragOver,
onDragStart: onColumnDragStart && ((event) => onColumnDragStart(column, event.currentTarget)),
onDragEnter: onColumnDragEnter && ((event) => onColumnDragEnter(event)),
onDragOver:
onColumnDragOver &&
((event) => {
onColumnDragOver(column, {
x: event.clientX,
y: event.clientY,
});
}),
};
const width = column.width!;

let ariaSort: any;
Expand All @@ -55,42 +66,49 @@ export const ColumnHeaderItem = React.memo(

return (
<div
className={cssClass}
className={classnames(
HEADER_CELL_CSS_CLASS,
showColumnRightBorder ? 'MuiDataGrid-withBorder' : '',
column.headerClassName,
column.headerAlign === 'center' && 'MuiDataGrid-colCellCenter',
column.headerAlign === 'right' && 'MuiDataGrid-colCellRight',
{ 'MuiDataGrid-colCellSortable': column.sortable },
)}
key={column.field}
data-field={column.field}
style={{
width,
minWidth: width,
maxWidth: width,
maxHeight: headerHeight,
minHeight: headerHeight,
}}
role="columnheader"
tabIndex={-1}
aria-colindex={colIndex + 1}
{...ariaSort}
>
{column.type === 'number' && (
<ColumnHeaderSortIcon
direction={column.sortDirection}
index={column.sortIndex}
hide={column.hideSortIcons}
/>
)}
{headerComponent || (
<ColumnHeaderTitle
label={column.headerName || column.field}
description={column.description}
columnWidth={width}
/>
)}
{column.type !== 'number' && (
<ColumnHeaderSortIcon
direction={column.sortDirection}
index={column.sortIndex}
hide={column.hideSortIcons}
/>
)}
<div className="MuiDataGrid-colCell-draggable" {...dragConfig}>
{column.type === 'number' && (
<ColumnHeaderSortIcon
direction={column.sortDirection}
index={column.sortIndex}
hide={column.hideSortIcons}
/>
)}
{headerComponent || (
<ColumnHeaderTitle
label={column.headerName || column.field}
description={column.description}
columnWidth={width}
/>
)}
{column.type !== 'number' && (
<ColumnHeaderSortIcon
direction={column.sortDirection}
index={column.sortIndex}
hide={column.hideSortIcons}
/>
)}
</div>
<ColumnHeaderSeparator
resizable={!disableColumnResize && column.resizable}
onResize={handleResize}
Expand Down
Loading

0 comments on commit 49237cd

Please sign in to comment.