diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ef82b468..e372036484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ - ⚠️ `onGridKeyDown` - ⚠️ `onGridKeyUp` - ⚠️ `onRowDoubleClick` + - ⚠️ `onHeaderDrop` + - ⚠️ `draggableHeaderCell` + - Check [#2007](https://github.com/adazzle/react-data-grid/pull/2007) on how to migrate - ⚠️ `rowsContainer` - ⚠️ Subrow props: `getSubRowDetails`, `onCellExpand`, `onDeleteSubRow`, and `onAddSubRow` - Check [#1853](https://github.com/adazzle/react-data-grid/pull/1853) on how to migrate @@ -56,6 +59,7 @@ - Check [#1845](https://github.com/adazzle/react-data-grid/pull/1845) on how to migrate - ⚠️ `column.getRowMetaData` - ⚠️ `column.filterable` + - ⚠️ `column.draggable` - ⚠️ `cellRangeSelection.{onStart,onUpdate,onEnd}` - ⚠️ `fromRowId`, `toRowId`, and `fromRowData` from `onRowsUpdate` argument - ⚠️ Stopped exporting `HeaderCell` diff --git a/package.json b/package.json index 195805a430..8d116456b1 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "mini-css-extract-plugin": "^0.9.0", "react": "^16.13.1", "react-contextmenu": "^2.13.0", + "react-dnd": "^10.0.2", + "react-dnd-html5-backend": "^10.0.2", "react-dom": "^16.13.1", "react-select": "^3.1.0", "react-virtualized": "^9.21.2", diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 30cfb81107..521f591f26 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -114,8 +114,6 @@ export interface DataGridProps { rowRenderer?: React.ComponentType>; rowGroupRenderer?: React.ComponentType; emptyRowsView?: React.ComponentType<{}>; - /** Component used to render a draggable header cell */ - draggableHeaderCell?: React.ComponentType<{ column: CalculatedColumn; onHeaderDrop: () => void }>; /** * Event props @@ -126,7 +124,6 @@ export interface DataGridProps { onScroll?: (scrollPosition: ScrollPosition) => void; /** Called when a column is resized */ onColumnResize?: (idx: number, width: number) => void; - onHeaderDrop?: () => void; onRowExpandToggle?: (event: RowExpandToggleEvent) => void; /** Function called whenever selected cell is changed */ onSelectedCellChange?: (position: Position) => void; @@ -427,8 +424,6 @@ function DataGrid({ columns={viewportColumns} onColumnResize={handleColumnResize} lastFrozenColumnIndex={lastFrozenColumnIndex} - draggableHeaderCell={props.draggableHeaderCell} - onHeaderDrop={props.onHeaderDrop} allRowsSelected={selectedRows?.size === rows.length} onSelectedRowsChange={onSelectedRowsChange} sortColumn={props.sortColumn} diff --git a/src/HeaderCell.test.tsx b/src/HeaderCell.test.tsx index 64aa98801a..00fafb3cfc 100644 --- a/src/HeaderCell.test.tsx +++ b/src/HeaderCell.test.tsx @@ -10,10 +10,6 @@ interface Row { } describe('HeaderCell', () => { - function DraggableHeaderCell() { - return
; - } - function setup(overrideProps = {}, columnProps = {}) { const props: HeaderCellProps = { column: { @@ -27,8 +23,6 @@ describe('HeaderCell', () => { }, lastFrozenColumnIndex: -1, onResize: jest.fn(), - onHeaderDrop() { }, - draggableHeaderCell: DraggableHeaderCell, allRowsSelected: false, onAllRowsSelectionChange() {}, ...overrideProps @@ -53,16 +47,4 @@ describe('HeaderCell', () => { expect(props.onResize).toHaveBeenCalledWith(props.column, 200); }); }); - - describe('Render draggableHeaderCell', () => { - it('should not render DraggableHeaderCell when draggable is false', () => { - const { wrapper } = setup({}, { draggable: false }); - expect(wrapper.find(DraggableHeaderCell)).toHaveLength(0); - }); - - it('should not render DraggableHeaderCell when draggable is true', () => { - const { wrapper } = setup({}, { draggable: true }); - expect(wrapper.find(DraggableHeaderCell)).toHaveLength(1); - }); - }); }); diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 1540a8652d..433856d5de 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -10,9 +10,7 @@ type SharedHeaderRowProps = Pick, | 'sortColumn' | 'sortDirection' | 'onSort' - | 'onHeaderDrop' | 'allRowsSelected' - | 'draggableHeaderCell' >; export interface HeaderCellProps extends SharedHeaderRowProps { @@ -78,17 +76,5 @@ export default function HeaderCell({ ); } - const DraggableHeaderCell = props.draggableHeaderCell; - if (column.draggable && DraggableHeaderCell) { - return ( - - {cell} - - ); - } - return cell; } diff --git a/src/HeaderRow.test.tsx b/src/HeaderRow.test.tsx index bdb8e9a021..1c9e9811fc 100644 --- a/src/HeaderRow.test.tsx +++ b/src/HeaderRow.test.tsx @@ -14,9 +14,7 @@ describe('HeaderRow', () => { onColumnResize() { }, onSort: jest.fn(), sortDirection: 'NONE', - allRowsSelected: false, - onHeaderDrop() { }, - draggableHeaderCell: () =>
+ allRowsSelected: false }; const setup = (testProps?: Partial>) => { @@ -77,9 +75,7 @@ describe('HeaderRow', () => { lastFrozenColumnIndex: 1, onSort: jest.fn(), allRowsSelected: false, - onColumnResize: jest.fn(), - onHeaderDrop() { }, - draggableHeaderCell: () =>
+ onColumnResize: jest.fn() }; it('passes classname property', () => { diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 9764684e54..925486876e 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -6,9 +6,7 @@ import { assertIsValidKey } from './utils'; import { DataGridProps } from './DataGrid'; type SharedDataGridProps = Pick, - | 'draggableHeaderCell' | 'rows' - | 'onHeaderDrop' | 'onSelectedRowsChange' | 'sortColumn' | 'sortDirection' @@ -53,10 +51,8 @@ function HeaderRow({ column={column} lastFrozenColumnIndex={props.lastFrozenColumnIndex} onResize={props.onColumnResize} - onHeaderDrop={props.onHeaderDrop} allRowsSelected={props.allRowsSelected} onAllRowsSelectionChange={handleAllRowsSelectionChange} - draggableHeaderCell={props.draggableHeaderCell} onSort={props.onSort} sortColumn={props.sortColumn} sortDirection={props.sortDirection} diff --git a/src/common/types.ts b/src/common/types.ts index 2aeb2a61a6..49fe8de758 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -25,8 +25,6 @@ export interface Column { summaryFormatter?: React.ComponentType>; /** Enables cell editing. If set and no editor property specified, then a textinput will be used as the cell editor */ editable?: boolean | ((row: TRow) => boolean); - /** Enable dragging of a column */ - draggable?: boolean; /** Determines whether column is frozen or not */ frozen?: boolean; /** Enable resizing of a column */ @@ -115,7 +113,7 @@ export interface EditorProps { onOverrideKeyDown: (e: KeyboardEvent) => void; } -export interface HeaderRendererProps { +export interface HeaderRendererProps { column: CalculatedColumn; allRowsSelected: boolean; onAllRowsSelectionChange: (checked: boolean) => void; diff --git a/stories/demos/CellNavigation.tsx b/stories/demos/CellNavigation.tsx index 49a859ca83..89bbc07008 100644 --- a/stories/demos/CellNavigation.tsx +++ b/stories/demos/CellNavigation.tsx @@ -66,7 +66,7 @@ function createRows(): Row[] { return rows; } -export default function ScrollToRow() { +export default function CellNavigation() { const [rows] = useState(createRows); const [cellNavigatioMode, setCellNavigationMode] = useState(CellNavigationMode.CHANGE_ROW); diff --git a/stories/demos/ColumnsReordering.tsx b/stories/demos/ColumnsReordering.tsx new file mode 100644 index 0000000000..782ab71c0e --- /dev/null +++ b/stories/demos/ColumnsReordering.tsx @@ -0,0 +1,130 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { DndProvider } from 'react-dnd'; +import Backend from 'react-dnd-html5-backend'; + +import { DraggableHeaderRenderer } from './components/HeaderRenderers'; +import DataGrid, { Column, HeaderRendererProps, SortDirection } from '../../src'; + +interface Row { + id: number; + task: string; + complete: number; + priority: string; + issueType: string; +} + +function createRows(): Row[] { + const rows = []; + for (let i = 1; i < 500; i++) { + rows.push({ + id: i, + task: `Task ${i}`, + complete: Math.min(100, Math.round(Math.random() * 110)), + priority: ['Critical', 'High', 'Medium', 'Low'][Math.floor((Math.random() * 3) + 1)], + issueType: ['Bug', 'Improvement', 'Epic', 'Story'][Math.floor((Math.random() * 3) + 1)] + }); + } + + return rows; +} + +function createColumns(): Column[] { + return [ + { + key: 'id', + name: 'ID', + width: 80 + }, + { + key: 'task', + name: 'Title', + resizable: true, + sortable: true + }, + { + key: 'priority', + name: 'Priority', + resizable: true, + sortable: true + }, + { + key: 'issueType', + name: 'Issue Type', + resizable: true, + sortable: true + }, + { + key: 'complete', + name: '% Complete', + resizable: true, + sortable: true + } + ]; +} + +export default function ColumnsReordering() { + const [rows] = useState(createRows); + const [columns, setColumns] = useState(createColumns); + const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['task', 'NONE']); + + const handleSort = useCallback((columnKey: string, direction: SortDirection) => { + setSort([columnKey, direction]); + }, []); + + const draggableColumns = useMemo(() => { + function HeaderRenderer(props: HeaderRendererProps) { + return ; + } + + function handleColumnsReorder(sourceKey: string, targetKey: string) { + const sourceColumnIndex = columns.findIndex(c => c.key === sourceKey); + const targetColumnIndex = columns.findIndex(c => c.key === targetKey); + const reorderedColumns = [...columns]; + + reorderedColumns.splice( + targetColumnIndex, + 0, + reorderedColumns.splice(sourceColumnIndex, 1)[0] + ); + + setColumns(reorderedColumns); + } + + return columns.map(c => { + if (c.key === 'id') return c; + return { ...c, headerRenderer: HeaderRenderer }; + }); + }, [columns]); + + const sortedRows = useMemo((): readonly Row[] => { + if (sortDirection === 'NONE') return rows; + + let sortedRows: Row[] = [...rows]; + + switch (sortColumn) { + case 'task': + case 'priority': + case 'issueType': + sortedRows = sortedRows.sort((a, b) => a[sortColumn].localeCompare(b[sortColumn])); + break; + case 'complete': + sortedRows = sortedRows.sort((a, b) => a[sortColumn] - b[sortColumn]); + break; + default: + } + + return sortDirection === 'DESC' ? sortedRows.reverse() : sortedRows; + }, [rows, sortDirection, sortColumn]); + + return ( + + + + ); +} diff --git a/stories/demos/components/HeaderRenderers/DraggableHeaderRenderer.tsx b/stories/demos/components/HeaderRenderers/DraggableHeaderRenderer.tsx new file mode 100644 index 0000000000..5b04c67435 --- /dev/null +++ b/stories/demos/components/HeaderRenderers/DraggableHeaderRenderer.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useDrag, useDrop, DragObjectWithType } from 'react-dnd'; + +import { HeaderRendererProps } from '../../../../src'; + + +interface ColumnDragObject extends DragObjectWithType { + key: string; +} + +function wrapRefs(...refs: React.Ref[]) { + return (handle: T | null) => { + for (const ref of refs) { + if (typeof ref === 'function') { + ref(handle); + } else if (ref !== null) { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065 + (ref as React.MutableRefObject).current = handle; + } + } + }; +} + +export function DraggableHeaderRenderer({ onColumnsReorder, ...props }: HeaderRendererProps & { onColumnsReorder: (sourceKey: string, targetKey: string) => void }) { + const [{ isDragging }, drag] = useDrag({ + item: { key: props.column.key, type: 'COLUMN_DRAG' }, + collect: monitor => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const [{ isOver }, drop] = useDrop({ + accept: 'COLUMN_DRAG', + drop({ key, type }: ColumnDragObject) { + if (type === 'COLUMN_DRAG') { + onColumnsReorder(key, props.column.key); + } + }, + collect: monitor => ({ + isOver: !!monitor.isOver(), + canDrop: !!monitor.canDrop() + }) + }); + + return ( +
+ {props.column.name} +
+ ); +} diff --git a/stories/demos/components/HeaderRenderers/index.ts b/stories/demos/components/HeaderRenderers/index.ts new file mode 100644 index 0000000000..4f84e87b49 --- /dev/null +++ b/stories/demos/components/HeaderRenderers/index.ts @@ -0,0 +1 @@ +export * from './DraggableHeaderRenderer'; diff --git a/stories/index.tsx b/stories/index.tsx index 53a55abda5..a4659649f4 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -14,6 +14,7 @@ import ContextMenu from './demos/ContextMenu'; import ScrollToRow from './demos/ScrollToRow'; import CellNavigation from './demos/CellNavigation'; import HeaderFilters from './demos/HeaderFilters'; +import ColumnsReordering from './demos/ColumnsReordering'; storiesOf('Demos', module) .add('Common Features', () => ) @@ -25,4 +26,5 @@ storiesOf('Demos', module) .add('Context Menu', () => ) .add('Scroll To Row', () => ) .add('Cell Navigation', () => ) - .add('Header Filters', () => ); + .add('Header Filters', () => ) + .add('Columns Reordering', () => );