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', () => ( - - - - + + + @@ -43,13 +38,140 @@ storiesOf('Table', module) {DATA.map(row => ( - - - + + + ))}
NameCompanyPhoneFirstLastDOB Email
{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); + }); +});