@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
720 lines (707 loc) • 26 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var PropTypes = require('prop-types');
var React = require('react');
var isEqual = require('lodash.isequal');
var getDerivedStateFromProps = require('./state/getDerivedStateFromProps.js');
var sorting = require('./state/sorting.js');
var cells = require('./tools/cells.js');
var denormalize = require('./tools/denormalize.js');
var events = require('../../tools/events.js');
var filter = require('./tools/filter.js');
var sorting$1 = require('./tools/sorting.js');
var instanceId = require('./tools/instanceId.js');
var Table = require('./Table.js');
var TableActionList = require('./TableActionList.js');
var TableBatchAction = require('./TableBatchAction.js');
var TableBatchActions = require('./TableBatchActions.js');
var TableBody = require('./TableBody.js');
var TableCell = require('./TableCell.js');
var TableContainer = require('./TableContainer.js');
var TableExpandHeader = require('./TableExpandHeader.js');
var TableExpandRow = require('./TableExpandRow.js');
var TableExpandedRow = require('./TableExpandedRow.js');
var TableHead = require('./TableHead.js');
var TableHeader = require('./TableHeader.js');
var TableRow = require('./TableRow.js');
var TableSelectAll = require('./TableSelectAll.js');
var TableSelectRow = require('./TableSelectRow.js');
var TableToolbar = require('./TableToolbar.js');
var TableToolbarAction = require('./TableToolbarAction.js');
var TableToolbarContent = require('./TableToolbarContent.js');
var TableToolbarSearch = require('./TableToolbarSearch.js');
var TableToolbarMenu = require('./TableToolbarMenu.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual);
const getInstanceId = instanceId["default"]();
const translationKeys = {
expandRow: 'carbon.table.row.expand',
collapseRow: 'carbon.table.row.collapse',
expandAll: 'carbon.table.all.expand',
collapseAll: 'carbon.table.all.collapse',
selectAll: 'carbon.table.all.select',
unselectAll: 'carbon.table.all.unselect',
selectRow: 'carbon.table.row.select',
unselectRow: 'carbon.table.row.unselect'
};
const defaultTranslations = {
[translationKeys.expandAll]: 'Expand all rows',
[translationKeys.collapseAll]: 'Collapse all rows',
[translationKeys.expandRow]: 'Expand current row',
[translationKeys.collapseRow]: 'Collapse current row',
[translationKeys.selectAll]: 'Select all rows',
[translationKeys.unselectAll]: 'Unselect all rows',
[translationKeys.selectRow]: 'Select row',
[translationKeys.unselectRow]: 'Unselect row'
};
const translateWithId = id => defaultTranslations[id];
/**
* Data Tables are used to represent a collection of resources, displaying a
* subset of their fields in columns, or headers. We prioritize direct updates
* to the state of what we're rendering, so internally we end up normalizing the
* given data and then denormalizing it when rendering.
*
* As a result, each part of the DataTable is accessible through look-up by id,
* and updating the state of the single entity will cascade updates to the
* consumer.
*/
class DataTable extends React__default["default"].Component {
constructor(_props) {
var _this;
super(_props);
_this = this;
/**
* Get the props associated with the given header. Mostly used for adding in
* sorting behavior.
*
* @param {object} config
* @param {string} config.header the header we want the props for
* @param {Function} config.onClick a custom click handler for the header
* @param {boolean} config.isSortable
* @returns {object}
*/
_rollupPluginBabelHelpers.defineProperty(this, "getHeaderProps", _ref => {
let {
header,
onClick,
isSortable = this.props.isSortable,
...rest
} = _ref;
const {
sortDirection,
sortHeaderKey
} = this.state;
return {
...rest,
key: header.key,
sortDirection,
isSortable,
isSortHeader: sortHeaderKey === header.key,
onClick: event => {
const nextSortState = sorting.getNextSortState(this.props, this.state, {
key: header.key
});
this.setState(nextSortState, () => {
onClick && this.handleOnHeaderClick(onClick, {
sortHeaderKey: header.key,
sortDirection: nextSortState.sortDirection
})(event);
});
}
};
});
/**
* Get the props associated with the given expand header.
*
* @param {object} config
* @param {Function} config.onClick a custom click handler for the expand header
* @param {Function} config.onExpand a custom click handler called when header is expanded
* @returns {object}
*/
_rollupPluginBabelHelpers.defineProperty(this, "getExpandHeaderProps", function () {
let {
onClick,
onExpand,
...rest
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
translateWithId: t
} = _this.props;
const {
isExpandedAll,
rowIds,
rowsById
} = _this.state;
const isExpanded = isExpandedAll || rowIds.every(id => rowsById[id].isExpanded);
const translationKey = isExpanded ? translationKeys.collapseAll : translationKeys.expandAll;
return {
...rest,
ariaLabel: t(translationKey),
isExpanded,
// Compose the event handlers so we don't overwrite a consumer's `onClick`
// handler
onExpand: events.composeEventHandlers([_this.handleOnExpandAll, onExpand, onClick ? _this.handleOnExpandHeaderClick(onClick, {
isExpanded
}) : null])
};
});
/**
* Decorate consumer's `onClick` event handler with sort parameters
*
* @param {Function} onClick
* @param {object} sortParams
* @returns {Function}
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnHeaderClick", (onClick, sortParams) => {
return e => onClick(e, sortParams);
});
/**
* Decorate consumer's `onClick` event handler with sort parameters
*
* @param {Function} onClick
* @param {object} expandParams
* @returns {Function}
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnExpandHeaderClick", (onClick, expandParams) => {
return e => onClick(e, expandParams);
});
/**
* Get the props associated with the given row. Mostly used for expansion.
*
* @param {object} config
* @param {object} config.row the row we want the props for
* @param {Function} config.onClick a custom click handler for the header
* @returns {object}
*/
_rollupPluginBabelHelpers.defineProperty(this, "getRowProps", _ref2 => {
let {
row,
onClick,
...rest
} = _ref2;
const {
translateWithId: t
} = this.props;
const translationKey = row.isExpanded ? translationKeys.collapseRow : translationKeys.expandRow;
return {
...rest,
key: row.id,
// Compose the event handlers so we don't overwrite a consumer's `onClick`
// handler
onExpand: events.composeEventHandlers([this.handleOnExpandRow(row.id), onClick]),
isExpanded: row.isExpanded,
ariaLabel: t(translationKey),
isSelected: row.isSelected,
disabled: row.disabled
};
});
/**
* Gets the props associated with selection for a header or a row, where
* applicable. Most often used to indicate selection status of the table or
* for a specific row.
*
* @param {object} [row] an optional row that we want to access the props for
* @param {Function} row.onClick
* @param {object} row.row
* @returns {object}
*/
_rollupPluginBabelHelpers.defineProperty(this, "getSelectionProps", function () {
let {
onClick,
row,
...rest
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
translateWithId: t
} = _this.props;
// If we're given a row, return the selection state values for that row
if (row) {
const translationKey = row.isSelected ? translationKeys.unselectRow : translationKeys.selectRow;
return {
...rest,
checked: row.isSelected,
onSelect: events.composeEventHandlers([_this.handleOnSelectRow(row.id), onClick]),
id: `${_this.getTablePrefix()}__select-row-${row.id}`,
name: `select-row-${row.id}`,
ariaLabel: t(translationKey),
disabled: row.disabled,
radio: _this.props.radio || null
};
}
// Otherwise, we're working on `TableSelectAll` which handles toggling the
// selection state of all rows.
const rowCount = _this.state.rowIds.length;
const selectedRowCount = _this.getSelectedRows().length;
const checked = rowCount > 0 && selectedRowCount === rowCount;
const indeterminate = rowCount > 0 && selectedRowCount > 0 && selectedRowCount !== rowCount;
const translationKey = checked || indeterminate ? translationKeys.unselectAll : translationKeys.selectAll;
return {
...rest,
ariaLabel: t(translationKey),
checked,
id: `${_this.getTablePrefix()}__select-all`,
indeterminate,
name: 'select-all',
onSelect: events.composeEventHandlers([_this.handleSelectAll, onClick])
};
});
_rollupPluginBabelHelpers.defineProperty(this, "getToolbarProps", function () {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
size
} = _this.props;
let isSmall = size === 'xs' || size === 'sm';
return {
...props,
size: isSmall ? 'sm' : undefined
};
});
_rollupPluginBabelHelpers.defineProperty(this, "getBatchActionProps", function () {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
shouldShowBatchActions
} = _this.state;
const totalSelected = _this.getSelectedRows().length;
return {
...props,
shouldShowBatchActions: shouldShowBatchActions && totalSelected > 0,
totalSelected,
onCancel: _this.handleOnCancel
};
});
/**
* Helper utility to get the Table Props.
*/
_rollupPluginBabelHelpers.defineProperty(this, "getTableProps", () => {
const {
useZebraStyles,
size,
isSortable,
useStaticWidth,
stickyHeader,
overflowMenuOnHover,
experimentalAutoAlign
} = this.props;
return {
useZebraStyles,
size,
isSortable,
useStaticWidth,
stickyHeader,
overflowMenuOnHover,
experimentalAutoAlign
};
});
/**
* Helper utility to get the TableContainer Props.
*/
_rollupPluginBabelHelpers.defineProperty(this, "getTableContainerProps", () => {
const {
stickyHeader,
useStaticWidth
} = this.props;
return {
stickyHeader,
useStaticWidth
};
});
/**
* Helper utility to get all the currently selected rows
* @returns {Array<string>} the array of rowIds that are currently selected
*/
_rollupPluginBabelHelpers.defineProperty(this, "getSelectedRows", () => this.state.rowIds.filter(id => {
const row = this.state.rowsById[id];
return row.isSelected && !row.disabled;
}));
/**
* Helper utility to get all of the available rows after applying the filter
* @returns {Array<string>} the array of rowIds that are currently included through the filter
* */
_rollupPluginBabelHelpers.defineProperty(this, "getFilteredRowIds", () => {
const filteredRowIds = typeof this.state.filterInputValue === 'string' ? this.props.filterRows({
rowIds: this.state.rowIds,
headers: this.props.headers,
cellsById: this.state.cellsById,
inputValue: this.state.filterInputValue,
getCellId: cells.getCellId
}) : this.state.rowIds;
if (filteredRowIds.length == 0) {
return [];
}
return filteredRowIds;
});
/**
* Helper for getting the table prefix for elements that require an
* `id` attribute that is unique.
*
* @returns {string}
*/
_rollupPluginBabelHelpers.defineProperty(this, "getTablePrefix", () => `data-table-${this.instanceId}`);
/**
* Helper for toggling all selected items in a state. Does not call
* setState, so use it when setting state.
* @param {object} initialState
* @returns {object} object to put into this.setState (use spread operator)
*/
_rollupPluginBabelHelpers.defineProperty(this, "setAllSelectedState", (initialState, isSelected, filteredRowIds) => {
const {
rowIds
} = initialState;
const isFiltered = rowIds.length != filteredRowIds.length;
return {
rowsById: rowIds.reduce((acc, id) => {
const row = {
...initialState.rowsById[id]
};
if (!row.disabled && (!isFiltered || filteredRowIds.includes(id))) {
row.isSelected = isSelected;
}
acc[id] = row; // Local mutation for performance with large tables
return acc;
}, {})
};
});
/**
* Handler for the `onCancel` event to hide the batch action bar and
* deselect all selected rows
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnCancel", () => {
this.setState(state => {
return {
shouldShowBatchActions: false,
...this.setAllSelectedState(state, false, this.getFilteredRowIds())
};
});
});
/**
* Handler for toggling the selection state of all rows in the database
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleSelectAll", () => {
this.setState(state => {
const filteredRowIds = this.getFilteredRowIds();
const {
rowsById
} = state;
const isSelected = !(Object.values(rowsById).filter(row => row.isSelected && !row.disabled).length > 0);
return {
shouldShowBatchActions: isSelected,
...this.setAllSelectedState(state, isSelected, filteredRowIds)
};
});
});
/**
* Handler for toggling the selection state of a given row.
*
* @param {string} rowId
* @returns {Function}
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnSelectRow", rowId => () => {
this.setState(state => {
const row = state.rowsById[rowId];
if (this.props.radio) {
// deselect all radio buttons
const rowsById = Object.entries(state.rowsById).reduce((p, c) => {
const [key, val] = c;
val.isSelected = false;
p[key] = val;
return p;
}, {});
return {
shouldShowBatchActions: false,
rowsById: {
...rowsById,
[rowId]: {
...row,
isSelected: !row.isSelected
}
}
};
}
const selectedRows = state.rowIds.filter(id => state.rowsById[id].isSelected).length;
// Predict the length of the selected rows after this change occurs
const selectedRowsCount = !row.isSelected ? selectedRows + 1 : selectedRows - 1;
return {
// Basic assumption here is that we want to show the batch action bar if
// the row is being selected. If it's being unselected, then see if we
// have a non-zero number of selected rows that batch actions could
// still apply to
shouldShowBatchActions: !row.isSelected || selectedRowsCount > 0,
rowsById: {
...state.rowsById,
[rowId]: {
...row,
isSelected: !row.isSelected
}
}
};
});
});
/**
* Handler for toggling the expansion state of a given row.
*
* @param {string} rowId
* @returns {Function}
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnExpandRow", rowId => () => {
this.setState(state => {
const row = state.rowsById[rowId];
const {
isExpandedAll
} = state;
return {
isExpandedAll: row.isExpanded ? false : isExpandedAll,
rowsById: {
...state.rowsById,
[rowId]: {
...row,
isExpanded: !row.isExpanded
}
}
};
});
});
/**
* Handler for changing the expansion state of all rows.
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnExpandAll", () => {
this.setState(state => {
const {
rowIds,
isExpandedAll
} = state;
return {
isExpandedAll: !isExpandedAll,
rowsById: rowIds.reduce((acc, id) => ({
...acc,
[id]: {
...state.rowsById[id],
isExpanded: !isExpandedAll
}
}), {})
};
});
});
/**
* Handler for transitioning to the next sort state of the table
*
* @param {string} headerKey the field for the header that we are sorting by
* @returns {Function}
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleSortBy", headerKey => () => {
this.setState(state => sorting.getNextSortState(this.props, state, {
key: headerKey
}));
});
/**
* Event handler for transitioning input value state changes for the table
* filter component.
*
* @param {Event} event
*/
_rollupPluginBabelHelpers.defineProperty(this, "handleOnInputValueChange", (event, defaultValue) => {
if (event.target) {
this.setState({
filterInputValue: event.target.value
});
}
if (defaultValue) {
this.setState({
filterInputValue: defaultValue
});
}
});
this.state = {
...getDerivedStateFromProps["default"](_props, {}),
isExpandedAll: false // Start with collapsed state, treat `undefined` as neutral state
};
this.instanceId = getInstanceId();
}
// if state needs to be updated then wait for only update after state is finished
shouldComponentUpdate(nextProps) {
if (this.props !== nextProps) {
const nextRowIds = nextProps.rows.map(row => row.id);
const rowIds = this.props.rows.map(row => row.id);
if (!isEqual__default["default"](nextRowIds, rowIds)) {
this.setState(state => getDerivedStateFromProps["default"](this.props, state));
return false;
}
const nextHeaders = nextProps.headers.map(header => header.key);
const headers = this.props.headers.map(header => header.key);
if (!isEqual__default["default"](nextHeaders, headers)) {
this.setState(state => getDerivedStateFromProps["default"](this.props, state));
return false;
}
if (!isEqual__default["default"](nextProps.rows, this.props.rows)) {
this.setState(state => getDerivedStateFromProps["default"](this.props, state));
return false;
}
}
return true;
}
render() {
// eslint-disable-next-line react/prop-types
const {
children,
filterRows,
headers,
render
} = this.props;
const {
filterInputValue,
rowIds,
rowsById,
cellsById
} = this.state;
const filteredRowIds = typeof filterInputValue === 'string' ? filterRows({
rowIds,
headers,
cellsById,
inputValue: filterInputValue,
getCellId: cells.getCellId
}) : rowIds;
const renderProps = {
// Data derived from state
rows: denormalize["default"](filteredRowIds, rowsById, cellsById),
headers: this.props.headers,
selectedRows: denormalize["default"](this.getSelectedRows(), rowsById, cellsById),
// Prop accessors/getters
getHeaderProps: this.getHeaderProps,
getExpandHeaderProps: this.getExpandHeaderProps,
getRowProps: this.getRowProps,
getSelectionProps: this.getSelectionProps,
getToolbarProps: this.getToolbarProps,
getBatchActionProps: this.getBatchActionProps,
getTableProps: this.getTableProps,
getTableContainerProps: this.getTableContainerProps,
// Custom event handlers
onInputChange: this.handleOnInputValueChange,
// Expose internal state change actions
sortBy: headerKey => this.handleSortBy(headerKey)(),
selectAll: this.handleSelectAll,
selectRow: rowId => this.handleOnSelectRow(rowId)(),
expandRow: rowId => this.handleOnExpandRow(rowId)(),
expandAll: this.handleOnExpandAll,
radio: this.props.radio
};
if (render !== undefined) {
return render(renderProps);
}
if (children !== undefined) {
return children(renderProps);
}
return null;
}
}
_rollupPluginBabelHelpers.defineProperty(DataTable, "propTypes", {
/**
* Experimental property. Allows table to align cell contents to the top if there is text wrapping in the content. Might have performance issues, intended for smaller tables
*/
experimentalAutoAlign: PropTypes__default["default"].bool,
/**
* Optional hook to manually control filtering of the rows from the
* TableToolbarSearch component
*/
filterRows: PropTypes__default["default"].func,
/**
* The `headers` prop represents the order in which the headers should
* appear in the table. We expect an array of objects to be passed in, where
* `key` is the name of the key in a row object, and `header` is the name of
* the header.
*/
headers: PropTypes__default["default"].arrayOf(PropTypes__default["default"].shape({
key: PropTypes__default["default"].string.isRequired,
header: PropTypes__default["default"].node.isRequired
})).isRequired,
/**
* Specify whether the table should be able to be sorted by its headers
*/
isSortable: PropTypes__default["default"].bool,
/**
* Provide a string for the current locale
*/
locale: PropTypes__default["default"].string,
/**
* Specify whether the overflow menu (if it exists) should be shown always, or only on hover
*/
overflowMenuOnHover: PropTypes__default["default"].bool,
/**
* Specify whether the control should be a radio button or inline checkbox
*/
radio: PropTypes__default["default"].bool,
/**
* The `rows` prop is where you provide us with a list of all the rows that
* you want to render in the table. The only hard requirement is that this
* is an array of objects, and that each object has a unique `id` field
* available on it.
*/
rows: PropTypes__default["default"].arrayOf(PropTypes__default["default"].shape({
id: PropTypes__default["default"].string.isRequired,
disabled: PropTypes__default["default"].bool,
isSelected: PropTypes__default["default"].bool,
isExpanded: PropTypes__default["default"].bool
})).isRequired,
/**
* Change the row height of table. Currently supports `xs`, `sm`, `md`, `lg`, and `xl`.
*/
size: PropTypes__default["default"].oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
/**
* Optional hook to manually control sorting of the rows.
*/
sortRow: PropTypes__default["default"].func,
/**
* Specify whether the header should be sticky.
* Still experimental: may not work with every combination of table props
*/
stickyHeader: PropTypes__default["default"].bool,
/**
* Optional method that takes in a message id and returns an
* internationalized string. See `DataTable.translationKeys` for all
* available message ids.
*/
translateWithId: PropTypes__default["default"].func,
/**
* `false` If true, will use a width of 'auto' instead of 100%
*/
useStaticWidth: PropTypes__default["default"].bool,
/**
* `true` to add useZebraStyles striping.
*/
useZebraStyles: PropTypes__default["default"].bool
});
_rollupPluginBabelHelpers.defineProperty(DataTable, "defaultProps", {
sortRow: sorting$1.defaultSortRow,
filterRows: filter.defaultFilterRows,
locale: 'en',
size: 'lg',
overflowMenuOnHover: true,
translateWithId
});
_rollupPluginBabelHelpers.defineProperty(DataTable, "translationKeys", Object.values(translationKeys));
DataTable.Table = Table.Table;
DataTable.TableActionList = TableActionList["default"];
DataTable.TableBatchAction = TableBatchAction["default"];
DataTable.TableBatchActions = TableBatchActions["default"];
DataTable.TableBody = TableBody["default"];
DataTable.TableCell = TableCell["default"];
DataTable.TableContainer = TableContainer["default"];
DataTable.TableExpandHeader = TableExpandHeader["default"];
DataTable.TableExpandRow = TableExpandRow["default"];
DataTable.TableExpandedRow = TableExpandedRow["default"];
DataTable.TableHead = TableHead["default"];
DataTable.TableHeader = TableHeader["default"];
DataTable.TableRow = TableRow["default"];
DataTable.TableSelectAll = TableSelectAll["default"];
DataTable.TableSelectRow = TableSelectRow["default"];
DataTable.TableToolbar = TableToolbar["default"];
DataTable.TableToolbarAction = TableToolbarAction["default"];
DataTable.TableToolbarContent = TableToolbarContent["default"];
DataTable.TableToolbarSearch = TableToolbarSearch["default"];
DataTable.TableToolbarMenu = TableToolbarMenu["default"];
var DataTable$1 = DataTable;
exports["default"] = DataTable$1;