UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

445 lines (396 loc) 21.6 kB
import _times from "lodash/times"; import _isNil from "lodash/isNil"; import _toString from "lodash/toString"; import _isEmpty from "lodash/isEmpty"; import _assign from "lodash/assign"; import _pick from "lodash/pick"; import _clamp from "lodash/clamp"; import _keys from "lodash/keys"; import _isNull from "lodash/isNull"; import _reduce from "lodash/reduce"; import _size from "lodash/size"; import _get from "lodash/get"; import _partial from "lodash/partial"; import _omit from "lodash/omit"; import _map from "lodash/map"; import _every from "lodash/every"; import _slice from "lodash/slice"; import _compact from "lodash/compact"; import _flow from "lodash/flow"; import _some from "lodash/some"; import _noop from "lodash/noop"; var _templateObject, _templateObject2; function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } import React, { useState } from 'react'; import PropTypes from 'react-peek/prop-types'; import { lucidClassNames } from '../../util/style-helpers'; import { findTypes, filterTypes, getFirst, omitProps } from '../../util/component-types'; import Checkbox from '../Checkbox/Checkbox'; import EmptyStateWrapper from '../EmptyStateWrapper/EmptyStateWrapper'; import ScrollTable from '../ScrollTable/ScrollTable'; import Table from '../Table/Table'; var Thead = ScrollTable.Thead, Tbody = ScrollTable.Tbody, Tr = ScrollTable.Tr, Th = ScrollTable.Th, Td = ScrollTable.Td; var cx = lucidClassNames.bind('&-DataTable'); var cxe = lucidClassNames.bind('&-DataTable-EmptyStateWrapper'); var SELECTOR_COLUMN_WIDTH = 41; var any = PropTypes.any, func = PropTypes.func, number = PropTypes.number, object = PropTypes.object, stringProps = PropTypes.string, bool = PropTypes.bool, arrayOf = PropTypes.arrayOf; var defaultProps = { emptyCellText: '--', isActionable: false, isSelectable: false, onRowClick: _noop, onSelect: _noop, onSelectAll: _noop, onSort: _noop, minRows: 10, hasFixedHeader: false, fixedColumnCount: 0 }; var defaultState = { // Represents the actively changing width as the cell is resized. activeWidth: {} }; export var DataTable = function DataTable(props) { var _useState = useState(defaultState), _useState2 = _slicedToArray(_useState, 2), state = _useState2[0], setState = _useState2[1]; var fixedHeaderUnfixedColumnsRef; var fixedBodyFixedColumnsRef; var handleSelect = function handleSelect(rowIndex, _ref) { var event = _ref.event; var data = props.data, onSelect = props.onSelect; onSelect(data[rowIndex], rowIndex, { props: props, event: event }); }; var handleSelectAll = function handleSelectAll(_ref2) { var event = _ref2.event; var onSelectAll = props.onSelectAll; onSelectAll({ props: props, event: event }); }; var handleRowClick = function handleRowClick(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 handleSort(field, event) { var onSort = props.onSort; event.stopPropagation(); event.preventDefault(); event.bubbles = false; onSort(field, { props: props, event: event }); }; var handleFixedBodyUnfixedColumnsScroll = function handleFixedBodyUnfixedColumnsScroll(event) { fixedHeaderUnfixedColumnsRef && (fixedHeaderUnfixedColumnsRef.scrollLeft = event.target.scrollLeft); fixedBodyFixedColumnsRef && (fixedBodyFixedColumnsRef.scrollTop = event.target.scrollTop); }; var handleResize = function handleResize(columnWidth, _ref3) { var field = _ref3.props.field; // setting latest column width to Tbody setState(function (state) { return { activeWidth: _objectSpread(_objectSpread({}, state.activeWidth), {}, _defineProperty({}, field, columnWidth)) }; }); }; var renderHeader = function renderHeader(startColumn, endColumn, childComponentElements, flattenedColumns) { var isSelectable = props.isSelectable, data = props.data; var hasGroupedColumns = _some(childComponentElements, function (childComponentElement) { return childComponentElement.type === DataTable.ColumnGroup; }); var columnSlicer = _flow(_compact, function (columns) { return _slice(columns, startColumn, endColumn); }); var allSelected = _every(data, 'isSelected'); return /*#__PURE__*/React.createElement(Thead, null, /*#__PURE__*/React.createElement(Tr, null, columnSlicer([isSelectable ? /*#__PURE__*/React.createElement(Th, { key: cx('&-row-selector'), rowSpan: hasGroupedColumns ? 2 : null, width: SELECTOR_COLUMN_WIDTH }, /*#__PURE__*/React.createElement(Checkbox, { isDisabled: !data || !data.length, isSelected: !!data && data.length > 0 && allSelected, isIndeterminate: !allSelected && !!data.find(function (d) { return d.isSelected; }), onSelect: handleSelectAll })) : null].concat(_map(childComponentElements, function (_ref4, index) { var props = _ref4.props, type = _ref4.type; return type === DataTable.Column ? /*#__PURE__*/React.createElement(Th, _extends({ onResize: props.isResizable ? handleResize : null }, _omit(props, ['children', 'title']), { onClick: DataTable.shouldColumnHandleSort(props) ? _partial(handleSort, props.field) : null, rowSpan: hasGroupedColumns ? 2 : null, field: props.field || index, key: _get(props, 'field', index) }), props.title || props.children) : /*#__PURE__*/React.createElement(Th, _extends({ colSpan: _size(filterTypes(props.children, DataTable.Column)) }, _omit(props, ['field', 'children', 'width', 'title']), { key: _get(props, 'field', index) }), props.title || props.children); })))), hasGroupedColumns ? /*#__PURE__*/React.createElement(Tr, null, _reduce(flattenedColumns, function (acc, _ref5, index) { var columnProps = _ref5.props, columnGroupProps = _ref5.columnGroupProps; return acc.concat(_isNull(columnGroupProps) ? [] : [/*#__PURE__*/React.createElement(Th, _extends({}, omitProps(columnProps, undefined, _keys(DataTable.Column.propTypes), false), { onClick: DataTable.shouldColumnHandleSort(columnProps) ? _partial(handleSort, columnProps.field) : null, style: { width: columnProps.width }, key: _get(columnProps, 'field', index) }), columnProps.title || columnProps.children)]); }, [])) : null); }; var renderBody = function renderBody(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 = _clamp(minRows - _size(data), 0, Infinity); var isFixedColumn = endColumn < Infinity; var columnSlicer = _flow(_compact, function (columns) { return _slice(columns, startColumn, endColumn); }); return /*#__PURE__*/React.createElement(Tbody, null, _map(data, function (row, index) { return /*#__PURE__*/React.createElement(Tr, _extends({}, _pick(row, ['isDisabled', 'isActive', 'isSelected']), { isActionable: isActionable, onClick: _partial(handleRowClick, index), key: 'row' + index, style: _assign(row.style, fixedRowHeight ? { height: fixedRowHeight } : {}) }), columnSlicer([isSelectable ? /*#__PURE__*/React.createElement(Td, { key: cx('&-row-selector'), width: SELECTOR_COLUMN_WIDTH, hasBorderRight: isFixedColumn && flattenedColumns.length === 0 }, /*#__PURE__*/React.createElement(Checkbox, { isSelected: row.isSelected, onSelect: _partial(handleSelect, index) })) : null].concat(_map(flattenedColumns, function (_ref6, columnIndex) { var columnProps = _ref6.props; var cellValue = _get(row, columnProps.field); var isEmpty = _isEmpty(_toString(cellValue)); return /*#__PURE__*/React.createElement(Td, _extends({}, _omit(columnProps, ['field', 'children', 'width', 'title', 'isSortable', 'isSorted', 'isResizable']), { hasBorderRight: !_isNil(columnProps.hasBorderRight) ? columnProps.hasBorderRight : isFixedColumn && columnIndex + 1 + (isSelectable ? 1 : 0) === endColumn, style: { width: // @ts-ignore state.activeWidth[columnProps.field || columnIndex] || columnProps.width }, key: 'row' + index + _get(columnProps, 'field', columnIndex), truncateContent: truncateContent }), isEmpty ? emptyCellText : cellValue); })))); }), _times(fillerRowCount, function (index) { return /*#__PURE__*/React.createElement(Tr, { isDisabled: true, key: 'row' + index, style: { height: fixedRowHeight || '32px' } }, columnSlicer([isSelectable ? /*#__PURE__*/React.createElement(Td, { key: cx('&-row-selector') }) : null].concat(_map(flattenedColumns, function (_ref7, columnIndex) { var columnProps = _ref7.props; return /*#__PURE__*/React.createElement(Td, _extends({}, _omit(columnProps, ['field', 'children', 'width', 'title', 'isSortable', 'isSorted', 'isResizable']), { style: { width: columnProps.width }, key: 'row' + index + _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 = _objectWithoutProperties(props, ["className", "data", "isFullWidth", "isLoading", "style", "hasFixedHeader", "fixedColumnCount", "anchorMessage"]); var childComponentElements = findTypes(props, [DataTable.Column, DataTable.ColumnGroup]); var flattenedColumns = _reduce(childComponentElements, function (acc, childComponentElement) { if (childComponentElement.type === DataTable.Column) { return acc.concat([{ props: childComponentElement.props, columnGroupProps: null }]); } if (childComponentElement.type === DataTable.ColumnGroup) { return acc.concat(_map(findTypes(childComponentElement.props, DataTable.Column), function (columnChildComponent) { return { props: columnChildComponent.props, columnGroupProps: childComponentElement.props }; })); } }, []); var emptyStateWrapper = getFirst(props, DataTable.EmptyStateWrapper) || /*#__PURE__*/React.createElement(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 /*#__PURE__*/React.createElement(EmptyStateWrapper, _extends({}, emptyStateWrapper.props, { isEmpty: _isEmpty(data), isLoading: isLoading, className: emptyStateWrapperClassName, anchorMessage: anchorMessage }), emptyStateWrapper.props.children, hasFixedHeader ? /*#__PURE__*/React.createElement("div", { className: cx('&-fixed') }, /*#__PURE__*/React.createElement("div", { className: cx('&-fixed-header') }, /*#__PURE__*/React.createElement("div", { className: cx('&-fixed-header-fixed-columns') }, fixedColumnCount > 0 ? /*#__PURE__*/React.createElement(Table, _extends({}, omitProps(passThroughs, undefined, _keys(DataTable.propTypes), false), { style: style, className: cx('&-fixed-header-fixed-columns-Table') }), renderHeader(0, fixedColumnCount, childComponentElements, flattenedColumns)) : null), /*#__PURE__*/React.createElement("div", { className: cx('&-fixed-header-unfixed-columns'), ref: function ref(_ref8) { return fixedHeaderUnfixedColumnsRef = _ref8; } }, /*#__PURE__*/React.createElement(Table, _extends({}, omitProps(passThroughs, undefined, _keys(DataTable.propTypes), false), { style: style, className: cx('&-fixed-header-unfixed-columns-Table') }), renderHeader(fixedColumnCount, Infinity, childComponentElements, flattenedColumns)))), /*#__PURE__*/React.createElement("div", { className: cx('&-fixed-body') }, /*#__PURE__*/React.createElement("div", { className: cx('&-fixed-body-fixed-columns'), ref: function ref(_ref9) { return fixedBodyFixedColumnsRef = _ref9; } }, fixedColumnCount > 0 ? /*#__PURE__*/React.createElement(Table, _extends({}, omitProps(passThroughs, undefined, _keys(DataTable.propTypes), false), { style: style, className: cx('&-fixed-body-fixed-columns-Table'), hasWordWrap: false /* try to protect against vertical overflow */ }), renderBody(0, fixedColumnCount, flattenedColumns)) : null), /*#__PURE__*/React.createElement("div", { onScroll: handleFixedBodyUnfixedColumnsScroll, className: cx('&-fixed-body-unfixed-columns') }, /*#__PURE__*/React.createElement("span", { className: cx('&-fixed-body-unfixed-columns-shadow') }), /*#__PURE__*/React.createElement(Table, _extends({}, omitProps(passThroughs, undefined, _keys(DataTable.propTypes), false), { style: style, className: cx('&-fixed-body-unfixed-columns-Table'), hasWordWrap: false /* try to protect against vertical overflow */ }), renderBody(fixedColumnCount, Infinity, flattenedColumns))))) : /*#__PURE__*/React.createElement(ScrollTable, _extends({ style: style, tableWidth: isFullWidth ? '100%' : undefined }, omitProps(passThroughs, undefined, _keys(DataTable.propTypes), false), { className: cx('&', { '&-full-width': isFullWidth }, className) }), renderHeader(0, Infinity, childComponentElements, flattenedColumns), renderBody(0, Infinity, flattenedColumns))); }; DataTable.displayName = 'DataTable'; DataTable.propTypes = { className: stringProps(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n\t\tClass names that are appended to the defaults.\n\t"]))), data: arrayOf(object), emptyCellText: stringProps(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n\t\tThe text to display in cells which have no data.\n\t"]))), isActionable: bool, isFullWidth: bool, isLoading: bool, isSelectable: bool, anchorMessage: bool, style: object, minRows: number, onRowClick: func, onSelect: func, onSelectAll: func, onSort: func, hasFixedHeader: bool, fixedColumnCount: number, fixedRowHeight: number, truncateContent: bool, Column: any, ColumnGroup: any }; DataTable.defaultProps = defaultProps; DataTable.peek = { description: "\n\t\t`DataTable` provides a simple abstraction over the `Table`\n\t\tcomponent to make it easier to define data-driven tables and render an\n\t\tarray of objects.\n\t", 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'] }; DataTable.EmptyStateWrapper = EmptyStateWrapper; // type IColumnProps = Overwrite<typeof Th, IColumnPropsRaw>; var Column = function Column(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 }; DataTable.Column = Column; var ColumnGroup = function ColumnGroup(_ref10) { var children = _ref10.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' }; DataTable.ColumnGroup = ColumnGroup; DataTable.shouldColumnHandleSort = function (column) { return _isNil(column.isSortable) ? column.isSorted : column.isSortable; }; export default DataTable;