diff --git a/.eslintrc.js b/.eslintrc.js index ef200d9401..e87dc2a438 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -357,7 +357,7 @@ const rules = { 'react/jsx-no-undef': 0, 'react/jsx-no-useless-fragment': 0, 'react/jsx-one-expression-per-line': 0, - 'react/jsx-curly-brace-presence': 0, + 'react/jsx-curly-brace-presence': 1, 'react/jsx-fragments': 1, 'react/jsx-pascal-case': 1, 'react/jsx-props-no-multi-spaces': 1, diff --git a/examples/components/ExampleList.js b/examples/components/ExampleList.js index 839abb9ba8..0d00165390 100644 --- a/examples/components/ExampleList.js +++ b/examples/components/ExampleList.js @@ -29,7 +29,6 @@ export default class ExampleList extends React.Component {
  • Grouping Example
  • Custom Filters Example
  • Immutable Data Grouping Example
  • -
  • Row Reordering Example
  • Draggable Header Example
  • Tree View Example
  • Tree View No Add Delete Example
  • diff --git a/examples/components/Examples.js b/examples/components/Examples.js index b041c8b8cf..bba6ace05c 100644 --- a/examples/components/Examples.js +++ b/examples/components/Examples.js @@ -28,7 +28,6 @@ import CellSelectionEvents from '../scripts/example21-cell-selection-events'; import Grouping from '../scripts/example21-grouping'; import CustomFilters from '../scripts/example22-custom-filters'; import ImmutableDataGrouping from '../scripts/example23-immutable-data-grouping'; -import RowOrdering from '../scripts/example23-row-reordering'; import DraggableHeader from '../scripts/example24-draggable-header'; import TreeView from '../scripts/example25-tree-view'; import TreeViewNoAddDelete from '../scripts/example26-tree-view-no-add-delete'; @@ -78,7 +77,6 @@ export default function Examples({ match }) { - diff --git a/examples/scripts/example13-all-features.js b/examples/scripts/example13-all-features.js index 5f9d1195e8..b511f49d70 100644 --- a/examples/scripts/example13-all-features.js +++ b/examples/scripts/example13-all-features.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDataGrid from 'react-data-grid'; +import ReactDataGrid, { SelectColumn } from 'react-data-grid'; import { Editors, Toolbar, Formatters } from 'react-data-grid-addons'; import update from 'immutability-helper'; import faker from 'faker'; @@ -16,7 +16,8 @@ const titles = ['Dr.', 'Mr.', 'Mrs.', 'Miss', 'Ms.']; class Example extends React.Component { constructor(props, context) { super(props, context); - this._columns = [ + this.columns = [ + SelectColumn, { key: 'id', name: 'ID', @@ -38,8 +39,8 @@ class Example extends React.Component { width: 200, resizable: true, events: { - onDoubleClick() { - console.log('The user double clicked on title column'); + onClick: (ev, { idx, rowIdx }) => { + this.grid.openCellEditor(rowIdx, idx); } } }, @@ -115,7 +116,10 @@ class Example extends React.Component { } ]; - this.state = { rows: this.createRows(2000) }; + this.state = { + rows: this.createRows(2000), + selectedRows: new Set() + }; } createRows = (numberOfRows) => { @@ -145,17 +149,6 @@ class Example extends React.Component { }; }; - getColumns = () => { - const clonedColumns = this._columns.slice(); - clonedColumns[2].events = { - onClick: (ev, { idx, rowIdx }) => { - this.grid.openCellEditor(rowIdx, idx); - } - }; - - return clonedColumns; - }; - handleGridRowsUpdated = ({ fromRow, toRow, updated }) => { const rows = this.state.rows.slice(); @@ -193,19 +186,24 @@ class Example extends React.Component { return this.state.rows.length; }; + onSelectedRowsChange = (selectedRows) => { + this.setState({ selectedRows }); + }; + render() { return ( this.grid = node} enableCellSelect - columns={this.getColumns()} + columns={this.columns} rowGetter={this.getRowAt} rowsCount={this.getSize()} onGridRowsUpdated={this.handleGridRowsUpdated} toolbar={} - enableRowSelect rowHeight={50} minHeight={600} + selectedRows={this.state.selectedRows} + onSelectedRowsChange={this.onSelectedRowsChange} /> ); } diff --git a/examples/scripts/example16-row-select.js b/examples/scripts/example16-row-select.js index 66a8005808..45e214d2e2 100644 --- a/examples/scripts/example16-row-select.js +++ b/examples/scripts/example16-row-select.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDataGrid from 'react-data-grid'; +import ReactDataGrid, { SelectColumn } from 'react-data-grid'; import exampleWrapper from '../components/exampleWrapper'; @@ -7,6 +7,7 @@ class Example extends React.Component { constructor(props) { super(props); this._columns = [ + SelectColumn, { key: 'id', name: 'ID' @@ -29,42 +30,33 @@ class Example extends React.Component { count: i * 1000 }); } - this.state = { rows, selectedIndexes: [] }; + this.state = { + rows, + selectedRows: new Set() + }; } rowGetter = (i) => { return this.state.rows[i]; }; - onRowsSelected = (rows) => { - this.setState({ selectedIndexes: this.state.selectedIndexes.concat(rows.map(r => r.rowIdx)) }); - }; - - onRowsDeselected = (rows) => { - const rowIndexes = rows.map(r => r.rowIdx); - this.setState({ selectedIndexes: this.state.selectedIndexes.filter(i => rowIndexes.indexOf(i) === -1) }); + onSelectedRowsChange = (selectedRows) => { + this.setState({ selectedRows }); }; render() { - const rowText = this.state.selectedIndexes.length === 1 ? 'row' : 'rows'; + const rowText = this.state.selectedRows.size === 1 ? 'row' : 'rows'; return (
    - {this.state.selectedIndexes.length} {rowText} selected + {this.state.selectedRows.size} {rowText} selected
    ); @@ -72,28 +64,7 @@ class Example extends React.Component { } const exampleDescription = ( -
    -

    Row selection is enabled via the rowSelection prop (object). The following keys control behaviour:

    -

    selectBy

    -

    - This allows rows to be selected programatically. The options are:
    - indexes - to select rows by index.
    - keys - to select rows by specified key and values.
    - isSelectedKey - to select rows by specified key (based on value being truthy or falsy). -

    -

    onRowsSelected/onRowsDeselected

    -

    - When rows are selected or de-selected, onRowsSelected or onRowsDeselected functions will be called (set as props) with an array of .
    - This allows for single or multiple selection to be implemented as desired, by either appending to or replacing the list of selected items. -

    -

    showCheckbox

    -

    Allows the row selection checkbox to be hidden (shown by default). Useful for selecting rows programatically and controlling selection via theonRowClick event.

    -

    enableShiftSelect

    -

    - Allows a continuous range of rows to be selected by holding the shift key when clicking the row selection checkbox. -

    -

    Note: These props supercede the existing enableRowSelect and onRowUpdated props which will be removed in a later release.

    -
    +
    ); export default exampleWrapper({ diff --git a/examples/scripts/example17-grid-events.js b/examples/scripts/example17-grid-events.js index df00931cac..240d0d2814 100644 --- a/examples/scripts/example17-grid-events.js +++ b/examples/scripts/example17-grid-events.js @@ -21,7 +21,10 @@ class Example extends React.Component { } ]; - this.state = { rows: this.createRows(1000) }; + this.state = { + rows: this.createRows(), + selectedRows: new Set() + }; } createRows = () => { @@ -30,8 +33,7 @@ class Example extends React.Component { rows.push({ id: i, title: `Title ${i}`, - count: i * 1000, - isSelected: false + count: i * 1000 }); } @@ -43,24 +45,28 @@ class Example extends React.Component { }; onRowClick = (rowIdx, row) => { - const rows = this.state.rows.slice(); - rows[rowIdx] = { ...row, isSelected: !row.isSelected }; - this.setState({ rows }); + const newSelectedRows = new Set(this.state.selectedRows); + if (newSelectedRows.has(row.id)) { + newSelectedRows.delete(row.id); + } else { + newSelectedRows.add(row.id); + } + this.onSelectedRowsChange(newSelectedRows); }; onKeyDown = (e) => { if (e.ctrlKey && e.keyCode === 65) { e.preventDefault(); - - const rows = []; - this.state.rows.forEach((r) => { - rows.push({ ...r, isSelected: true }); - }); - - this.setState({ rows }); + const newSelectedRows = new Set(this.state.selectedRows); + this.state.rows.forEach(row => newSelectedRows.add(row.id)); + this.onSelectedRowsChange(newSelectedRows); } }; + onSelectedRowsChange = (selectedRows) => { + this.setState({ selectedRows }); + }; + render() { return ( ); } diff --git a/examples/scripts/example21-cell-selection-events.js b/examples/scripts/example21-cell-selection-events.js index fc49cefbca..b22edcaf6d 100644 --- a/examples/scripts/example21-cell-selection-events.js +++ b/examples/scripts/example21-cell-selection-events.js @@ -31,10 +31,6 @@ class Example extends React.Component { return this._rows[index]; }; - onRowSelect = (rows) => { - this.setState({ selectedRows: rows }); - }; - onCellSelected = ({ rowIdx, idx }) => { this.grid.openCellEditor(rowIdx, idx); }; @@ -55,9 +51,7 @@ class Example extends React.Component { columns={this._columns} rowGetter={this.rowGetter} rowsCount={this._rows.length} - enableRowSelect="multi" minHeight={500} - onRowSelect={this.onRowSelect} enableCellSelect onCellSelected={this.onCellSelected} onCellDeSelected={this.onCellDeSelected} diff --git a/examples/scripts/example23-row-reordering.js b/examples/scripts/example23-row-reordering.js deleted file mode 100644 index fe189e385c..0000000000 --- a/examples/scripts/example23-row-reordering.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactDataGrid, { Row } from 'react-data-grid'; -import { Draggable, Data } from 'react-data-grid-addons'; - -import exampleWrapper from '../components/exampleWrapper'; - -const { Container: DraggableContainer, RowActionsCell, DropTargetRowContainer } = Draggable; -const { Selectors } = Data; -const RowRenderer = DropTargetRowContainer(Row); - -class Example extends React.Component { - static propTypes = { - rowKey: PropTypes.string.isRequired - }; - - static defaultProps = { rowKey: 'id' }; - - constructor(props, context) { - super(props, context); - this._columns = [ - { - key: 'id', - name: 'ID' - }, - { - key: 'task', - name: 'Title' - }, - { - key: 'priority', - name: 'Priority' - }, - { - key: 'issueType', - name: 'Issue Type' - } - ]; - - this.state = { rows: this.createRows(1000), selectedIds: [1, 2] }; - } - - getRandomDate = (start, end) => { - return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toLocaleDateString(); - }; - - createRows = (numberOfRows) => { - const rows = []; - for (let i = 1; i < numberOfRows; 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)], - startDate: this.getRandomDate(new Date(2015, 3, 1), new Date()), - completeDate: this.getRandomDate(new Date(), new Date(2016, 0, 1)) - }); - } - return rows; - }; - - rowGetter = (i) => { - return this.state.rows[i]; - }; - - isDraggedRowSelected = (selectedRows, rowDragSource) => { - if (selectedRows && selectedRows.length > 0) { - const key = this.props.rowKey; - return selectedRows.filter(r => r[key] === rowDragSource.data[key]).length > 0; - } - return false; - }; - - reorderRows = (e) => { - const selectedRows = Selectors.getSelectedRowsByKey({ rowKey: this.props.rowKey, selectedKeys: this.state.selectedIds, rows: this.state.rows }); - const draggedRows = this.isDraggedRowSelected(selectedRows, e.rowSource) ? selectedRows : [e.rowSource.data]; - const undraggedRows = this.state.rows.filter(function(r) { - return draggedRows.indexOf(r) === -1; - }); - const args = [e.rowTarget.idx, 0].concat(draggedRows); - Array.prototype.splice.apply(undraggedRows, args); - this.setState({ rows: undraggedRows }); - }; - - onRowsSelected = (rows) => { - this.setState({ selectedIds: this.state.selectedIds.concat(rows.map(r => r.row[this.props.rowKey])) }); - }; - - onRowsDeselected = (rows) => { - const rowIds = rows.map(r => r.row[this.props.rowKey]); - this.setState({ selectedIds: this.state.selectedIds.filter(i => rowIds.indexOf(i) === -1) }); - }; - - render() { - return ( - - } - rowSelection={{ - showCheckbox: true, - enableShiftSelect: true, - onRowsSelected: this.onRowsSelected, - onRowsDeselected: this.onRowsDeselected, - selectBy: { - keys: { rowKey: this.props.rowKey, values: this.state.selectedIds } - } - }} - /> - - ); - } -} - -export default exampleWrapper({ - WrappedComponent: Example, - exampleName: 'Row Reordering', - exampleDescription: 'This examples demonstrates how single or multiple rows can be dragged to a different positions using components from Draggable React Addons', - examplePath: './scripts/example23-row-reordering.js' -}); diff --git a/examples/scripts/example24-draggable-header.js b/examples/scripts/example24-draggable-header.js index f6f33db548..c431835aff 100644 --- a/examples/scripts/example24-draggable-header.js +++ b/examples/scripts/example24-draggable-header.js @@ -25,29 +25,13 @@ class Example extends React.Component { }; onHeaderDrop = (source, target) => { - const stateCopy = { ...this.state }; - const columnSourceIndex = this.state.columns.findIndex( - i => i.key === source - ); - const columnTargetIndex = this.state.columns.findIndex( - i => i.key === target - ); - - stateCopy.columns.splice( - columnTargetIndex, - 0, - stateCopy.columns.splice(columnSourceIndex, 1)[0] - ); - - const emptyColumns = { ...this.state, columns: [] }; - this.setState( - emptyColumns - ); - - const reorderedColumns = { ...this.state, columns: stateCopy.columns }; - this.setState( - reorderedColumns - ); + const columns = [...this.state.columns]; + const columnSourceIndex = columns.findIndex(i => i.key === source); + const columnTargetIndex = columns.findIndex(i => i.key === target); + const temp = { ...columns[columnSourceIndex] }; + columns[columnSourceIndex] = { ...columns[columnTargetIndex] }; + columns[columnTargetIndex] = temp; + this.setState({ columns }); }; state = { diff --git a/jest.config.js b/jest.config.js index 1c36ede9b7..4c6cf01ddb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,9 +28,6 @@ module.exports = { setupFiles: [ '/test/setupTests.js' ], - setupFilesAfterEnv: [ - '@testing-library/react/cleanup-after-each' - ], testMatch: [ '/packages/*/src/**/*.spec.(js|ts|tsx)', '/examples/**/*.spec.(js|ts|tsx)', diff --git a/package.json b/package.json index 73bb0ba451..2fc6543a70 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", "eslint": "^6.5.1", - "eslint-plugin-react": "^7.15.0", + "eslint-plugin-react": "^7.15.1", "eslint-plugin-react-hooks": "^2.1.1", "eslint-plugin-sonarjs": "^0.4.0", "faker": "^4.1.0", diff --git a/packages/react-data-grid-addons/src/__tests__/Grid.spec.js b/packages/react-data-grid-addons/src/__tests__/Grid.spec.js index 1b6f73f980..8d35e7f61c 100644 --- a/packages/react-data-grid-addons/src/__tests__/Grid.spec.js +++ b/packages/react-data-grid-addons/src/__tests__/Grid.spec.js @@ -1,9 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { mount } from 'enzyme'; -import TestUtils from 'react-dom/test-utils'; -import Grid, { CheckboxEditor } from 'react-data-grid'; +import Grid from 'react-data-grid'; describe('Grid', () => { const setup = (extraProps) => { @@ -47,14 +46,6 @@ describe('Grid', () => { return wrapper.find('Grid'); }; - const buildFakeEvent = (addedData) => { - return { - preventDefault() {}, - stopPropagation() {}, - ...addedData - }; - }; - it('should create a new instance of Grid', () => { const { wrapper } = setup(); expect(wrapper.instance()).toBeDefined(); @@ -163,134 +154,6 @@ describe('Grid', () => { }); }); - describe('When row selection enabled', () => { - let wrapper; - let columns; - let selectRowCol; - - beforeEach(() => { - ({ wrapper, columns } = setup({ enableRowSelect: true })); - selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; - }); - - it('should render an additional Select Row column', () => { - expect(getBaseGrid(wrapper).props().columnMetrics.columns.length).toEqual(columns.length + 1); - expect(TestUtils.isElementOfType(selectRowCol.formatter, CheckboxEditor)).toBe(true); - }); - }); - - describe('When selection enabled and using rowSelection props', () => { - let selectRowCol; - let rows; - let _selectedRows; - let _deselectedRows; - let wrapper; - - beforeEach(() => { - _deselectedRows = []; - rows = [{ id: '1', isSelected: true }, { id: '2', isSelected: false }, { id: '3', isSelected: false }, { id: '4', isSelected: false }]; - const columns = [{ name: 'Id', key: 'id' }]; - wrapper = setup({ - rowsCount: rows.length, - rowGetter(i) { return rows[i]; }, - columns, - rowSelection: { - enableShiftSelect: true, - selectBy: { isSelectedKey: 'isSelected' }, - onRowsSelected(selectedRows) { - _selectedRows = selectedRows; - }, - onRowsDeselected(deselectedRows) { - _deselectedRows = deselectedRows; - } - } - }).wrapper; - selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; - }); - - it('should call rowSelection.onRowsSelected when row selected', () => { - selectRowCol.onCellChange(1, '', rows[1], buildFakeEvent()); - expect(_selectedRows.length).toBe(1); - expect(_selectedRows[0].rowIdx).toBe(1); - expect(_selectedRows[0].row).toBe(rows[1]); - }); - - it('should call rowSelection.onRowsDeselected when row de-selected', () => { - selectRowCol.onCellChange(0, '', rows[0], buildFakeEvent()); - expect(_deselectedRows.length).toBe(1); - expect(_deselectedRows[0].rowIdx).toBe(0); - expect(_deselectedRows[0].row).toBe(rows[0]); - }); - - describe('checking header checkbox', () => { - it('should call rowSelection.onRowsSelected with all rows', () => { - let _selectedRows = []; - const rows = [{ id: '1' }, { id: '2' }]; - const columns = [{ name: 'Id', key: 'id' }]; - const { wrapper } = setup({ - enableRowSelect: true, - rowsCount: rows.length, - rowGetter(i) { return rows[i]; }, - columns, - rowSelection: { - selectBy: { indexes: [] }, - onRowsSelected(selectedRows) { - _selectedRows = selectedRows; - } - } - }); - - const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; - - // header checkbox - const checkboxWrapper = document.createElement('div'); - checkboxWrapper.innerHTML = ''; - const checkbox = checkboxWrapper.querySelector('input'); - const fakeEvent = buildFakeEvent({ currentTarget: checkbox }); - const SelectAll = selectRowCol.headerRenderer; - const selectAllWrapper = mount(SelectAll); - selectAllWrapper.props().onChange(fakeEvent); - - expect(_selectedRows.length).toBe(2); - }); - }); - - describe('un-checking header checkbox', () => { - it('then unchecking should call rowSelection.onRowsDeselected with all rows', () => { - let _deselectedRows = []; - const rows = [{ id: '1' }, { id: '2' }]; - const columns = [{ name: 'Id', key: 'id' }]; - const { wrapper } = setup({ - enableRowSelect: true, - rowsCount: rows.length, - rowGetter(i) { return rows[i]; }, - columns, - rowSelection: { - selectBy: { indexes: [0, 1] }, - onRowsDeselected(deselectedRows) { - _deselectedRows = deselectedRows; - } - } - }); - - const selectRowCol = getBaseGrid(wrapper).props().columnMetrics.columns[0]; - - // header checkbox - const checkboxWrapper = document.createElement('div'); - checkboxWrapper.innerHTML = ''; - const checkbox = checkboxWrapper.querySelector('input'); - const SelectAll = selectRowCol.headerRenderer; - const selectAllWrapper = mount(SelectAll); - - checkbox.checked = false; - const fakeEvent = buildFakeEvent({ currentTarget: checkbox }); - selectAllWrapper.props().onChange(fakeEvent); - - expect(_deselectedRows.length).toBe(2); - }); - }); - }); - describe('Table width', () => { describe('providing table width as prop', () => { it('should set the width of the table', () => { diff --git a/packages/react-data-grid-addons/src/draggable/DragDropContainer.js b/packages/react-data-grid-addons/src/draggable/DragDropContainer.js index 4818d176f9..f6212f028f 100644 --- a/packages/react-data-grid-addons/src/draggable/DragDropContainer.js +++ b/packages/react-data-grid-addons/src/draggable/DragDropContainer.js @@ -39,7 +39,7 @@ class DraggableContainer extends Component {
    {grid} diff --git a/packages/react-data-grid-addons/src/draggable/DropTargetRowContainer.js b/packages/react-data-grid-addons/src/draggable/DropTargetRowContainer.js deleted file mode 100644 index f8e2d0d182..0000000000 --- a/packages/react-data-grid-addons/src/draggable/DropTargetRowContainer.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { DropTarget } from 'react-dnd'; -import { rowComparer } from 'react-data-grid'; - -const rowDropTarget = (Row) => class extends React.Component { - shouldComponentUpdate(nextProps) { - return rowComparer(nextProps, this.props); - } - - moveRow() { - ReactDOM.findDOMNode(this).classList.add('slideUp'); - } - - render() { - const { connectDropTarget, isOver, canDrop } = this.props; - const overlayTop = this.props.idx * this.props.height; - return connectDropTarget( -
    - this.row = node} {...this.props} /> - {isOver && canDrop && ( -
    - )} -
    - ); - } -}; - -const target = { - drop(props, monitor, component) { - // Obtain the dragged item - component.moveRow(); - const rowSource = monitor.getItem(); - const rowTarget = { idx: props.idx, data: props.row }; - props.onRowDrop({ rowSource, rowTarget }); - } -}; - -function collect(connect, monitor) { - return { - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - draggedRow: monitor.getItem() - }; -} - -export default (Row) => DropTarget('Row', target, collect, { arePropsEqual: (nextProps, currentProps) => !rowComparer(nextProps, currentProps) })(rowDropTarget(Row)); diff --git a/packages/react-data-grid-addons/src/draggable/RowActionsCell.js b/packages/react-data-grid-addons/src/draggable/RowActionsCell.js deleted file mode 100644 index be4868f570..0000000000 --- a/packages/react-data-grid-addons/src/draggable/RowActionsCell.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DragSource } from 'react-dnd'; -import { CheckboxEditor } from 'react-data-grid'; - -class RowActionsCell extends React.Component { - static propTypes = { - rowIdx: PropTypes.number.isRequired, - connectDragSource: PropTypes.func.isRequired, - connectDragPreview: PropTypes.func.isRequired, - isDragging: PropTypes.bool.isRequired, - isRowHovered: PropTypes.bool, - column: PropTypes.object, - dependentValues: PropTypes.object, - value: PropTypes.bool, - rowSelection: PropTypes.object.isRequired - }; - - static defaultProps = { - rowIdx: 0 - }; - - renderRowIndex() { - return ( -
    - { this.props.rowIdx + 1 } -
    - ); - } - - render() { - const { connectDragSource, rowSelection } = this.props; - const rowHandleStyle = rowSelection != null ? { position: 'absolute', marginTop: '5px' } : {}; - const isSelected = this.props.value; - const editorClass = isSelected ? 'rdg-actions-checkbox selected' : 'rdg-actions-checkbox'; - - return connectDragSource( -
    -
    - {!isSelected ? this.renderRowIndex() : null} - {rowSelection != null && ( -
    - -
    - )} -
    ); - } -} - -function collect(connect, monitor) { - return { - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging(), - connectDragPreview: connect.dragPreview() - }; -} - -const rowIndexSource = { - beginDrag(props) { - return { idx: props.rowIdx, data: props.dependentValues }; - }, - endDrag(props) { - return { idx: props.rowIdx, data: props.dependentValues }; - } -}; - -export default DragSource('Row', rowIndexSource, collect)(RowActionsCell); diff --git a/packages/react-data-grid-addons/src/draggable/RowDragLayer.js b/packages/react-data-grid-addons/src/draggable/RowDragLayer.js index 4daff42c4b..16e4b4706c 100644 --- a/packages/react-data-grid-addons/src/draggable/RowDragLayer.js +++ b/packages/react-data-grid-addons/src/draggable/RowDragLayer.js @@ -2,8 +2,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { DragLayer } from 'react-dnd'; -import * as Selectors from '../data/Selectors'; - const layerStyles = { cursor: '-webkit-grabbing', position: 'fixed', @@ -40,28 +38,16 @@ class CustomDragLayer extends Component { y: PropTypes.number.isRequired }), isDragging: PropTypes.bool.isRequired, - rowSelection: PropTypes.object, + selectedRows: PropTypes.object, rows: PropTypes.array.isRequired, columns: PropTypes.array.isRequired }; - isDraggedRowSelected(selectedRows) { - const { item, rowSelection } = this.props; - if (selectedRows && selectedRows.length > 0) { - const key = rowSelection.selectBy.keys.rowKey; - return selectedRows.filter(r => r[key] === item.data[key]).length > 0; - } - return false; - } - getDraggedRows() { let draggedRows; - const { rowSelection } = this.props; - if (rowSelection && rowSelection.selectBy.keys) { - const { rows } = this.props; - const { rowKey, values } = rowSelection.selectBy.keys; - const selectedRows = Selectors.getSelectedRowsByKey({ rowKey, selectedKeys: values, rows }); - draggedRows = this.isDraggedRowSelected(selectedRows) ? selectedRows : [this.props.rows[this.props.item.idx]]; + const { selectedRows } = this.props; + if (selectedRows && selectedRows.size > 0) { + draggedRows = [...selectedRows]; } else { draggedRows = [this.props.rows[this.props.item.idx]]; } diff --git a/packages/react-data-grid-addons/src/draggable/__tests__/DropTargetRowContainer.spec.js b/packages/react-data-grid-addons/src/draggable/__tests__/DropTargetRowContainer.spec.js deleted file mode 100644 index f99755fd4d..0000000000 --- a/packages/react-data-grid-addons/src/draggable/__tests__/DropTargetRowContainer.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { Component } from 'react'; -import { mount } from 'enzyme'; -import TestBackend from 'react-dnd-test-backend'; -import { DragDropContext } from 'react-dnd'; - -import dropTargetRowContainer from '../DropTargetRowContainer'; -import { DragTestSource } from './TestDragSources'; -import { _helpers } from 'react-data-grid'; -const { test: { GridPropHelpers } } = _helpers; - -class fakeRow extends Component { - render() { - return fake row; - } -} - -/** - * Wraps a component into a DragDropContext that uses the TestBackend. - */ -function wrapInTestContext(DecoratedComponent) { - return DragDropContext(TestBackend)(class extends React.Component { - render() { - return ; - } - }); -} - -describe('', () => { - let ComponentUnderTest; - let wrapper; - let backend; - let registry; - let manager; - const props = { - onRowDrop: jest.fn(), - idx: 1, - row: { id: 5, country: 'England' }, - columns: GridPropHelpers.columns, - cellMetaData: GridPropHelpers.cellMetaData - }; - - beforeEach(() => { - ComponentUnderTest = wrapInTestContext(dropTargetRowContainer(fakeRow)); - wrapper = mount(); - manager = wrapper.instance().getManager(); - backend = manager.getBackend(); - registry = manager.getRegistry(); - }); - - it('renders the injected row', () => { - expect(wrapper.find(fakeRow).length).toEqual(1); - }); - - it('should call onRowDrop with correct parameters when source is dropped', () => { - const rowTargetKey = Object.keys(registry.handlers).filter(k => registry.handlers[k].monitor && registry.handlers[k].monitor.targetId)[0]; - const rowTargetId = registry.handlers[rowTargetKey].monitor.targetId; - const draggedRowItem = { idx: 3, data: { id: 11, country: 'Ireland', county: 'Wicklow' } }; - const sourceId = registry.addSource('Row', new DragTestSource(draggedRowItem)); - backend.simulateBeginDrag([sourceId]); - backend.simulateHover([rowTargetId]); - backend.simulateDrop(); - expect(props.onRowDrop).toHaveBeenCalled(); - expect(props.onRowDrop.mock.calls.length).toEqual(1); - const { rowSource, rowTarget } = props.onRowDrop.mock.calls[0][0]; - expect(rowSource).toEqual(draggedRowItem); - expect(rowTarget.idx).toEqual(1); - expect(rowTarget.data).toEqual(props.row); - }); -}); diff --git a/packages/react-data-grid-addons/src/draggable/index.ts b/packages/react-data-grid-addons/src/draggable/index.ts index a35d91116c..52ad22ad43 100644 --- a/packages/react-data-grid-addons/src/draggable/index.ts +++ b/packages/react-data-grid-addons/src/draggable/index.ts @@ -1,4 +1,2 @@ export { default as Container } from './DragDropContainer'; export { default as DraggableHeaderCell } from './DraggableHeaderCell'; -export { default as RowActionsCell } from './RowActionsCell'; -export { default as DropTargetRowContainer } from './DropTargetRowContainer'; diff --git a/packages/react-data-grid-addons/src/editors/index.ts b/packages/react-data-grid-addons/src/editors/index.ts index 3449aa41a6..5c9d1b5a4b 100644 --- a/packages/react-data-grid-addons/src/editors/index.ts +++ b/packages/react-data-grid-addons/src/editors/index.ts @@ -1,2 +1,2 @@ -export { SimpleTextEditor, CheckboxEditor } from 'react-data-grid'; +export { SimpleTextEditor } from 'react-data-grid'; export { default as DropDownEditor } from './DropDownEditor'; diff --git a/packages/react-data-grid/src/Canvas.tsx b/packages/react-data-grid/src/Canvas.tsx index b373177427..0af00a83aa 100644 --- a/packages/react-data-grid/src/Canvas.tsx +++ b/packages/react-data-grid/src/Canvas.tsx @@ -5,7 +5,6 @@ import Row from './Row'; import RowsContainerDefault from './RowsContainer'; import RowGroup from './RowGroup'; import { InteractionMasks } from './masks'; -import * as rowUtils from './RowUtils'; import { getColumnScrollPosition, isPositionStickySupported } from './utils'; import { EventTypes } from './common/enums'; import { CalculatedColumn, Position, ScrollPosition, SubRowDetails, RowRenderer, RowRendererProps, RowData } from './common/types'; @@ -13,16 +12,16 @@ import { ViewportProps } from './Viewport'; import { HorizontalRangeToRender, VerticalRangeToRender } from './utils/viewportUtils'; type SharedViewportProps = Pick, -'rowKey' +| 'rowKey' | 'rowGetter' | 'rowsCount' | 'selectedRows' +| 'onSelectedRowsChange' | 'rowRenderer' | 'cellMetaData' | 'rowHeight' | 'scrollToRowIndex' | 'contextMenu' -| 'rowSelection' | 'getSubRowDetails' | 'rowGroupRenderer' | 'enableCellSelect' @@ -52,7 +51,8 @@ type RendererProps = Pick, 'columns' | 'cellMetaData' | 'colOv row: R; subRowDetails?: SubRowDetails; height: number; - isSelected: boolean; + isRowSelected: boolean; + onRowSelectionChange(rowIdx: number, row: R, checked: boolean, isShiftClick: boolean): void; scrollLeft: number; }; @@ -62,6 +62,7 @@ export default class Canvas extends React.PureComponent> { private readonly canvas = React.createRef(); private readonly interactionMasks = React.createRef>(); private readonly rows = new Map & React.Component>>(); + private lastSelectedRowIdx = -1; private unsubscribeScrollToColumn?(): void; componentDidMount() { @@ -146,24 +147,33 @@ export default class Canvas extends React.PureComponent> { return scrollVariation > 0 ? rowHeight - scrollVariation : 0; } - isRowSelected(idx: number, row: R) { - // Use selectedRows if set - if (this.props.selectedRows) { - const selectedRow = this.props.selectedRows.find(r => { - const rowKeyValue = rowUtils.get(row, this.props.rowKey); - return r[this.props.rowKey] === rowKeyValue; - }); - return !!(selectedRow && selectedRow.isSelected); - } + isRowSelected(row: R): boolean { + return this.props.selectedRows !== undefined && this.props.selectedRows.has(row[this.props.rowKey]); + } - // Else use new rowSelection props - if (this.props.rowSelection) { - const { keys, indexes, isSelectedKey } = this.props.rowSelection as { [key: string]: unknown }; - return rowUtils.isRowSelected(keys as { rowKey?: string; values?: string[] } | null, indexes as number[] | null, isSelectedKey as string | null, row, idx); + handleRowSelectionChange = (rowIdx: number, row: R, checked: boolean, isShiftClick: boolean) => { + if (!this.props.onSelectedRowsChange) return; + + const { rowKey } = this.props; + const newSelectedRows = new Set(this.props.selectedRows); + + if (checked) { + newSelectedRows.add(row[rowKey]); + const previousRowIdx = this.lastSelectedRowIdx; + this.lastSelectedRowIdx = rowIdx; + if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { + const step = Math.sign(rowIdx - previousRowIdx); + for (let i = previousRowIdx + step; i !== rowIdx; i += step) { + newSelectedRows.add(this.props.rowGetter(i)[rowKey]); + } + } + } else { + newSelectedRows.delete(row[rowKey]); + this.lastSelectedRowIdx = -1; } - return false; - } + this.props.onSelectedRowsChange(newSelectedRows); + }; setScrollLeft(scrollLeft: number) { if (isPositionStickySupported()) return; @@ -267,7 +277,7 @@ export default class Canvas extends React.PureComponent> { } render() { - const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, rowHeight, rowsCount, width, height, rowGetter, contextMenu } = this.props; + const { rowOverscanStartIdx, rowOverscanEndIdx, cellMetaData, columns, colOverscanStartIdx, colOverscanEndIdx, colVisibleStartIdx, colVisibleEndIdx, lastFrozenColumnIndex, rowHeight, rowsCount, width, height, rowGetter, contextMenu, isScrolling, scrollLeft } = this.props; const RowsContainer = this.props.RowsContainer || RowsContainerDefault; const rows = this.getRows(rowOverscanStartIdx, rowOverscanEndIdx) @@ -286,14 +296,15 @@ export default class Canvas extends React.PureComponent> { row, height: rowHeight, columns, - isSelected: this.isRowSelected(rowIdx, row), + isRowSelected: this.isRowSelected(row), + onRowSelectionChange: this.handleRowSelectionChange, cellMetaData, subRowDetails, colOverscanStartIdx, colOverscanEndIdx, lastFrozenColumnIndex, - isScrolling: this.props.isScrolling, - scrollLeft: this.props.scrollLeft + isScrolling, + scrollLeft }); }); @@ -331,7 +342,7 @@ export default class Canvas extends React.PureComponent> { onHitTopBoundary={this.onHitTopCanvas} onHitLeftBoundary={this.handleHitColummBoundary} onHitRightBoundary={this.handleHitColummBoundary} - scrollLeft={this.props.scrollLeft} + scrollLeft={scrollLeft} scrollTop={this.props.scrollTop} getRowHeight={this.getRowHeight} getRowTop={this.getRowTop} diff --git a/packages/react-data-grid/src/Cell.tsx b/packages/react-data-grid/src/Cell.tsx index c8ecd9511d..3b1192a849 100644 --- a/packages/react-data-grid/src/Cell.tsx +++ b/packages/react-data-grid/src/Cell.tsx @@ -180,6 +180,8 @@ export default class Cell extends React.Component> implements Ce onDeleteSubRow={cellMetaData.onDeleteSubRow} cellControls={cellControls} isScrolling={isScrolling} + isRowSelected={this.props.isRowSelected} + onRowSelectionChange={this.props.onRowSelectionChange} /> ); const events = this.getEvents(); diff --git a/packages/react-data-grid/src/Cell/CellContent.tsx b/packages/react-data-grid/src/Cell/CellContent.tsx index 232dd50dba..d0be3418a8 100644 --- a/packages/react-data-grid/src/Cell/CellContent.tsx +++ b/packages/react-data-grid/src/Cell/CellContent.tsx @@ -7,7 +7,7 @@ import { CellProps } from '../Cell'; import CellValue from './CellValue'; export type CellContentProps = Pick, -'idx' +| 'idx' | 'rowIdx' | 'rowData' | 'column' @@ -17,11 +17,12 @@ export type CellContentProps = Pick, | 'tooltip' | 'height' | 'cellControls' +| 'isRowSelected' +| 'onRowSelectionChange' > & Pick, 'onDeleteSubRow' >; - export default function CellContent({ idx, rowIdx, @@ -33,7 +34,9 @@ export default function CellContent({ height, onDeleteSubRow, cellControls, - isScrolling + isScrolling, + isRowSelected, + onRowSelectionChange }: CellContentProps) { const isExpandCell = expandableOptions ? expandableOptions.field === column.key : false; const treeDepth = expandableOptions ? expandableOptions.treeDepth : 0; @@ -74,6 +77,8 @@ export default function CellContent({ column={column} value={value} isScrolling={isScrolling} + isRowSelected={isRowSelected} + onRowSelectionChange={onRowSelectionChange} /> {cellControls} diff --git a/packages/react-data-grid/src/Cell/CellValue.tsx b/packages/react-data-grid/src/Cell/CellValue.tsx index edd91d3eec..38dffdf846 100644 --- a/packages/react-data-grid/src/Cell/CellValue.tsx +++ b/packages/react-data-grid/src/Cell/CellValue.tsx @@ -5,14 +5,16 @@ import { SimpleCellFormatter } from '../formatters'; import { CellContentProps } from './CellContent'; type CellValueProps = Pick, -'rowIdx' +| 'rowIdx' | 'rowData' | 'column' | 'value' | 'isScrolling' +| 'isRowSelected' +| 'onRowSelectionChange' >; -export default function CellValue({ rowIdx, rowData, column, value, isScrolling }: CellValueProps) { +export default function CellValue({ rowIdx, rowData, column, value, isScrolling, isRowSelected, onRowSelectionChange }: CellValueProps) { function getFormatterDependencies(row: R) { // convention based method to get corresponding Id or Name of any Name or Id property const { getRowMetaData } = column; @@ -26,11 +28,14 @@ export default function CellValue({ rowIdx, rowData, column, value, isScrolli function getFormatterProps() { return { - value, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + value: value as any, //FIXME: fix value type column, rowIdx, isScrolling, row: rowData, + isRowSelected, + onRowSelectionChange, dependentValues: getFormatterDependencies(rowData) }; } @@ -42,8 +47,7 @@ export default function CellValue({ rowIdx, rowData, column, value, isScrolli } if (isValidElementType(formatter)) { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - return React.createElement(formatter, { ...getFormatterProps(), value: value as any }); //FIXME: fix value type + return React.createElement(formatter, getFormatterProps()); } return ; diff --git a/packages/react-data-grid/src/Columns.tsx b/packages/react-data-grid/src/Columns.tsx new file mode 100644 index 0000000000..c52090cd2a --- /dev/null +++ b/packages/react-data-grid/src/Columns.tsx @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react'; +import SelectCellFormatter from './formatters/SelectCellFormatter'; +import { + Column, + FormatterProps, + HeaderRowProps +} from './common/types'; + +// TODO: fix type +export const SelectColumn: Column = { + key: 'select-row', + name: '', + width: 60, + filterable: false, + frozen: true, + headerRenderer: (props: HeaderRowProps) => ( + + ), + formatter: (props: FormatterProps) => ( + props.onRowSelectionChange(props.rowIdx, props.row, value, isShiftClick)} + /> + ) +}; diff --git a/packages/react-data-grid/src/Grid.tsx b/packages/react-data-grid/src/Grid.tsx index 6a429e6513..49e78a427d 100644 --- a/packages/react-data-grid/src/Grid.tsx +++ b/packages/react-data-grid/src/Grid.tsx @@ -3,13 +3,13 @@ import { isValidElementType } from 'react-is'; import Header, { HeaderHandle, HeaderProps } from './Header'; import Viewport, { ScrollState } from './Viewport'; -import { HeaderRowData, CellMetaData, RowSelection, InteractionMasksMetaData, SelectedRow, ColumnMetrics } from './common/types'; +import { HeaderRowData, CellMetaData, InteractionMasksMetaData, ColumnMetrics } from './common/types'; import { DEFINE_SORT } from './common/enums'; import { ReactDataGridProps } from './ReactDataGrid'; import { EventBus } from './masks'; type SharedDataGridProps = Pick, -'draggableHeaderCell' +| 'draggableHeaderCell' | 'getValidFilterValues' | 'rowGetter' | 'rowsCount' @@ -25,8 +25,10 @@ type SharedDataGridProps = Pick, | 'overscanRowCount' | 'overscanColumnCount' | 'enableIsScrolling' +| 'selectedRows' +| 'onSelectedRowsChange' > & Required, -'rowKey' +| 'rowKey' | 'enableCellSelect' | 'rowHeight' | 'minHeight' @@ -42,19 +44,25 @@ type SharedDataGridProps = Pick, export interface GridProps extends SharedDataGridProps { headerRows: [HeaderRowData, HeaderRowData | undefined]; cellMetaData: CellMetaData; - selectedRows?: SelectedRow[]; - rowSelection?: RowSelection; rowOffsetHeight: number; eventBus: EventBus; interactionMasksMetaData: InteractionMasksMetaData; onSort(columnKey: keyof R, sortDirection: DEFINE_SORT): void; - onViewportKeydown(e: React.KeyboardEvent): void; - onViewportKeyup(e: React.KeyboardEvent): void; + onViewportKeydown?(e: React.KeyboardEvent): void; + onViewportKeyup?(e: React.KeyboardEvent): void; onColumnResize(idx: number, width: number): void; viewportWidth: number; } -export default function Grid({ emptyRowsView, headerRows, viewportWidth, ...props }: GridProps) { +export default function Grid({ + rowKey, + rowsCount, + emptyRowsView, + headerRows, + viewportWidth, + selectedRows, + ...props +}: GridProps) { const header = useRef(null); const scrollLeft = useRef(0); @@ -74,7 +82,10 @@ export default function Grid({ emptyRowsView, headerRows, viewportWidth, ...p
    { React.createElement(Header as React.FunctionComponent, { + rowKey, + rowsCount, ref: header, + rowGetter: props.rowGetter, columnMetrics: props.columnMetrics, onColumnResize: props.onColumnResize, headerRows, @@ -84,22 +95,25 @@ export default function Grid({ emptyRowsView, headerRows, viewportWidth, ...p draggableHeaderCell: props.draggableHeaderCell, onSort: props.onSort, onHeaderDrop: props.onHeaderDrop, + allRowsSelected: selectedRows !== undefined && selectedRows.size === rowsCount, + onSelectedRowsChange: props.onSelectedRowsChange, getValidFilterValues: props.getValidFilterValues, cellMetaData: props.cellMetaData }) } - {props.rowsCount === 0 && isValidElementType(emptyRowsView) ? ( + {rowsCount === 0 && isValidElementType(emptyRowsView) ? (
    {createElement(emptyRowsView)}
    ) : ( - rowKey={props.rowKey} + rowKey={rowKey} rowHeight={props.rowHeight} rowRenderer={props.rowRenderer} rowGetter={props.rowGetter} - rowsCount={props.rowsCount} - selectedRows={props.selectedRows} + rowsCount={rowsCount} + selectedRows={selectedRows} + onSelectedRowsChange={props.onSelectedRowsChange} columnMetrics={props.columnMetrics} onScroll={onScroll} cellMetaData={props.cellMetaData} @@ -107,7 +121,6 @@ export default function Grid({ emptyRowsView, headerRows, viewportWidth, ...p minHeight={props.minHeight} scrollToRowIndex={props.scrollToRowIndex} contextMenu={props.contextMenu} - rowSelection={props.rowSelection} getSubRowDetails={props.getSubRowDetails} rowGroupRenderer={props.rowGroupRenderer} enableCellSelect={props.enableCellSelect} diff --git a/packages/react-data-grid/src/Header.tsx b/packages/react-data-grid/src/Header.tsx index 0678522be8..ce71401199 100644 --- a/packages/react-data-grid/src/Header.tsx +++ b/packages/react-data-grid/src/Header.tsx @@ -8,20 +8,26 @@ import { CalculatedColumn, HeaderRowData } from './common/types'; import { GridProps } from './Grid'; type SharedGridProps = Pick, -'columnMetrics' +| 'rowKey' +| 'rowsCount' +| 'rowGetter' +| 'columnMetrics' | 'onColumnResize' | 'headerRows' | 'rowOffsetHeight' | 'sortColumn' | 'sortDirection' | 'draggableHeaderCell' +| 'onSelectedRowsChange' | 'onSort' | 'onHeaderDrop' | 'getValidFilterValues' | 'cellMetaData' >; -export type HeaderProps = SharedGridProps; +export interface HeaderProps extends SharedGridProps { + allRowsSelected: boolean; +} export interface HeaderHandle { setScrollLeft(scrollLeft: number): void; @@ -64,6 +70,19 @@ export default forwardRef(function Header(props: HeaderProps, ref: React.R setResizing(null); } + function handleAllRowsSelectionChange(checked: boolean) { + if (!props.onSelectedRowsChange) return; + + const newSelectedRows = new Set(); + if (checked) { + for (let i = 0; i < props.rowsCount; i++) { + newSelectedRows.add(props.rowGetter(i)[props.rowKey]); + } + } + + props.onSelectedRowsChange(newSelectedRows); + } + function getHeaderRow(row: HeaderRowData, ref: React.RefObject>) { return ( @@ -78,6 +97,8 @@ export default forwardRef(function Header(props: HeaderProps, ref: React.R filterable={row.filterable} onFilterChange={row.onFilterChange} onHeaderDrop={props.onHeaderDrop} + allRowsSelected={props.allRowsSelected} + onAllRowsSelectionChange={handleAllRowsSelectionChange} sortColumn={props.sortColumn} sortDirection={props.sortDirection} onSort={props.onSort} diff --git a/packages/react-data-grid/src/HeaderCell.tsx b/packages/react-data-grid/src/HeaderCell.tsx index 8e223bb827..86fab4ae36 100644 --- a/packages/react-data-grid/src/HeaderCell.tsx +++ b/packages/react-data-grid/src/HeaderCell.tsx @@ -18,6 +18,8 @@ interface Props { onResize(column: CalculatedColumn, width: number): void; onResizeEnd(): void; onHeaderDrop?(): void; + allRowsSelected: boolean; + onAllRowsSelectionChange(checked: boolean): void; draggableHeaderCell?: React.ComponentType<{ column: CalculatedColumn; onHeaderDrop(): void }>; className?: string; } @@ -103,7 +105,7 @@ export default class HeaderCell extends React.Component> { } getCell() { - const { height, column, rowType } = this.props; + const { height, column, rowType, allRowsSelected, onAllRowsSelectionChange } = this.props; const renderer = this.props.renderer || SimpleCellRenderer; if (isElement(renderer)) { // if it is a string, it's an HTML element, and column is not a valid property, so only pass height @@ -112,7 +114,7 @@ export default class HeaderCell extends React.Component> { } return React.cloneElement(renderer, { column, height }); } - return React.createElement(renderer, { column, rowType }); + return React.createElement(renderer, { column, rowType, allRowsSelected, onAllRowsSelectionChange }); } setScrollLeft(scrollLeft: number) { diff --git a/packages/react-data-grid/src/HeaderRow.tsx b/packages/react-data-grid/src/HeaderRow.tsx index f2b94c6522..61773e4cc3 100644 --- a/packages/react-data-grid/src/HeaderRow.tsx +++ b/packages/react-data-grid/src/HeaderRow.tsx @@ -9,8 +9,9 @@ import { CalculatedColumn, AddFilterEvent } from './common/types'; import { HeaderProps } from './Header'; type SharedHeaderProps = Pick, -'draggableHeaderCell' +| 'draggableHeaderCell' | 'onHeaderDrop' +| 'allRowsSelected' | 'sortColumn' | 'sortDirection' | 'onSort' @@ -22,12 +23,13 @@ export interface HeaderRowProps extends SharedHeaderProps { columns: CalculatedColumn[]; onColumnResize(column: CalculatedColumn, width: number): void; onColumnResizeEnd(): void; + onAllRowsSelectionChange(checked: boolean): void; filterable?: boolean; onFilterChange?(args: AddFilterEvent): void; rowType: HeaderRowType; } -export default class HeaderRow extends React.PureComponent> { +export default class HeaderRow extends React.Component> { static displayName = 'HeaderRow'; private readonly headerRow = React.createRef(); @@ -66,6 +68,8 @@ export default class HeaderRow extends React.PureComponent> onSort={this.props.onSort} sortDirection={sortDirection} sortDescendingFirst={sortDescendingFirst} + allRowsSelected={this.props.allRowsSelected} + onAllRowsSelectionChange={this.props.onAllRowsSelectionChange} /> ); } @@ -105,6 +109,8 @@ export default class HeaderRow extends React.PureComponent> onResize={this.props.onColumnResize} onResizeEnd={this.props.onColumnResizeEnd} onHeaderDrop={this.props.onHeaderDrop} + allRowsSelected={this.props.allRowsSelected} + onAllRowsSelectionChange={this.props.onAllRowsSelectionChange} draggableHeaderCell={this.props.draggableHeaderCell} /> ); diff --git a/packages/react-data-grid/src/ReactDataGrid.tsx b/packages/react-data-grid/src/ReactDataGrid.tsx index edb09237b4..c544d3b7ae 100644 --- a/packages/react-data-grid/src/ReactDataGrid.tsx +++ b/packages/react-data-grid/src/ReactDataGrid.tsx @@ -10,10 +10,6 @@ import React, { import Grid from './Grid'; import ToolbarContainer, { ToolbarProps } from './ToolbarContainer'; -import CheckboxEditor, { CheckboxEditorProps } from './common/editors/CheckboxEditor'; -import { SelectAll } from './formatters'; -import * as rowUtils from './RowUtils'; -import KeyCodes from './KeyCodes'; import { getColumnMetrics } from './ColumnMetrics'; import { ScrollState } from './Viewport'; import { RowsContainerProps } from './RowsContainer'; @@ -26,7 +22,6 @@ import { CellCopyPasteEvent, CellMetaData, CheckCellIsEditableEvent, - Column, ColumnList, CommitEvent, GridRowsUpdatedEvent, @@ -35,12 +30,9 @@ import { Position, RowExpandToggleEvent, RowGetter, - RowSelection, - RowSelectionParams, SelectedRange, SubRowDetails, SubRowOptions, - SelectedRow, RowRendererProps } from './common/types'; @@ -53,8 +45,6 @@ export interface ReactDataGridProps { headerRowHeight?: number; /** The height of the header filter row in pixels */ headerFiltersHeight?: number; - /** Deprecated: Legacy prop to turn on row selection. Use rowSelection props instead*/ - enableRowSelect?: boolean | string; /** Component used to render toolbar above the grid */ toolbar?: React.ReactElement> | React.ComponentType>; cellRangeSelection?: { @@ -64,8 +54,6 @@ export interface ReactDataGridProps { }; /** Minimum column width in pixels */ minColumnWidth?: number; - /** Component to render the UI in the header row for selecting all rows */ - selectAllRenderer?: React.ComponentType>; /** Function called whenever row is clicked */ onRowClick?(rowIdx: number, rowData: R, column: CalculatedColumn): void; /** Function called whenever row is double clicked */ @@ -78,20 +66,11 @@ export interface ReactDataGridProps { onGridKeyUp?(event: React.KeyboardEvent): void; /** Function called whenever keyboard key is pressed down */ onGridKeyDown?(event: React.KeyboardEvent): void; - onRowSelect?(rowData: R[]): void; - rowSelection?: { - enableShiftSelect?: boolean; - /** Function called whenever rows are selected */ - onRowsSelected?(args: RowSelectionParams[]): void; - /** Function called whenever rows are deselected */ - onRowsDeselected?(args: RowSelectionParams[]): void; - /** toggle whether to show a checkbox in first column to select rows */ - showCheckbox?: boolean; - /** Method by which rows should be selected */ - selectBy: RowSelection; - }; - /** Custom checkbox formatter */ - rowActionsCell?: React.ComponentType>; + + selectedRows?: Set; + /** Function called whenever row selection is changed */ + onSelectedRowsChange?(selectedRows: Set): void; + /** * Callback called whenever row data is updated * When editing is enabled, this callback will be called for the following scenarios @@ -178,16 +157,11 @@ export interface ReactDataGridProps { enableIsScrolling?: boolean; } -export interface ReactDataGridHandle { +export interface ReactDataGridHandle { scrollToColumn(colIdx: number): void; selectCell(position: Position, openEditor?: boolean): void; handleToggleFilter(): void; openCellEditor(rowIdx: number, colIdx: number): void; - getSelectedRows(): SelectedRow[] | undefined; -} - -function isRowSelected(keys: unknown, indexes: unknown, isSelectedKey: unknown, rowData: R, rowIdx: number) { - return rowUtils.isRowSelected(keys as { rowKey?: string; values?: string[] } | null, indexes as number[] | null, isSelectedKey as string | null, rowData, rowIdx); } /** @@ -205,219 +179,37 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ minHeight = 350, minWidth: width, enableCellSelect = false, - enableRowSelect = false, enableCellAutoFocus = true, cellNavigationMode = CellNavigationMode.NONE, - selectAllRenderer = SelectAll, editorPortalTarget = document.body, columns, rowsCount, rowGetter, - rowSelection, cellRangeSelection, - onRowSelect, onClearFilters, + selectedRows, + onSelectedRowsChange, ...props -}: ReactDataGridProps, ref: React.Ref>) { - const [selectedRows, setSelectedRows] = useState[]>([]); +}: ReactDataGridProps, ref: React.Ref) { const [canFilter, setCanFilter] = useState(false); - const [lastRowIdxUiSelected, setLastRowIdxUiSelected] = useState(-1); const [sortColumn, setSortColumn] = useState(props.sortColumn); const [sortDirection, setSortDirection] = useState(props.sortDirection); const [columnWidths, setColumnWidths] = useState(() => new Map()); const [eventBus] = useState(() => new EventBus()); - const [_keysDown] = useState(() => new Set()); const [gridWidth, setGridWidth] = useState(0); const gridRef = useRef(null); - const selectAllCheckboxRef = useRef(null); const viewportWidth = (width || gridWidth) - 2; // 2 for border width; - const gridColumns = useMemo>(() => { - function isSingleKeyDown(keyCode: number) { - return _keysDown.has(keyCode) && _keysDown.size === 1; - } - - function getSelectedRow(rows: SelectedRow[], key: unknown) { - return rows.find(r => r[rowKey] === key); - } - - function canUseNewRowSelection() { - return rowSelection && rowSelection.selectBy; - } - - // return false if not a shift select so can be handled as normal row selection - function handleShiftSelect(rowIdx: number) { - if (rowSelection && lastRowIdxUiSelected > -1 && isSingleKeyDown(KeyCodes.Shift)) { - const { keys, indexes, isSelectedKey } = rowSelection.selectBy as { [key: string]: unknown }; - const isPreviouslySelected = isRowSelected(keys, indexes, isSelectedKey, rowGetter(rowIdx), rowIdx); - - if (isPreviouslySelected) return false; - - let handled = false; - - if (rowIdx > lastRowIdxUiSelected) { - const rowsSelected = []; - - for (let i = lastRowIdxUiSelected + 1; i <= rowIdx; i++) { - rowsSelected.push({ rowIdx: i, row: rowGetter(i) }); - } - - if (typeof rowSelection.onRowsSelected === 'function') { - rowSelection.onRowsSelected(rowsSelected); - } - - handled = true; - } else if (rowIdx < lastRowIdxUiSelected) { - const rowsSelected = []; - - for (let i = rowIdx; i <= lastRowIdxUiSelected - 1; i++) { - rowsSelected.push({ rowIdx: i, row: rowGetter(i) }); - } - - if (typeof rowSelection.onRowsSelected === 'function') { - rowSelection.onRowsSelected(rowsSelected); - } - - handled = true; - } - - if (handled) { - setLastRowIdxUiSelected(rowIdx); - } - - return handled; - } - - return false; - } - - function handleNewRowSelect(rowIdx: number, rowData: R) { - const { current } = selectAllCheckboxRef; - if (current && current.checked === true) { - current.checked = false; - } - - if (rowSelection) { - const { keys, indexes, isSelectedKey } = rowSelection.selectBy as { [key: string]: unknown }; - const isPreviouslySelected = isRowSelected(keys, indexes, isSelectedKey, rowData, rowIdx); - - setLastRowIdxUiSelected(isPreviouslySelected ? -1 : rowIdx); - const cb = isPreviouslySelected ? rowSelection.onRowsDeselected : rowSelection.onRowsSelected; - if (typeof cb === 'function') { - cb([{ rowIdx, row: rowData }]); - } - } - } - - // columnKey not used here as this function will select the whole row, - // but needed to match the function signature in the CheckboxEditor - function handleRowSelect(rowIdx: number, columnKey: keyof R, rowData: R, event: React.ChangeEvent) { - event.stopPropagation(); - - if (canUseNewRowSelection()) { - if (rowSelection && rowSelection.enableShiftSelect === true) { - if (!handleShiftSelect(rowIdx)) { - handleNewRowSelect(rowIdx, rowData); - } - } else { - handleNewRowSelect(rowIdx, rowData); - } - } else { // Fallback to old onRowSelect handler - const newSelectedRows = enableRowSelect === 'single' ? [] : [...selectedRows]; - const selectedRow = getSelectedRow(newSelectedRows, rowData[rowKey]); - if (selectedRow) { - selectedRow.isSelected = !selectedRow.isSelected; - } else { - (rowData as SelectedRow).isSelected = true; - newSelectedRows.push(rowData as SelectedRow); - } - setSelectedRows(newSelectedRows); - if (onRowSelect) { - onRowSelect(newSelectedRows.filter(r => r.isSelected === true)); - } - } - } - - function handleCheckboxChange(e: React.ChangeEvent) { - const allRowsSelected = e.currentTarget.checked; - if (rowSelection && canUseNewRowSelection()) { - const { keys, indexes, isSelectedKey } = rowSelection.selectBy as { [key: string]: unknown }; - - if (allRowsSelected && typeof rowSelection.onRowsSelected === 'function') { - const selectedRows = []; - for (let i = 0; i < rowsCount; i++) { - const rowData = rowGetter(i); - if (!isRowSelected(keys, indexes, isSelectedKey, rowData, i)) { - selectedRows.push({ rowIdx: i, row: rowData }); - } - } - - if (selectedRows.length > 0) { - rowSelection.onRowsSelected(selectedRows); - } - } else if (!allRowsSelected && typeof rowSelection.onRowsDeselected === 'function') { - const deselectedRows = []; - for (let i = 0; i < rowsCount; i++) { - const rowData = rowGetter(i); - if (isRowSelected(keys, indexes, isSelectedKey, rowData, i)) { - deselectedRows.push({ rowIdx: i, row: rowData }); - } - } - - if (deselectedRows.length > 0) { - rowSelection.onRowsDeselected(deselectedRows); - } - } - } else { - const selectedRows: SelectedRow[] = []; - for (let i = 0; i < rowsCount; i++) { - const row = { ...rowGetter(i), isSelected: allRowsSelected }; - selectedRows.push(row); - } - setSelectedRows(selectedRows); - if (typeof onRowSelect === 'function') { - onRowSelect(selectedRows.filter(r => r.isSelected === true)); - } - } - } - - if (props.rowActionsCell || (enableRowSelect && !rowSelection) || (rowSelection && rowSelection.showCheckbox !== false)) { - const SelectAllComponent = selectAllRenderer; - const headerRenderer = enableRowSelect === 'single' - ? undefined - : ; - const Formatter = (props.rowActionsCell ? props.rowActionsCell : CheckboxEditor) as unknown as React.ComponentClass<{ rowSelection: unknown }>; - const selectColumn = { - key: 'select-row', - name: '', - formatter: , - onCellChange: handleRowSelect, - filterable: false, - headerRenderer, - width: 60, - frozen: true, - getRowMetaData: (rowData: R) => rowData, - cellClass: props.rowActionsCell ? 'rdg-row-actions-cell' : '' - } as unknown as Column; - - return Array.isArray(columns) - ? [selectColumn, ...columns] - : columns.unshift(selectColumn); - } - - return columns; - }, [_keysDown, columns, enableRowSelect, lastRowIdxUiSelected, onRowSelect, props.rowActionsCell, rowGetter, rowKey, rowSelection, rowsCount, selectAllRenderer, selectedRows]); - const columnMetrics = useMemo(() => { if (viewportWidth <= 0) return null; return getColumnMetrics({ - columns: gridColumns, + columns, minColumnWidth, viewportWidth, columnWidths }); - }, [columnWidths, gridColumns, minColumnWidth, viewportWidth]); + }, [columnWidths, columns, minColumnWidth, viewportWidth]); useLayoutEffect(() => { // Do not calculate the width if minWidth is provided @@ -471,27 +263,7 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ eventBus.dispatch(EventTypes.DRAG_ENTER, overRowIdx); } - function handleViewportKeyDown(e: React.KeyboardEvent) { - // Track which keys are currently down for shift clicking etc - _keysDown.add(e.keyCode); - - const { onGridKeyDown } = props; - if (onGridKeyDown) { - onGridKeyDown(e); - } - } - - function handleViewportKeyUp(e: React.KeyboardEvent) { - // Track which keys are currently down for shift clicking etc - _keysDown.delete(e.keyCode); - - const { onGridKeyUp } = props; - if (onGridKeyUp) { - onGridKeyUp(e); - } - } - - function handlerCellClick({ rowIdx, idx }: Position) { + function handleCellClick({ rowIdx, idx }: Position) { const { onRowClick } = props; selectCell({ rowIdx, idx }); @@ -575,18 +347,6 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ ]; } - function getRowSelectionProps() { - return rowSelection && rowSelection.selectBy; - } - - function getSelectedRows(): SelectedRow[] | undefined { - if (rowSelection) { - return; - } - - return selectedRows.filter(r => r.isSelected === true); - } - function openCellEditor(rowIdx: number, idx: number) { selectCell({ rowIdx, idx }, true); } @@ -599,13 +359,12 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ scrollToColumn, selectCell, handleToggleFilter, - openCellEditor, - getSelectedRows + openCellEditor })); const cellMetaData: CellMetaData = { rowKey, - onCellClick: handlerCellClick, + onCellClick: handleCellClick, onCellContextMenu: handleCellContextMenu, onCellDoubleClick: handleCellDoubleClick, onCellExpand: props.onCellExpand, @@ -662,15 +421,15 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ rowRenderer={props.rowRenderer} rowGroupRenderer={props.rowGroupRenderer} cellMetaData={cellMetaData} - selectedRows={getSelectedRows()} - rowSelection={getRowSelectionProps()} + selectedRows={selectedRows} + onSelectedRowsChange={onSelectedRowsChange} rowOffsetHeight={rowOffsetHeight} sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort} minHeight={minHeight} - onViewportKeydown={handleViewportKeyDown} - onViewportKeyup={handleViewportKeyUp} + onViewportKeydown={props.onGridKeyDown} + onViewportKeyup={props.onGridKeyUp} onColumnResize={handleColumnResize} scrollToRowIndex={props.scrollToRowIndex} contextMenu={props.contextMenu} @@ -696,8 +455,8 @@ const ReactDataGridBase = forwardRef(function ReactDataGrid({ }); // This is a temporary class to expose instance methods as ForwardRef does work well with generics -export default class ReactDataGrid extends React.Component> implements ReactDataGridHandle { - private readonly gridRef = React.createRef>(); +export default class ReactDataGrid extends React.Component> implements ReactDataGridHandle { + private readonly gridRef = React.createRef(); selectCell(position: Position, openEditor?: boolean | undefined): void { this.gridRef.current!.selectCell(position, openEditor); @@ -715,15 +474,11 @@ export default class ReactDataGrid extends React.Component[] | undefined { - return this.gridRef.current!.getSelectedRows(); - } - render() { return ( } + ref={this.gridRef} /> ); } diff --git a/packages/react-data-grid/src/Row.tsx b/packages/react-data-grid/src/Row.tsx index 6893a663b3..074600928d 100644 --- a/packages/react-data-grid/src/Row.tsx +++ b/packages/react-data-grid/src/Row.tsx @@ -46,7 +46,7 @@ export default class Row extends React.Component> impleme getCell(column: CalculatedColumn) { const Renderer = this.props.cellRenderer!; - const { idx, cellMetaData, isScrolling, row, lastFrozenColumnIndex, scrollLeft } = this.props; + const { idx, cellMetaData, isScrolling, row, lastFrozenColumnIndex, scrollLeft, isRowSelected, onRowSelectionChange } = this.props; const { key } = column; const cellProps: CellRendererProps & { ref: (cell: CellRenderer | null) => void } = { @@ -61,7 +61,9 @@ export default class Row extends React.Component> impleme expandableOptions: this.getExpandableOptions(key), isScrolling, scrollLeft, - lastFrozenColumnIndex + lastFrozenColumnIndex, + isRowSelected, + onRowSelectionChange }; return ; // FIXME: fix key type @@ -85,9 +87,9 @@ export default class Row extends React.Component> impleme } getCellValue(key: keyof R) { - const { isSelected, row } = this.props; + const { isRowSelected, row } = this.props; if (key === 'select-row') { - return isSelected; + return isRowSelected; } return rowUtils.get(row, key); @@ -121,7 +123,7 @@ export default class Row extends React.Component> impleme const className = classNames( 'react-grid-Row', `react-grid-Row--${this.props.idx % 2 === 0 ? 'even' : 'odd'}`, - { 'row-selected': this.props.isSelected }, + { 'row-selected': this.props.isRowSelected }, this.props.extraClasses, { 'rdg-scrolling': this.props.isScrolling } ); diff --git a/packages/react-data-grid/src/RowGroup.tsx b/packages/react-data-grid/src/RowGroup.tsx index e45594ad2a..0569cb44d0 100644 --- a/packages/react-data-grid/src/RowGroup.tsx +++ b/packages/react-data-grid/src/RowGroup.tsx @@ -29,7 +29,7 @@ interface Props { renderBaseRow(p: RowRendererProps): React.ReactElement; } -const RowGroup = forwardRef>(function RowGroup(props, ref) { +export default forwardRef>(function RowGroup(props, ref) { function onRowExpandToggle(expand?: boolean) { const { onRowExpandToggle } = props.cellMetaData; if (onRowExpandToggle) { @@ -57,8 +57,6 @@ const RowGroup = forwardRef>(function RowGroup(props, ); }); -export default RowGroup; - interface DefaultBaseProps extends Props { onRowExpandClick(): void; onRowExpandToggle(expand?: boolean): void; diff --git a/packages/react-data-grid/src/RowUtils.ts b/packages/react-data-grid/src/RowUtils.ts index 56cdcefeb7..ccccb8269e 100644 --- a/packages/react-data-grid/src/RowUtils.ts +++ b/packages/react-data-grid/src/RowUtils.ts @@ -7,30 +7,3 @@ export function get(row: R, property: keyof R) { return row[property]; } - -interface Keys { - rowKey?: string; - values?: string[]; -} - -export function isRowSelected( - keys?: Keys | null, - indexes?: number[] | null, - isSelectedKey?: string | null, - rowData?: R, - rowIdx?: number -): boolean { - if (Array.isArray(indexes) && typeof rowIdx === 'number') { - return indexes.includes(rowIdx); - } - - if (rowData && keys && keys.rowKey && Array.isArray(keys.values)) { - return keys.values.includes(rowData[keys.rowKey as K] as unknown as string); - } - - if (rowData && typeof isSelectedKey === 'string') { - return !!rowData[isSelectedKey as K]; - } - - return false; -} diff --git a/packages/react-data-grid/src/Viewport.tsx b/packages/react-data-grid/src/Viewport.tsx index 38730d9acb..57cb6e2a56 100644 --- a/packages/react-data-grid/src/Viewport.tsx +++ b/packages/react-data-grid/src/Viewport.tsx @@ -13,19 +13,19 @@ export interface ScrollState { } type SharedGridProps = Pick, -'rowKey' +| 'rowKey' | 'rowHeight' | 'rowRenderer' | 'rowGetter' | 'rowsCount' | 'selectedRows' +| 'onSelectedRowsChange' | 'columnMetrics' | 'cellMetaData' | 'rowOffsetHeight' | 'minHeight' | 'scrollToRowIndex' | 'contextMenu' -| 'rowSelection' | 'getSubRowDetails' | 'rowGroupRenderer' | 'enableCellSelect' @@ -141,6 +141,7 @@ export default function Viewport({ rowGetter={props.rowGetter} rowsCount={rowsCount} selectedRows={props.selectedRows} + onSelectedRowsChange={props.onSelectedRowsChange} rowRenderer={props.rowRenderer} scrollTop={scrollTop} scrollLeft={scrollLeft} @@ -150,7 +151,6 @@ export default function Viewport({ onScroll={onScroll} scrollToRowIndex={props.scrollToRowIndex} contextMenu={props.contextMenu} - rowSelection={props.rowSelection} getSubRowDetails={props.getSubRowDetails} rowGroupRenderer={props.rowGroupRenderer} isScrolling={isScrolling} diff --git a/packages/react-data-grid/src/__tests__/Canvas.spec.tsx b/packages/react-data-grid/src/__tests__/Canvas.spec.tsx index 74d2f35abe..532f9f1ba3 100644 --- a/packages/react-data-grid/src/__tests__/Canvas.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Canvas.spec.tsx @@ -96,43 +96,21 @@ describe('Canvas Tests', () => { }); describe('Row Selection', () => { - const COLUMNS = [{ key: 'id', name: 'ID', idx: 0, width: 100, left: 100 }]; + it('renders row selected', () => { + const rowGetter = () => ({ id: 1 }); - describe('selectBy index', () => { - it('renders row selected', () => { - const rowGetter = () => ({ id: 1 }); - - const props = { rowOverscanStartIdx: 0, rowOverscanEndIdx: 1, COLUMNS, rowGetter, rowsCount: 1, rowSelection: { indexes: [0] } }; - const wrapper = renderComponent(props); - - const rows = getRows(wrapper); - expect(rows[0].props.isSelected).toBe(true); - }); - }); - - describe('selectBy keys', () => { - it('renders row selected', () => { - const rowGetter = () => { return { id: 1 }; }; - - const props = { rowOverscanStartIdx: 0, rowOverscanEndIdx: 1, COLUMNS, rowGetter, rowsCount: 1, rowSelection: { keys: { rowKey: 'id', values: [1] } } }; - const wrapper = renderComponent(props); - - const rows = getRows(wrapper); - expect(rows[0].props.isSelected).toBe(true); + const wrapper = renderComponent({ + rowOverscanStartIdx: 0, + rowOverscanEndIdx: 1, + columns: [{ key: 'id', name: 'ID', idx: 0, width: 100, left: 100 }], + rowGetter, + rowsCount: 1, + rowKey: 'id', + selectedRows: new Set([1]) }); - }); - - - describe('selectBy `isSelectedKey`', () => { - it('renders row selected', () => { - const rowGetter = (i: number) => i === 0 ? { id: 1, isSelected: true } : {}; - - const props = { rowOverscanStartIdx: 0, rowOverscanEndIdx: 1, COLUMNS, rowGetter, rowsCount: 1, rowSelection: { isSelectedKey: 'isSelected' } }; - const wrapper = renderComponent(props); + const rows = getRows(wrapper); - const rows = getRows(wrapper); - expect(rows[0].props.isSelected).toBe(true); - }); + expect(rows[0].props.isRowSelected).toBe(true); }); }); diff --git a/packages/react-data-grid/src/__tests__/Cell.spec.tsx b/packages/react-data-grid/src/__tests__/Cell.spec.tsx index 32f096838e..215293816c 100644 --- a/packages/react-data-grid/src/__tests__/Cell.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Cell.spec.tsx @@ -45,6 +45,8 @@ const testProps: CellProps = { rowData: { id: 1, description: 'Wicklow' }, height: 40, isScrolling: false, + isRowSelected: false, + onRowSelectionChange() {}, scrollLeft: 0, lastFrozenColumnIndex: -1 }; @@ -93,6 +95,8 @@ describe('Cell Tests', () => { rowData: helpers.rowGetter(11), expandableOptions, isScrolling: false, + isRowSelected: false, + onRowSelectionChange() {}, scrollLeft: 0, lastFrozenColumnIndex: -1 }; @@ -126,6 +130,8 @@ describe('Cell Tests', () => { rowData: helpers.rowGetter(11), expandableOptions, isScrolling: false, + isRowSelected: false, + onRowSelectionChange() {}, scrollLeft: 0, lastFrozenColumnIndex: -1, ...propsOverride diff --git a/packages/react-data-grid/src/__tests__/Grid.spec.js b/packages/react-data-grid/src/__tests__/Grid.spec.js index cf189452a2..05a58e3c8c 100644 --- a/packages/react-data-grid/src/__tests__/Grid.spec.js +++ b/packages/react-data-grid/src/__tests__/Grid.spec.js @@ -64,7 +64,6 @@ describe('Rendering Grid component', () => { rowRenderer: jest.fn(), emptyRowsView: jest.fn(), selectedRows: jest.fn(), - rowSelection: { isSelectedKey: 'selectedKey' }, rowsCount: 14, sortColumn: 'sortColumn', sortDirection: 'ASC', diff --git a/packages/react-data-grid/src/__tests__/Header.spec.tsx b/packages/react-data-grid/src/__tests__/Header.spec.tsx index 69585b5b4f..fcb3d6cd58 100644 --- a/packages/react-data-grid/src/__tests__/Header.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Header.spec.tsx @@ -12,6 +12,10 @@ const SCROLL_BAR_SIZE = 17; describe('Header Unit Tests', () => { function getProps(): HeaderProps { return { + rowKey: 'id', + rowsCount: 1, + rowGetter() { return {}; }, + allRowsSelected: false, columnMetrics: { columns: helpers.columns, minColumnWidth: 80, @@ -63,6 +67,10 @@ describe('Header Unit Tests', () => { return shallow(React.createElement(Header as React.FunctionComponent>, props)); } const testRequiredProps: HeaderProps = { + rowKey: 'id', + rowsCount: 1, + rowGetter() { return {}; }, + allRowsSelected: false, columnMetrics: { columns: helpers.columns, minColumnWidth: 81, @@ -84,6 +92,10 @@ describe('Header Unit Tests', () => { rowOffsetHeight: 30 }; const testAllProps: HeaderProps = { + rowKey: 'id', + rowsCount: 1, + rowGetter() { return {}; }, + allRowsSelected: false, columnMetrics: { columns: helpers.columns, minColumnWidth: 80, diff --git a/packages/react-data-grid/src/__tests__/HeaderCell.spec.tsx b/packages/react-data-grid/src/__tests__/HeaderCell.spec.tsx index 4ec03b9ac3..ace155e7dd 100644 --- a/packages/react-data-grid/src/__tests__/HeaderCell.spec.tsx +++ b/packages/react-data-grid/src/__tests__/HeaderCell.spec.tsx @@ -30,6 +30,8 @@ describe('Header Cell Tests', () => { name: 'bla', onHeaderDrop() { }, draggableHeaderCell: DraggableHeaderCell, + allRowsSelected: false, + onAllRowsSelectionChange() {}, ...overrideProps }; const wrapper = mount>(); @@ -64,7 +66,12 @@ describe('Header Cell Tests', () => { it('pass the column as property to cell renderer if it is a function', () => { const rendererFunction = jest.fn(() =>
    Custom
    ); const { props } = setup({ renderer: rendererFunction }); - expect(rendererFunction).toHaveBeenCalledWith({ column: props.column, rowType: HeaderRowType.HEADER }, {}); + expect(rendererFunction).toHaveBeenCalledWith({ + column: props.column, + rowType: HeaderRowType.HEADER, + allRowsSelected: false, + onAllRowsSelectionChange: expect.any(Function) + }, {}); }); it('should not pass the column as property to cell renderer if it is a jsx object', () => { diff --git a/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx b/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx index 0a48bd62ef..6a3911faf6 100644 --- a/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx +++ b/packages/react-data-grid/src/__tests__/HeaderRow.spec.tsx @@ -17,6 +17,8 @@ describe('Header Row Unit Tests', () => { onSort: jest.fn(), sortDirection: DEFINE_SORT.NONE, height: 35, + allRowsSelected: false, + onAllRowsSelectionChange() {}, onFilterChange() { }, onHeaderDrop() { }, draggableHeaderCell: () =>
    @@ -132,6 +134,8 @@ describe('Header Row Unit Tests', () => { columns: helpers.columns, onSort: jest.fn(), rowType: HeaderRowType.HEADER, + allRowsSelected: false, + onAllRowsSelectionChange() {}, onColumnResize: jest.fn(), onColumnResizeEnd: jest.fn(), onFilterChange() { }, diff --git a/packages/react-data-grid/src/__tests__/Row.spec.tsx b/packages/react-data-grid/src/__tests__/Row.spec.tsx index 22279d6d8e..6ef1c25142 100644 --- a/packages/react-data-grid/src/__tests__/Row.spec.tsx +++ b/packages/react-data-grid/src/__tests__/Row.spec.tsx @@ -48,7 +48,9 @@ describe('Row', () => { colOverscanEndIdx: 20, isScrolling: true, scrollLeft: 0, - lastFrozenColumnIndex: -1 + lastFrozenColumnIndex: -1, + isRowSelected: false, + onRowSelectionChange() {} }; it('passes classname property', () => { diff --git a/packages/react-data-grid/src/__tests__/RowUtils.spec.ts b/packages/react-data-grid/src/__tests__/RowUtils.spec.ts deleted file mode 100644 index 6e9b202c1f..0000000000 --- a/packages/react-data-grid/src/__tests__/RowUtils.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as RowUtils from '../RowUtils'; - -describe('RowUtils Tests', () => { - describe('isRowSelected', () => { - describe('using index', () => { - it('should return true', () => { - const result = RowUtils.isRowSelected(null, [0, 2, 4], null, {}, 2); - expect(result).toBe(true); - }); - - it('should return false', () => { - const result = RowUtils.isRowSelected(null, [0, 2, 4], null, {}, 1); - expect(result).toBe(false); - }); - }); - - describe('using keys', () => { - it('should return true', () => { - const keyProps = { rowKey: 'name', values: ['tim', 'willim', 'deigo'] }; - const rowData = { id: 1, name: 'tim' }; - const result = RowUtils.isRowSelected(keyProps, null, null, rowData, 0); - expect(result).toBe(true); - }); - - it('should return false', () => { - const keyProps = { rowKey: 'name', values: ['tim', 'willim', 'deigo'] }; - const rowData = { id: 1, name: 'john' }; - const result = RowUtils.isRowSelected(keyProps, null, null, rowData, 0); - expect(result).toBe(false); - }); - }); - - describe('using `isSelectedKey`', () => { - it('should return true', () => { - const rowData = { id: 1, name: 'tim', isSelected: true }; - const result = RowUtils.isRowSelected(null, null, 'isSelected', rowData, 0); - expect(result).toBe(true); - }); - - it('should return false', () => { - const rowData = { id: 1, name: 'tim', isSelected: false }; - const result = RowUtils.isRowSelected(null, null, 'isSelected', rowData, 0); - expect(result).toBe(false); - }); - }); - }); -}); diff --git a/packages/react-data-grid/src/common/cells/headerCells/SortableHeaderCell.tsx b/packages/react-data-grid/src/common/cells/headerCells/SortableHeaderCell.tsx index 17b9977780..269c30330e 100644 --- a/packages/react-data-grid/src/common/cells/headerCells/SortableHeaderCell.tsx +++ b/packages/react-data-grid/src/common/cells/headerCells/SortableHeaderCell.tsx @@ -15,10 +15,12 @@ export interface Props { onSort(columnKey: keyof R, direction: DEFINE_SORT): void; sortDirection: DEFINE_SORT; sortDescendingFirst: boolean; + allRowsSelected: boolean; + onAllRowsSelectionChange(checked: boolean): void; } export default function SortableHeaderCell(props: Props) { - const { column, rowType, onSort, sortDirection, sortDescendingFirst } = props; + const { column, rowType, onSort, sortDirection, sortDescendingFirst, allRowsSelected, onAllRowsSelectionChange } = props; function onClick() { let direction; switch (sortDirection) { @@ -40,7 +42,12 @@ export default function SortableHeaderCell(props: Props) { ? column.name : isElement(headerRenderer) ? React.cloneElement(headerRenderer, { column }) - : React.createElement(headerRenderer, { column, rowType }); + : React.createElement(headerRenderer, { + column, + rowType, + allRowsSelected, + onAllRowsSelectionChange + }); return (
    diff --git a/packages/react-data-grid/src/common/cells/headerCells/__tests__/SortableHeaderCell.spec.tsx b/packages/react-data-grid/src/common/cells/headerCells/__tests__/SortableHeaderCell.spec.tsx index b1cc1b6f58..c1379fdddb 100644 --- a/packages/react-data-grid/src/common/cells/headerCells/__tests__/SortableHeaderCell.spec.tsx +++ b/packages/react-data-grid/src/common/cells/headerCells/__tests__/SortableHeaderCell.spec.tsx @@ -21,6 +21,8 @@ describe('', () => { onSort: jest.fn(), sortDirection: DEFINE_SORT.NONE, sortDescendingFirst: false, + allRowsSelected: false, + onAllRowsSelectionChange() {}, ...overrideProps }; const wrapper = shallow(); diff --git a/packages/react-data-grid/src/common/editors/CheckboxEditor.tsx b/packages/react-data-grid/src/common/editors/CheckboxEditor.tsx deleted file mode 100644 index ac9201874e..0000000000 --- a/packages/react-data-grid/src/common/editors/CheckboxEditor.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { CalculatedColumn } from '../types'; - -export interface CheckboxEditorProps { - value?: boolean; - rowIdx: number; - column: CalculatedColumn; - dependentValues: unknown; -} - -export default function CheckboxEditor({ value, rowIdx, column, dependentValues }: CheckboxEditorProps) { - function handleChange(event: React.ChangeEvent) { - if (column.onCellChange) { - column.onCellChange(rowIdx, column.key, dependentValues, event); - } - } - - return ( - - ); -} diff --git a/packages/react-data-grid/src/common/editors/__tests__/CheckboxEditor.spec.tsx b/packages/react-data-grid/src/common/editors/__tests__/CheckboxEditor.spec.tsx deleted file mode 100644 index db6287e682..0000000000 --- a/packages/react-data-grid/src/common/editors/__tests__/CheckboxEditor.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import CheckboxEditor from '../CheckboxEditor'; -import { CalculatedColumn } from '../../types'; - -describe('CheckboxEditor', () => { - function setup(value = true) { - const testColumn: CalculatedColumn<{ columnKey?: string }> = { - idx: 0, - name: 'columnKey', - key: 'columnKey', - width: 100, - left: 0 - }; - - return mount( - - ); - } - - it('should be selected if value prop is true', () => { - const wrapper = setup(); - expect(wrapper.find('input').prop('checked')).toBe(true); - }); - - it('should not be selected if value prop is false', () => { - const wrapper = setup(false); - expect(wrapper.find('input').prop('checked')).toBe(false); - }); -}); diff --git a/packages/react-data-grid/src/common/editors/index.ts b/packages/react-data-grid/src/common/editors/index.ts index e82f1eeb5d..d58276ced9 100644 --- a/packages/react-data-grid/src/common/editors/index.ts +++ b/packages/react-data-grid/src/common/editors/index.ts @@ -1,2 +1 @@ -export { default as CheckboxEditor } from './CheckboxEditor'; export { default as SimpleTextEditor } from './SimpleTextEditor'; diff --git a/packages/react-data-grid/src/common/types.ts b/packages/react-data-grid/src/common/types.ts index 3d089eb426..69242bca32 100644 --- a/packages/react-data-grid/src/common/types.ts +++ b/packages/react-data-grid/src/common/types.ts @@ -5,8 +5,6 @@ import { HeaderRowType, UpdateActions } from './enums'; export type Omit = Pick>; -export type SelectedRow = TRow & { isSelected: boolean }; - interface ColumnValue { /** The name of the column. By default it will be displayed in the header cell */ name: string; @@ -39,12 +37,11 @@ interface ColumnValue>; /** Header renderer for each header cell */ + // TODO: finalize API headerRenderer?: React.ReactElement | React.ComponentType>; /** Component to be used to filter the data of the column */ filterRenderer?: React.ComponentType>; - // TODO: these props are only used by checkbox editor and we should remove them - onCellChange?(rowIdx: number, key: keyof TRow, dependentValues: TDependentValue, event: React.SyntheticEvent): void; getRowMetaData?(rowData: TRow, column: CalculatedColumn): TDependentValue; } @@ -130,6 +127,8 @@ export interface FormatterProps { value: TValue; column: CalculatedColumn; row: TRow; + isRowSelected: boolean; + onRowSelectionChange(rowIdx: number, row: TRow, checked: boolean, isShiftClick: boolean): void; isScrolling?: boolean; dependentValues?: TDependentValue; } @@ -149,6 +148,8 @@ export interface EditorProps { export interface HeaderRowProps { column: CalculatedColumn; rowType: HeaderRowType; + allRowsSelected: boolean; + onAllRowsSelectionChange(checked: boolean): void; } export interface CellRendererProps { @@ -163,6 +164,8 @@ export interface CellRendererProps { scrollLeft: number; expandableOptions?: ExpandableOptions; lastFrozenColumnIndex: number; + isRowSelected: boolean; + onRowSelectionChange(rowIdx: number, row: TRow, checked: boolean, isShiftClick: boolean): void; } export interface RowRendererProps { @@ -171,7 +174,6 @@ export interface RowRendererProps { row: TRow; cellRenderer?: React.ComponentType>; cellMetaData: CellMetaData; - isSelected?: boolean; idx: number; extraClasses?: string; subRowDetails?: SubRowDetails; @@ -180,6 +182,8 @@ export interface RowRendererProps { isScrolling?: boolean; scrollLeft: number; lastFrozenColumnIndex: number; + isRowSelected: boolean; + onRowSelectionChange(rowIdx: number, row: TRow, checked: boolean, isShiftClick: boolean): void; } export interface FilterRendererProps { @@ -277,8 +281,6 @@ export interface RowGroupMetaData { getRowRenderer?(props: unknown, rowIdx: number): React.ReactElement; } -export type RowSelection = { indexes?: number[] } | { isSelectedKey?: string } | { keys?: { values: unknown[]; rowKey: string } }; - export interface HeaderRowData { rowType: HeaderRowType; height: number; @@ -329,8 +331,3 @@ export interface CheckCellIsEditableEvent extends Position { row: TRow; column: CalculatedColumn; } - -export interface RowSelectionParams { - rowIdx: number; - row: TRow; -} diff --git a/packages/react-data-grid/src/common/utils/RowComparer.ts b/packages/react-data-grid/src/common/utils/RowComparer.ts index e1e6a20423..567aa1ee29 100644 --- a/packages/react-data-grid/src/common/utils/RowComparer.ts +++ b/packages/react-data-grid/src/common/utils/RowComparer.ts @@ -5,7 +5,7 @@ export default function shouldRowUpdate(nextProps: RowRendererProps, curre || nextProps.row !== currentProps.row || currentProps.colOverscanStartIdx !== nextProps.colOverscanStartIdx || currentProps.colOverscanEndIdx !== nextProps.colOverscanEndIdx - || currentProps.isSelected !== nextProps.isSelected + || currentProps.isRowSelected !== nextProps.isRowSelected || currentProps.isScrolling !== nextProps.isScrolling || nextProps.height !== currentProps.height || currentProps.extraClasses !== nextProps.extraClasses; diff --git a/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts b/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts index db4156e0cd..dcd1914980 100644 --- a/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts +++ b/packages/react-data-grid/src/common/utils/__tests__/RowComparer.spec.ts @@ -18,6 +18,8 @@ const defaultProps: RowRendererProps = { height: 60, columns, row: {}, + isRowSelected: false, + onRowSelectionChange() {}, colOverscanStartIdx: 0, colOverscanEndIdx: 1, isScrolling: false, diff --git a/packages/react-data-grid/src/formatters/SelectAll.tsx b/packages/react-data-grid/src/formatters/SelectAll.tsx deleted file mode 100644 index d593fd11e1..0000000000 --- a/packages/react-data-grid/src/formatters/SelectAll.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { forwardRef } from 'react'; - -export interface SelectAllProps { - onChange(event: React.ChangeEvent): void; -} - -export const SelectAll = forwardRef(function SelectAll({ onChange }, ref) { - return ( - - ); -}); diff --git a/packages/react-data-grid/src/formatters/SelectCellFormatter.tsx b/packages/react-data-grid/src/formatters/SelectCellFormatter.tsx new file mode 100644 index 0000000000..01c96f37a4 --- /dev/null +++ b/packages/react-data-grid/src/formatters/SelectCellFormatter.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface SelectCellFormatterProps { + value: boolean; + onChange(value: boolean, isShiftClick: boolean): void; +} + +export default function SelectCellFormatter({ value, onChange }: SelectCellFormatterProps) { + function handleChange(e: React.ChangeEvent) { + onChange(e.target.checked, (e.nativeEvent as MouseEvent).shiftKey); + } + + return ( + + ); +} diff --git a/packages/react-data-grid/src/formatters/index.ts b/packages/react-data-grid/src/formatters/index.ts index 722e796eef..43bce134d3 100644 --- a/packages/react-data-grid/src/formatters/index.ts +++ b/packages/react-data-grid/src/formatters/index.ts @@ -1,2 +1 @@ export * from './SimpleCellFormatter'; -export * from './SelectAll'; diff --git a/packages/react-data-grid/src/index.ts b/packages/react-data-grid/src/index.ts index b7a122e925..9631dc24c1 100644 --- a/packages/react-data-grid/src/index.ts +++ b/packages/react-data-grid/src/index.ts @@ -5,6 +5,7 @@ export { default as Row } from './Row'; export { default as HeaderCell } from './HeaderCell'; import * as _utils from './common/utils'; import * as _helpers from './helpers'; +export * from './Columns'; export * from './formatters'; export * from './common/editors'; export * from './common/enums'; diff --git a/packages/react-data-grid/src/masks/CellMask.tsx b/packages/react-data-grid/src/masks/CellMask.tsx index 0afc9faa31..a97e423d45 100644 --- a/packages/react-data-grid/src/masks/CellMask.tsx +++ b/packages/react-data-grid/src/masks/CellMask.tsx @@ -4,7 +4,7 @@ import { Dimension } from '../common/types'; export type CellMaskProps = React.HTMLAttributes & Dimension; -const CellMask = forwardRef(function CellMask({ width, height, top, left, zIndex, className, ...props }, ref) { +export default forwardRef(function CellMask({ width, height, top, left, zIndex, className, ...props }, ref) { return (
    (function CellMask({ w /> ); }); - -export default CellMask; diff --git a/packages/react-data-grid/src/masks/CopyMask.tsx b/packages/react-data-grid/src/masks/CopyMask.tsx index baaf5308c4..1d0f5ff997 100644 --- a/packages/react-data-grid/src/masks/CopyMask.tsx +++ b/packages/react-data-grid/src/masks/CopyMask.tsx @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import CellMask, { CellMaskProps } from './CellMask'; -const CopyMask = forwardRef(function CopyMask(props, ref) { +export default forwardRef(function CopyMask(props, ref) { return ( (function CopyMask(pro /> ); }); - -export default CopyMask; diff --git a/packages/react-data-grid/src/masks/InteractionMasks.tsx b/packages/react-data-grid/src/masks/InteractionMasks.tsx index dc6e3ff150..f5114e93e4 100644 --- a/packages/react-data-grid/src/masks/InteractionMasks.tsx +++ b/packages/react-data-grid/src/masks/InteractionMasks.tsx @@ -458,18 +458,18 @@ export default class InteractionMasks extends React.Component { if (this.selectionMask.current && !this.isFocused()) { this.selectionMask.current.focus(); } - } + }; selectFirstCell(): void { this.selectCell({ rowIdx: 0, idx: 0 }); } selectCell = (cell: Position, openEditor?: boolean): void => { - const callback = openEditor ? this.openEditor : undefined; + const callback = openEditor ? this.openEditor : this.focus; // Close the editor to commit any pending changes if (this.state.isEditorEnabled) { this.closeEditor(); diff --git a/packages/react-data-grid/src/masks/SelectionMask.tsx b/packages/react-data-grid/src/masks/SelectionMask.tsx index 138f112adb..fceb43049a 100644 --- a/packages/react-data-grid/src/masks/SelectionMask.tsx +++ b/packages/react-data-grid/src/masks/SelectionMask.tsx @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import CellMask, { CellMaskProps } from './CellMask'; -const SelectionMask = forwardRef(function SelectionMask(props, ref) { +export default forwardRef(function SelectionMask(props, ref) { return ( (function Selecti /> ); }); - -export default SelectionMask; diff --git a/packages/react-data-grid/src/masks/__tests__/InteractionMasks.spec.tsx b/packages/react-data-grid/src/masks/__tests__/InteractionMasks.spec.tsx index ddf6a02f08..b10f495992 100644 --- a/packages/react-data-grid/src/masks/__tests__/InteractionMasks.spec.tsx +++ b/packages/react-data-grid/src/masks/__tests__/InteractionMasks.spec.tsx @@ -213,11 +213,11 @@ describe('', () => { it('should give focus to InteractionMasks once a selection has ended', () => { // We have to use mount, rather than shallow, so that InteractionMasks has a ref to it's node, used for focusing - const { props } = setup(undefined, undefined, true); + const { wrapper, props } = setup(undefined, undefined, true); props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - jest.spyOn(InteractionMasks.prototype, 'focus').mockImplementation(() => { }); + jest.spyOn(wrapper.instance(), 'focus').mockImplementation(() => { }); props.eventBus.dispatch(EventTypes.SELECT_END); - expect(InteractionMasks.prototype.focus).toHaveBeenCalled(); + expect(wrapper.instance().focus).toHaveBeenCalled(); }); }); });