Repository: gregnb/mui-datatables
Pull request #975 (2019-10-03 05:27:59)

Upgrade testing performance and simplify
This test sometimes times out when the test suite is run due to the way it is building new array data. It turns out that we don't need a new data set for this test, as we can use smaller rows per page number to get the same result on the existing data set in the test suite.


Patch:
diff --git a/README.md b/README.md
index a99b7b7..4ae3c79 100644
--- a/README.md
+++ b/README.md
@@ -135,14 +135,16 @@ The component accepts the following props:
 |**`page`**|number||User provided starting page for pagination
 |**`count`**|number||User provided override for total number of rows
 |**`serverSide`**|boolean|false|Enable remote data source
+|**`serverSideFilterList`**|array|[]|Sets the filter list display when using serverSide: true
 |**`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
+|**`filterType`**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom')`
+|**`textLabels`**|object||User provided labels to localize text
 |**`pagination`**|boolean|true|Enable/disable pagination
 |**`selectableRows`**|string|'multiple'|Numbers of rows that can be selected. Options are "multiple", "single", "none".
 |**`selectableRowsOnClick`**|boolean|false|Enable/disable select toggle when row is clicked. When False, only checkbox will trigger this action.
-|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {[dataindex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
+|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {dataindex: boolean}, data: arrayOfObjects: {index, dataIndex})) => boolean`.
+|**`isRowExpandable`**|function||Enable/disable expansion or collapse on certain expandable rows with custom function. Will be considered true if not provided. `function(dataIndex: number, expandedRows: object(lookup: {dataIndex: number}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
 |**`selectableRowsHeader`**|boolean|true|Show/hide the select all/deselect all checkbox header for selectable rows
 |**`expandableRows`**|boolean|false|Enable/disable expandable rows
 |**`expandableRowsOnClick`**|boolean|false|Enable/disable expand trigger when row is clicked. When False, only expand icon will trigger this action.
@@ -155,6 +157,7 @@ The component accepts the following props:
 |**`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`
+|**`customFilterDialogFooter `**|function||Add a custom footer to the filter dialog. `customFilterDialogFooter(filterList: array) => 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', 'scrollMaxHeight' (limits height of table), 'scrollFullHeight' (table takes on as much height as needed to display all rows set in rowsPerPage)
@@ -166,6 +169,7 @@ The component accepts the following props:
 |**`sort`**|boolean|true|Enable/disable sort on all columns
 |**`filter`**|boolean|true|Show/hide filter icon from toolbar
 |**`search`**|boolean|true|Show/hide search icon from toolbar
+|**`searchOpen`**|boolean|false|Initially displays search bar  
 |**`searchText`**|string||Initial search text
 |**`searchPlaceholder`**|string||Search text placeholder. [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-search/index.js)
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
@@ -174,6 +178,7 @@ The component accepts the following props:
 |**`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`
+|**`onRowsExpand`**|function||Callback function that triggers when row(s) are expanded. `function(currentRowsExpanded: array, allRowsExpanded: array) => void`
 |**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: object(lookup: {[dataIndex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => void OR false` (Returning `false` prevents row deletion.)
 |**`onRowClick`**|function||Callback function that triggers when a row is clicked. `function(rowData: string[], rowMeta: { dataIndex: number, rowIndex: number }) => void`
 |**`onCellClick`**|function||Callback function that triggers when a cell is clicked. `function(colData: any, cellMeta: { colIndex: number, rowIndex: number, dataIndex: number }) => void`
@@ -181,8 +186,10 @@ The component accepts the following props:
 |**`onChangeRowsPerPage`**|function||Callback function that triggers when the number of rows per page has changed. `function(numberOfRows: number) => void`
 |**`onSearchChange`**|function||Callback function that triggers when the search text value has changed. `function(searchText: string) => void`
 |**`onSearchOpen`**|function||Callback function that triggers when the searchbox opens. `function() => void`
+|**`onFilterDialogOpen`**|function||Callback function that triggers when the filter dialog opens. `function() => void`
+|**`onFilterDialogClose`**|function||Callback function that triggers when the filter dialog closes. `function() => void`
+|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array, type: enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom', 'chip', 'reset')) => void`
 |**`onSearchClose`**|function||Callback function that triggers when the searchbox closes. `function() => void`
-|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array) => void`
 |**`onColumnSortChange`**|function||Callback function that triggers when a column has been sorted. `function(changedColumn: string, direction: string) => void`
 |**`onColumnViewChange`**|function||Callback function that triggers when a column view has been changed. `function(changedColumn: string, action: string) => void`
 |**`onTableChange`**|function||Callback function that triggers when table state has changed. `function(action: string, tableState: object) => void`
diff --git a/examples/examples.js b/examples/examples.js
index 59972ed..6746175 100644
--- a/examples/examples.js
+++ b/examples/examples.js
@@ -22,6 +22,7 @@ import OnDownload from "./on-download";
 import OnTableInit from "./on-table-init";
 import ResizableColumns from "./resizable-columns";
 import SelectableRows from "./selectable-rows";
+import ServerSideFilters from "./serverside-filters";
 import ServerSideOptions from "./serverside-options";
 import ServerSidePagination from "./serverside-pagination";
 import Simple from "./simple";
@@ -38,8 +39,8 @@ export default {
     'Column Option Update': ColumnOptionsUpdate,
     'Component': Component,
     'CSV Export': CSVExport,
-    'Custom ActionColumns': CustomActionColumns,
-    'Customize columns': CustomizeColumns,
+    'Custom Action Columns': CustomActionColumns,
+    'Customize Columns': CustomizeColumns,
     'Customize Filter': CustomizeFilter,
     'Customize Footer': CustomizeFooter,
     'Customize Rows': CustomizeRows,
@@ -50,15 +51,16 @@ export default {
     'Customize Toolbar': CustomizeToolbar,
     'Customize Toolbar Select': CustomizeToolbarSelect,
     'Data As Objects': DataAsObjects,
-    'expandableRows': ExpandableRows,
-    'fixedHeader': FixedHeader,
+    'Expandable Rows': ExpandableRows,
+    'Fixed Header': FixedHeader,
     'Hide Columns Print': HideColumnsPrint,
     'OnDownload': OnDownload,
     'OnTableInit': OnTableInit,
-    'resizableColumns': ResizableColumns,
-    'selectableRows': SelectableRows,
-    'serverSide Options': ServerSideOptions,
-    'serverSide Pagination': ServerSidePagination,
+    'Resizable Columns': ResizableColumns,
+    'Selectable Rows': SelectableRows,
+    'ServerSide Filters': ServerSideFilters,
+    'ServerSide Options': ServerSideOptions,
+    'ServerSide Pagination': ServerSidePagination,
     'Simple': Simple,
     'Simple No Toolbar': SimpleNoToolbar,
     'Text Localization': TextLocalization,
diff --git a/examples/expandable-rows/index.js b/examples/expandable-rows/index.js
index ba67d66..4ef1dca 100644
--- a/examples/expandable-rows/index.js
+++ b/examples/expandable-rows/index.js
@@ -82,7 +82,12 @@ class Example extends React.Component {
       responsive: 'scrollMaxHeight',
       expandableRows: true,
       expandableRowsOnClick: true,
-      rowsExpanded: [0, 2, 3],
+      isRowExpandable: (dataIndex, expandedRows) => {
+        // Prevent expand/collapse of any row after the 5th (but allow those already expanded to be collapsed)
+        if (expandedRows.data.length > 4 && expandedRows.data.filter(d => d.dataIndex === dataIndex).length === 0) return false;
+        return true;
+      },
+      rowsExpanded: [0, 1],
       renderExpandableRow: (rowData, rowMeta) => {
         const colSpan = rowData.length + 1;
         return (
@@ -92,7 +97,8 @@ class Example extends React.Component {
             </TableCell>
           </TableRow>
         );
-      }
+      },
+      onRowsExpand: (curExpanded, allExpanded) => console.log(curExpanded, allExpanded)
     };
 
     return (
diff --git a/examples/serverside-filters/index.js b/examples/serverside-filters/index.js
new file mode 100644
index 0000000..6ebfaf7
--- /dev/null
+++ b/examples/serverside-filters/index.js
@@ -0,0 +1,196 @@
+import { Button, CircularProgress } from '@material-ui/core';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src';
+
+class Example extends React.Component {
+  state = {
+    serverSideFilterList: [],
+    filters: [[], [], [], [], []],
+    isLoading: false,
+    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', 'Chicago', 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'],
+    ]
+  };
+
+  // mock async function
+  xhrRequest = (url, filterList) => {
+    return new Promise(resolve => {
+      window.setTimeout(
+        () => {
+          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', 'Chicago', 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 newData = [
+            ['Lane Wilson', 'Commercial Specialist', 'Chicago', 19, '$65,000'],
+            ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000']
+          ];
+
+          if (
+            !filterList[0].length
+            && !filterList[1].length
+            && !filterList[2].length
+            && !filterList[3].length
+            && !filterList[4].length
+          ) {
+            resolve({ data });
+          } else {
+            resolve({ data: newData });
+          }
+        },
+        2000
+      );
+    });
+  }
+
+  handleFilterSubmit = filterList => () => {
+    console.log('Submitting filters: ', filterList);
+
+    this.setState({ isLoading: true, filters: filterList });
+
+    // fake async request
+    this.xhrRequest(`/myApiServer?filters=${filterList}`, filterList).then(res => {
+      this.setState({ isLoading: false, data: res.data, serverSideFilterList: filterList });
+    });
+  };
+
+  render() {
+    const columns = [
+      {
+        name: 'Name',
+        options: {
+          filter: true,
+          filterList: this.state.filters[0],
+        },
+      },
+      {
+        label: 'Title',
+        name: 'Title',
+        options: {
+          filter: true,
+          filterList: this.state.filters[1],
+        },
+      },
+      {
+        name: 'Location',
+        options: {
+          filter: true,
+          filterList: this.state.filters[2],
+        },
+      },
+      {
+        name: 'Age',
+        options: {
+          filter: true,
+          filterList: this.state.filters[3],
+        },
+      },
+      {
+        name: 'Salary',
+        options: {
+          filter: true,
+          filterList: this.state.filters[4],
+        },
+      },
+    ];
+
+    const options = {
+      filter: true,
+      serverSideFilterList: this.state.serverSideFilterList,
+      filterType: 'dropdown',
+      responsive: 'scrollMaxHeight',
+      serverSide: true,
+      onFilterDialogOpen: () => {
+        console.log('filter dialog opened');
+      },
+      onFilterDialogClose: () => {
+        console.log('filter dialog closed');
+      },
+      onFilterChange: (column, filterList, type) => {
+        if (type === 'chip') {
+          console.log('updating filters via chip');
+          this.handleFilterSubmit(filterList)();
+        }
+      },
+      customFilterDialogFooter: filterList => {
+        return (
+          <div style={{ marginTop: '40px' }}>
+            <Button variant="contained" onClick={this.handleFilterSubmit(filterList)}>Apply Filters*</Button>
+            <p><em>*(Simulates selecting "Chicago" from "Location")</em></p>
+          </div>
+        );
+      }
+    };
+
+    return (
+      <React.Fragment>
+        {this.state.isLoading && (
+          <div style={{ position: 'absolute', top: '50%', left: '50%' }}>
+            <CircularProgress />
+          </div>
+        )}
+        <MUIDataTable title={'ACME Employee list'} data={this.state.data} columns={columns} options={options} />
+      </React.Fragment>
+    );
+  }
+}
+
+export default Example;
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index d0fe3d5..b466dd8 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -142,14 +142,20 @@ class MUIDataTable extends React.Component {
       customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customRowRender: PropTypes.func,
+      customFilterDialogFooter: PropTypes.func,
       onRowClick: PropTypes.func,
+      onRowsExpand: PropTypes.func,
       onRowsSelect: PropTypes.func,
       resizableColumns: PropTypes.bool,
       selectableRows: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['none', 'single', 'multiple'])]),
       selectableRowsOnClick: PropTypes.bool,
       isRowSelectable: PropTypes.func,
+      isRowExpandable: PropTypes.func,
       selectableRowsHeader: PropTypes.bool,
       serverSide: PropTypes.bool,
+      onFilterChange: PropTypes.func,
+      onFilterDialogOpen: PropTypes.func,
+      onFilterDialogClose: PropTypes.func,
       onTableChange: PropTypes.func,
       onTableInit: PropTypes.func,
       caseSensitive: PropTypes.bool,
@@ -166,6 +172,7 @@ class MUIDataTable extends React.Component {
       customSort: PropTypes.func,
       customSearch: PropTypes.func,
       search: PropTypes.bool,
+      searchOpen: PropTypes.bool,
       searchText: PropTypes.string,
       searchPlaceholder: PropTypes.string,
       print: PropTypes.bool,
@@ -262,7 +269,7 @@ class MUIDataTable extends React.Component {
       return;
     });
 
-    this.handleOptionDeprecation(props);
+    this.handleOptionDeprecation();
   }
 
   initializeTable(props) {
@@ -278,6 +285,7 @@ class MUIDataTable extends React.Component {
     filterType: 'dropdown',
     pagination: true,
     textLabels,
+    serverSideFilterList: [],
     expandableRows: false,
     expandableRowsOnClick: false,
     resizableColumns: false,
@@ -304,19 +312,19 @@ class MUIDataTable extends React.Component {
     },
   });
 
-  handleOptionDeprecation = props => {
-    if (typeof props.options.selectableRows === 'boolean') {
+  handleOptionDeprecation = () => {
+    if (typeof this.options.selectableRows === 'boolean') {
       console.error(
         'Using a boolean for selectableRows has been deprecated. Please use string option: multiple | single | none',
       );
-      this.options.selectableRows = props.options.selectableRows ? 'multiple' : 'none';
+      this.options.selectableRows = this.options.selectableRows ? 'multiple' : 'none';
     }
-    if (['scrollMaxHeight', 'scrollFullHeight', 'stacked'].indexOf(props.options.responsive) === -1) {
+    if (['scrollMaxHeight', 'scrollFullHeight', 'stacked'].indexOf(this.options.responsive) === -1) {
       console.error(
         'Invalid option value for responsive. Please use string option: scrollMaxHeight | scrollFullHeight | stacked',
       );
     }
-    if (props.options.responsive === 'scroll') {
+    if (this.options.responsive === 'scroll') {
       console.error('This option has been deprecated. It is being replaced by scrollMaxHeight');
     }
   };
@@ -413,7 +421,7 @@ class MUIDataTable extends React.Component {
             column.options.sortDirection = 'none';
           }
 
-          if (column.options.sortDirection !== undefined) {
+          if (column.options.sortDirection !== undefined && column.options.sortDirection !== 'none') {
             if (sortDirectionSet) {
               console.error('sortDirection is set for more than one column. Only the first column will be considered.');
               column.options.sortDirection = 'none';
@@ -629,7 +637,7 @@ class MUIDataTable extends React.Component {
     let isFiltered = false;
     let isSearchFound = false;
     let displayRow = [];
-    const data = this.state.data.length ? this.state.data : this.props.data;
+
     for (let index = 0; index < row.length; index++) {
       let columnDisplay = row[index];
       let columnValue = row[index];
@@ -946,7 +954,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('resetFilters');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(null, this.state.filterList);
+          this.options.onFilterChange(null, this.state.filterList, 'reset');
         }
       },
     );
@@ -962,6 +970,9 @@ class MUIDataTable extends React.Component {
           case 'checkbox':
             filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
             break;
+          case 'chip':
+            filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
+            break;
           case 'multiselect':
             filterList[index] = value === '' ? [] : value;
             break;
@@ -987,7 +998,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('filterChange');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(column, this.state.filterList);
+          this.options.onFilterChange(column, this.state.filterList, type);
         }
       },
     );
@@ -1020,31 +1031,43 @@ class MUIDataTable extends React.Component {
 
   toggleExpandRow = row => {
     const { dataIndex } = row;
-    let expandedRows = [...this.state.expandedRows.data];
-    let rowPos = -1;
-
-    for (let cIndex = 0; cIndex < expandedRows.length; cIndex++) {
-      if (expandedRows[cIndex].dataIndex === dataIndex) {
-        rowPos = cIndex;
+    const { isRowExpandable } = this.options;
+    let { expandedRows } = this.state;
+    const expandedRowsData = [...expandedRows.data];
+    let shouldCollapseExpandedRow = false;
+    let hasRemovedRow = false;
+    let removedRow = [];
+
+    for (var cIndex = 0; cIndex < expandedRowsData.length; cIndex++) {
+      if (expandedRowsData[cIndex].dataIndex === dataIndex) {
+        shouldCollapseExpandedRow = true;
         break;
       }
     }
 
-    if (rowPos >= 0) {
-      expandedRows.splice(rowPos, 1);
+    if (shouldCollapseExpandedRow) {
+      if ((isRowExpandable && isRowExpandable(dataIndex, expandedRows) || !isRowExpandable)) {
+        removedRow = expandedRowsData.splice(cIndex, 1);
+        hasRemovedRow = true;
+      }
     } else {
-      expandedRows.push(row);
+      if (isRowExpandable && isRowExpandable(dataIndex, expandedRows)) expandedRowsData.push(row);
+      else if (!isRowExpandable) expandedRowsData.push(row);
     }
 
     this.setState(
       {
+        curExpandedRows: hasRemovedRow ? removedRow : [row],
         expandedRows: {
-          lookup: buildMap(expandedRows),
-          data: expandedRows,
+          lookup: buildMap(expandedRowsData),
+          data: expandedRowsData,
         },
       },
       () => {
         this.setTableAction('expandRow');
+        if (this.options.onRowsExpand) {
+          this.options.onRowsExpand(this.state.curExpandedRows, this.state.expandedRows.data);
+        }
       },
     );
   };
@@ -1223,6 +1246,7 @@ class MUIDataTable extends React.Component {
       previousSelectedRow,
       expandedRows,
       searchText,
+      serverSideFilterList,
     } = this.state;
 
     const rowCount = this.state.count || displayData.length;
@@ -1282,6 +1306,7 @@ class MUIDataTable extends React.Component {
         )}
         <TableFilterList
           options={this.options}
+          serverSideFilterList={this.props.options.serverSideFilterList || []}
           filterListRenderers={columns.map(c => {
             return c.customFilterListRender ? c.customFilterListRender : f => f;
           })}
@@ -1333,7 +1358,6 @@ class MUIDataTable extends React.Component {
           options={this.options}
           page={page}
           rowCount={rowCount}
-          rowsPerPageOptions={this.options.rowsPerPageOptions}
           rowsPerPage={rowsPerPage}
           changeRowsPerPage={this.changeRowsPerPage}
           changePage={this.changePage}
diff --git a/src/components/Popover.js b/src/components/Popover.js
index a912316..f51d97c 100644
--- a/src/components/Popover.js
+++ b/src/components/Popover.js
@@ -78,7 +78,8 @@ class Popover extends React.Component {
           ref={el => this.popoverEl}
           anchorOrigin={anchorOriginSpecs}
           transformOrigin={transformOriginSpecs}
-          {...providedProps}>
+          {...providedProps}
+        >
           {content}
         </MuiPopover>
         {triggerEl}
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index efa91e9..a8ed200 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -29,6 +29,8 @@ class TableBody extends React.Component {
     filterList: PropTypes.array,
     /** Callback to execute when row is clicked */
     onRowClick: PropTypes.func,
+    /** Table rows expanded */
+    expandedRows: PropTypes.object,
     /** Table rows selected */
     selectedRows: PropTypes.object,
     /** Callback to trigger table row select */
@@ -100,6 +102,15 @@ class TableBody extends React.Component {
     }
   }
 
+  isRowExpandable(dataIndex) {
+    const { options, expandedRows } = this.props;
+    if (options.isRowExpandable) {
+      return options.isRowExpandable(dataIndex, expandedRows);
+    } else {
+      return true;
+    }
+  }
+
   handleRowSelect = (data, event) => {
     let shiftKey = event && event.nativeEvent ? event.nativeEvent.shiftKey : false;
     let shiftAdjacentRows = [];
@@ -177,7 +188,11 @@ class TableBody extends React.Component {
       this.handleRowSelect(selectRow, event);
     }
     // Check if we should trigger row expand when row is clicked anywhere
-    if (this.props.options.expandableRowsOnClick && this.props.options.expandableRows) {
+    if (
+      this.props.options.expandableRowsOnClick &&
+      this.props.options.expandableRows &&
+      this.isRowExpandable(data.dataIndex, this.props.expandedRows)
+    ) {
       const expandRow = { index: data.rowIndex, dataIndex: data.dataIndex };
       this.props.toggleExpandRow(expandRow);
     }
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index b0ed72e..7975360 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -266,7 +266,7 @@ class TableFilter extends React.Component {
   }
 
   render() {
-    const { classes, columns, options, onFilterReset } = this.props;
+    const { classes, columns, options, onFilterReset, customFooter, filterList } = this.props;
     const textLabels = options.textLabels.filter;
     const filterGridColumns = columns.filter(col => col.filter).length === 1 ? 1 : 2;
 
@@ -309,6 +309,7 @@ class TableFilter extends React.Component {
             }
           })}
         </GridList>
+        {customFooter ? customFooter(filterList) : ''}
       </div>
     );
   }
diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.js
index 6866a23..61d3c0b 100644
--- a/src/components/TableFilterList.js
+++ b/src/components/TableFilterList.js
@@ -35,30 +35,42 @@ class TableFilterList extends React.Component {
   };
 
   render() {
-    const { classes, filterList, filterUpdate, filterListRenderers, columnNames } = this.props;
+    const { classes, filterList, filterUpdate, filterListRenderers, columnNames, serverSideFilterList } = this.props;
+    const { serverSide } = this.props.options;
+
+    const customFilterChip = (item, index) => (
+      <Chip
+        label={filterListRenderers[index](item)}
+        key={index}
+        onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
+        className={classes.chip}
+      />
+    );
+
+    const filterChip = (index, data, colIndex) => (
+      <Chip
+        label={filterListRenderers[index](data)}
+        key={colIndex}
+        onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'chip')}
+        className={classes.chip}
+      />
+    );
 
     return (
       <div className={classes.root}>
-        {filterList.map((item, index) => {
+        {serverSide ? serverSideFilterList.map((item, index) => {
+          if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
+            return customFilterChip(item, index);
+          }
+
+          return item.map((data, colIndex) => filterChip(index, 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 customFilterChip(item, index);
           }
 
-          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}
-            />
-          ));
+          return item.map((data, colIndex) => filterChip(index, data, colIndex));
         })}
       </div>
     );
diff --git a/src/components/TableHead.js b/src/components/TableHead.js
index fd1a53c..ab2d05d 100644
--- a/src/components/TableHead.js
+++ b/src/components/TableHead.js
@@ -30,7 +30,7 @@ class TableHead extends React.Component {
   };
 
   render() {
-    const { classes, columns, count, options, data, page, setCellRef, selectedRows } = this.props;
+    const { classes, columns, count, options, data, setCellRef, selectedRows } = this.props;
 
     const numSelected = (selectedRows && selectedRows.data.length) || 0;
     const isDeterminate = numSelected > 0 && numSelected < count;
diff --git a/src/components/TableSelectCell.js b/src/components/TableSelectCell.js
index d044dc6..491fffc 100644
--- a/src/components/TableSelectCell.js
+++ b/src/components/TableSelectCell.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import Checkbox from '@material-ui/core/Checkbox';
 import TableCell from '@material-ui/core/TableCell';
+import IconButton from '@material-ui/core/IconButton';
 import { withStyles } from '@material-ui/core/styles';
 import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
 
@@ -107,7 +108,7 @@ class TableSelectCell extends React.Component {
     return (
       <TableCell className={cellClass} padding="checkbox">
         <div style={{ display: 'flex', alignItems: 'center' }}>
-          {expandableOn && <KeyboardArrowRight id="expandable-button" className={iconClass} onClick={onExpand} />}
+          {expandableOn && <IconButton onClick={onExpand} disabled={isHeaderCell} ><KeyboardArrowRight id="expandable-button" className={iconClass} /></IconButton>}
           {selectableOn !== 'none' && renderCheckBox()}
         </div>
       </TableCell>
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 1f264de..a7ef8c4 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -80,7 +80,7 @@ export const defaultToolbarStyles = theme => ({
 class TableToolbar extends React.Component {
   state = {
     iconActive: null,
-    showSearch: Boolean(this.props.searchText || this.props.options.searchText),
+    showSearch: Boolean(this.props.searchText || this.props.options.searchText || this.props.options.searchOpen),
     searchText: this.props.searchText || null,
   };
 
@@ -98,15 +98,19 @@ class TableToolbar extends React.Component {
     if (options.downloadOptions && options.downloadOptions.filterOptions) {
       // check rows first:
       if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
-        dataToDownload = displayData.map(row => {
+        dataToDownload = displayData.map((row, index) => {
           let i = -1;
 
+          // Help to preserve sort order in custom render columns
+          row.index = index;
+
           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;
+              // if we have a custom render, which will appear as a react element, we must grab the actual value from data
+              // TODO: Create a utility function for checking whether or not something is a react object
+              return typeof column === 'object' && column !== null && !Array.isArray(column) ? data[row.index].data[i] : column;
             }),
           };
         });
@@ -126,10 +130,26 @@ class TableToolbar extends React.Component {
   };
 
   setActiveIcon = iconName => {
-    this.setState(() => ({
+    this.setState(prevState => ({
       showSearch: this.isSearchShown(iconName),
       iconActive: iconName,
-    }));
+      prevIconActive: prevState.iconActive
+    }), () => {
+      const { iconActive, prevIconActive } = this.state;
+
+      if (iconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogOpen');
+        if (this.props.options.onFilterDialogOpen) {
+          this.props.options.onFilterDialogOpen();
+        }
+      }
+      if (iconActive === undefined && prevIconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogClose');
+        if (this.props.options.onFilterDialogClose) {
+          this.props.options.onFilterDialogClose();
+        }
+      }
+    });
   };
 
   isSearchShown = iconName => {
@@ -301,6 +321,7 @@ class TableToolbar extends React.Component {
               }
               content={
                 <TableFilter
+                  customFooter={options.customFilterDialogFooter}
                   columns={columns}
                   options={options}
                   filterList={filterList}


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index c6265ec..4dfa498 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -606,6 +606,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -627,6 +628,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -638,6 +640,55 @@ describe('<MUIDataTable />', function() {
     assert.strictEqual(actualResult.prop('label'), 'Name: Joe James');
   });
 
+  it('should render filter Chip(s) when options.serverSide = true and serverSideFilterList is populated', () => {
+    const serverSideFilterList = [['Joe James'], [], [], [], []];
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={serverSideFilterList}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 1);
+  });
+
+  it('should not render filter Chip(s) when options.serverSide = true and serverSideFilterList is not populated', () => {
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={[]}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 0);
+  });
+
   it('should remove entry from filterList when calling filterUpdate method with type=dropdown and an empty array', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
     const table = shallowWrapper.dive();
@@ -902,15 +953,16 @@ describe('<MUIDataTable />', function() {
   });
 
   it('should recalculate page when calling changeRowsPerPage method', () => {
-    const data = new Array(29).fill('').map(() => ['Joe James', 'Test Corp', 'Yonkers', 'NY']);
-    const mountWrapper = mount(shallow(<MUIDataTable columns={columns} data={data} />).get(0));
+    const mountWrapper = mount(shallow(<MUIDataTable columns={columns} data={data} options={{ rowsPerPage: 2 }} />).get(0));
     const instance = mountWrapper.instance();
 
-    instance.changePage(2);
-    instance.changeRowsPerPage(15);
-
-    const state = mountWrapper.state();
+    instance.changePage(1);
+    let state = mountWrapper.state();
     assert.equal(state.page, 1);
+
+    instance.changeRowsPerPage(4);
+    state = mountWrapper.state();
+    assert.equal(state.page, 0);
   });
 
   it('should update page position when calling changePage method', () => {
@@ -1109,6 +1161,32 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(JSON.stringify(newDisplayData), expectedResult);
   });
 
+  it('should call onRowsExpand when row is expanded or collapsed', () => {
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      onRowsExpand: spy()
+    };
+    const mountWrapper = mount(<MUIDataTable columns={columns} data={data} options={options} />);
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 1);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], [{ index: 2, dataIndex: 2 }]));
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 2);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], []));
+  });
+
   it('should not remove selected data on selectRowDelete when type=cell when onRowsDelete returns false', () => {
     const options = {
       onRowsDelete: () => false,
diff --git a/test/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index 71a0423..c105eec 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -307,12 +307,47 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should not gather expanded row data when clicking row with expandableRowsOnClick=true when it is disabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? false : true,
+    };
+    const toggleExpandRow = spy((_, data) => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isUndefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 0);
+  });
+
   it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable via selectedRows.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -347,12 +382,12 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
-  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via index.', () => {
+  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via dataIndex.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -387,6 +422,41 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should gather expanded row data when clicking row with expandableRowsOnClick=true when it is enabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? true : false,
+    };
+    const toggleExpandRow = spy(data => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isDefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 1);
+  });
+
   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 };
diff --git a/test/MUIDataTableFilter.test.js b/test/MUIDataTableFilter.test.js
index 7c2fcb2..c5b3cb6 100644
--- a/test/MUIDataTableFilter.test.js
+++ b/test/MUIDataTableFilter.test.js
@@ -186,6 +186,27 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 13);
   });
 
+  it('should render a filter dialog with custom footer when customFooter is provided', () => {
+    const CustomFooter = () => <div id="custom-footer">customFooter</div>;
+    const options = { textLabels };
+    const filterList = [[], [], [], []];
+    const onFilterUpdate = spy();
+
+    const shallowWrapper = shallow(
+      <TableFilter
+        customFooter={CustomFooter}
+        columns={columns}
+        onFilterUpdate={onFilterUpdate}
+        filterData={filterData}
+        filterList={filterList}
+        options={options}
+      />,
+    ).dive();
+
+    const actualResult = shallowWrapper.find('#custom-footer');
+    assert.strictEqual(actualResult.length, 1);
+  });
+
   it('should trigger onFilterUpdate prop callback when calling method handleCheckboxChange', () => {
     const options = { filterType: 'checkbox', textLabels };
     const filterList = [[], [], [], []];
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index 32405ed..9e5a76f 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -79,6 +79,16 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.props().options.searchText, 'searchText');
   });
 
+  it('should render a toolbar with search if option.searchOpen = true', () => {
+    const newOptions = { ...options, searchOpen: true };
+    const mountWrapper = mount(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    );
+    const actualResult = mountWrapper.find(TableSearch);
+    assert.strictEqual(actualResult.length, 1);
+    assert.strictEqual(actualResult.props().options.searchText, undefined);
+  });
+
   it('should render a toolbar with no search icon if option.search = false', () => {
     const newOptions = { ...options, search: false };
     const mountWrapper = mount(
@@ -182,6 +192,36 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
+  it('should call onFilterDialogOpen when opening filters via toolbar', () => {
+    const onFilterDialogOpen = spy();
+    const newOptions = { ...options, onFilterDialogOpen };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogOpen.callCount, 1);
+  });
+
+  it('should call onFilterDialogClose when closing filters dialog', () => {
+    const onFilterDialogClose = spy();
+    const newOptions = { ...options, onFilterDialogClose };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+    instance.setActiveIcon(undefined);
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogClose.callCount, 1);
+  });
+
   it('should set icon when calling method setActiveIcon', () => {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,