Repository: gregnb/mui-datatables
Pull request #715 (2019-06-24 17:10:40)

add test for #711
passing test for #711 (originally from my issue #657)

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

Coverage increased (+0.2%) to 76.579% when pulling **5bb80957c498f78ec87c4a57ab896a857393f2be on ericpyle:patch-3** into **a929a3f173e5bbcd2d3c65ba4b793546f5cbda16 on gregnb:master**.


Patch:
diff --git a/README.md b/README.md
index b7e9c9c..4bc6098 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ The component accepts the following props:
 |:--:|:-----|:-----|
 |**`title`**|array|Title used to caption table
 |**`columns`**|array|Columns used to describe table. Must be either an array of simple strings or objects describing a column
-|**`data`**|array|Data used to describe table. Must be an array containing objects. (Arrays containing just strings or numbers also supported) 
+|**`data`**|array|Data used to describe table. Must be an array containing objects. (Arrays containing just strings or numbers also supported)
 |**`options`**|object|Options used to describe table
 
 #### Options:
@@ -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
@@ -167,7 +168,7 @@ The component accepts the following props:
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
 |**`download`**|boolean|true|Show/hide download icon from toolbar
 |**`downloadOptions`**|object||Options to change the output of the CSV file. Default options: `{filename: 'tableDownload.csv', separator: ','}`
-|**`onDownload`**|function||A callback function that triggers when the user downloads the CSV file. In the callback, you can control what is written to the CSV file. `function(buildHead: (columns) => string, buildBody: (data) => string, columns, data) => string`
+|**`onDownload`**|function||A callback function that triggers when the user downloads the CSV file. In the callback, you can control what is written to the CSV file. `function(buildHead: (columns) => string, buildBody: (data) => string, columns, data) => string`. Return `false` to cancel download of file.
 |**`viewColumns`**|boolean|true|Show/hide viewColumns icon from toolbar
 |**`onRowsSelect`**|function||Callback function that triggers when row(s) are selected. `function(currentRowsSelected: array, allRowsSelected: array) => void`
 |**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: object(lookup: {dataindex: boolean}, data: arrayOfObjects: {index, dataIndex})) => void OR false` (Returning `false` prevents row deletion.)
@@ -216,10 +217,10 @@ const columns = [
 |**`empty`**|boolean|false|This denotes whether the column has data or not (for use with intentionally empty columns)
 |**`viewColumns`**|boolean|true|Allow user to toggle column visibility through 'View Column' list
 |**`filterList`**|array||Filter value list [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js)
-|**`filterOptions`**|{names, logic}||With filter options, it's possible to use custom names for the filter fields [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js) and/or custom filter logic [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-filter/index.js)
+|**`filterOptions`**|{names, logic, display}||With filter options, it's possible to use custom names for the filter fields [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js), custom filter logic [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-filter/index.js), and custom rendering [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-filter/index.js)
 |**`customFilterListRender`**|function||Function that returns a string used as the chip label. `function(value) => string` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js)
 |**`filter`**|boolean|true|Display column in filter list
-|**`filterType `**|string|'dropdown'|Choice of filtering view. Takes priority over global filterType option.`enum('checkbox', 'dropdown', 'multiselect', 'textField')`.
+|**`filterType `**|string|'dropdown'|Choice of filtering view. Takes priority over global filterType option.`enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom')` Use 'custom' if you are supplying your own rendering via `filterOptions`.
 |**`sort`**|boolean|true|Enable/disable sorting on column
 |**`searchable`**|boolean|true|Exclude/include column from search results
 |**`sortDirection`**|string||Set default sort order `enum('asc', 'desc')`
