diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82209d78213..c179fc6246c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
- Added some opacity options to `EuiLineSeries` and `EuiAreaSeries` ([#1198](https://github.com/elastic/eui/pull/1198))
- Added `initialFocus` prop for focus trapping to `EuiPopover` and `EuiModal` ([#1099](https://github.com/elastic/eui/pull/1099))
+- Added table footer support with `EuiTableFooter` and `EuiTableFooterCell` ([#1202](https://github.com/elastic/eui/pull/1202))
## [`4.1.0`](https://github.com/elastic/eui/tree/v4.1.0)
diff --git a/src-docs/src/views/tables/basic/props_info.js b/src-docs/src/views/tables/basic/props_info.js
index 0225826dc47..d2f3f9ede60 100644
--- a/src-docs/src/views/tables/basic/props_info.js
+++ b/src-docs/src/views/tables/basic/props_info.js
@@ -161,7 +161,7 @@ export const propsInfo = {
description: 'Describes the data types of the displayed value (serves as a rendering hint for the table)',
required: false,
defaultValue: { value: '"auto"' },
- type: { name: '"auto" | string" | "number" | "date" | "boolean"' }
+ type: { name: '"auto" | "string" | "number" | "date" | "boolean"' }
},
width: {
description: 'A CSS width property. Hints for the required width of the column',
@@ -190,6 +190,11 @@ export const propsInfo = {
description: `Describe a custom renderer function for the content`,
required: false,
type: { name: '(value, item) => PropTypes.node' }
+ },
+ footer: {
+ description: `Content to display in the footer beneath this column`,
+ required: false,
+ type: { name: 'string | PropTypes.element | ({ items, pagination }) => PropTypes.node' }
}
}
}
diff --git a/src-docs/src/views/tables/custom/custom.js b/src-docs/src/views/tables/custom/custom.js
index 2c78b8be84f..204b60ed8d6 100644
--- a/src-docs/src/views/tables/custom/custom.js
+++ b/src-docs/src/views/tables/custom/custom.js
@@ -19,6 +19,8 @@ import {
EuiSpacer,
EuiTable,
EuiTableBody,
+ EuiTableFooter,
+ EuiTableFooterCell,
EuiTableHeader,
EuiTableHeaderCell,
EuiTableHeaderCellCheckbox,
@@ -37,6 +39,8 @@ import {
SortableProperties,
} from '../../../../../src/services';
+import { isFunction } from '../../../../../src/services/predicate';
+
export default class extends Component {
constructor(props) {
super(props);
@@ -221,6 +225,7 @@ export default class extends Component {
}, {
id: 'title',
label: 'Title',
+ footer: Title,
alignment: LEFT_ALIGNMENT,
isSortable: true,
hideForMobile: true,
@@ -234,15 +239,25 @@ export default class extends Component {
}, {
id: 'health',
label: 'Health',
+ footer: '',
alignment: LEFT_ALIGNMENT,
}, {
id: 'dateCreated',
label: 'Date created',
+ footer: 'Date created',
alignment: LEFT_ALIGNMENT,
isSortable: true,
}, {
id: 'magnitude',
label: 'Orders of magnitude',
+ footer: ({ items, pagination }) => {
+ const { pageIndex, pageSize } = pagination;
+ const startIndex = pageIndex * pageSize;
+ const pageOfItems = items.slice(startIndex, Math.min(startIndex + pageSize, items.length));
+ return (
+ Total: {pageOfItems.reduce((acc, cur) => acc + cur.magnitude, 0)}
+ );
+ },
alignment: RIGHT_ALIGNMENT,
isSortable: true,
}, {
@@ -546,6 +561,63 @@ export default class extends Component {
return rows;
}
+ renderFooterCells() {
+ const footers = [];
+
+ const items = this.items;
+ const pagination = {
+ pageIndex: this.pager.getCurrentPageIndex(),
+ pageSize: this.state.itemsPerPage,
+ totalItemCount: this.pager.getTotalPages()
+ };
+
+ this.columns.forEach(column => {
+ const footer = this.getColumnFooter(column, { items, pagination });
+ if (column.isMobileHeader) {
+ return; // exclude columns that only exist for mobile headers
+ }
+
+ if (footer) {
+ footers.push(
+
+ {footer}
+
+ );
+ } else {
+ footers.push(
+
+ {undefined}
+
+ );
+ }
+ });
+
+ return footers;
+ }
+
+ getColumnFooter = (column, { items, pagination }) => {
+ if (column.footer === null) {
+ return null;
+ }
+
+ if (column.footer) {
+ if (isFunction(column.footer)) {
+ return column.footer({ items, pagination });
+ }
+ return column.footer;
+ }
+
+ return undefined;
+ }
+
render() {
let optionalActionButtons;
@@ -586,6 +658,10 @@ export default class extends Component {
{this.renderRows()}
+
+
+ {this.renderFooterCells()}
+
diff --git a/src-docs/src/views/tables/footer/footer.js b/src-docs/src/views/tables/footer/footer.js
new file mode 100644
index 00000000000..dfcab718148
--- /dev/null
+++ b/src-docs/src/views/tables/footer/footer.js
@@ -0,0 +1,228 @@
+import React, {
+ Component,
+ Fragment,
+} from 'react';
+import { formatDate } from '../../../../../src/services/format';
+import { createDataStore } from '../data_store';
+
+import {
+ EuiBasicTable,
+ EuiLink,
+ EuiHealth,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '../../../../../src/components';
+
+import { uniq } from 'lodash';
+
+/*
+Example user object:
+
+{
+ id: '1',
+ firstName: 'john',
+ lastName: 'doe',
+ github: 'johndoe',
+ dateOfBirth: Date.now(),
+ nationality: 'NL',
+ online: true
+}
+
+Example country object:
+
+{
+ code: 'NL',
+ name: 'Netherlands',
+ flag: '🇳🇱'
+}
+*/
+
+const store = createDataStore();
+
+export class Table extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ pageIndex: 0,
+ pageSize: 5,
+ sortField: 'firstName',
+ sortDirection: 'asc',
+ selectedItems: [],
+ };
+
+ this.renderStatus = this.renderStatus.bind(this);
+ }
+
+ onTableChange = ({ page = {}, sort = {} }) => {
+ const {
+ index: pageIndex,
+ size: pageSize,
+ } = page;
+
+ const {
+ field: sortField,
+ direction: sortDirection,
+ } = sort;
+
+ this.setState({
+ pageIndex,
+ pageSize,
+ sortField,
+ sortDirection,
+ });
+ };
+
+ onSelectionChange = (selectedItems) => {
+ this.setState({ selectedItems });
+ };
+
+ onClickDelete = () => {
+ const { selectedItems } = this.state;
+ store.deleteUsers(...selectedItems.map(user => user.id));
+
+ this.setState({
+ selectedItems: []
+ });
+ };
+
+ renderDeleteButton() {
+ const { selectedItems } = this.state;
+
+ if (selectedItems.length === 0) {
+ return;
+ }
+
+ return (
+
+ Delete {selectedItems.length} Users
+
+ );
+ }
+
+ renderStatus(online) {
+ const color = online ? 'success' : 'danger';
+ const label = online ? 'Online' : 'Offline';
+ return {label};
+ }
+
+ render() {
+ const {
+ pageIndex,
+ pageSize,
+ sortField,
+ sortDirection,
+ } = this.state;
+
+ const {
+ pageOfItems,
+ totalItemCount,
+ } = store.findUsers(pageIndex, pageSize, sortField, sortDirection);
+
+ const deleteButton = this.renderDeleteButton();
+
+ const columns = [{
+ field: 'firstName',
+ name: 'First Name',
+ footer: Page totals:,
+ sortable: true,
+ truncateText: true,
+ hideForMobile: true,
+ }, {
+ field: 'lastName',
+ name: 'Last Name',
+ truncateText: true,
+ hideForMobile: true,
+ }, {
+ field: 'firstName',
+ name: 'Full Name',
+ isMobileHeader: true,
+ render: (name, item) => (
+
+ {item.firstName} {item.lastName}
+ {this.renderStatus(item.online)}
+
+ ),
+ }, {
+ field: 'github',
+ name: 'Github',
+ footer: ({ items }) => (
+ {uniq(items, 'github').length} users
+ ),
+ render: (username) => (
+
+ {username}
+
+ )
+ }, {
+ field: 'dateOfBirth',
+ name: 'Date of Birth',
+ dataType: 'date',
+ render: (date) => formatDate(date, 'dobLong'),
+ sortable: true
+ }, {
+ field: 'nationality',
+ name: 'Nationality',
+ footer: ({ items }) => (
+ {uniq(items, 'nationality').length} countries
+ ),
+ render: (countryCode) => {
+ const country = store.getCountry(countryCode);
+ return `${country.flag} ${country.name}`;
+ }
+ }, {
+ field: 'online',
+ name: 'Online',
+ footer: ({ items }) => (
+ {items.filter(i => !!i.online).length} online
+ ),
+ dataType: 'boolean',
+ render: (online) => (
+ this.renderStatus(online)
+ ),
+ sortable: true,
+ hideForMobile: true,
+ }];
+
+ const pagination = {
+ pageIndex: pageIndex,
+ pageSize: pageSize,
+ totalItemCount: totalItemCount,
+ pageSizeOptions: [3, 5, 8]
+ };
+
+ const sorting = {
+ sort: {
+ field: sortField,
+ direction: sortDirection,
+ },
+ };
+
+ const selection = {
+ selectable: (user) => user.online,
+ selectableMessage: (selectable) => !selectable ? 'User is currently offline' : undefined,
+ onSelectionChange: this.onSelectionChange
+ };
+
+ return (
+
+ {deleteButton}
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/tables/footer/footer_section.js b/src-docs/src/views/tables/footer/footer_section.js
new file mode 100644
index 00000000000..bbe03a1efeb
--- /dev/null
+++ b/src-docs/src/views/tables/footer/footer_section.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import {
+ EuiBasicTable,
+ EuiCode
+} from '../../../../../src/components';
+import { GuideSectionTypes } from '../../../components';
+import { renderToHtml } from '../../../services';
+
+import { Table } from './footer';
+const source = require('!!raw-loader!./footer');
+const html = renderToHtml(Table);
+
+export const section = {
+ title: 'Adding a footer to a BasicTable',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: source,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: html,
+ }
+ ],
+ text: (
+
+ The following example shows how to add a footer to your table by
+ adding footer to your column definitions. If one
+ or more of your columns contains a footer definition,
+ the footer area will be visible. By default, columns with no footer specified
+ (undefined) will render an empty cell to preserve the table layout. Check out
+ the "Custom Table" section below for more examples of how you can
+ work with table footers in EUI.
+
+ ),
+ components: { EuiBasicTable },
+ demo: ,
+};
diff --git a/src-docs/src/views/tables/footer/index.js b/src-docs/src/views/tables/footer/index.js
new file mode 100644
index 00000000000..605daf9c477
--- /dev/null
+++ b/src-docs/src/views/tables/footer/index.js
@@ -0,0 +1 @@
+export { section } from './footer_section';
diff --git a/src-docs/src/views/tables/mobile/mobile.js b/src-docs/src/views/tables/mobile/mobile.js
index f533730b235..b0203532e59 100644
--- a/src-docs/src/views/tables/mobile/mobile.js
+++ b/src-docs/src/views/tables/mobile/mobile.js
@@ -111,11 +111,13 @@ export class Table extends Component {
name: 'Clone',
description: 'Clone this person',
icon: 'copy',
+ type: 'icon',
onClick: this.cloneUser
}, {
name: 'Delete',
description: 'Delete this person',
icon: 'trash',
+ type: 'icon',
color: 'danger',
onClick: this.deleteUser
}];
diff --git a/src-docs/src/views/tables/tables_example.js b/src-docs/src/views/tables/tables_example.js
index 50062122ef9..b2065c9dee2 100644
--- a/src-docs/src/views/tables/tables_example.js
+++ b/src-docs/src/views/tables/tables_example.js
@@ -10,6 +10,7 @@ import { section as basicSection } from './basic';
import { section as paginatedSection } from './paginated';
import { section as sortingSection } from './sorting';
import { section as selectionSection } from './selection';
+import { section as footerSection } from './footer';
import { section as expandingRowsSection } from './expanding_rows';
import { section as actionsSection } from './actions';
import {
@@ -48,6 +49,7 @@ export const TableExample = {
paginatedSection,
sortingSection,
selectionSection,
+ footerSection,
expandingRowsSection,
actionsSection,
inMemorySection,
diff --git a/src/components/basic_table/__snapshots__/basic_table.test.js.snap b/src/components/basic_table/__snapshots__/basic_table.test.js.snap
index d0f8527044d..3a9fbbee5d4 100644
--- a/src/components/basic_table/__snapshots__/basic_table.test.js.snap
+++ b/src/components/basic_table/__snapshots__/basic_table.test.js.snap
@@ -339,6 +339,432 @@ exports[`EuiBasicTable empty renders a string as a custom message 1`] = `
`;
+exports[`EuiBasicTable footers do not render without a column footer definition 1`] = `
+
+
+
+
+
+
+ Below is a table of
+ 3
+ items.
+
+
+
+
+ Name
+
+
+ ID
+
+
+ Age
+
+
+
+
+
+
+ name1
+
+
+ 1
+
+
+ 20
+
+
+
+
+
+
+ name2
+
+
+ 2
+
+
+ 21
+
+
+
+
+
+
+ name3
+
+
+ 3
+
+
+ 22
+
+
+
+
+
+
+
+`;
+
+exports[`EuiBasicTable footers render with pagination, selection, sorting, and footer 1`] = `
+
+
+
+
+
+
+
+
+ Below is a table of
+ 3
+ items.
+
+
+
+
+
+
+
+ Name
+
+
+ ID
+
+
+ Age
+
+
+
+
+
+
+
+
+
+ name1
+
+
+ 1
+
+
+ 20
+
+
+
+
+
+
+
+
+
+ name2
+
+
+ 2
+
+
+ 21
+
+
+
+
+
+
+
+
+
+ name3
+
+
+ 3
+
+
+ 22
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ ID
+
+
+
+ sum:
+ 63
+
+ total items:
+ 5
+
+
+
+
+
+
+
+`;
+
exports[`EuiBasicTable itemIdToExpandedRowMap renders an expanded row 1`] = `
PropTypes.node (also see [services/value_renderer] for basic implementations)
+ render: PropTypes.func, // ((value, record) => PropTypes.node (also see [services/value_renderer] for basic implementations)
+ footer: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.element,
+ PropTypes.func, // ({ items, pagination }) => PropTypes.node
+ ])
};
export const FieldDataColumnType = PropTypes.shape(FieldDataColumnTypeShape);
@@ -185,6 +192,17 @@ function getCellProps(item, column, cellProps) {
return {};
}
+function getColumnFooter(column, { items, pagination }) {
+ if (column.footer) {
+ if (isFunction(column.footer)) {
+ return column.footer({ items, pagination });
+ }
+ return column.footer;
+ }
+
+ return undefined;
+}
+
export class EuiBasicTable extends Component {
static propTypes = BasicTablePropTypes;
static defaultProps = {
@@ -348,6 +366,7 @@ export class EuiBasicTable extends Component {
const caption = this.renderTableCaption();
const head = this.renderTableHead();
const body = this.renderTableBody();
+ const footer = this.renderTableFooter();
return (
{ this.tableElement = element; }}
@@ -357,6 +376,7 @@ export class EuiBasicTable extends Component {
{caption}
{head}
{body}
+ {footer}
);
@@ -509,6 +529,55 @@ export class EuiBasicTable extends Component {
return
{headers};
}
+ renderTableFooter() {
+ const { items, columns, pagination, selection } = this.props;
+
+ const footers = [];
+ let hasDefinedFooter = false;
+
+ if (selection) {
+ // Create an empty cell to compensate for additional selection column
+ footers.push(
+
+ {undefined}
+
+ );
+ }
+
+ columns.forEach(column => {
+ const footer = getColumnFooter(column, { items, pagination });
+ if (column.isMobileHeader) {
+ return; // exclude columns that only exist for mobile headers
+ }
+
+ if (footer) {
+ footers.push(
+
+ {footer}
+
+ );
+ hasDefinedFooter = true;
+ } else {
+ // Footer is undefined, so create an empty cell to preserve layout
+ footers.push(
+
+ {undefined}
+
+ );
+ }
+ });
+
+ return footers.length && hasDefinedFooter ?
{footers} : null;
+ }
+
renderTableBody() {
if (this.props.error) {
return this.renderErrorBody(this.props.error);
@@ -750,6 +819,7 @@ export class EuiBasicTable extends Component {
field, // eslint-disable-line no-unused-vars
description, // eslint-disable-line no-unused-vars
sortable, // eslint-disable-line no-unused-vars
+ footer, // eslint-disable-line no-unused-vars
...rest
} = column;
const columnAlign = align || this.getAlignForDataType(dataType);
diff --git a/src/components/basic_table/basic_table.test.js b/src/components/basic_table/basic_table.test.js
index edcdd56118d..41135b381aa 100644
--- a/src/components/basic_table/basic_table.test.js
+++ b/src/components/basic_table/basic_table.test.js
@@ -460,6 +460,99 @@ describe('EuiBasicTable', () => {
expect(component).toMatchSnapshot();
});
+
+ describe('footers', () => {
+ test('do not render without a column footer definition', () => {
+ const props = {
+ items: [
+ { id: '1', name: 'name1', age: 20 },
+ { id: '2', name: 'name2', age: 21 },
+ { id: '3', name: 'name3', age: 22 }
+ ],
+ itemId: 'id',
+ columns: [
+ {
+ field: 'name',
+ name: 'Name',
+ description: 'your name'
+ },
+ {
+ field: 'id',
+ name: 'ID',
+ description: 'your id'
+ },
+ {
+ field: 'age',
+ name: 'Age',
+ description: 'your age'
+ }
+ ],
+ onChange: () => { }
+ };
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('render with pagination, selection, sorting, and footer', () => {
+ const props = {
+ items: [
+ { id: '1', name: 'name1', age: 20 },
+ { id: '2', name: 'name2', age: 21 },
+ { id: '3', name: 'name3', age: 22 }
+ ],
+ itemId: 'id',
+ columns: [
+ {
+ field: 'name',
+ name: 'Name',
+ description: 'your name',
+ sortable: true,
+ footer:
Name
+ },
+ {
+ field: 'id',
+ name: 'ID',
+ description: 'your id',
+ footer: 'ID'
+ },
+ {
+ field: 'age',
+ name: 'Age',
+ description: 'your age',
+ footer: ({ items, pagination }) => (
+
+ sum:
+ {items.reduce((acc, cur) => acc + cur.age, 0)}
+ total items:
+ {pagination.totalItemCount}
+
+ )
+ }
+ ],
+ pagination: {
+ pageIndex: 0,
+ pageSize: 3,
+ totalItemCount: 5
+ },
+ selection: {
+ onSelectionChanged: () => undefined
+ },
+ sorting: {
+ sort: { field: 'name', direction: 'asc' }
+ },
+ onChange: () => { }
+ };
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+
test('with pagination, selection, sorting and column renderer', () => {
const props = {
items: [
diff --git a/src/components/index.js b/src/components/index.js
index eec8bd44080..221a8d87b6c 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -280,6 +280,8 @@ export {
export {
EuiTable,
EuiTableBody,
+ EuiTableFooter,
+ EuiTableFooterCell,
EuiTableHeader,
EuiTableHeaderButton,
EuiTableHeaderCell,
diff --git a/src/components/table/__snapshots__/table_footer.test.js.snap b/src/components/table/__snapshots__/table_footer.test.js.snap
new file mode 100644
index 00000000000..99922987ece
--- /dev/null
+++ b/src/components/table/__snapshots__/table_footer.test.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiTableFooter is rendered 1`] = `
+Array [
+
+
+ ,
+ "children",
+]
+`;
diff --git a/src/components/table/__snapshots__/table_footer_cell.test.js.snap b/src/components/table/__snapshots__/table_footer_cell.test.js.snap
new file mode 100644
index 00000000000..ae842e57ec1
--- /dev/null
+++ b/src/components/table/__snapshots__/table_footer_cell.test.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiTableFooterCell align defaults to left 1`] = `
+
+`;
+
+exports[`EuiTableFooterCell align renders center when specified 1`] = `
+
+`;
+
+exports[`EuiTableFooterCell align renders right when specified 1`] = `
+
+`;
+
+exports[`EuiTableFooterCell is rendered 1`] = `
+
+`;
diff --git a/src/components/table/__snapshots__/table_header.test.js.snap b/src/components/table/__snapshots__/table_header.test.js.snap
new file mode 100644
index 00000000000..09b2da1459f
--- /dev/null
+++ b/src/components/table/__snapshots__/table_header.test.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiTableHeader is rendered 1`] = `
+Array [
+
+
+ ,
+ "children",
+]
+`;
diff --git a/src/components/table/_mixins.scss b/src/components/table/_mixins.scss
index fe3f67e71d3..b4b52765e31 100644
--- a/src/components/table/_mixins.scss
+++ b/src/components/table/_mixins.scss
@@ -6,7 +6,6 @@
@mixin euiTableCellCheckbox {
@include euiTableCell;
- border-top: none;
width: $euiTableCellCheckboxWidth;
vertical-align: middle;
}
diff --git a/src/components/table/_responsive.scss b/src/components/table/_responsive.scss
index f2b7bf76d6b..eb87f574c96 100644
--- a/src/components/table/_responsive.scss
+++ b/src/components/table/_responsive.scss
@@ -9,6 +9,10 @@
display: none; // Use mobile versions of selecting and filtering instead
}
+ tfoot {
+ display: none; // Not supporting responsive footer content
+ }
+
// Make each row a Panel
@include euiPanel($selector: 'euiTableRow');
diff --git a/src/components/table/_table.scss b/src/components/table/_table.scss
index 7e6c99262b6..0662541c82e 100644
--- a/src/components/table/_table.scss
+++ b/src/components/table/_table.scss
@@ -22,6 +22,7 @@
}
}
+.euiTableFooterCell,
.euiTableHeaderCell {
@include euiTableCell;
@include euiTitle("xxs");
@@ -68,6 +69,7 @@
.euiTableHeaderCellCheckbox {
@include euiTableCellCheckbox;
+ border-top: none;
}
.euiTableRow {
@@ -113,6 +115,12 @@
@include euiTableCellCheckbox;
}
+// Must come after .euiTableRowCell selector for border to be removed
+.euiTableFooterCell {
+ background-color: $euiColorLightestShade;
+ border-bottom: none;
+}
+
/**
* 1. Vertically align all children.
* 2. The padding on this div allows the ellipsis to show if the content is truncated. If
@@ -125,16 +133,16 @@
padding: $euiTableCellContentPadding; /* 2 */
}
- /**
- * 1. Prevent very long single words (e.g. the name of a field in a document) from overflowing
- * the cell.
- */
- .euiTableCellContent__text {
- min-width: 0;
- text-overflow: ellipsis;
- word-break: break-all; /* 1 */ // Fallback for FF and IE
- word-break: break-word; /* 1 */
- }
+/**
+ * 1. Prevent very long single words (e.g. the name of a field in a document) from overflowing
+ * the cell.
+ */
+.euiTableCellContent__text {
+ min-width: 0;
+ text-overflow: ellipsis;
+ word-break: break-all; /* 1 */ // Fallback for FF and IE
+ word-break: break-word; /* 1 */
+}
.euiTableCellContent--alignRight {
justify-content: flex-end;
@@ -147,6 +155,7 @@
}
.euiTableHeaderCell,
+.euiTableFooterCell,
.euiTableCellContent--truncateText {
white-space: nowrap; /* 3 */
diff --git a/src/components/table/index.js b/src/components/table/index.js
index da56b30da2f..3abb0e41b3f 100644
--- a/src/components/table/index.js
+++ b/src/components/table/index.js
@@ -1,5 +1,7 @@
export { EuiTable } from './table';
export { EuiTableBody } from './table_body';
+export { EuiTableFooter } from './table_footer';
+export { EuiTableFooterCell } from './table_footer_cell';
export { EuiTableHeader } from './table_header';
export { EuiTableHeaderButton } from './table_header_button';
export { EuiTableHeaderCell } from './table_header_cell';
diff --git a/src/components/table/table_footer.js b/src/components/table/table_footer.js
new file mode 100644
index 00000000000..ee2d389bf94
--- /dev/null
+++ b/src/components/table/table_footer.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export const EuiTableFooter = ({ children, className, ...rest }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+EuiTableFooter.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+};
diff --git a/src/components/table/table_footer.test.js b/src/components/table/table_footer.test.js
new file mode 100644
index 00000000000..de333a6f051
--- /dev/null
+++ b/src/components/table/table_footer.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test/required_props';
+
+import { EuiTableFooter } from './table_footer';
+
+describe('EuiTableFooter', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ children
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/table/table_footer_cell.js b/src/components/table/table_footer_cell.js
new file mode 100644
index 00000000000..d5a44b1255f
--- /dev/null
+++ b/src/components/table/table_footer_cell.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import {
+ LEFT_ALIGNMENT,
+ RIGHT_ALIGNMENT,
+ CENTER_ALIGNMENT
+} from '../../services';
+
+const ALIGNMENT = [
+ LEFT_ALIGNMENT,
+ RIGHT_ALIGNMENT,
+ CENTER_ALIGNMENT
+];
+
+export const EuiTableFooterCell = ({
+ children,
+ align,
+ colSpan,
+ className,
+ ...rest
+}) => {
+ const classes = classNames('euiTableFooterCell', className);
+ const contentClasses = classNames('euiTableCellContent', className, {
+ 'euiTableCellContent--alignRight': align === RIGHT_ALIGNMENT,
+ 'euiTableCellContent--alignCenter': align === CENTER_ALIGNMENT,
+ });
+
+ return (
+
+
+ {children}
+
+ |
+ );
+};
+
+EuiTableFooterCell.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ align: PropTypes.oneOf(ALIGNMENT),
+ colSpan: PropTypes.number,
+};
+
+EuiTableFooterCell.defaultProps = {
+ align: LEFT_ALIGNMENT,
+};
diff --git a/src/components/table/table_footer_cell.test.js b/src/components/table/table_footer_cell.test.js
new file mode 100644
index 00000000000..f2c7b604a4a
--- /dev/null
+++ b/src/components/table/table_footer_cell.test.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test/required_props';
+
+import { EuiTableFooterCell } from './table_footer_cell';
+
+import {
+ RIGHT_ALIGNMENT,
+ CENTER_ALIGNMENT
+} from '../../services';
+
+describe('EuiTableFooterCell', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ children
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('align', () => {
+ test('defaults to left', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+
+ test('renders right when specified', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+
+ test('renders center when specified', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/components/table/table_header.test.js b/src/components/table/table_header.test.js
new file mode 100644
index 00000000000..2480e709998
--- /dev/null
+++ b/src/components/table/table_header.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test/required_props';
+
+import { EuiTableHeader } from './table_header';
+
+describe('EuiTableHeader', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ children
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});