diff --git a/package-lock.json b/package-lock.json
index 469556094..9e2b91142 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7704,6 +7704,11 @@
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
"integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
},
+ "lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -7760,6 +7765,11 @@
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw="
},
+ "lodash.orderby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz",
+ "integrity": "sha1-5pfwTOXXhSL1TZM4syuBozk+TrM="
+ },
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
@@ -7806,6 +7816,11 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
"dev": true
},
+ "lodash.without": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz",
+ "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw="
+ },
"lolex": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz",
diff --git a/package.json b/package.json
index 4a2fad7bf..7d955d908 100644
--- a/package.json
+++ b/package.json
@@ -41,8 +41,11 @@
"enzyme-adapter-react-16": "^1.1.1",
"fecha": "^2.3.0",
"lodash.flow": "^3.5.0",
+ "lodash.includes": "^4.3.0",
"lodash.noop": "^3.0.1",
"lodash.range": "^3.2.0",
+ "lodash.orderby": "^4.6.0",
+ "lodash.without": "^4.4.0",
"prop-types": "^15.5.10",
"react-fontawesome": "^1.4.0",
"react-onclickoutside": "^6.7.1",
diff --git a/src/components/UncontrolledTable.js b/src/components/UncontrolledTable.js
new file mode 100644
index 000000000..25ed887f5
--- /dev/null
+++ b/src/components/UncontrolledTable.js
@@ -0,0 +1,201 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import includes from 'lodash.includes';
+import orderBy from 'lodash.orderby';
+import without from 'lodash.without';
+import Button from './Button';
+import Icon from './Icon';
+import Paginator from './Paginator';
+import SortableTable from './SortableTable';
+
+export default class UncontrolledTable extends React.Component {
+ static propTypes = {
+ ...SortableTable.propTypes,
+ pageSize: PropTypes.number,
+ sort: PropTypes.shape({
+ column: PropTypes.string,
+ ascending: PropTypes.bool
+ })
+ }
+
+ static defaultProps = {
+ ...SortableTable.defaultProps,
+ pageSize: 10,
+ sort: {
+ ascending: true
+ }
+ }
+
+ state = {
+ sort: this.props.sort,
+ expanded: [],
+ selected: [],
+ page: 0
+ }
+
+ sortedData = (rows, column, ascending) => orderBy(
+ rows,
+ [column],
+ [ascending ? 'asc' : 'desc']
+ );
+
+ sortBy = (column, ascending) => {
+ if (this.state.sort.column === column) {
+ this.setState({
+ sort: {
+ column,
+ ascending
+ }
+ });
+ } else {
+ this.setState({
+ sort: {
+ column,
+ ascending: true
+ }
+ });
+ }
+ };
+
+ get someSelected() {
+ return this.state.selected.length > 0;
+ }
+
+ get allSelected() {
+ return this.props.rows.length && (this.state.selected.length === this.props.rows.length);
+ }
+
+ selected(value) {
+ return includes(this.state.selected, value);
+ }
+
+ toggleSelection = (value) => {
+ const selected = this.state.selected;
+ const newSelection = includes(selected, value) ?
+ without(selected, value) :
+ [...selected, value];
+
+ this.setState({ selected: newSelection }, () => this.props.onSelect(newSelection));
+ };
+
+ toggleAll = () => {
+ const newSelection = this.allSelected ? [] : this.props.rows;
+
+ this.setState({ selected: newSelection },
+ this.props.onSelect(newSelection)
+ );
+ };
+
+ expanded(value) {
+ return includes(this.state.expanded, value);
+ }
+
+ toggleExpanded = (value) => {
+ const expanded = this.state.expanded;
+ const newExpanded = includes(expanded, value) ?
+ without(expanded, value) :
+ [...expanded, value];
+
+ this.setState({ expanded: newExpanded });
+ };
+
+ setPage = (page) => {
+ this.setState({ page });
+ }
+
+ componentWillReceiveProps(nextProps) {
+ // Clear selection if rows or selectable change
+ if (nextProps.rows !== this.props.rows ||
+ nextProps.selectable !== this.props.selectable) {
+ this.setState({ selected: [] });
+ }
+ if (nextProps.rows !== this.props.rows ||
+ nextProps.expandable !== this.props.expandable) {
+ this.setState({ expanded: [] });
+ }
+ }
+
+ render() {
+ const { page, sort } = this.state;
+ const { ascending, column } = sort;
+ const { columns, expandable, pageSize, paginated, rowClassName, rowExpanded, rows, selectable, onSelect, ...props } = this.props;
+ const cols = columns
+ .filter(col => !col.hidden)
+ .map(col => (col.sortable !== false) ?
+ {
+ active: column === col.key,
+ ascending,
+ onSort: asc => this.sortBy(col.key, asc),
+ ...col
+ } : col
+ );
+
+ if (selectable) {
+ cols.unshift({
+ align: 'center',
+ key: 'select',
+ header: (
+
+ ),
+ cell: row => (
+ this.toggleSelection(row)}
+ />
+ ),
+ width: '1%'
+ });
+ }
+
+ if (expandable) {
+ cols.push({
+ align: 'center',
+ key: 'expand',
+ cell: row => (
+
+ ),
+ width: '1%'
+ });
+ }
+
+ const start = page * pageSize;
+ const end = start + pageSize;
+ const sortedRows = this.sortedData(rows, column, ascending);
+ const visibleRows = paginated ? sortedRows.slice(start, end) : sortedRows;
+
+ return (
+
+
classnames({ 'table-info': this.selected(row) }, rowClassName(row))}
+ rowExpanded={row => expandable && this.expanded(row) && rowExpanded(row)}
+ />
+ {paginated && [
+
,
+ this.setPage(pg - 1)}
+ perPage={pageSize}
+ totalItems={rows.length}
+ />
+ ]}
+
+ );
+ }
+}
diff --git a/src/index.js b/src/index.js
index 6507990e3..53563320b 100755
--- a/src/index.js
+++ b/src/index.js
@@ -126,6 +126,7 @@ import SummaryBoxItem from './components/SummaryBoxItem.js';
import Table from './components/Table.js';
import TimeInput from './components/TimeInput.js';
import Tooltip from './components/Tooltip.js';
+import UncontrolledTable from './components/UncontrolledTable.js';
import ValidatedFormGroup from './components/ValidatedFormGroup.js';
import Waiting from './components/Waiting.js';
@@ -261,6 +262,7 @@ export {
SummaryBoxItem,
TimeInput,
Tooltip,
+ UncontrolledTable,
ValidatedFormGroup,
Waiting,
};
diff --git a/stories/SortableTable.js b/stories/SortableTable.js
deleted file mode 100644
index 1362626c2..000000000
--- a/stories/SortableTable.js
+++ /dev/null
@@ -1,294 +0,0 @@
-import React from 'react';
-import { storiesOf } from '@storybook/react';
-import { boolean, select } from '@storybook/addon-knobs';
-import { action } from '@storybook/addon-actions';
-import fecha from 'fecha';
-import { orderBy } from 'lodash';
-import { Button, Card, SortableTable } from '../src';
-
-const DATA = [
- { key: '111', expanded: false, first: 'Nicole', last: 'Grant', email: 'nicole.grant@example.com', dob: new Date(1968, 6, 15) },
- { key: '222', expanded: false, first: 'Alberto', last: 'Kennedy', email: 'alberto.kennedy@example.com', dob: new Date(1972, 7, 17) },
- { key: '333', expanded: false, first: 'Arron', last: 'Douglas', email: 'arron.douglas@example.com', dob: new Date(1982, 4, 1) },
- { key: '444', expanded: false, first: 'Reginald', last: 'Rhodes', email: 'reginald.rhodes@example.com', dob: new Date(1968, 8, 14) },
- { key: '555', expanded: false, first: 'Jimmy', last: 'Mendoza', email: 'jimmy.mendoza@example.com', dob: new Date(1964, 1, 1) },
- { key: '666', expanded: false, first: 'Georgia', last: 'Montgomery', email: 'georgia.montgomery@example.com', dob: new Date(1960, 6, 4) },
- { key: '777', expanded: true, first: 'Serenity', last: 'Thomas', email: 'serenity.thomas@example.com', dob: new Date(1973, 0, 11) },
- { key: '888', expanded: false, first: 'Tonya', last: 'Elliott', email: 'tonya.elliott@example.com', dob: new Date(1954, 7, 17) },
- { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) },
- { key: '000', expanded: false, first: 'Reginald', last: 'Rice', email: 'reginald.rice@example.com', dob: new Date(1984, 4, 15) }
-];
-
-class StatefulExample extends React.Component {
- static displayName = 'SortableTable';
- state = {
- column: 'last',
- ascending: true
- }
-
- sortedData = (column, ascending) => orderBy(
- DATA,
- [column],
- [ascending ? 'asc' : 'desc']
- );
-
- sortBy = (column, ascending) => {
- if (this.state.column === column) {
- this.setState({
- ascending
- });
- } else {
- this.setState({
- column,
- ascending: true
- });
- }
- };
-
- rowOnClick = (row) => {
- alert(`clicked ${row.key}`);
- };
-
- render() {
- const { ascending, column } = this.state;
- const { bordered, hover, size, striped } = this.props;
-
- return (
- row.first,
- onSort: asc => this.sortBy('first', asc),
- width: '20%'
- },
- {
- active: column === 'last',
- ascending,
- header: 'Last',
- key: 'last',
- cell: row => row.last,
- onSort: asc => this.sortBy('last', asc),
- width: '30%'
- },
- {
- active: column === 'dob',
- ascending,
- header: 'DOB',
- key: 'dob',
- cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
- onSort: asc => this.sortBy('dob', asc),
- width: '15%'
- },
- {
- active: column === 'email',
- ascending,
- header: Email,
- key: 'email',
- cell: row => {row.email},
- onSort: asc => this.sortBy('email', asc),
- width: '35%'
- }
- ]}
- rows={this.sortedData(column, ascending)}
- rowExpanded={row => row.expanded && }
- rowOnClick={this.rowOnClick}
- />
- );
- }
-}
-
-storiesOf('SortableTable', module)
- .addWithInfo('Live example', () => {
- const column = select('active', ['first', 'last', 'dob', 'email'], 'last');
- const ascending = boolean('ascending', true);
- return (
-
-
- Note: This is an uncontrolled example, will not sort on click. See 'Stateful Example' story.
-
-
row.first,
- onSort: action('onSort', 'First')
- },
- {
- active: column === 'last',
- ascending,
- header: 'Last',
- key: 'last',
- cell: row => row.last,
- onSort: action('onSort', 'Last')
- },
- {
- active: column === 'dob',
- ascending,
- header: 'DOB',
- key: 'dob',
- cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
- onSort: action('onSort', 'DOB')
- },
- {
- active: column === 'email',
- ascending,
- header: Email,
- key: 'email',
- cell: row => {row.email},
- onSort: action('onSort', 'Email')
- }
- ]}
- rows={DATA}
- />
-
- );
- })
- .add('Stateful example', () => (
-
-
-
- Story Source
- {`
-class StatefulExample extends React.Component {
- static displayName = 'SortableTable';
- state = {
- column: 'last',
- ascending: true
- }
-
- sortedData = (column, ascending) => orderBy(
- DATA,
- [column],
- [ascending ? 'asc' : 'desc']
- );
-
- sortBy = (column, ascending) => {
- if (this.state.column === column) {
- this.setState({
- ascending
- });
- } else {
- this.setState({
- column,
- ascending: true
- });
- }
- };
-
- render() {
- const { ascending, column } = this.state;
- const { bordered, hover, size, striped } = this.props;
-
- return (
- row.first,
- onSort: asc => this.sortBy('first', asc),
- width: '20%'
- },
- {
- active: column === 'last',
- ascending,
- header: 'Last',
- key: 'last',
- cell: row => row.last,
- onSort: asc=> this.sortBy('last', asc),
- width: '30%'
- },
- {
- active: column === 'dob',
- ascending,
- header: 'DOB',
- key: 'dob',
- cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
- onSort: asc => this.sortBy('dob', asc),
- width: '15%'
- },
- {
- active: column === 'email',
- ascending,
- header: Email,
- key: 'email',
- cell: row => {row.email},
- onSort: asc => this.sortBy('email', asc),
- width: '35%'
- }
- ]}
- rows={this.sortedData(column, ascending)}
- rowExpanded={row => row.expanded && }
- />
- );
- }
-}
- `}
-
-
-
- ))
- .addWithInfo('Align column', () => (
-
-
- Note: This is an uncontrolled example, will not sort on click. See 'Stateful Example' story.
-
-
row.first,
- },
- {
- align: 'left',
- header: 'Left Align',
- key: 'last',
- cell: row => row.last,
- },
- {
- align: 'center',
- header: 'Center Align',
- key: 'dob',
- cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
- },
- {
- align: 'right',
- header: 'Right Align',
- key: 'email',
- cell: row => {row.email},
- }
- ]}
- rows={DATA}
- />
-
- ));
-
diff --git a/stories/Table.js b/stories/Table.js
index 3e18ea0d4..2b2c603eb 100644
--- a/stories/Table.js
+++ b/stories/Table.js
@@ -1,31 +1,26 @@
import React from 'react';
+import fecha from 'fecha';
import { storiesOf } from '@storybook/react';
-
-import { Table } from '../src';
+import { action } from '@storybook/addon-actions';
import { text, boolean, number, object, select } from '@storybook/addon-knobs';
+import { Table, SortableTable, UncontrolledTable } from '../src';
const DATA = [
- {
- name: 'Compson, Ms. Quentin',
- company: 'Jefferson County',
- phone: '(662) 555-1212',
- email: 'qcbaby@faulkner.com'
- }, {
- name: 'Trump, Paul',
- company: 'Appfolio Inc.',
- phone: '(805) 555-1213',
- email: 'paul.trump@bogus.com'
- }, {
- name: 'Walker, Jon',
- company: 'CTO Appfolio Inc.',
- phone: '(805) 555-1212',
- email: 'jon@walker.com'
- }
+ { key: '111', expanded: false, first: 'Nicole', last: 'Grant', email: 'nicole.grant@example.com', dob: new Date(1968, 6, 15) },
+ { key: '222', expanded: false, first: 'Alberto', last: 'Kennedy', email: 'alberto.kennedy@example.com', dob: new Date(1972, 7, 17) },
+ { key: '333', expanded: false, first: 'Arron', last: 'Douglas', email: 'arron.douglas@example.com', dob: new Date(1982, 4, 1) },
+ { key: '444', expanded: false, first: 'Reginald', last: 'Rhodes', email: 'reginald.rhodes@example.com', dob: new Date(1968, 8, 14) },
+ { key: '555', expanded: false, first: 'Jimmy', last: 'Mendoza', email: 'jimmy.mendoza@example.com', dob: new Date(1964, 1, 1) },
+ { key: '666', expanded: false, first: 'Georgia', last: 'Montgomery', email: 'georgia.montgomery@example.com', dob: new Date(1960, 6, 4) },
+ { key: '777', expanded: true, first: 'Serenity', last: 'Thomas', email: 'serenity.thomas@example.com', dob: new Date(1973, 0, 11) },
+ { key: '888', expanded: false, first: 'Tonya', last: 'Elliott', email: 'tonya.elliott@example.com', dob: new Date(1954, 7, 17) },
+ { key: '999', expanded: false, first: 'Maxine', last: 'Turner', email: 'maxine.turner@example.com', dob: new Date(1961, 8, 19) },
+ { key: '000', expanded: false, first: 'Max', last: 'Headroom', email: 'max.headroom@example.com', dob: new Date(1984, 6, 1) }
];
storiesOf('Table', module)
.addWithInfo('Live example', () => (
-
- Name |
- Company |
- Phone |
+ First |
+ Last |
+ DOB |
Email |
@@ -43,13 +38,140 @@ storiesOf('Table', module)
{DATA.map(row => (
- {row.name} |
- {row.company} |
- {row.phone} |
+ {row.first} |
+ {row.last} |
+ {fecha.format(row.dob, 'MM/DD/YYYY')} |
{row.email} |
))}
- )
-);
+ ))
+ .addWithInfo('SortableTable', () => {
+ const column = select('active', ['first', 'last', 'dob', 'email'], 'last');
+ const ascending = boolean('ascending', true);
+ return (
+
+
+ Note: This is an uncontrolled example, will not sort on click. See 'UncontrolledTable' story.
+
+
row.first,
+ onSort: action('onSort', 'First')
+ },
+ {
+ active: column === 'last',
+ ascending,
+ header: 'Last',
+ key: 'last',
+ cell: row => row.last,
+ onSort: action('onSort', 'Last')
+ },
+ {
+ active: column === 'dob',
+ ascending,
+ header: 'DOB',
+ key: 'dob',
+ cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
+ onSort: action('onSort', 'DOB')
+ },
+ {
+ active: column === 'email',
+ ascending,
+ header: Email,
+ key: 'email',
+ cell: row => {row.email},
+ onSort: action('onSort', 'Email')
+ }
+ ]}
+ rows={DATA}
+ />
+
+ );
+ })
+ .addWithInfo('UncontrolledTable', () => (
+
+
row.first,
+ width: '20%'
+ },
+ {
+ header: 'Last',
+ key: 'last',
+ cell: row => row.last,
+ width: '30%'
+ },
+ {
+ header: 'DOB',
+ key: 'dob',
+ cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
+ width: '15%'
+ },
+ {
+ header: 'Email',
+ key: 'email',
+ cell: row => {row.email},
+ width: '35%'
+ }
+ ]}
+ rows={DATA}
+ rowExpanded={row => {row.first} {row.last}
}
+ sort={{ column: 'last', ascending: true }}
+ expandable={boolean('expandable', false)}
+ selectable={boolean('selectable', false)}
+ paginated={boolean('paginated', false)}
+ pageSize={number('pageSize', 10)}
+ onSelect={action('onSelect')}
+ />
+
+ ))
+ .addWithInfo('Align column', () => (
+
+
+ Note: This is an uncontrolled example, will not sort on click. See 'UncontrolledTable' story.
+
+
row.first,
+ },
+ {
+ align: 'left',
+ header: 'Left Align',
+ key: 'last',
+ cell: row => row.last,
+ },
+ {
+ align: 'center',
+ header: 'Center Align',
+ key: 'dob',
+ cell: row => fecha.format(row.dob, 'MM/DD/YYYY'),
+ },
+ {
+ align: 'right',
+ header: 'Right Align',
+ key: 'email',
+ cell: row => {row.email},
+ }
+ ]}
+ rows={DATA}
+ />
+
+ ));
+
diff --git a/stories/index.js b/stories/index.js
index 49e0e65ba..65e786552 100644
--- a/stories/index.js
+++ b/stories/index.js
@@ -36,7 +36,6 @@ import './Paginator';
import './Popover';
import './Progress';
import './Select';
-import './SortableTable';
import './Spinner';
import './Steps';
import './SummaryBox';
diff --git a/test/components/UncontrolledTable.spec.js b/test/components/UncontrolledTable.spec.js
new file mode 100644
index 000000000..ab51c41e1
--- /dev/null
+++ b/test/components/UncontrolledTable.spec.js
@@ -0,0 +1,314 @@
+import React from 'react';
+import assert from 'assert';
+import sinon from 'sinon';
+import { mount } from 'enzyme';
+
+import { UncontrolledTable } from '../../src';
+
+describe('', () => {
+ it('should render correctly', () => {
+ const wrapper = mount();
+ assert(wrapper);
+ });
+
+ it('should accept all normal Table props', () => {
+ const wrapper = mount();
+ const table = wrapper.find('table');
+
+ assert(wrapper.find('.table-responsive').exists(), 'responsive missing');
+ assert(table.hasClass('table-bordered'), 'bordered missing');
+ assert(table.hasClass('table-striped'), 'striped missing');
+ assert(table.hasClass('table-dark'), 'dark missing');
+ assert(table.hasClass('table-hover'), 'hover missing');
+ });
+
+ it('should render all columns', () => {
+ const columns = [
+ { header: 'Alpha' },
+ { header: 'Bravo' },
+ { header: 'Charlie' },
+ { header: 'Delta' }
+ ];
+ const wrapper = mount();
+ const headers = wrapper.find('th');
+ assert.equal(headers.length, columns.length);
+ headers.forEach((th, i) => assert.equal(th.text(), columns[i].header));
+ });
+
+ it('should render all rows', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const wrapper = mount();
+ const cells = wrapper.find('td');
+ assert.equal(cells.length, rows.length);
+ cells.forEach((td, i) => assert.equal(td.text(), rows[i]));
+ });
+
+ it('should render header components', () => {
+ const classNames = ['alpha', 'bravo', 'charlie', 'delta'];
+ const columns = classNames.map(name => ({
+ header: {name}
+ }));
+ const wrapper = mount();
+ const headers = wrapper.find('span');
+ headers.forEach((th, i) => assert(th.hasClass(classNames[i])));
+ });
+
+ it('should render cell components', () => {
+ const classNames = ['alpha', 'bravo', 'charlie', 'delta'];
+ const columns = classNames.map(name => ({
+ header: name,
+ cell: row => {row}
+ }));
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const wrapper = mount();
+ const trs = wrapper.find('tr');
+ assert.equal(trs.length, rows.length + 1); // +1 includes thead
+
+ classNames.forEach(name => {
+ const cells = wrapper.find(`.${name}`);
+ assert.equal(cells.length, rows.length, 'Column cell not rendered for each row');
+ });
+ });
+
+ it('should not render tfoot if no footers specified', () => {
+ const classNames = ['alpha', 'bravo', 'charlie', 'delta'];
+ const columns = classNames.map(name => ({
+ header: name
+ }));
+ const wrapper = mount();
+
+ const footer = wrapper.find('tfoot');
+ assert.equal(footer.exists(), false);
+ });
+
+ it('should render footer components', () => {
+ const classNames = ['alpha', 'bravo', 'charlie', 'delta'];
+ const columns = classNames.map(name => ({
+ header: name,
+ footer: {name}
+ }));
+ const wrapper = mount();
+
+ const footer = wrapper.find('tfoot');
+ assert(footer.exists());
+
+ const footers = wrapper.find('span');
+ assert.equal(footers.length, classNames.length);
+ footers.forEach((th, i) => assert(th.hasClass(classNames[i])));
+ });
+
+ it('should render sorting controls when sortable present', () => {
+ const columns = [
+ { header: 'Alpha' },
+ { header: 'Bravo' },
+ { header: 'Charlie' },
+ { header: 'Delta', sortable: false }
+ ];
+ const wrapper = mount();
+ const sortIcons = wrapper.find('Icon');
+ assert.equal(sortIcons.length, 3);
+ });
+
+ it('should render correct sort icons when specified', () => {
+ const columns = [
+ { key: 'alpha', header: 'Alpha' },
+ { key: 'bravo', header: 'Bravo' },
+ { key: 'charlie', header: 'Charlie' },
+ { key: 'delta', header: 'Delta', sortable: false }
+ ];
+ const wrapper = mount();
+
+ const activeColumnIcon = wrapper.find('Icon[name="caret-up"]');
+ assert.equal(activeColumnIcon.length, 1);
+
+ const activeDescColumnIcon = wrapper.find('Icon[name="caret-down"]');
+ assert.equal(activeDescColumnIcon.length, 0);
+
+ const inactiveColumnIcons = wrapper.find('Icon[name="sort"]');
+ assert.equal(inactiveColumnIcons.length, 2);
+ });
+
+ it('should not render colgroup if no widths specified', () => {
+ const columns = [
+ { header: 'Alpha' },
+ { header: 'Bravo' },
+ { header: 'Charlie' },
+ { header: 'Delta' }
+ ];
+ const wrapper = mount();
+
+ const colgroup = wrapper.find('colgroup');
+ assert.equal(colgroup.exists(), false);
+ });
+
+ it('should render column widths if specified', () => {
+ const columns = [
+ { header: 'Alpha', width: '20%' },
+ { header: 'Bravo', width: '30%' },
+ { header: 'Charlie', width: '15%' },
+ { header: 'Delta', width: '35%' }
+ ];
+ const wrapper = mount();
+
+ const colgroup = wrapper.find('colgroup');
+ assert(colgroup.exists());
+
+ const col = wrapper.find('col');
+ columns.forEach((column, i) => assert.equal(col.get(i).props.style.width, column.width));
+ });
+
+ it('should render row className when specified', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const wrapper = mount(
+ row}
+ />
+ );
+ const trs = wrapper.find('tbody tr');
+ assert.equal(trs.length, rows.length);
+ trs.forEach((tr, i) => {
+ assert(tr.hasClass(rows[i]));
+ });
+ });
+
+ it('should render expandable column when specified', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const wrapper = mount(
+ Hey}
+ />
+ );
+
+ const ths = wrapper.find('th');
+ assert.equal(ths.length, columns.length + 1); // For expanded column
+ });
+
+ it('should supply onClick row handler when specified', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const onClick = sinon.stub();
+ const wrapper = mount(
+
+ );
+ wrapper.find('tbody tr').first().simulate('click');
+ sinon.assert.calledWith(onClick, 'Alpha');
+ });
+
+ it('should render correct align when present', () => {
+ const columns = [
+ { header: 'Default', cell: () => '-', footer: '-' },
+ { header: 'Left', cell: () => '-', footer: '-', align: 'left' },
+ { header: 'Center', cell: () => '-', footer: '-', align: 'center' },
+ { header: 'Right', cell: () => '-', footer: '-', align: 'right' },
+ ];
+ const wrapper = mount();
+
+ assert.equal(wrapper.find('thead th.text-left').length, 1, 'thead th.text-left incorrect');
+ assert.equal(wrapper.find('thead th.text-center').length, 1, 'thead th.text-center incorrect');
+ assert.equal(wrapper.find('thead th.text-right').length, 1, 'thead th.text-right incorrect');
+
+ assert.equal(wrapper.find('tbody td.text-left').length, 3, 'tbody td.text-left incorrect');
+ assert.equal(wrapper.find('tbody td.text-center').length, 3, 'tbody td.text-center incorrect');
+ assert.equal(wrapper.find('tbody td.text-right').length, 3, 'tbody td.text-right incorrect');
+
+ assert.equal(wrapper.find('tfoot td.text-left').length, 1, 'tfoot td.text-left incorrect');
+ assert.equal(wrapper.find('tfoot td.text-center').length, 1, 'tfoot td.text-center incorrect');
+ assert.equal(wrapper.find('tfoot td.text-right').length, 1, 'tfoot td.text-right incorrect');
+ });
+
+ it('should render correct column classnames when present', () => {
+ const columns = [
+ { header: 'Default', cell: () => '-', footer: '-' },
+ { header: 'Left', cell: () => '-', footer: '-', className: 'whatever' }
+ ];
+ const wrapper = mount();
+
+ assert.equal(wrapper.find('thead th.whatever').length, 1, 'thead th.whatever incorrect');
+
+ assert.equal(wrapper.find('tbody td.whatever').length, 3, 'tbody td.whatever incorrect');
+
+ assert.equal(wrapper.find('tfoot td.whatever').length, 1, 'tfoot td.whatever incorrect');
+ });
+
+ it('should render select column when specified', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
+ const wrapper = mount(
+
+ );
+
+ const ths = wrapper.find('th');
+ assert.equal(ths.length, columns.length + 1); // For selectable column
+ });
+
+ it('should call onSelect when selectable row picked', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'];
+ const onSelect = sinon.stub();
+ const wrapper = mount(
+
+ );
+ wrapper
+ .find({ type: 'checkbox' })
+ .first()
+ .simulate('change', { target: { checked: true } });
+ sinon.assert.calledWith(onSelect, rows);
+ });
+
+ it('should show correct rows when paginated specified', () => {
+ const columns = [{ header: 'Name', cell: row => row }];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'];
+ const wrapper = mount(
+
+ );
+ const trs = wrapper.find('tbody tr');
+ assert.equal(trs.length, 4);
+ // TODO assert rows
+ });
+
+ it('should show correct rows on page change');
+
+ it('should show correct rows on sort change');
+
+ it('should hide columns when hidden', () => {
+ const columns = [
+ { header: 'Name', cell: row => row },
+ { header: 'Nope', cell: () => 'Nope', hidden: true },
+ ];
+ const rows = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'];
+ const wrapper = mount(
+
+ );
+ assert.equal(wrapper.find('th').length, 1);
+ assert.equal(wrapper.find('td').length, rows.length);
+ });
+});