@@ -381,7 +382,7 @@ npm run dev
 ```
 open  http://localhost:5050/ in browser
 
-After you make your changes locally, you can run the test suite with `npm test`. 
+After you make your changes locally, you can run the test suite with `npm test`.
 
 ## License
 The files included in this repository are licensed under the MIT license.
diff --git a/examples/csv-export/index.js b/examples/csv-export/index.js
index 1be1059..40d9b04 100644
--- a/examples/csv-export/index.js
+++ b/examples/csv-export/index.js
@@ -1,55 +1,68 @@
-import React from "react";
-import ReactDOM from "react-dom";
-import MUIDataTable from "../../src/";
+import Button from '@material-ui/core/Button';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src/';
 
 class Example extends React.Component {
+  state = {
+    downloadFile: true,
+  };
 
   render() {
-
     const columns = [
-      "Name",
-      "Title",
-      "Location",
-      "Age",
+      'Name',
+      'Title',
       {
-          name: "Salary",
-          options: {
-              download: false,
-          }
+        name: 'Location',
+        options: {
+          display: 'false'
+        }
+      },
+      {
+        name: 'Age',
+        options: {
+          customBodyRender: value => <div><span>{value}</span></div>
+        }
+      },
+      {
+        name: 'Salary',
+        options: {
+          download: false,
+        },
       },
     ];
 
     const 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],
+      ['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 = {
@@ -61,17 +74,28 @@ class Example extends React.Component {
       downloadOptions: {
           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;
       },
       onRowsSelect: (rowsSelected, allRows) => {
         console.log(rowsSelected, allRows);
       },
-      onRowsDelete: (rowsDeleted) => {
-        console.log(rowsDeleted, "were deleted!");
+      onRowsDelete: rowsDeleted => {
+        console.log(rowsDeleted, 'were deleted!');
       },
-      onChangePage: (numberRows) => {
+      onChangePage: numberRows => {
         console.log(numberRows);
       },
-      onSearchChange: (searchText) => {
+      onSearchChange: searchText => {
         console.log(searchText);
       },
       onColumnSortChange: (column, direction) => {
@@ -88,14 +112,27 @@ class Example extends React.Component {
       },
       onRowClick: (rowData, rowState) => {
         console.log(rowData, rowState);
-      }
+      },
     };
 
     return (
-      <MUIDataTable title={"ACME Employee list CSV"} data={data} columns={columns} options={options} />
+      <React.Fragment>
+        <Button
+          style={{
+            right: '17rem',
+            position: 'absolute',
+            top: '1.5rem',
+            background: 'gray',
+            color: 'white',
+            zIndex: 10,
+          }}
+          onClick={() => this.setState(prevState => ({ downloadFile: !prevState.downloadFile }))}>
+          {this.state.downloadFile ? 'Disable' : 'Enable'} Download
+        </Button>
+        <MUIDataTable title={'ACME Employee list CSV'} data={data} columns={columns} options={options} />
+      </React.Fragment>
     );
-
   }
 }
 
-ReactDOM.render(<Example />, document.getElementById("app-root"));
+ReactDOM.render(<Example />, document.getElementById('app-root'));
diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js
index d13f05d..a829180 100644
--- a/examples/customize-filter/index.js
+++ b/examples/customize-filter/index.js
@@ -1,104 +1,141 @@
-import React from "react";
-import ReactDOM from "react-dom";
-import MUIDataTable from "../../src";
+import { FormGroup, FormLabel, TextField } from '@material-ui/core';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src';
 
 class Example extends React.Component {
-
   render() {
-
     const columns = [
       {
-        name: "Name",
+        name: 'Name',
         options: {
           filter: true,
           display: 'excluded',
-        }
+        },
       },
       {
-        label: "Modified Title Label",
-        name: "Title",
+        label: 'Modified Title Label',
+        name: 'Title',
         options: {
           filter: true,
-        }
+        },
       },
       {
-        name: "Location",
+        name: 'Location',
         options: {
           print: false,
           filter: false,
-        }
+        },
       },
       {
-        name: "Age",
+        name: 'Age',
         options: {
           filter: true,
+          filterType: 'custom',
+          filterList: [25, 50],
+          customFilterListRender: v => {
+            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: ["young", "adult", "middle-age", "senior", "elderly"],
+            names: [],
             logic(age, filters) {
-              const show = (filters.indexOf("young") >= 0 && age <= 35) ||
-                (filters.indexOf("adult") >= 0 && age > 35 && age <= 45) ||
-                (filters.indexOf("middle-age") >= 0 && age > 45 && age <= 65) ||
-                (filters.indexOf("senior") >= 0 && age > 65 && age <= 75) ||
-                (filters.indexOf("elderly") >= 0 && age > 75);
-              const filtered = !show;
-              return filtered;
-            }
+              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;
+            },
+            display: (filterList, onChange, index, column) => (
+              <div>
+                <FormLabel>Age</FormLabel>
+                <FormGroup row>
+                  <TextField
+                    label="min"
+                    value={filterList[index][0] || ''}
+                    onChange={event => {
+                      filterList[index][0] = event.target.value;
+                      onChange(filterList[index], index, column);
+                    }}
+                    style={{ width: '45%', marginRight: '5%' }}
+                  />
+                  <TextField
+                    label="max"
+                    value={filterList[index][1] || ''}
+                    onChange={event => {
+                      filterList[index][1] = event.target.value;
+                      onChange(filterList[index], index, column);
+                    }}
+                    style={{ width: '45%' }}
+                  />
+                </FormGroup>
+              </div>
+            ),
           },
           print: false,
-        }
+        },
       },
       {
-        name: "Salary",
+        name: 'Salary',
         options: {
           filter: true,
-          filterType: "checkbox",
+          filterType: 'checkbox',
           filterOptions: {
-            names: ["Lower wages", "Average wages", "Higher wages"],
+            names: ['Lower wages', 'Average wages', 'Higher wages'],
             logic(salary, filterVal) {
-              salary = salary.replace(/[^\d]/g, "");
-              const show = (filterVal.indexOf("Lower wages") >= 0 && salary < 100000) ||
-                (filterVal.indexOf("Average wages") >= 0 && salary >= 100000 && salary < 200000) ||
-                (filterVal.indexOf("Higher wages") >= 0 && salary >= 200000);
+              salary = salary.replace(/[^\d]/g, '');
+              const show =
+                (filterVal.indexOf('Lower wages') >= 0 && salary < 100000) ||
+                (filterVal.indexOf('Average wages') >= 0 && salary >= 100000 && salary < 200000) ||
+                (filterVal.indexOf('Higher wages') >= 0 && salary >= 200000);
               return !show;
-            }
+            },
           },
-          sort: false
-        }
-      }
+          sort: false,
+        },
+      },
     ];
 
-
     const data = [
-      ["Gabby George", "Business Analyst", "Minneapolis", 30, "$100,000"],
-      ["Aiden Lloyd", "Business Consultant", "Dallas", 55, "$200,000"],
-      ["Jaden Collins", "Attorney", "Santa Ana", 27, "$500,000"],
-      ["Franky Rees", "Business Analyst", "St. Petersburg", 22, "$50,000"],
-      ["Aaren Rose", "Business Consultant", "Toledo", 28, "$75,000"],
-      ["Blake Duncan", "Business Management Analyst", "San Diego", 65, "$94,000"],
-      ["Frankie Parry", "Agency Legal Counsel", "Jacksonville", 71, "$210,000"],
-      ["Lane Wilson", "Commercial Specialist", "Omaha", 19, "$65,000"],
-      ["Robin Duncan", "Business Analyst", "Los Angeles", 20, "$77,000"],
-      ["Mel Brooks", "Business Consultant", "Oklahoma City", 37, "$135,000"],
-      ["Harper White", "Attorney", "Pittsburgh", 52, "$420,000"],
-      ["Kris Humphrey", "Agency Legal Counsel", "Laredo", 30, "$150,000"],
-      ["Frankie Long", "Industrial Analyst", "Austin", 31, "$170,000"],
-      ["Brynn Robbins", "Business Analyst", "Norfolk", 22, "$90,000"],
-      ["Justice Mann", "Business Consultant", "Chicago", 24, "$133,000"],
-      ["Addison Navarro", "Business Management Analyst", "New York", 50, "$295,000"],
-      ["Jesse Welch", "Agency Legal Counsel", "Seattle", 28, "$200,000"],
-      ["Eli Mejia", "Commercial Specialist", "Long Beach", 65, "$400,000"],
-      ["Gene Leblanc", "Industrial Analyst", "Hartford", 34, "$110,000"],
-      ["Danny Leon", "Computer Scientist", "Newark", 60, "$220,000"],
-      ["Lane Lee", "Corporate Counselor", "Cincinnati", 52, "$180,000"],
-      ["Jesse Hall", "Business Analyst", "Baltimore", 44, "$99,000"],
-      ["Danni Hudson", "Agency Legal Counsel", "Tampa", 37, "$90,000"],
-      ["Terry Macdonald", "Commercial Specialist", "Miami", 39, "$140,000"],
-      ["Justice Mccarthy", "Attorney", "Tucson", 26, "$330,000"],
-      ["Silver Carey", "Computer Scientist", "Memphis", 47, "$250,000"],
-      ["Franky Miles", "Industrial Analyst", "Buffalo", 49, "$190,000"],
-      ["Glen Nixon", "Corporate Counselor", "Arlington", 44, "$80,000"],
-      ["Gabby Strickland", "Business Process Consultant", "Scottsdale", 26, "$45,000"],
-      ["Mason Ray", "Computer Scientist", "San Francisco", 39, "$142,000"]
+      ['Gabby George', 'Business Analyst', 'Minneapolis', 30, '$100,000'],
+      ['Aiden Lloyd', 'Business Consultant', 'Dallas', 55, '$200,000'],
+      ['Jaden Collins', 'Attorney', 'Santa Ana', 27, '$500,000'],
+      ['Franky Rees', 'Business Analyst', 'St. Petersburg', 22, '$50,000'],
+      ['Aaren Rose', 'Business Consultant', 'Toledo', 28, '$75,000'],
+      ['Blake Duncan', 'Business Management Analyst', 'San Diego', 65, '$94,000'],
+      ['Frankie Parry', 'Agency Legal Counsel', 'Jacksonville', 71, '$210,000'],
+      ['Lane Wilson', 'Commercial Specialist', 'Omaha', 19, '$65,000'],
+      ['Robin Duncan', 'Business Analyst', 'Los Angeles', 20, '$77,000'],
+      ['Mel Brooks', 'Business Consultant', 'Oklahoma City', 37, '$135,000'],
+      ['Harper White', 'Attorney', 'Pittsburgh', 52, '$420,000'],
+      ['Kris Humphrey', 'Agency Legal Counsel', 'Laredo', 30, '$150,000'],
+      ['Frankie Long', 'Industrial Analyst', 'Austin', 31, '$170,000'],
+      ['Brynn Robbins', 'Business Analyst', 'Norfolk', 22, '$90,000'],
+      ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000'],
+      ['Addison Navarro', 'Business Management Analyst', 'New York', 50, '$295,000'],
+      ['Jesse Welch', 'Agency Legal Counsel', 'Seattle', 28, '$200,000'],
+      ['Eli Mejia', 'Commercial Specialist', 'Long Beach', 65, '$400,000'],
+      ['Gene Leblanc', 'Industrial Analyst', 'Hartford', 34, '$110,000'],
+      ['Danny Leon', 'Computer Scientist', 'Newark', 60, '$220,000'],
+      ['Lane Lee', 'Corporate Counselor', 'Cincinnati', 52, '$180,000'],
+      ['Jesse Hall', 'Business Analyst', 'Baltimore', 44, '$99,000'],
+      ['Danni Hudson', 'Agency Legal Counsel', 'Tampa', 37, '$90,000'],
+      ['Terry Macdonald', 'Commercial Specialist', 'Miami', 39, '$140,000'],
+      ['Justice Mccarthy', 'Attorney', 'Tucson', 26, '$330,000'],
+      ['Silver Carey', 'Computer Scientist', 'Memphis', 47, '$250,000'],
+      ['Franky Miles', 'Industrial Analyst', 'Buffalo', 49, '$190,000'],
+      ['Glen Nixon', 'Corporate Counselor', 'Arlington', 44, '$80,000'],
+      ['Gabby Strickland', 'Business Process Consultant', 'Scottsdale', 26, '$45,000'],
+      ['Mason Ray', 'Computer Scientist', 'San Francisco', 39, '$142,000'],
     ];
 
     const options = {
@@ -108,10 +145,9 @@ class Example extends React.Component {
     };
 
     return (
-      <MUIDataTable title={"ACME Employee list - customizeFilter"} data={data} columns={columns} options={options} />
+      <MUIDataTable title={'ACME Employee list - customizeFilter'} data={data} columns={columns} options={options} />
     );
-
   }
 }
 
-ReactDOM.render(<Example />, document.getElementById("app-root"));
\ No newline at end of file
+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/examples/on-table-init/index.js b/examples/on-table-init/index.js
index 27ee565..700139f 100644
--- a/examples/on-table-init/index.js
+++ b/examples/on-table-init/index.js
@@ -6,7 +6,9 @@ class Example extends React.Component {
   constructor(props) {
     super(props);
 
-    this.tableState = {};
+    this.state = {
+      table: {}
+    };
     this.handleTableInit = this.handleTableInit.bind(this);
     this.handleTableChange = this.handleTableChange.bind(this);
   }
@@ -18,61 +20,62 @@ class Example extends React.Component {
    */
   handleTableInit = (action, tableState) => {
     console.log('handleTableInit: ', tableState);
-    this.tableState = tableState;
+    this.setState({ table: tableState });
   };
 
   handleTableChange = (action, tableState) => {
-    this.tableState = tableState;
+    console.log('handleTableChange: ', tableState);
+    this.setState({ table: tableState });
   };
 
-  render() {
-    const columns = ['Name', 'Title', 'Location', 'Age', 'Salary'];
+  columns = ['Name', 'Title', 'Location', 'Age', 'Salary'];
 
-    const 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],
-    ];
+  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: 'scroll',
-      rowsPerPage: 10,
-      download: false, // hide csv download option
-      onTableInit: this.handleTableInit,
-      onTableChange: this.handleTableChange,
-    };
+  options = {
+    filter: true,
+    selectableRows: true,
+    filterType: 'dropdown',
+    responsive: 'scroll',
+    rowsPerPage: 10,
+    download: false, // hide csv download option
+    onTableInit: this.handleTableInit,
+    onTableChange: this.handleTableChange,
+  };
 
-    return <MUIDataTable title={'ACME Employee list'} data={data} columns={columns} options={options} />;
+  render() {
+    return <MUIDataTable title={'ACME Employee list'} data={this.data} columns={this.columns} options={this.options} />;
   }
 }
 
diff --git a/examples/selectable-rows/index.js b/examples/selectable-rows/index.js
index 9860ba9..4323549 100644
--- a/examples/selectable-rows/index.js
+++ b/examples/selectable-rows/index.js
@@ -3,6 +3,9 @@ import ReactDOM from "react-dom";
 import MUIDataTable from "../../src/";
 
 class Example extends React.Component {
+  state = {
+    rowsSelected: []
+  };
 
   render() {
 
@@ -49,12 +52,15 @@ class Example extends React.Component {
 
     const options = {
       filter: true,
-      selectableRows: true,
+      selectableRows: 'multiple',
+      selectableRowsOnClick: true,
       filterType: 'dropdown',
       responsive: 'stacked',
       rowsPerPage: 10,
+      rowsSelected: this.state.rowsSelected,
       onRowsSelect: (rowsSelected, allRows) => {
         console.log(rowsSelected, allRows);
+        this.setState({ rowsSelected: allRows.map(row => row.dataIndex) });
       },
       onRowsDelete: (rowsDeleted) => {
         if (rowsDeleted.data[0].dataIndex === 0) {
diff --git a/package-lock.json b/package-lock.json
index 1867ee3..ad7cc5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "mui-datatables",
-  "version": "2.5.1",
+  "version": "2.7.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 23ed267..c233cf8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mui-datatables",
-  "version": "2.5.1",
+  "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 52fecf8..1a6b4f7 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -1,25 +1,24 @@
-import React from 'react';
-import PropTypes from 'prop-types';
 import Paper from '@material-ui/core/Paper';
+import { withStyles } from '@material-ui/core/styles';
 import MuiTable from '@material-ui/core/Table';
-import TableToolbar from './components/TableToolbar';
-import TableToolbarSelect from './components/TableToolbarSelect';
-import TableFilterList from './components/TableFilterList';
-import TableBody from './components/TableBody';
-import TableResize from './components/TableResize';
-import TableHead from './components/TableHead';
-import TableFooter from './components/TableFooter';
 import classnames from 'classnames';
 import cloneDeep from 'lodash.clonedeep';
-import merge from 'lodash.merge';
-import isEqual from 'lodash.isequal';
 import find from 'lodash.find';
 import isUndefined from 'lodash.isundefined';
+import merge from 'lodash.merge';
+import PropTypes from 'prop-types';
+import React from 'react';
+import TableBody from './components/TableBody';
+import TableFilterList from './components/TableFilterList';
+import TableFooter from './components/TableFooter';
+import TableHead from './components/TableHead';
+import TableResize from './components/TableResize';
+import TableToolbar from './components/TableToolbar';
+import TableToolbarSelect from './components/TableToolbarSelect';
 import textLabels from './textLabels';
-import { withStyles } from '@material-ui/core/styles';
 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,
@@ -96,9 +103,10 @@ class MUIDataTable extends React.Component {
               PropTypes.shape({
                 names: PropTypes.array,
                 logic: PropTypes.func,
+                display: PropTypes.func,
               }),
             ]),
-            filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField']),
+            filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']),
             customHeadRender: PropTypes.func,
             customBodyRender: PropTypes.func,
             customFilterListRender: PropTypes.func,
@@ -109,7 +117,7 @@ class MUIDataTable extends React.Component {
     /** Options used to describe table */
     options: PropTypes.shape({
       responsive: PropTypes.oneOf(['stacked', 'scroll']),
-      filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField']),
+      filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']),
       textLabels: PropTypes.object,
       pagination: PropTypes.bool,
       expandableRows: PropTypes.bool,
@@ -133,6 +141,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,
@@ -147,6 +156,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,
     }),
@@ -478,8 +491,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;
 
@@ -494,6 +513,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) {
@@ -508,6 +562,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),
@@ -559,7 +614,7 @@ class MUIDataTable extends React.Component {
       const filterVal = filterList[index];
       const caseSensitive = this.options.caseSensitive;
       const filterType = column.filterType || this.options.filterType;
-      if (filterVal.length) {
+      if (filterVal.length || filterType === 'custom') {
         if (column.filterOptions && column.filterOptions.logic) {
           if (column.filterOptions.logic(columnValue, filterVal)) isFiltered = true;
         } else if (filterType === 'textField' && !this.hasSearchText(columnVal, filterVal, caseSensitive)) {
@@ -844,7 +899,7 @@ class MUIDataTable extends React.Component {
   filterUpdate = (index, value, column, type) => {
     this.setState(
       prevState => {
-        const filterList = cloneDeep(prevState.filterList);
+        const filterList = prevState.filterList.slice(0);
         const filterPos = filterList[index].indexOf(value);
 
         switch (type) {
@@ -854,6 +909,9 @@ class MUIDataTable extends React.Component {
           case 'multiselect':
             filterList[index] = value === '' ? [] : value;
             break;
+          case 'custom':
+            filterList[index] = value;
+            break;
           default:
             filterList[index] = filterPos >= 0 || value === '' ? [] : [value];
         }
@@ -986,7 +1044,7 @@ class MUIDataTable extends React.Component {
           let rowPos = -1;
 
           for (let cIndex = 0; cIndex < selectedRows.length; cIndex++) {
-            if (selectedRows[cIndex].index === index) {
+            if (selectedRows[cIndex].dataIndex === dataIndex) {
               rowPos = cIndex;
               break;
             }
@@ -1094,7 +1152,7 @@ class MUIDataTable extends React.Component {
     const rowCount = this.state.count || displayData.length;
     const rowsPerPage = this.options.pagination ? this.state.rowsPerPage : displayData.length;
     const showToolbar = hasToolbarItem(this.options, title);
-    const columnNames = columns.map(column => ({ name: column.name }));
+    const columnNames = columns.map(column => ({ name: column.name, filterType: column.filterType }));
 
     return (
       <Paper
@@ -1140,7 +1198,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 e47e9b9..8207b4f 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -55,13 +55,7 @@ class TableBody extends React.Component {
     const toIndex = Math.min(count, (page + 1) * rowsPerPage);
 
     if (page > totalPages && totalPages !== 0) {
-      throw new Error(
-        'Provided options.page of `' +
-          page +
-          '` is greater than the total available page length of `' +
-          totalPages +
-          '`',
-      );
+      console.warn('Current page is out of range.');
     }
 
     for (let rowIndex = fromIndex; rowIndex < count && rowIndex < toIndex; rowIndex++) {
@@ -122,13 +116,15 @@ class TableBody extends React.Component {
       return;
     }
 
-    // Don't trigger onRowClick if the event was actually a row selection
-    if (event.target.id && event.target.id.startsWith('MUIDataTableSelectCell')) {
-      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 && this.props.options.selectableRows !== 'none') {
+    if (
+      this.props.options.selectableRowsOnClick &&
+      this.props.options.selectableRows !== 'none' &&
+      this.isRowSelectable(data.dataIndex)
+    ) {
       const selectRow = { index: data.rowIndex, dataIndex: data.dataIndex };
       this.handleRowSelect(selectRow);
     }
@@ -138,6 +134,9 @@ class TableBody extends React.Component {
       this.props.toggleExpandRow(expandRow);
     }
 
+    // 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);
   };
 
@@ -163,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, {
@@ -188,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 02f033a..0ea4c2f 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -1,19 +1,19 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
+import { Grid, GridList, GridListTile, TextField } from '@material-ui/core';
 import Button from '@material-ui/core/Button';
-import Typography from '@material-ui/core/Typography';
+import Checkbox from '@material-ui/core/Checkbox';
 import FormControl from '@material-ui/core/FormControl';
-import FormGroup from '@material-ui/core/FormGroup';
 import FormControlLabel from '@material-ui/core/FormControlLabel';
-import InputLabel from '@material-ui/core/InputLabel';
+import FormGroup from '@material-ui/core/FormGroup';
 import Input from '@material-ui/core/Input';
+import InputLabel from '@material-ui/core/InputLabel';
+import ListItemText from '@material-ui/core/ListItemText';
 import MenuItem from '@material-ui/core/MenuItem';
 import Select from '@material-ui/core/Select';
-import Checkbox from '@material-ui/core/Checkbox';
-import ListItemText from '@material-ui/core/ListItemText';
 import { withStyles } from '@material-ui/core/styles';
-import { TextField, Grid, GridList, GridListTile } from '@material-ui/core';
+import Typography from '@material-ui/core/Typography';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
 
 export const defaultFilterStyles = theme => ({
   root: {
@@ -139,6 +139,10 @@ class TableFilter extends React.Component {
     this.props.onFilterUpdate(index, event.target.value, column, 'textField');
   };
 
+  handleCustomChange = (value, index, column) => {
+    this.props.onFilterUpdate(index, value, column.name, column.filterType);
+  };
+
   renderCheckbox(column, index) {
     const { classes, filterData, filterList } = this.props;
 
@@ -229,7 +233,7 @@ class TableFilter extends React.Component {
   }
 
   renderMultiselect(column, index) {
-    const { classes, filterData, filterList, options } = this.props;
+    const { classes, filterData, filterList } = this.props;
 
     return (
       <GridListTile key={index} cols={1}>
@@ -264,6 +268,26 @@ class TableFilter extends React.Component {
     );
   }
 
+  renderCustomField(column, index) {
+    const { classes, filterList, options } = this.props;
+    const display =
+      (column.filterOptions && column.filterOptions.display) ||
+      (options.filterOptions && options.filterOptions.display);
+
+    if (!display) {
+      console.error('Property "display" is required when using custom filter type.');
+      return;
+    }
+
+    return (
+      <GridListTile key={index} cols={1}>
+        <div className={classes.textFieldRoot}>
+          <FormControl key={index}>{display(filterList, this.handleCustomChange, index, column)}</FormControl>
+        </div>
+      </GridListTile>
+    );
+  }
+
   render() {
     const { classes, columns, options, onFilterReset } = this.props;
     const textLabels = options.textLabels.filter;
@@ -285,13 +309,14 @@ class TableFilter extends React.Component {
               className={classes.resetLink}
               tabIndex={0}
               aria-label={textLabels.reset}
+              data-testid={'filterReset-button'}
               onClick={onFilterReset}>
               {textLabels.reset}
             </Button>
           </div>
           <div className={classes.filtersSelected} />
         </div>
-        <GridList cellHeight="auto" cols={filterGridColumns} cellHeight={70} spacing={34}>
+        <GridList cellHeight="auto" cols={filterGridColumns} spacing={34}>
           {columns.map((column, index) => {
             if (column.filter) {
               const filterType = column.filterType || options.filterType;
@@ -301,6 +326,8 @@ class TableFilter extends React.Component {
                 ? this.renderMultiselect(column, index)
                 : filterType === 'textField'
                 ? this.renderTextField(column, index)
+                : filterType === 'custom'
+                ? this.renderCustomField(column, index)
                 : this.renderSelect(column, index);
             }
           })}
diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.js
index 4fa7df8..6866a23 100644
--- a/src/components/TableFilterList.js
+++ b/src/components/TableFilterList.js
@@ -1,7 +1,7 @@
-import React from 'react';
-import PropTypes from 'prop-types';
 import Chip from '@material-ui/core/Chip';
 import { withStyles } from '@material-ui/core/styles';
+import PropTypes from 'prop-types';
+import React from 'react';
 
 const defaultFilterListStyles = {
   root: {
@@ -23,7 +23,10 @@ class TableFilterList extends React.Component {
     filterListRenderers: PropTypes.array.isRequired,
     /** Columns used to describe table */
     columnNames: PropTypes.PropTypes.arrayOf(
-      PropTypes.oneOfType([PropTypes.string, PropTypes.shape({ name: PropTypes.string.isRequired })]),
+      PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.shape({ name: PropTypes.string.isRequired, filterType: PropTypes.string }),
+      ]),
     ).isRequired,
     /** Callback to trigger filter update */
     onFilterUpdate: PropTypes.func,
@@ -36,16 +39,27 @@ class TableFilterList extends React.Component {
 
     return (
       <div className={classes.root}>
-        {filterList.map((item, index) =>
-          item.map((data, colIndex) => (
+        {filterList.map((item, index) => {
+          if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
+            return (
+              <Chip
+                label={filterListRenderers[index](item)}
+                key={index}
+                onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
+                className={classes.chip}
+              />
+            );
+          }
+
+          return item.map((data, colIndex) => (
             <Chip
               label={filterListRenderers[index](data)}
               key={colIndex}
               onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'checkbox')}
               className={classes.chip}
             />
-          )),
-        )}
+          ));
+        })}
       </div>
     );
   }
diff --git a/src/components/TableHeadCell.js b/src/components/TableHeadCell.js
index 0708d38..a5aafba 100644
--- a/src/components/TableHeadCell.js
+++ b/src/components/TableHeadCell.js
@@ -31,6 +31,8 @@ const defaultHeadCellStyles = theme => ({
     display: 'flex',
     verticalAlign: 'top',
     cursor: 'pointer',
+  },
+  sortLabelRoot: {
     height: '10px',
   },
   sortActive: {
@@ -38,10 +40,16 @@ const defaultHeadCellStyles = theme => ({
   },
   toolButton: {
     display: 'flex',
-    height: '10px',
     outline: 'none',
     cursor: 'pointer',
   },
+  hintIconAlone: {
+    marginTop: '-3px',
+    marginLeft: '3px',
+  },
+  hintIconWithSortIcon: {
+    marginTop: '-3px',
+  },
 });
 
 class TableHeadCell extends React.Component {
@@ -77,6 +85,7 @@ class TableHeadCell extends React.Component {
     const sortActive = sortDirection !== null && sortDirection !== undefined ? true : false;
 
     const sortLabelProps = {
+      classes: { root: classes.sortLabelRoot },
       active: sortActive,
       hideSortIcon: true,
       ...(sortDirection ? { direction: sortDirection } : {}),
@@ -133,28 +142,32 @@ class TableHeadCell extends React.Component {
                     open={isHintTooltipOpen}
                     onOpen={() => this.setState({ isSortTooltipOpen: false, isHintTooltipOpen: true })}
                     onClose={() => this.setState({ isHintTooltipOpen: false })}>
-                    <HelpIcon fontSize="small" />
+                    <HelpIcon
+                      className={!sortActive ? classes.hintIconAlone : classes.hintIconWithSortIcon}
+                      fontSize="small"
+                    />
                   </Tooltip>
                 )}
               </div>
             </span>
           </Tooltip>
         ) : (
-          children
+          <div className={classes.sortAction}>
+            {children}
+            {hint && (
+              <Tooltip
+                title={hint}
+                placement={'bottom-end'}
+                classes={{
+                  tooltip: classes.tooltip,
+                }}
+                enterDelay={300}
+                classes={{ popper: classes.mypopper }}>
+                <HelpIcon className={classes.hintIconAlone} fontSize="small" />
+              </Tooltip>
+            )}
+          </div>
         )}
-        {!options.sort ||
-          (!sort && hint && (
-            <Tooltip
-              title={hint}
-              placement={'bottom-end'}
-              classes={{
-                tooltip: classes.tooltip,
-              }}
-              enterDelay={300}
-              classes={{ popper: classes.mypopper }}>
-              <HelpIcon fontSize="small" />
-            </Tooltip>
-          ))}
       </TableCell>
     );
   }
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..70d9bc4 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 => {
@@ -195,6 +225,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 +235,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 +249,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 +264,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 +284,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')}>
diff --git a/src/utils.js b/src/utils.js
index dac9967..ed93628 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -65,6 +65,11 @@ function createCSVDownload(columns, data, options) {
   const csv = options.onDownload
     ? options.onDownload(buildHead, buildBody, columns, data)
     : `${CSVHead}${CSVBody}`.trim();
+
+  if (options.onDownload && csv === false) {
+    return;
+  }
+
   const blob = new Blob([csv], { type: 'text/csv' });
 
   /* taken from react-csv */


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/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index 2fb80ac..1fb866e 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -235,6 +235,42 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable.', () => {
+    let selectedRowData;
+    const options = {
+      selectableRows: true,
+      selectableRowsOnClick: true,
+      isRowSelectable: dataIndex => (dataIndex === 2 ? false : true),
+    };
+    const selectRowUpdate = (type, data) => (selectedRowData = data);
+    const toggleExpandRow = spy();
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        selectRowUpdate={selectRowUpdate}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isUndefined(selectedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 0);
+  });
+
   it('should gather expanded row data when clicking row with expandableRows=true and expandableRowsOnClick=true.', () => {
     let expandedRowData;
     const options = { selectableRows: true, expandableRows: true, expandableRowsOnClick: true };
@@ -338,6 +374,36 @@ describe('<TableBody />', function() {
     assert.strictEqual(options.onRowClick.callCount, 0);
   });
 
+  it('should not call onRowClick when clicking to select a row', () => {
+    const options = { selectableRows: true, selectableRowsOnClick: true, onRowClick: spy() };
+    const selectRowUpdate = spy();
+    const toggleExpandRow = spy();
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        selectRowUpdate={selectRowUpdate}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('TableSelectCell')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowClick.callCount, 0);
+  });
+
   it('should call onRowClick when Row is clicked', () => {
     const options = { selectableRows: true, onRowClick: spy() };
     const selectRowUpdate = stub();
diff --git a/test/MUIDataTableFilter.test.js b/test/MUIDataTableFilter.test.js
index ab52813..7c2fcb2 100644
--- a/test/MUIDataTableFilter.test.js
+++ b/test/MUIDataTableFilter.test.js
@@ -1,13 +1,13 @@
-import React from 'react';
-import { spy, stub } from 'sinon';
-import { mount, shallow } from 'enzyme';
-import { assert, expect, should } from 'chai';
-import textLabels from '../src/textLabels';
-import Select from '@material-ui/core/Select';
 import Checkbox from '@material-ui/core/Checkbox';
+import Select from '@material-ui/core/Select';
 import TextField from '@material-ui/core/TextField';
-import TableFilter from '../src/components/TableFilter';
 import Typography from '@material-ui/core/Typography';
+import { assert } from 'chai';
+import { mount, shallow } from 'enzyme';
+import React from 'react';
+import { spy } from 'sinon';
+import TableFilter from '../src/components/TableFilter';
+import textLabels from '../src/textLabels';
 
 describe('<TableFilter />', function() {
   let data;
@@ -50,7 +50,7 @@ describe('<TableFilter />', function() {
     assert.deepEqual(labels, ['First Name', 'Company', 'City Label', 'State']);
   });
 
-  it("should data table filter view with checkboxes if filterType = 'checkbox'", () => {
+  it("should render data table filter view with checkboxes if filterType = 'checkbox'", () => {
     const options = { filterType: 'checkbox', textLabels };
     const filterList = [[], [], [], []];
     const shallowWrapper = mount(
@@ -61,20 +61,20 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 13);
   });
 
-  it('should data table filter view with no checkboxes if filter=false for each column', () => {
+  it('should render data table filter view with no checkboxes if filter=false for each column', () => {
     const options = { filterType: 'checkbox', textLabels };
     const filterList = [[], [], [], []];
     columns = columns.map(item => (item.filter = false));
 
-    const shallowWrapper = mount(
+    const mountWrapper = mount(
       <TableFilter columns={columns} filterData={filterData} filterList={filterList} options={options} />,
     );
 
-    const actualResult = shallowWrapper.find(Checkbox);
+    const actualResult = mountWrapper.find(Checkbox);
     assert.strictEqual(actualResult.length, 0);
   });
 
-  it("should data table filter view with selects if filterType = 'select'", () => {
+  it("should render data table filter view with selects if filterType = 'select'", () => {
     const options = { filterType: 'select', textLabels };
     const filterList = [['Joe James'], [], [], []];
 
@@ -86,7 +86,7 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 4);
   });
 
-  it('should data table filter view no selects if filter=false for each column', () => {
+  it('should render data table filter view no selects if filter=false for each column', () => {
     const options = { filterType: 'select', textLabels };
     const filterList = [['Joe James'], [], [], []];
     columns = columns.map(item => (item.filter = false));
@@ -99,7 +99,7 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
-  it("should data table filter view with checkbox selects if filterType = 'multiselect'", () => {
+  it("should render data table filter view with checkbox selects if filterType = 'multiselect'", () => {
     const options = { filterType: 'multiselect', textLabels };
     const filterList = [['Joe James', 'John Walsh'], [], [], []];
 
@@ -111,6 +111,31 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 4);
   });
 
+  it("should data table custom filter view with if filterType = 'custom' and a valid display filterOption is provided", () => {
+    const options = {
+      filterType: 'custom',
+      textLabels,
+      filterOptions: {
+        names: [],
+        logic(city, filters) {
+          return false;
+        },
+        display: (filterList, onChange, index, column) => (
+          <div>
+            <TextField id="custom-filter-render">Custom Filter Render</TextField>
+          </div>
+        ),
+      },
+    };
+    const filterList = [[], [], [], []];
+    const mountWrapper = mount(
+      <TableFilter columns={columns} filterData={filterData} filterList={filterList} options={options} />,
+    );
+
+    const actualResult = mountWrapper.find('#custom-filter-render');
+    assert.isAtLeast(actualResult.length, 1);
+  });
+
   it("should render column.label as filter label if filterType = 'textField'", () => {
     const options = { filterType: 'textField', textLabels };
     const filterList = [[], [], [], []];
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index 3d6bf24..dd9fcce 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -1,16 +1,15 @@
-import React from 'react';
-import { spy, stub } from 'sinon';
-import { mount, shallow } from 'enzyme';
-import { assert, expect, should } from 'chai';
 import IconButton from '@material-ui/core/IconButton';
-import SearchIcon from '@material-ui/icons/Search';
 import DownloadIcon from '@material-ui/icons/CloudDownload';
+import FilterIcon from '@material-ui/icons/FilterList';
 import PrintIcon from '@material-ui/icons/Print';
+import SearchIcon from '@material-ui/icons/Search';
 import ViewColumnIcon from '@material-ui/icons/ViewColumn';
-import ClearIcon from '@material-ui/icons/Clear';
-import FilterIcon from '@material-ui/icons/FilterList';
-import TableToolbar from '../src/components/TableToolbar';
+import { assert } from 'chai';
+import { mount, shallow } from 'enzyme';
+import React from 'react';
+import { spy } from 'sinon';
 import TableSearch from '../src/components/TableSearch';
+import TableToolbar from '../src/components/TableToolbar';
 import textLabels from '../src/textLabels';
 
 describe('<TableToolbar />', function() {