Repository: gregnb/mui-datatables
Pull request #859 (2019-08-23 22:00:15)

Fix for #858
#858 - Trigger sorting via keyboard doesnt't seem to work 

coveralls: 
[![Coverage Status](https://coveralls.io/builds/25319625/badge)](https://coveralls.io/builds/25319625)

Coverage decreased (-0.3%) to 74.944% when pulling **2876f0ab901f8a44813db9b6282d37a277e7cbe2 on segore:master** into **d1914e4156aea4ff7d6d8ad9794da3ca4b156758 on gregnb:master**.


Patch:
diff --git a/README.md b/README.md
index f3a7aa9..9a1c656 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,7 @@ The component accepts the following props:
 |**`filter`**|boolean|true|Show/hide filter icon from toolbar
 |**`search`**|boolean|true|Show/hide search icon from toolbar
 |**`searchText`**|string||Initial search text
+|**`searchPlaceholder`**|string||Search text placeholder. [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-search/index.js)
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
 |**`download`**|boolean|true|Show/hide download icon from toolbar
 |**`downloadOptions`**|object|`{filename: 'tableDownload.csv', separator: ','}`|Options to change the output of the CSV file: `filename`: string, `separator`: string, `filterOptions`: object(`useDisplayedColumnsOnly`: boolean, `useDisplayedRowsOnly`: boolean)
diff --git a/examples/customize-search/index.js b/examples/customize-search/index.js
index 8ff1701..a958358 100644
--- a/examples/customize-search/index.js
+++ b/examples/customize-search/index.js
@@ -83,6 +83,7 @@ class Example extends React.Component {
       responsive: 'stacked',
       page: 1,
       searchText: this.state.searchText,
+      searchPlaceholder: 'Your Custom Search Placeholder',
       customSearch: (searchQuery, currentRow, columns) => {
         let isFound = false;
         currentRow.forEach(col => {
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 1bce47c..f0bb7c5 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -152,6 +152,7 @@ class MUIDataTable extends React.Component {
       customSearch: PropTypes.func,
       search: PropTypes.bool,
       searchText: PropTypes.string,
+      searchPlaceholder: PropTypes.string,
       print: PropTypes.bool,
       viewColumns: PropTypes.bool,
       download: PropTypes.bool,
@@ -191,6 +192,7 @@ class MUIDataTable extends React.Component {
       data: [],
       lookup: {},
     },
+    previousSelectedRow: null,
     expandedRows: {
       data: [],
       lookup: {},
@@ -580,6 +582,7 @@ class MUIDataTable extends React.Component {
       const sortedData = this.sortTable(tableData, sortIndex, sortDirection);
       tableData = sortedData.data;
     }
+
     /* set source data and display Data set source set */
     this.setState(
       prevState => ({
@@ -592,6 +595,7 @@ class MUIDataTable extends React.Component {
         count: this.options.count,
         data: tableData,
         displayData: this.getDisplayData(columns, tableData, filterList, searchText),
+        previousSelectedRow: null,
       }),
       callback,
     );
@@ -833,6 +837,7 @@ class MUIDataTable extends React.Component {
             data: sortedData.data,
             displayData: this.getDisplayData(columns, sortedData.data, prevState.filterList, prevState.searchText),
             selectedRows: sortedData.selectedRows,
+            previousSelectedRow: null,
           };
         }
 
@@ -948,6 +953,7 @@ class MUIDataTable extends React.Component {
           displayData: this.options.serverSide
             ? prevState.displayData
             : this.getDisplayData(prevState.columns, prevState.data, filterList, prevState.searchText),
+          previousSelectedRow: null,
         };
       },
       () => {
@@ -1015,7 +1021,7 @@ class MUIDataTable extends React.Component {
     );
   };
 
-  selectRowUpdate = (type, value) => {
+  selectRowUpdate = (type, value, shiftAdjacentRows = []) => {
     // safety check
     const { selectableRows } = this.options;
     if (selectableRows === 'none') {
@@ -1053,6 +1059,7 @@ class MUIDataTable extends React.Component {
               data: newRows,
               lookup: selectedMap,
             },
+            previousSelectedRow: null,
           };
         },
         () => {
@@ -1078,11 +1085,31 @@ class MUIDataTable extends React.Component {
 
           if (rowPos >= 0) {
             selectedRows.splice(rowPos, 1);
+
+            // handle rows affected by shift+click
+            if (shiftAdjacentRows.length > 0) {
+              let shiftAdjacentMap = buildMap(shiftAdjacentRows);
+              for (let cIndex = selectedRows.length - 1; cIndex >= 0; cIndex--) {
+                if (shiftAdjacentMap[selectedRows[cIndex].dataIndex]) {
+                  selectedRows.splice(cIndex, 1);
+                }
+              }
+            }
           } else if (selectableRows === 'single') {
             selectedRows = [value];
           } else {
             // multiple
             selectedRows.push(value);
+
+            // handle rows affected by shift+click
+            if (shiftAdjacentRows.length > 0) {
+              let selectedMap = buildMap(selectedRows);
+              shiftAdjacentRows.forEach(aRow => {
+                if (!selectedMap[aRow.dataIndex]) {
+                  selectedRows.push(aRow);
+                }
+              });
+            }
           }
 
           return {
@@ -1090,6 +1117,7 @@ class MUIDataTable extends React.Component {
               lookup: buildMap(selectedRows),
               data: selectedRows,
             },
+            previousSelectedRow: value,
           };
         },
         () => {
@@ -1108,6 +1136,7 @@ class MUIDataTable extends React.Component {
       this.setState(
         {
           selectedRows: { data, lookup },
+          previousSelectedRow: null,
         },
         () => {
           this.setTableAction('rowsSelect');
@@ -1171,6 +1200,7 @@ class MUIDataTable extends React.Component {
       filterData,
       filterList,
       selectedRows,
+      previousSelectedRow,
       expandedRows,
       searchText,
     } = this.state;
@@ -1257,6 +1287,7 @@ class MUIDataTable extends React.Component {
               rowsPerPage={rowsPerPage}
               selectedRows={selectedRows}
               selectRowUpdate={this.selectRowUpdate}
+              previousSelectedRow={previousSelectedRow}
               expandedRows={expandedRows}
               toggleExpandRow={this.toggleExpandRow}
               options={this.options}
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index dedb641..a366e8b 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -6,6 +6,7 @@ import TableBodyCell from './TableBodyCell';
 import TableBodyRow from './TableBodyRow';
 import TableSelectCell from './TableSelectCell';
 import { withStyles } from '@material-ui/core/styles';
+import cloneDeep from 'lodash.clonedeep';
 
 const defaultBodyStyles = {
   root: {},
@@ -32,6 +33,8 @@ class TableBody extends React.Component {
     selectedRows: PropTypes.object,
     /** Callback to trigger table row select */
     selectRowUpdate: PropTypes.func,
+    /** The most recent row to have been selected/unselected */
+    previousSelectedRow: PropTypes.object,
     /** Data used to search table against */
     searchText: PropTypes.string,
     /** Toggle row expandable */
@@ -86,8 +89,10 @@ class TableBody extends React.Component {
     return expandedRows.lookup && expandedRows.lookup[dataIndex] ? true : false;
   }
 
-  isRowSelectable(dataIndex) {
-    const { options, selectedRows } = this.props;
+  isRowSelectable(dataIndex, selectedRows) {
+    const { options } = this.props;
+    selectedRows = selectedRows || this.props.selectedRows;
+
     if (options.isRowSelectable) {
       return options.isRowSelectable(dataIndex, selectedRows);
     } else {
@@ -95,12 +100,54 @@ class TableBody extends React.Component {
     }
   }
 
-  handleRowSelect = data => {
-    this.props.selectRowUpdate('cell', data);
+  handleRowSelect = (data, event) => {
+    let shiftKey = event && event.nativeEvent ? event.nativeEvent.shiftKey : false;
+    let shiftAdjacentRows = [];
+    let previousSelectedRow = this.props.previousSelectedRow;
+
+    // If the user is pressing shift and has previously clicked another row.
+    if (shiftKey && previousSelectedRow && previousSelectedRow.index < this.props.data.length) {
+      let curIndex = previousSelectedRow.index;
+
+      // Create a copy of the selectedRows object. This will be used and modified
+      // below when we see if we can add adjacent rows.
+      let selectedRows = cloneDeep(this.props.selectedRows);
+
+      // Add the clicked on row to our copy of selectedRows (if it isn't already present).
+      let clickedDataIndex = this.props.data[data.index].dataIndex;
+      if (selectedRows.data.filter(d => d.dataIndex === clickedDataIndex).length === 0) {
+        selectedRows.data.push({
+          index: data.index,
+          dataIndex: clickedDataIndex
+        });
+        selectedRows.lookup[clickedDataIndex] = true;
+      }
+
+      while (curIndex !== data.index) {
+        let dataIndex = this.props.data[curIndex].dataIndex;
+
+        if (this.isRowSelectable(dataIndex, selectedRows)) {
+          let lookup = {
+            index: curIndex,
+            dataIndex: dataIndex,
+          };
+          
+          // Add adjacent row to temp selectedRow object if it isn't present.
+          if (selectedRows.data.filter(d => d.dataIndex === dataIndex).length === 0) {
+            selectedRows.data.push(lookup);
+            selectedRows.lookup[dataIndex] = true;
+          }
+
+          shiftAdjacentRows.push(lookup);
+        }
+        curIndex = data.index > curIndex ? curIndex + 1 : curIndex - 1;
+      }
+    }
+    this.props.selectRowUpdate('cell', data, shiftAdjacentRows);
   };
 
   handleRowClick = (row, data, event) => {
-    // Don't trigger onRowClick if the event was actually the expandable icon
+    // Don't trigger onRowClick if the event was actually the expandable icon.
     if (
       event.target.id === 'expandable-button' ||
       (event.target.nodeName === 'path' && event.target.parentNode.id === 'expandable-button')
@@ -127,7 +174,7 @@ class TableBody extends React.Component {
       this.isRowSelectable(data.dataIndex)
     ) {
       const selectRow = { index: data.rowIndex, dataIndex: data.dataIndex };
-      this.handleRowSelect(selectRow);
+      this.handleRowSelect(selectRow, event);
     }
     // Check if we should trigger row expand when row is clicked anywhere
     if (this.props.options.expandableRowsOnClick && this.props.options.expandableRows) {
diff --git a/src/components/TableHeadCell.js b/src/components/TableHeadCell.js
index a5aafba..2e3a2f5 100644
--- a/src/components/TableHeadCell.js
+++ b/src/components/TableHeadCell.js
@@ -75,6 +75,15 @@ class TableHeadCell extends React.Component {
     isHintTooltipOpen: false,
   };
 
+  handleKeyboardSortinput = (e) => {
+    if (e.key === 'Enter')
+    {
+      this.props.toggleSort(this.props.index);
+    }
+
+    return false;
+  }
+
   handleSortClick = () => {
     this.props.toggleSort(this.props.index);
   };
@@ -117,7 +126,7 @@ class TableHeadCell extends React.Component {
             onClose={() => this.setState({ isSortTooltipOpen: false })}>
             <span
               role="button"
-              onKeyUp={this.handleClickSort}
+              onKeyUp={this.handleKeyboardSortinput}
               onClick={this.handleSortClick}
               className={classes.toolButton}
               tabIndex={0}>
diff --git a/src/components/TableSearch.js b/src/components/TableSearch.js
index 9eec1d1..1d88da3 100644
--- a/src/components/TableSearch.js
+++ b/src/components/TableSearch.js
@@ -69,6 +69,7 @@ class TableSearch extends React.Component {
             onChange={this.handleTextChange}
             fullWidth={true}
             inputRef={el => (this.searchField = el)}
+            placeholder={options.searchPlaceholder}
           />
           <IconButton className={classes.clearIcon} onClick={onHide}>
             <ClearIcon />
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index c25125c..6711eca 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -248,11 +248,16 @@ class TableToolbar extends React.Component {
             <span>
               <ReactToPrint
                 trigger={() => (
-                  <Tooltip title={print}>
-                    <IconButton data-testid={print + '-iconButton'} aria-label={print} classes={{ root: classes.icon }}>
-                      <PrintIcon />
-                    </IconButton>
-                  </Tooltip>
+                  <span>
+                    <Tooltip title={print}>
+                      <IconButton
+                        data-testid={print + '-iconButton'}
+                        aria-label={print}
+                        classes={{ root: classes.icon }}>
+                        <PrintIcon />
+                      </IconButton>
+                    </Tooltip>
+                  </span>
                 )}
                 content={() => this.props.tableRef()}
               />
diff --git a/src/components/TableToolbarSelect.js b/src/components/TableToolbarSelect.js
index 05bd1bb..7f1bbe4 100644
--- a/src/components/TableToolbarSelect.js
+++ b/src/components/TableToolbarSelect.js
@@ -16,8 +16,8 @@ const defaultToolbarSelectStyles = theme => ({
     zIndex: 120,
     justifyContent: 'space-between',
     alignItems: 'center',
-    paddingTop: theme.spacing.unit,
-    paddingBottom: theme.spacing.unit,
+    paddingTop: typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing.unit,
+    paddingBottom: typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing.unit,
   },
   title: {
     paddingLeft: '26px',
diff --git a/webpack.config.js b/webpack.config.js
index b98ca64..e507b2a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,7 @@ const webpack = require('webpack');
 
 module.exports = {
   entry: {
-    app: "./examples/customize-filter/index.js"
+    app: "./examples/customize-search/index.js"
   },
   stats: "verbose",
   context: __dirname,


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index e9e14f5..27bb4fd 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -75,12 +75,12 @@ describe('<MUIDataTable />', function() {
 
   it('should render a table', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
-    assert.strictEqual(
+    assert.include(
+      ['Paper', 'ForwardRef(Paper)'],
       shallowWrapper
         .dive()
         .dive()
         .name(),
-      'Paper',
     );
   });
 
@@ -920,6 +920,39 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.data, expectedResult);
   });
 
+  it('should allow multiple cells to be selected when selectableRows=multiple and selectRowUpdate method with type=cell and there are adjacent rows.', () => {
+    const options = { selectableRows: 'multiple' };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.selectRowUpdate('cell', { index: 0, dataIndex: 0 }, [
+      { index: 0, dataIndex: 0 },
+      { index: 1, dataIndex: 1 },
+    ]);
+    shallowWrapper.update();
+
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 1, dataIndex: 1 }];
+    const state = shallowWrapper.state();
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+  });
+
+  it('should allow multiple cells to be selected and then unselected when selectableRows=multiple and selectRowUpdate method with type=cell and there are adjacent rows.', () => {
+    const options = { selectableRows: 'multiple' };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.selectRowUpdate('cell', { index: 0, dataIndex: 0 }, [
+      { index: 1, dataIndex: 1 },
+      { index: 2, dataIndex: 2 },
+    ]);
+    instance.selectRowUpdate('cell', { index: 1, dataIndex: 1 }, [{ index: 0, dataIndex: 0 }]);
+    shallowWrapper.update();
+
+    const expectedResult = [{ index: 2, dataIndex: 2 }];
+    const state = shallowWrapper.state();
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+  });
+
   it('should not update selectedRows when using rowsSelected option with type=none', () => {
     const options = {
       selectableRows: 'none',
diff --git a/test/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index bb4613f..553cec8 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -202,6 +202,42 @@ describe('<TableBody />', function() {
     assert.strictEqual(selectRowUpdate.callCount, 1);
   });
 
+  it('should select the adjacent rows when a row is shift+clicked and a previous row has been selected.', () => {
+    let adjacentRows = [];
+    const options = { sort: true, selectableRows: true, selectableRowsOnClick: true };
+    const previousSelectedRow = { index: 0, dataIndex: 0 };
+    const selectRowUpdate = (type, data, adjacent) => {
+      adjacentRows = adjacent;
+    };
+    const selectedRows = {data: [], lookup: {}};
+    const toggleExpandRow = () => {};
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={selectedRows}
+        selectRowUpdate={selectRowUpdate}
+        previousSelectedRow={previousSelectedRow}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-3')
+      .first()
+      .simulate('click', { nativeEvent: { shiftKey: true } });
+
+    assert.strictEqual(adjacentRows.length, 3);
+  });
+
   it('should gather selected row data when clicking row with selectableRowsOnClick=true.', () => {
     let selectedRowData;
     const options = { selectableRows: true, selectableRowsOnClick: true };
diff --git a/test/MUIDataTableSearch.test.js b/test/MUIDataTableSearch.test.js
index 0d1f38e..9c9ad6d 100644
--- a/test/MUIDataTableSearch.test.js
+++ b/test/MUIDataTableSearch.test.js
@@ -45,6 +45,19 @@ describe('<TableSearch />', function() {
     assert.strictEqual(actualResult.find(TextField).props().value, 'nextText');
   });
 
+  it('should render a search bar with placeholder when searchPlaceholder is set', () => {
+    const options = { textLabels, searchPlaceholder: 'TestingPlaceholder' };
+    const onSearch = () => {};
+    const onHide = () => {};
+
+    const mountWrapper = mount(
+        <TableSearch onSearch={onSearch} onHide={onHide} options={options} />,
+    );
+    const actualResult = mountWrapper.find(TextField);
+    assert.strictEqual(actualResult.length, 1);
+    assert.strictEqual(actualResult.props().placeholder, 'TestingPlaceholder');
+  });
+
   it('should trigger handleTextChange prop callback when calling method handleTextChange', () => {
     const options = { onSearchChange: () => true, textLabels };
     const onSearch = spy();
diff --git a/test/setup-mocha-env.js b/test/setup-mocha-env.js
index c56683a..4cf7850 100644
--- a/test/setup-mocha-env.js
+++ b/test/setup-mocha-env.js
@@ -54,6 +54,8 @@ function setupDom() {
 
   global.window.cancelAnimationFrame = () => {};
   global.getComputedStyle = global.window.getComputedStyle;
+  global.HTMLInputElement = global.window.HTMLInputElement;
+  global.Element = global.window.Element;
 
   Object.defineProperty(global.window.URL, 'createObjectURL', { value: () => {} });
   global.Blob = () => '';