lucid-ui
Version:
A UI component library from Xandr.
452 lines (446 loc) • 25.7 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataTable = void 0;
/* eslint-disable react/prop-types */
var lodash_1 = __importDefault(require("lodash"));
var react_1 = __importStar(require("react"));
var prop_types_1 = __importDefault(require("prop-types"));
var style_helpers_1 = require("../../util/style-helpers");
var component_types_1 = require("../../util/component-types");
var Checkbox_1 = __importDefault(require("../Checkbox/Checkbox"));
var EmptyStateWrapper_1 = __importDefault(require("../EmptyStateWrapper/EmptyStateWrapper"));
var ScrollTable_1 = __importDefault(require("../ScrollTable/ScrollTable"));
var Table_1 = __importDefault(require("../Table/Table"));
var Thead = ScrollTable_1.default.Thead, Tbody = ScrollTable_1.default.Tbody, Tr = ScrollTable_1.default.Tr, Th = ScrollTable_1.default.Th, Td = ScrollTable_1.default.Td;
var cx = style_helpers_1.lucidClassNames.bind('&-DataTable');
var cxe = style_helpers_1.lucidClassNames.bind('&-DataTable-EmptyStateWrapper');
var SELECTOR_COLUMN_WIDTH = 41;
var any = prop_types_1.default.any, func = prop_types_1.default.func, number = prop_types_1.default.number, object = prop_types_1.default.object, stringProps = prop_types_1.default.string, bool = prop_types_1.default.bool, arrayOf = prop_types_1.default.arrayOf;
/** TODO: Remove the nonPassThroughs when the component is converted to a functional component */
var nonPassThroughs = [
'className',
'data',
'emptyCellText',
'isActionable',
'isFullWidth',
'isLoading',
'isSelectable',
'anchorMessage',
'style',
'minRows',
'hasFixedHeader',
'fixedColumnCount',
'fixedRowHeight',
'truncateContent',
'initialState',
'onRowClick',
'onSelect',
'onSelectAll',
'onSort',
'Column',
'ColumnGroup',
'onResize',
];
/** TODO: Remove the nonColumnProps when the component is converted to a functional component */
var omittedColumnProps = ['field', 'title', 'initialState'];
var defaultProps = {
emptyCellText: '--',
isActionable: false,
isSelectable: false,
onRowClick: lodash_1.default.noop,
onSelect: lodash_1.default.noop,
onSelectAll: lodash_1.default.noop,
onSort: lodash_1.default.noop,
minRows: 10,
hasFixedHeader: false,
fixedColumnCount: 0,
};
var defaultState = {
// Represents the actively changing width as the cell is resized.
activeWidth: {},
};
var DataTable = function (props) {
var _a = (0, react_1.useState)(defaultState), state = _a[0], setState = _a[1];
var fixedHeaderUnfixedColumnsRef;
var fixedBodyFixedColumnsRef;
var handleSelect = function (rowIndex, _a) {
var event = _a.event;
var data = props.data, onSelect = props.onSelect;
onSelect(data[rowIndex], rowIndex, { props: props, event: event });
};
var handleSelectAll = function (_a) {
var event = _a.event;
var onSelectAll = props.onSelectAll;
onSelectAll({ props: props, event: event });
};
var handleRowClick = function (rowIndex, event) {
var data = props.data, onRowClick = props.onRowClick;
var targetTagName = event.target.tagName.toLowerCase();
if (targetTagName === 'td' || targetTagName === 'tr') {
onRowClick(data[rowIndex], rowIndex, { props: props, event: event });
}
};
var handleSort = function (field, event) {
var onSort = props.onSort;
event.stopPropagation();
event.preventDefault();
event.bubbles = false;
onSort(field, { props: props, event: event });
};
var handleFixedBodyUnfixedColumnsScroll = function (event) {
fixedHeaderUnfixedColumnsRef &&
(fixedHeaderUnfixedColumnsRef.scrollLeft = event.target.scrollLeft);
fixedBodyFixedColumnsRef &&
(fixedBodyFixedColumnsRef.scrollTop = event.target.scrollTop);
};
var handleResize = function (columnWidth, _a) {
var field = _a.props.field;
// setting latest column width to Tbody
setState(function (state) {
var _a;
return ({
activeWidth: __assign(__assign({}, state.activeWidth), (_a = {}, _a[field] = columnWidth, _a)),
});
});
};
var renderHeader = function (startColumn, endColumn, childComponentElements, flattenedColumns) {
var isSelectable = props.isSelectable, data = props.data, truncateContent = props.truncateContent;
var hasGroupedColumns = lodash_1.default.some(childComponentElements, function (childComponentElement) {
return childComponentElement.type === exports.DataTable.ColumnGroup;
});
var columnSlicer = lodash_1.default.flow(lodash_1.default.compact, function (columns) {
return lodash_1.default.slice(columns, startColumn, endColumn);
});
var allSelected = lodash_1.default.every(data, 'isSelected');
return (react_1.default.createElement(Thead, null,
react_1.default.createElement(Tr, null, columnSlicer([
isSelectable ? (react_1.default.createElement(Th, { key: cx('&-row-selector'), rowSpan: hasGroupedColumns ? 2 : null, width: SELECTOR_COLUMN_WIDTH },
react_1.default.createElement(Checkbox_1.default, { isDisabled: !data || !data.length, isSelected: !!data && data.length > 0 && allSelected, isIndeterminate: !allSelected && !!data.find(function (d) { return d.isSelected; }), onSelect: handleSelectAll }))) : null,
].concat(lodash_1.default.map(childComponentElements, function (_a, index) {
var props = _a.props, type = _a.type;
return type === exports.DataTable.Column ? (react_1.default.createElement(Th, __assign({ onResize: props.isResizable ? handleResize : null }, lodash_1.default.omit(props, ['children', 'title']), { onClick: exports.DataTable.shouldColumnHandleSort(props)
? lodash_1.default.partial(handleSort, props.field)
: null, rowSpan: hasGroupedColumns ? 2 : null, field: props.field || index, key: lodash_1.default.get(props, 'field', index), truncateContent: truncateContent }), props.title || props.children)) : (react_1.default.createElement(Th, __assign({ colSpan: lodash_1.default.size((0, component_types_1.filterTypes)(props.children, exports.DataTable.Column)) }, lodash_1.default.omit(props, ['field', 'children', 'width', 'title']), { key: lodash_1.default.get(props, 'field', index) }), props.title || props.children));
})))),
hasGroupedColumns ? (react_1.default.createElement(Tr, null, lodash_1.default.reduce(flattenedColumns, function (acc, _a, index) {
var columnProps = _a.props, columnGroupProps = _a.columnGroupProps;
return acc.concat(lodash_1.default.isNull(columnGroupProps)
? []
: [
react_1.default.createElement(Th, __assign({}, lodash_1.default.omit(columnProps, omittedColumnProps), { onClick: exports.DataTable.shouldColumnHandleSort(columnProps)
? lodash_1.default.partial(handleSort, columnProps.field)
: null, style: {
width: columnProps.width,
}, key: lodash_1.default.get(columnProps, 'field', index) }), columnProps.title || columnProps.children),
]);
}, []))) : null));
};
var renderBody = function (startColumn, endColumn, flattenedColumns) {
var data = props.data, isSelectable = props.isSelectable, isActionable = props.isActionable, minRows = props.minRows, emptyCellText = props.emptyCellText, fixedRowHeight = props.fixedRowHeight, truncateContent = props.truncateContent;
var fillerRowCount = lodash_1.default.clamp(minRows - lodash_1.default.size(data), 0, Infinity);
var isFixedColumn = endColumn < Infinity;
var columnSlicer = lodash_1.default.flow(lodash_1.default.compact, function (columns) {
return lodash_1.default.slice(columns, startColumn, endColumn);
});
return (react_1.default.createElement(Tbody, null,
lodash_1.default.map(data, function (row, index) { return (react_1.default.createElement(Tr, __assign({}, lodash_1.default.pick(row, ['isDisabled', 'isActive', 'isSelected']), { isActionable: isActionable, onClick: lodash_1.default.partial(handleRowClick, index), key: 'row' + index, style: lodash_1.default.assign(row.style, fixedRowHeight ? { height: fixedRowHeight } : {}) }), columnSlicer([
isSelectable ? (react_1.default.createElement(Td, { key: cx('&-row-selector'), width: SELECTOR_COLUMN_WIDTH, hasBorderRight: isFixedColumn && flattenedColumns.length === 0 },
react_1.default.createElement(Checkbox_1.default, { isSelected: row.isSelected, onSelect: lodash_1.default.partial(handleSelect, index) }))) : null,
].concat(lodash_1.default.map(flattenedColumns, function (_a, columnIndex) {
var columnProps = _a.props;
var cellValue = lodash_1.default.get(row, columnProps.field);
var isEmpty = lodash_1.default.isEmpty(lodash_1.default.toString(cellValue));
var currentWidth = state.activeWidth[columnProps.field || columnIndex] ||
columnProps.width;
return (react_1.default.createElement(Td, __assign({}, lodash_1.default.omit(columnProps, [
'field',
'children',
'width',
'title',
'isSortable',
'isSorted',
'isResizable',
]), { hasBorderRight: !lodash_1.default.isNil(columnProps.hasBorderRight)
? columnProps.hasBorderRight
: isFixedColumn &&
columnIndex + 1 + (isSelectable ? 1 : 0) ===
endColumn, style: { width: currentWidth }, key: 'row' +
index +
lodash_1.default.get(columnProps, 'field', columnIndex), truncateContent: truncateContent }), isEmpty
? emptyCellText
: lodash_1.default.isFunction(cellValue)
? cellValue(currentWidth)
: cellValue));
}))))); }),
lodash_1.default.times(fillerRowCount, function (index) { return (react_1.default.createElement(Tr, { isDisabled: true, key: 'row' + index, style: { height: fixedRowHeight || '32px' } }, columnSlicer([isSelectable ? react_1.default.createElement(Td, { key: cx('&-row-selector') }) : null].concat(lodash_1.default.map(flattenedColumns, function (_a, columnIndex) {
var columnProps = _a.props;
return (react_1.default.createElement(Td, __assign({}, lodash_1.default.omit(columnProps, [
'field',
'children',
'width',
'title',
'isSortable',
'isSorted',
'isResizable',
]), { style: {
width: columnProps.width,
}, key: 'row' + index + lodash_1.default.get(columnProps, 'field', columnIndex) })));
}))))); })));
};
var className = props.className, data = props.data, isFullWidth = props.isFullWidth, isLoading = props.isLoading, style = props.style, hasFixedHeader = props.hasFixedHeader, fixedColumnCount = props.fixedColumnCount, anchorMessage = props.anchorMessage, passThroughs = __rest(props, ["className", "data", "isFullWidth", "isLoading", "style", "hasFixedHeader", "fixedColumnCount", "anchorMessage"]);
var childComponentElements = (0, component_types_1.findTypes)(props, [
exports.DataTable.Column,
exports.DataTable.ColumnGroup,
]);
var flattenedColumns = lodash_1.default.reduce(childComponentElements, function (acc, childComponentElement) {
if (childComponentElement.type === exports.DataTable.Column) {
return acc.concat([
{ props: childComponentElement.props, columnGroupProps: null },
]);
}
if (childComponentElement.type === exports.DataTable.ColumnGroup) {
return acc.concat(lodash_1.default.map((0, component_types_1.findTypes)(childComponentElement.props, exports.DataTable.Column), function (columnChildComponent) { return ({
props: columnChildComponent.props,
columnGroupProps: childComponentElement.props,
}); }));
}
}, []);
var emptyStateWrapper = (0, component_types_1.getFirst)(props, exports.DataTable.EmptyStateWrapper) || (react_1.default.createElement(exports.DataTable.EmptyStateWrapper, { Title: 'No items found.', Body: 'Try creating a new object or removing a filter.' }));
var emptyStateWrapperClassName = cxe({
'&-has-fixed-header': hasFixedHeader,
}, emptyStateWrapper.props.className);
return (react_1.default.createElement(EmptyStateWrapper_1.default, __assign({}, emptyStateWrapper.props, { isEmpty: lodash_1.default.isEmpty(data), isLoading: isLoading, className: emptyStateWrapperClassName, anchorMessage: anchorMessage }),
emptyStateWrapper.props.children,
hasFixedHeader ? (react_1.default.createElement("div", { className: cx('&-fixed') },
react_1.default.createElement("div", { className: cx('&-fixed-header') },
react_1.default.createElement("div", { className: cx('&-fixed-header-fixed-columns') }, fixedColumnCount > 0 ? (react_1.default.createElement(Table_1.default, __assign({}, lodash_1.default.omit(passThroughs, nonPassThroughs), { style: style, className: cx('&-fixed-header-fixed-columns-Table') }), renderHeader(0, fixedColumnCount, childComponentElements, flattenedColumns))) : null),
react_1.default.createElement("div", { className: cx('&-fixed-header-unfixed-columns'), ref: function (ref) { return (fixedHeaderUnfixedColumnsRef = ref); } },
react_1.default.createElement(Table_1.default, __assign({}, lodash_1.default.omit(passThroughs, nonPassThroughs), { style: style, className: cx('&-fixed-header-unfixed-columns-Table') }), renderHeader(fixedColumnCount, Infinity, childComponentElements, flattenedColumns)))),
react_1.default.createElement("div", { className: cx('&-fixed-body') },
react_1.default.createElement("div", { className: cx('&-fixed-body-fixed-columns'), ref: function (ref) { return (fixedBodyFixedColumnsRef = ref); } }, fixedColumnCount > 0 ? (react_1.default.createElement(Table_1.default, __assign({}, lodash_1.default.omit(passThroughs, nonPassThroughs), { style: style, className: cx('&-fixed-body-fixed-columns-Table'), hasWordWrap: false /* try to protect against vertical overflow */ }), renderBody(0, fixedColumnCount, flattenedColumns))) : null),
react_1.default.createElement("div", { onScroll: handleFixedBodyUnfixedColumnsScroll, className: cx('&-fixed-body-unfixed-columns') },
react_1.default.createElement("span", { className: cx('&-fixed-body-unfixed-columns-shadow') }),
react_1.default.createElement(Table_1.default, __assign({}, lodash_1.default.omit(passThroughs, nonPassThroughs), { style: style, className: cx('&-fixed-body-unfixed-columns-Table'), hasWordWrap: false /* try to protect against vertical overflow */ }), renderBody(fixedColumnCount, Infinity, flattenedColumns)))))) : (react_1.default.createElement(ScrollTable_1.default, __assign({ style: style, tableWidth: isFullWidth ? '100%' : undefined }, lodash_1.default.omit(passThroughs, nonPassThroughs), { className: cx('&', {
'&-full-width': isFullWidth,
}, className) }),
renderHeader(0, Infinity, childComponentElements, flattenedColumns),
renderBody(0, Infinity, flattenedColumns)))));
};
exports.DataTable = DataTable;
exports.DataTable.displayName = 'DataTable';
exports.DataTable.propTypes = {
/**
Class names that are appended to the defaults.
*/
className: stringProps,
/**
Array of objects to be rendered in the table. Object keys match the
\`field\` of each defined \`DataTable.Column\`.
*/
data: arrayOf(object),
/**
The text to display in cells which have no data.
*/
emptyCellText: stringProps,
/**
Render each row item to be navigable, allowing \`onRowClick\` to be
triggered.
*/
isActionable: bool,
/**
If \`true\`, the table will be set to fill the width of its parent
container.
*/
isFullWidth: bool,
/**
Controls the visibility of the \`LoadingMessage\`.
*/
isLoading: bool,
/**
Render a checkbox in the first column allowing \`onSelect\` and
\`onSelectAll\` to be triggered.
*/
isSelectable: bool,
/**
Position the \`EmptyMessage\` and \`LoadingMessage\` near the top of the container.
By default, they are vertically aligned to the middle of the table. Useful
for tables with many rows that extend past the viewport.
*/
anchorMessage: bool,
/**
Styles that are passed through to the root container.
*/
style: object,
/**
The minimum number of rows to rendered. If not enough data is provided,
the remainder will be shown as empty rows.
*/
minRows: number,
/**
Handler for row click. Signature is
\`(object, index, { props, event }) => {...}\`
*/
onRowClick: func,
/**
Handler for checkbox selection. Signature is
\`(object, index, { props, event }) => {...}\`
*/
onSelect: func,
/**
Handler for checkbox selection in the table header. Signature is
\`({ props, event }) => {...}\`
*/
onSelectAll: func,
/**
Handler for column header click (for sorting). Signature is
\`(field, { props, event }) => {...}\`
*/
onSort: func,
hasFixedHeader: bool /**
If \`true\` the table will have a fixed header set. *Note* this feature
imposes some limitations with respect to the styling and usage of your
table. Here are those caveats:
- Each \`DataTable.Column\` *must* have an explicit pixel width defined
on it. If the combined width of all the columns is greater than the
parent container, the user will be able to horizontally scroll.
- The outermost wrapper element emitted by this component will be set to
100% height. It's your responsibility to put this component inside
another container that limits its height so that the resulting table can
scroll vertically and keep the fixed headers.
- Scroll bars will always be present. This is prevent misalignment of the
header and the table body that can occur when scroll bars show up and
take width. MacOS browsers depend on OS level settings to determine how
scrollbars show up. Having them always present saves us from writing a
bunch of terrible code to detect scroll bars and account for their width.
- Using fixed headers means multiple tables will be rendered under the
hood. We use \`table-layout: fixed\` behavior to make sure we can sync
columns widths between the header and the body.
- Does not support \`DataTable.ColumnGroup\`s at this time. It's possible
we could support them at some point but its not a priority at the moment.
- You have to be careful about table cell height overflow. There are
cases where this can break the alignment of fixed columns.
*/,
/**
Sets the number of columns you want to have fixed. You must specify
\`fixedRowHeight\` and enable \`hasFixedHeader\`when setting this prop.
*/
fixedColumnCount: number,
/**
Determines the height of every row in the DataTable. It's required when
using the \`fixedColumnCount\` prop.
*/
fixedRowHeight: number,
/**
Truncates \`Table.Td\` content with ellipses, must be used with \`hasFixedHeader\`
*/
truncateContent: bool,
Column: any /**
*Child Element*
Used to define a column of the table. It accepts the same props as
\`Table.Th\` in addition to:
- the required prop \`field\`
- the optional prop \`title\`
*/,
ColumnGroup: any /**
*Child Element*
_Note_: column groups are *not* compatible with \`hasFixedHeader\`.
Used to Group defined \`Column\`s in the table. It accepts the same props
as \`Table.Th\` in addition to:
- the optional prop \`title\`
*/,
/**
If \`isResizable\` is true,
it is called when the user resizes the a header cell in the table.
*/
onResize: func,
};
exports.DataTable.defaultProps = defaultProps;
exports.DataTable.peek = {
description: "`DataTable` provides a simple abstraction over the `Table` component to make it easier to define data-driven tables and render an array of objects.",
notes: {
overview: "\n\t\t\t`DataTable` provides a simple abstraction over the `Table`\n\t\t\tcomponent to make it easier to define data-driven tables and render an\n\t\t\tarray of objects.\n\t\t",
intendedUse: "\n\t\t\t`DataTable` is optimized for our two main uses, full page and in-line tables.\n\n\t\t\t**Full page table**\n\t\t\t\n\t\t\tTables that cover the entire page, or are the main focus on the page. Generally used for managing and monitoring objects.\n\t\t\t\n\t\t\t**In-line tables**\n\t\t\t\n\t\t\tTables insides containers such as `Dialog` or `Panel`. Generally used for details panels and actions dialogs.\n\t\t\t\t\t\t\t\t\n\t\t\t**Styling notes**\n\t\t\t\n\t\t\t- Preferred column alignment shown in `basic` example, column header alignment should match column content\n\t\t\t\t- strings left-aligned\n\t\t\t\t- currency right-aligned\n\t\t\t\t- icons/buttons centered\n\t\t\t- Use grey footer for full page tables, `hasLightFooter={false}`\n\t\t\t- Use white footer for in-line tables, `hasLightFooter={true}`\n\t\t",
technicalRecommendations: "\n\t\t\t- There is a pre-styled state for tables with no data, see the `empty` example\n\t\t\t- There should be no row hover state if the rows are not clickable, see example (?)\n\t\t",
},
categories: ['table'],
madeFrom: ['Checkbox', 'EmptyStateWrapper', 'ScrollTable'],
};
exports.DataTable.EmptyStateWrapper = EmptyStateWrapper_1.default;
// type IColumnProps = Overwrite<typeof Th, IColumnPropsRaw>;
var Column = function (props) {
return null;
};
Column.displayName = 'DataTable.Column';
Column.peek = {
description: "\n\t\tRenders a `Th` for the table. It accepts all the props of `Table.Th`\n\t",
};
Column.propTypes = {
field: stringProps.isRequired,
title: stringProps,
isResizable: bool,
};
exports.DataTable.Column = Column;
var ColumnGroup = function (_a) {
var children = _a.children;
return children;
};
ColumnGroup.displayName = 'DataTable.ColumnGroup';
ColumnGroup.peek = {
description: "\n\t\tRenders a group of `Th`s. It accepts all the props of Table.Th\n\t",
};
ColumnGroup.propTypes = {
title: stringProps,
};
ColumnGroup.defaultProps = {
align: 'center',
};
exports.DataTable.ColumnGroup = ColumnGroup;
exports.DataTable.shouldColumnHandleSort = function (column) {
return lodash_1.default.isNil(column.isSortable) ? column.isSorted : column.isSortable;
};
exports.default = exports.DataTable;
//# sourceMappingURL=DataTable.js.map