Repository: gregnb/mui-datatables
Pull request #771 (2019-07-16 13:33:50)

Allow to custom table search render
Our designer like to make crazy things and we need to adjust searchRender a bit.

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

Coverage decreased (-0.04%) to 75.913% when pulling **000ba5753d058915775814577088d96456b36b44 on Vallher:feature/custom-TableSearch** into **47f844d013b7652928d1aaaef63f626c97455c28 on gregnb:master**.


Patch:
diff --git a/README.md b/README.md
index 4a3dea7..fc29dce 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ The component accepts the following props:
 |**`count`**|number||User provided override for total number of rows
 |**`serverSide`**|boolean|false|Enable remote data source
 |**`rowsSelected`**|array||User provided selected rows
+|**`rowsExpanded`**|array||User provided expanded rows
 |**`filterType `**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField')`
 |**`textLabels `**|object||User provided labels to localize text
 |**`pagination`**|boolean|true|Enable/disable pagination
@@ -152,6 +153,7 @@ The component accepts the following props:
 |**`customRowRender `**|function||Override default row rendering with custom function. `customRowRender(data, dataIndex, rowIndex) => React Component`
 |**`customSort`**|function||Override default sorting with custom function. `function(data: array, colIndex: number, order: string) => array`
 |**`customSearch `**|function||Override default search with custom function. `customSearch(searchQuery: string, currentRow: array, columns: array) => boolean`
+|**`customSearchRender `**|function||Render a custom table search. `customSearchRender(searchText: string, handleSearch, hideSearch, options) => React Component`
 |**`elevation`**|number|4|Shadow depth applied to Paper component
 |**`caseSensitive `**|boolean|false|Enable/disable case sensitivity for search
 |**`responsive`**|string|'stacked'|Enable/disable responsive table views. Options: 'stacked', 'scroll'
diff --git a/examples/csv-export/index.js b/examples/csv-export/index.js
index 6970dca..40d9b04 100644
--- a/examples/csv-export/index.js
+++ b/examples/csv-export/index.js
@@ -12,8 +12,18 @@ class Example extends React.Component {
     const columns = [
       'Name',
       'Title',
-      'Location',
-      'Age',
+      {
+        name: 'Location',
+        options: {
+          display: 'false'
+        }
+      },
+      {
+        name: 'Age',
+        options: {
+          customBodyRender: value => <div><span>{value}</span></div>
+        }
+      },
       {
         name: 'Salary',
         options: {
@@ -62,10 +72,20 @@ class Example extends React.Component {
       responsive: 'stacked',
       rowsPerPage: 10,
       downloadOptions: {
-        filename: 'excel-format.csv',
-        separator: ';',
+          filename: 'excel-format.csv',
+          separator: ';',
+          filterOptions: {
+            useDisplayedColumnsOnly: true,
+            useDisplayedRowsOnly: true,
+          }
+      },
+      onDownload: (buildHead, buildBody, columns, data) => {
+        if (this.state.downloadFile) {
+          return `${buildHead(columns)}${buildBody(data)}`.trim();
+        }
+
+        return false;
       },
-      onDownload: () => this.state.downloadFile,
       onRowsSelect: (rowsSelected, allRows) => {
         console.log(rowsSelected, allRows);
       },
diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js
index db58014..a829180 100644
--- a/examples/customize-filter/index.js
+++ b/examples/customize-filter/index.js
@@ -32,25 +32,26 @@ class Example extends React.Component {
         options: {
           filter: true,
           filterType: 'custom',
+          filterList: [25, 50],
           customFilterListRender: v => {
-            if (v['min'] && v['max']) {
-              return `Min Age: ${v['min']}, Max Age: ${v['max']}`;
-            } else if (v['min']) {
-              return `Min Age: ${v['min']}`;
-            } else if (v['max']) {
-              return `Max Age: ${v['max']}`;
+            if (v[0] && v[1]) {
+              return `Min Age: ${v[0]}, Max Age: ${v[1]}`;
+            } else if (v[0]) {
+              return `Min Age: ${v[0]}`;
+            } else if (v[1]) {
+              return `Max Age: ${v[1]}`;
             }
             return false;
           },
           filterOptions: {
             names: [],
             logic(age, filters) {
-              if (filters['min'] && filters['max']) {
-                return age < filters['min'] || age > filters['max'];
-              } else if (filters['min']) {
-                return age < filters['min'];
-              } else if (filters['max']) {
-                return age > filters['max'];
+              if (filters[0] && filters[1]) {
+                return age < filters[0] || age > filters[1];
+              } else if (filters[0]) {
+                return age < filters[0];
+              } else if (filters[1]) {
+                return age > filters[1];
               }
               return false;
             },
@@ -60,18 +61,18 @@ class Example extends React.Component {
                 <FormGroup row>
                   <TextField
                     label="min"
-                    value={filterList[index]['min'] || ''}
+                    value={filterList[index][0] || ''}
                     onChange={event => {
-                      filterList[index]['min'] = event.target.value;
+                      filterList[index][0] = event.target.value;
                       onChange(filterList[index], index, column);
                     }}
                     style={{ width: '45%', marginRight: '5%' }}
                   />
                   <TextField
                     label="max"
-                    value={filterList[index]['max'] || ''}
+                    value={filterList[index][1] || ''}
                     onChange={event => {
-                      filterList[index]['max'] = event.target.value;
+                      filterList[index][1] = event.target.value;
                       onChange(filterList[index], index, column);
                     }}
                     style={{ width: '45%' }}
diff --git a/examples/customize-search-render/CustomSearchRender.js b/examples/customize-search-render/CustomSearchRender.js
new file mode 100644
index 0000000..0fa6589
--- /dev/null
+++ b/examples/customize-search-render/CustomSearchRender.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import Grow from '@material-ui/core/Grow';
+import TextField from '@material-ui/core/TextField';
+import IconButton from '@material-ui/core/IconButton';
+import ClearIcon from '@material-ui/icons/Clear';
+import { withStyles } from '@material-ui/core/styles';
+
+const defaultSearchStyles = theme => ({
+  main: {
+    display: 'flex',
+    flex: '1 0 auto',
+  },
+  searchText: {
+    flex: '0.8 0',
+  },
+  clearIcon: {
+    '&:hover': {
+      color: theme.palette.error.main,
+    },
+  },
+});
+
+class CustomSearchRender extends React.Component {
+  handleTextChange = event => {
+    const { onSearchChange } = this.props.options;
+
+    if (onSearchChange) {
+      onSearchChange(event.target.value);
+    }
+
+    this.props.onSearch(event.target.value);
+  };
+
+  componentDidMount() {
+    document.addEventListener('keydown', this.onKeyDown, false);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown', this.onKeyDown, false);
+  }
+
+  onKeyDown = event => {
+    if (event.keyCode === 27) {
+      this.props.onHide();
+    }
+  };
+
+  render() {
+    const { classes, options, onHide, searchText } = this.props;
+
+    return (
+      <Grow appear in={true} timeout={300}>
+        <div className={classes.main} ref={el => (this.rootRef = el)}>
+          <TextField
+            placeholder={'Custom TableSearch without search icon'}
+            className={classes.searchText}
+            InputProps={{
+              'aria-label': options.textLabels.toolbar.search,
+            }}
+            value={searchText || ''}
+            onChange={this.handleTextChange}
+            fullWidth={true}
+            inputRef={el => (this.searchField = el)}
+          />
+          <IconButton className={classes.clearIcon} onClick={onHide}>
+            <ClearIcon />
+          </IconButton>
+        </div>
+      </Grow>
+    );
+  }
+}
+
+export default withStyles(defaultSearchStyles, { name: 'CustomSearchRender' })(CustomSearchRender);
diff --git a/examples/customize-search-render/index.js b/examples/customize-search-render/index.js
new file mode 100644
index 0000000..db816c1
--- /dev/null
+++ b/examples/customize-search-render/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src/';
+import CustomSearchRender from './CustomSearchRender';
+
+class Example extends React.Component {
+
+  render () {
+
+    const columns = ['Name', 'Title', 'Location', 'Age', 'Salary'];
+
+    let data = [
+      ['Gabby George', 'Business Analyst', 'Minneapolis', 30, 100000],
+      ['Aiden Lloyd', 'Business Consultant', 'Dallas', 55, 200000],
+      ['Jaden Collins', 'Attorney', 'Santa Ana', 27, 500000],
+      ['Franky Rees', 'Business Analyst', 'St. Petersburg', 22, 50000],
+      ['Aaren Rose', 'Business Consultant', 'Toledo', 28, 75000],
+      ['Blake Duncan', 'Business Management Analyst', 'San Diego', 65, 94000],
+      ['Frankie Parry', 'Agency Legal Counsel', 'Jacksonville', 71, 210000],
+      ['Lane Wilson', 'Commercial Specialist', 'Omaha', 19, 65000],
+      ['Robin Duncan', 'Business Analyst', 'Los Angeles', 20, 77000],
+      ['Mel Brooks', 'Business Consultant', 'Oklahoma City', 37, 135000],
+      ['Harper White', 'Attorney', 'Pittsburgh', 52, 420000],
+      ['Kris Humphrey', 'Agency Legal Counsel', 'Laredo', 30, 150000],
+      ['Frankie Long', 'Industrial Analyst', 'Austin', 31, 170000],
+      ['Brynn Robbins', 'Business Analyst', 'Norfolk', 22, 90000],
+      ['Justice Mann', 'Business Consultant', 'Chicago', 24, 133000],
+      ['Addison Navarro', 'Business Management Analyst', 'New York', 50, 295000],
+      ['Jesse Welch', 'Agency Legal Counsel', 'Seattle', 28, 200000],
+      ['Eli Mejia', 'Commercial Specialist', 'Long Beach', 65, 400000],
+      ['Gene Leblanc', 'Industrial Analyst', 'Hartford', 34, 110000],
+      ['Danny Leon', 'Computer Scientist', 'Newark', 60, 220000],
+      ['Lane Lee', 'Corporate Counselor', 'Cincinnati', 52, 180000],
+      ['Jesse Hall', 'Business Analyst', 'Baltimore', 44, 99000],
+      ['Danni Hudson', 'Agency Legal Counsel', 'Tampa', 37, 90000],
+      ['Terry Macdonald', 'Commercial Specialist', 'Miami', 39, 140000],
+      ['Justice Mccarthy', 'Attorney', 'Tucson', 26, 330000],
+      ['Silver Carey', 'Computer Scientist', 'Memphis', 47, 250000],
+      ['Franky Miles', 'Industrial Analyst', 'Buffalo', 49, 190000],
+      ['Glen Nixon', 'Corporate Counselor', 'Arlington', 44, 80000],
+      ['Gabby Strickland', 'Business Process Consultant', 'Scottsdale', 26, 45000],
+      ['Mason Ray', 'Computer Scientist', 'San Francisco', 39, 142000]
+    ];
+
+    const options = {
+      filter: true,
+      selectableRows: true,
+      filterType: 'dropdown',
+      responsive: 'stacked',
+      rowsPerPage: 10,
+      customSearchRender: (searchText, handleSearch, hideSearch, options) => {
+        return (
+          <CustomSearchRender
+            searchText={searchText}
+            onSearch={handleSearch}
+            onHide={hideSearch}
+            options={options}
+          />
+        );
+      }
+    };
+
+    return (
+      <MUIDataTable title={'ACME Employee list'} data={data} columns={columns} options={options} />
+    );
+
+  }
+}
+
+ReactDOM.render(<Example />, document.getElementById('app-root'));
diff --git a/examples/expandable-rows/index.js b/examples/expandable-rows/index.js
index 5d3eba3..f541a54 100644
--- a/examples/expandable-rows/index.js
+++ b/examples/expandable-rows/index.js
@@ -82,6 +82,7 @@ class Example extends React.Component {
       responsive: 'scroll',
       expandableRows: true,
       expandableRowsOnClick: true,
+      rowsExpanded: [0, 2, 3],
       renderExpandableRow: (rowData, rowMeta) => {
         const colSpan = rowData.length + 1;
         return (
diff --git a/package-lock.json b/package-lock.json
index 502ee3d..ad7cc5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "mui-datatables",
-  "version": "2.6.3",
+  "version": "2.7.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 37703e7..c233cf8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mui-datatables",
-  "version": "2.6.3",
+  "version": "2.7.0",
   "description": "Datatables for React using Material-UI",
   "main": "dist/index.js",
   "files": [
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 72e80c6..fbdb5cb 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -4,7 +4,6 @@ import MuiTable from '@material-ui/core/Table';
 import classnames from 'classnames';
 import cloneDeep from 'lodash.clonedeep';
 import find from 'lodash.find';
-import isEqual from 'lodash.isequal';
 import isUndefined from 'lodash.isundefined';
 import merge from 'lodash.merge';
 import PropTypes from 'prop-types';
@@ -19,7 +18,7 @@ import TableToolbarSelect from './components/TableToolbarSelect';
 import textLabels from './textLabels';
 import { buildMap, getCollatorComparator, sortCompare } from './utils';
 
-const defaultTableStyles = {
+const defaultTableStyles = theme => ({
   root: {},
   paper: {},
   tableRoot: {
@@ -31,6 +30,14 @@ const defaultTableStyles = {
     height: '100%',
     maxHeight: '499px',
   },
+  responsiveStacked: {
+    overflowX: 'auto',
+    overflow: 'auto',
+    [theme.breakpoints.down('sm')]: {
+      overflowX: 'hidden',
+      overflow: 'hidden',
+    },
+  },
   caption: {
     position: 'absolute',
     left: '-3000px',
@@ -52,7 +59,7 @@ const defaultTableStyles = {
       },
     },
   },
-};
+});
 
 const TABLE_LOAD = {
   INITIAL: 1,
@@ -119,6 +126,7 @@ class MUIDataTable extends React.Component {
       customToolbar: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customToolbarSelect: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
+      customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customRowRender: PropTypes.func,
       onRowClick: PropTypes.func,
       resizableColumns: PropTypes.bool,
@@ -134,6 +142,7 @@ class MUIDataTable extends React.Component {
       page: PropTypes.number,
       count: PropTypes.number,
       rowsSelected: PropTypes.array,
+      rowsExpanded: PropTypes.array,
       rowsPerPage: PropTypes.number,
       rowsPerPageOptions: PropTypes.array,
       filter: PropTypes.bool,
@@ -148,6 +157,10 @@ class MUIDataTable extends React.Component {
       downloadOptions: PropTypes.shape({
         filename: PropTypes.string,
         separator: PropTypes.string,
+        filterOptions: PropTypes.shape({
+          useDisplayedColumnsOnly: PropTypes.bool,
+          useDisplayedRowsOnly: PropTypes.bool,
+        }),
       }),
       onDownload: PropTypes.func,
     }),
@@ -201,8 +214,8 @@ class MUIDataTable extends React.Component {
   componentDidMount() {
     this.setHeadResizeable(this.headCellRefs, this.tableRef);
 
-    // When we have a search, we must reset page to view it
-    if (this.props.options.searchText) this.setState({ page: 0 });
+    // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user.
+    if (this.props.options.searchText && !this.props.options.serverSide) this.setState({ page: 0 });
   }
 
   componentDidUpdate(prevProps) {
@@ -213,8 +226,8 @@ class MUIDataTable extends React.Component {
       this.updateOptions(this.props);
     }
 
-    if (this.props.options.searchText !== prevProps.options.searchText) {
-      // When we have a search, we must reset page to view it
+    if (this.props.options.searchText !== prevProps.options.searchText && !this.props.options.serverSide) {
+      // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user.
       this.setState({ page: 0 });
     }
 
@@ -479,8 +492,14 @@ class MUIDataTable extends React.Component {
       lookup: {},
     };
 
+    let expandedRowsData = {
+      data: [],
+      lookup: {},
+    };
+
     if (TABLE_LOAD.INITIAL) {
-      if (options.rowsSelected && options.rowsSelected.length) {
+      // Multiple row selection customization
+      if (options.rowsSelected && options.rowsSelected.length && options.selectableRows === 'multiple') {
         options.rowsSelected.forEach(row => {
           let rowPos = row;
 
@@ -495,6 +514,41 @@ class MUIDataTable extends React.Component {
           selectedRowsData.lookup[row] = true;
         });
       }
+
+      // Single row selection customization
+      if (options.rowsSelected && options.rowsSelected.length === 1 && options.selectableRows === 'single') {
+        let rowPos = options.rowsSelected[0];
+
+        for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
+          if (this.state.displayData[cIndex].dataIndex === options.rowsSelected[0]) {
+            rowPos = cIndex;
+            break;
+          }
+        }
+
+        selectedRowsData.data.push({ index: rowPos, dataIndex: options.rowsSelected[0] });
+        selectedRowsData.lookup[options.rowsSelected[0]] = true;
+      } else if (options.rowsSelected && options.rowsSelected.length > 1 && options.selectableRows === 'single') {
+        console.error(
+          'Multiple values provided for selectableRows, but selectableRows set to "single". Either supply only a single value or use "multiple".',
+        );
+      }
+
+      if (options.rowsExpanded && options.rowsExpanded.length && options.expandableRows) {
+        options.rowsExpanded.forEach(row => {
+          let rowPos = row;
+
+          for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
+            if (this.state.displayData[cIndex].dataIndex === row) {
+              rowPos = cIndex;
+              break;
+            }
+          }
+
+          expandedRowsData.data.push({ index: rowPos, dataIndex: row });
+          expandedRowsData.lookup[row] = true;
+        });
+      }
     }
 
     if (!options.serverSide && sortIndex !== null) {
@@ -509,6 +563,7 @@ class MUIDataTable extends React.Component {
         filterList: filterList,
         searchText: searchText,
         selectedRows: selectedRowsData,
+        expandedRows: expandedRowsData,
         count: options.count,
         data: tableData,
         displayData: this.getDisplayData(columns, tableData, filterList, searchText),
@@ -1144,7 +1199,7 @@ class MUIDataTable extends React.Component {
         />
         <div
           style={{ position: 'relative' }}
-          className={this.options.responsive === 'scroll' ? classes.responsiveScroll : null}>
+          className={this.options.responsive === 'scroll' ? classes.responsiveScroll : classes.responsiveStacked}>
           {this.options.resizableColumns && (
             <TableResize
               key={rowCount}
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index 568dc3f..8207b4f 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -116,6 +116,9 @@ class TableBody extends React.Component {
       return;
     }
 
+    // Don't trigger onRowClick if the event was actually a row selection via checkbox
+    if (event.target.id && event.target.id.startsWith('MUIDataTableSelectCell')) return;
+
     // Check if we should toggle row select when row is clicked anywhere
     if (
       this.props.options.selectableRowsOnClick &&
@@ -131,13 +134,8 @@ class TableBody extends React.Component {
       this.props.toggleExpandRow(expandRow);
     }
 
-    // Don't trigger onRowClick if the event was actually a row selection
-    if (
-      (event.target.id && event.target.id.startsWith('MUIDataTableSelectCell')) ||
-      this.props.options.selectableRowsOnClick
-    ) {
-      return;
-    }
+    // Don't trigger onRowClick if the event was actually a row selection via click
+    if (this.props.options.selectableRowsOnClick) return;
 
     this.props.options.onRowClick && this.props.options.onRowClick(row, data, event);
   };
@@ -164,6 +162,7 @@ class TableBody extends React.Component {
                   options={options}
                   rowSelected={options.selectableRows !== 'none' ? this.isRowSelected(dataIndex) : false}
                   onClick={this.handleRowClick.bind(null, row, { rowIndex, dataIndex })}
+                  data-testid={'MUIDataTableBodyRow-' + dataIndex}
                   id={'MUIDataTableBodyRow-' + dataIndex}>
                   <TableSelectCell
                     onChange={this.handleRowSelect.bind(null, {
@@ -189,6 +188,7 @@ class TableBody extends React.Component {
                           {...(columns[columnIndex].setCellProps
                             ? columns[columnIndex].setCellProps(column, dataIndex, columnIndex)
                             : {})}
+                          data-testid={`MuiDataTableBodyCell-${columnIndex}-${rowIndex}`}
                           dataIndex={dataIndex}
                           rowIndex={rowIndex}
                           colIndex={columnIndex}
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index 154ba23..0ea4c2f 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -309,6 +309,7 @@ class TableFilter extends React.Component {
               className={classes.resetLink}
               tabIndex={0}
               aria-label={textLabels.reset}
+              data-testid={'filterReset-button'}
               onClick={onFilterReset}>
               {textLabels.reset}
             </Button>
diff --git a/src/components/TablePagination.js b/src/components/TablePagination.js
index caf6ef6..fcc6768 100644
--- a/src/components/TablePagination.js
+++ b/src/components/TablePagination.js
@@ -69,18 +69,21 @@ class TablePagination extends React.Component {
             labelDisplayedRows={({ from, to, count }) => `${from}-${to} ${textLabels.displayRows} ${count}`}
             backIconButtonProps={{
               id: 'pagination-back',
+              'data-testid': 'pagination-back',
               'aria-label': textLabels.previous,
             }}
             nextIconButtonProps={{
               id: 'pagination-next',
+              'data-testid': 'pagination-next',
               'aria-label': textLabels.next,
             }}
             SelectProps={{
               id: 'pagination-input',
-              SelectDisplayProps: { id: 'pagination-rows' },
+              SelectDisplayProps: { id: 'pagination-rows', 'data-testid': 'pagination-rows' },
               MenuProps: {
                 id: 'pagination-menu',
-                MenuListProps: { id: 'pagination-menu-list' },
+                'data-testid': 'pagination-menu',
+                MenuListProps: { id: 'pagination-menu-list', 'data-testid': 'pagination-menu-list' },
               },
             }}
             rowsPerPageOptions={options.rowsPerPageOptions}
diff --git a/src/components/TableSearch.js b/src/components/TableSearch.js
index 5cacbd6..9eec1d1 100644
--- a/src/components/TableSearch.js
+++ b/src/components/TableSearch.js
@@ -62,6 +62,7 @@ class TableSearch extends React.Component {
             className={classes.searchText}
             autoFocus={true}
             InputProps={{
+              'data-test-id': options.textLabels.toolbar.search,
               'aria-label': options.textLabels.toolbar.search,
             }}
             value={searchText || ''}
diff --git a/src/components/TableSelectCell.js b/src/components/TableSelectCell.js
index f22f328..6f501cb 100644
--- a/src/components/TableSelectCell.js
+++ b/src/components/TableSelectCell.js
@@ -32,11 +32,7 @@ const defaultSelectCellStyles = theme => ({
     zIndex: 110,
     backgroundColor: theme.palette.background.paper,
   },
-  checkboxRoot: {
-    '&$checked': {
-      color: theme.palette.primary.main,
-    },
-  },
+  checkboxRoot: {},
   checked: {},
   disabled: {},
 });
@@ -104,6 +100,7 @@ class TableSelectCell extends React.Component {
             checked: classes.checked,
             disabled: classes.disabled,
           }}
+          color="primary"
           disabled={!isRowSelectable}
           {...otherProps}
         />
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 2cbd812..41bc119 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -95,8 +95,38 @@ class TableToolbar extends React.Component {
   }
 
   handleCSVDownload = () => {
-    const { data, columns, options } = this.props;
-    createCSVDownload(columns, data, options);
+    const { data, displayData, columns, options } = this.props;
+    let dataToDownload = data;
+    let columnsToDownload = columns;
+
+    if (options.downloadOptions && options.downloadOptions.filterOptions) {
+      // check rows first:
+      if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
+        dataToDownload = displayData.map(row => {
+          let i = -1;
+
+          return {
+            data: row.data.map(column => {
+              i += 1;
+
+              // if we have a custom render, we must grab the actual value from data
+              return typeof column === 'object' ? data[row.dataIndex].data[i] : column;
+            }),
+          };
+        });
+      }
+
+      // now, check columns:
+      if (options.downloadOptions.filterOptions.useDisplayedColumnsOnly) {
+        columnsToDownload = columns.filter((_, index) => _.display === 'true');
+
+        dataToDownload = dataToDownload.map(row => {
+          row.data = row.data.filter((_, index) => columns[index].display === 'true');
+          return row;
+        });
+      }
+    }
+    createCSVDownload(columnsToDownload, dataToDownload, options);
   };
 
   setActiveIcon = iconName => {
@@ -174,12 +204,16 @@ class TableToolbar extends React.Component {
       <Toolbar className={classes.root} role={'toolbar'} aria-label={'Table Toolbar'}>
         <div className={classes.left}>
           {showSearch === true ? (
-            <TableSearch
-              searchText={searchText}
-              onSearch={this.handleSearch}
-              onHide={this.hideSearch}
-              options={options}
-            />
+            options.customSearchRender ? (
+              options.customSearchRender(searchText, this.handleSearch, this.hideSearch, options)
+            ) : (
+              <TableSearch
+                searchText={searchText}
+                onSearch={this.handleSearch}
+                onHide={this.hideSearch}
+                options={options}
+              />
+            )
           ) : typeof title !== 'string' ? (
             title
           ) : (
@@ -195,6 +229,7 @@ class TableToolbar extends React.Component {
             <Tooltip title={search} disableFocusListener>
               <IconButton
                 aria-label={search}
+                data-testid={search + '-iconButton'}
                 buttonRef={el => (this.searchButton = el)}
                 classes={{ root: this.getActiveIcon(classes, 'search') }}
                 onClick={this.setActiveIcon.bind(null, 'search')}>
@@ -204,7 +239,11 @@ class TableToolbar extends React.Component {
           )}
           {options.download && (
             <Tooltip title={downloadCsv}>
-              <IconButton aria-label={downloadCsv} classes={{ root: classes.icon }} onClick={this.handleCSVDownload}>
+              <IconButton
+                data-testid={downloadCsv + '-iconButton'}
+                aria-label={downloadCsv}
+                classes={{ root: classes.icon }}
+                onClick={this.handleCSVDownload}>
                 <DownloadIcon />
               </IconButton>
             </Tooltip>
@@ -214,7 +253,7 @@ class TableToolbar extends React.Component {
               <ReactToPrint
                 trigger={() => (
                   <Tooltip title={print}>
-                    <IconButton aria-label={print} classes={{ root: classes.icon }}>
+                    <IconButton data-testid={print + '-iconButton'} aria-label={print} classes={{ root: classes.icon }}>
                       <PrintIcon />
                     </IconButton>
                   </Tooltip>
@@ -229,6 +268,7 @@ class TableToolbar extends React.Component {
               trigger={
                 <Tooltip title={viewColumns} disableFocusListener>
                   <IconButton
+                    data-testid={viewColumns + '-iconButton'}
                     aria-label={viewColumns}
                     classes={{ root: this.getActiveIcon(classes, 'viewcolumns') }}
                     onClick={this.setActiveIcon.bind(null, 'viewcolumns')}>
@@ -248,6 +288,7 @@ class TableToolbar extends React.Component {
               trigger={
                 <Tooltip title={filterTable} disableFocusListener>
                   <IconButton
+                    data-testid={filterTable + '-iconButton'}
                     aria-label={filterTable}
                     classes={{ root: this.getActiveIcon(classes, 'filter') }}
                     onClick={this.setActiveIcon.bind(null, 'filter')}>


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index f4a05f3..cbb7f00 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { spy } from 'sinon';
 import { mount, shallow } from 'enzyme';
 import { assert, expect } from 'chai';
+import cloneDeep from 'lodash.clonedeep';
 import MUIDataTable from '../src/MUIDataTable';
 import TableFilterList from '../src/components/TableFilterList';
 import TablePagination from '../src/components/TablePagination';
@@ -429,6 +430,21 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.filterList, [['Joe James'], [], [], [], []]);
   });
 
+  it('should apply columns prop change for filterList', () => {
+    const mountShallowWrapper = mount(shallow(<MUIDataTable columns={columns} data={data} />).get(0));
+    const instance = mountShallowWrapper.instance();
+    instance.initializeTable(mountShallowWrapper.props());
+    // now use updated columns props
+    const newColumns = cloneDeep(columns);
+    newColumns[0].options.filterList = ['Joe James'];
+    mountShallowWrapper.setProps({ columns: newColumns });
+    mountShallowWrapper.update();
+    instance.setTableData(mountShallowWrapper.props(), 1 /* instance.TABLE_LOAD.INITIAL */);
+
+    const updatedState = mountShallowWrapper.state();
+    assert.deepEqual(updatedState.filterList, [['Joe James'], [], [], [], []]);
+  });
+
   it('should create Chip when filterList is populated', () => {
     const filterList = [['Joe James'], [], [], [], []];
     const filterListRenderers = [
@@ -772,7 +788,7 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.data, [{ index: 1, dataIndex: 1 }]);
   });
 
-  it('If selectableRows=multiple, multiple cells can be selected when calling selectRowUpdate method with type=cell.', () => {
+  it('should allow multiple cells to be selected when selectableRows=multiple and selectRowUpdate method with type=cell.', () => {
     const options = { selectableRows: 'multiple' };
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
     const instance = shallowWrapper.instance();
@@ -799,6 +815,39 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.data, expectedResult);
   });
 
+  it('should update selectedRows when using rowsSelected option with type=multiple', () => {
+    const options = {
+      selectableRows: 'multiple',
+      rowsSelected: [0, 3],
+    };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+  });
+
+  it('should update expandedRows when using expandableRows option with default rowsExpanded', () => {
+    const options = {
+      expandableRows: true,
+      rowsExpanded: [0, 3],
+      renderExpandableRow: () => (
+        <tr>
+          <td>opened</td>
+        </tr>
+      ),
+    };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.expandedRows.data, expectedResult);
+  });
+
   it('should remove selected data on selectRowDelete when type=cell', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
     const instance = shallowWrapper.instance();
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index dd9fcce..dd00ccf 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -124,6 +124,22 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
+  it('should render a toolbar with custom search when option.customSearchRender is provided', () => {
+    const CustomSearchRender = () => <h1>customSearchRender</h1>;
+    const newOptions = { ...options, customSearchRender: CustomSearchRender };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    )
+      .dive()
+      .dive()
+      .dive();
+    const instance = shallowWrapper.instance();
+    instance.setActiveIcon('search');
+    shallowWrapper.update();
+    const actualResult = shallowWrapper.find('h1');
+    assert.strictEqual(actualResult.length, 1);
+  });
+
   it('should render a toolbar with a search clicking search icon', () => {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,