@hackoregon/component-library
Version:
Official repo for Hack Oregon React component library
256 lines (244 loc) • 6.7 kB
JavaScript
import React, { Component } from "react";
import PropTypes from "prop-types";
import { css } from "emotion";
import shortid from "shortid";
/* eslint-disable jsx-a11y/no-static-element-interactions */
// Classes
const defaultTableClass = css`
width: 100%;
height: 100%;
margin-bottom: 1rem;
background-color: transparent;
border-collapse: collapse;
`;
const defaultHeaderClass = css`
box-sizing: border-box;
`;
const defaultHeaderCellClass = item => css`
border-bottom: ${item.underlined ? "1pt solid black" : ""};
border-collapse: separate;
padding: 0.25rem 1rem;
text-align: center;
`;
const defaultColumnClass = css`
box-sizing: border-box;
`;
const defaultColumnCellClass = item => css`
border-bottom: 2pt solid black;
border-collapse: separate;
cursor: pointer;
padding: 0.25rem 1rem;
text-align: ${item.align || "left"};
`;
const defaultRowClass = css``;
const defaultRowCellClass = item => css`
border-bottom: 1pt solid lightgray;
padding: 0.25rem 1rem;
text-align: ${item.align || "left"};
`;
// Default components
const DefaultColumn = ({
columns,
sortTable,
columnClass,
columnCellClass
}) => (
<tr className={columnClass || defaultColumnClass}>
{columns.map(item => (
<th
key={shortid.generate()}
className={columnCellClass || defaultColumnCellClass(item)}
colSpan={item.colSpan}
onClick={() => sortTable(item.key)}
>
{item.header}
</th>
))}
</tr>
);
DefaultColumn.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.string,
key: PropTypes.string
})
).isRequired,
columnClass: PropTypes.string,
columnCellClass: PropTypes.string,
sortTable: PropTypes.func.isRequired
};
const DefaultHeader = ({ className, columns }) => (
<tr className={className || defaultHeaderClass}>
{columns.map(item => (
<th
key={shortid.generate()}
className={defaultHeaderCellClass(item)}
colSpan={item.colSpan || "1"}
>
{item.header}
</th>
))}
</tr>
);
DefaultHeader.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.string,
key: PropTypes.string
})
).isRequired,
className: PropTypes.string
};
const DefaultRow = ({ data, columns, rowClass, rowCellClass }) => (
<tr className={rowClass || defaultRowClass}>
{columns.map(item => (
<td
key={shortid.generate()}
className={rowCellClass || defaultRowCellClass(item)}
>
{data[item.key]}
</td>
))}
</tr>
);
DefaultRow.propTypes = {
data: PropTypes.shape({}).isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.string,
key: PropTypes.string
})
).isRequired,
rowClass: PropTypes.string,
rowCellClass: PropTypes.string
};
export default class DataTable extends Component {
constructor(props) {
super(props);
this.state = {
asc: true,
sorted: props.data.data,
flattenedColumns: this.flattenColumns(props.data.columns)
};
}
compare = (sortpivot, asc) => {
// Returns a sorting function
return (a, b) => {
// force null and undefined to the bottom
let A =
a[sortpivot] === null || a[sortpivot] === undefined ? "" : a[sortpivot];
let B =
b[sortpivot] === null || b[sortpivot] === undefined ? "" : b[sortpivot];
// force any string values to lowercase for sorting accuracy
A =
typeof a[sortpivot] === "string"
? a[sortpivot].toLowerCase()
: a[sortpivot];
B =
typeof b[sortpivot] === "string"
? b[sortpivot].toLowerCase()
: b[sortpivot];
// Return either 1 or -1 to indicate a sort priority
if (asc) {
if (A > B) return 1;
if (A < B) return -1;
// returning 0, undefined or any falsey value will use subsequent sorts or
// the index as a tiebreaker
return 0;
}
if (A > B) return -1;
if (A < B) return 1;
// returning 0, undefined or any falsey value will use subsequent sorts or
// the index as a tiebreaker
return 0;
};
};
flattenColumns = topLevelColumns => {
return Object.values(topLevelColumns).reduce(
(accumulator, currentValue) => {
if (currentValue.columns) {
accumulator.push(...currentValue.columns);
} else {
accumulator.push(currentValue);
}
return accumulator;
},
[]
);
};
generateHeaderAndColumns = topLevelColumns => {
const { HeaderComponent, ColumnComponent } = this.props;
const Header = HeaderComponent || DefaultHeader;
const Column = ColumnComponent || DefaultColumn;
const headerValues = [];
const columnValues = [];
Object.values(topLevelColumns).forEach(item => {
if (item.columns) {
headerValues.push({
...item,
colSpan: item.columns.length,
underlined: true
});
columnValues.push(...item.columns);
} else {
headerValues.push({
colSpan: "1",
key: item.key,
underlined: false
});
columnValues.push({ ...item });
}
});
return (
<thead>
{<Header columns={headerValues} />}
{<Column columns={columnValues} sortTable={this.sortTable} />}
</thead>
);
};
sortTable = (sortKey = "") => {
const { sorted: prevSorted, asc } = this.state;
const sorted = Object.values(prevSorted).sort(this.compare(sortKey, asc));
this.setState(prevState => {
return {
sorted,
asc: !prevState.asc
};
});
};
render() {
const { data, tableClass, RowComponent } = this.props;
const { sorted, flattenedColumns } = this.state;
const Row = RowComponent || DefaultRow;
return (
<table className={tableClass || defaultTableClass}>
{this.generateHeaderAndColumns(data.columns)}
<tbody>
{Object.keys(sorted).map(id => (
<Row
key={shortid.generate()}
id={id}
columns={flattenedColumns}
data={sorted[id]}
/>
))}
</tbody>
</table>
);
}
}
DataTable.propTypes = {
data: PropTypes.shape({
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.string,
key: PropTypes.string
})
).isRequired,
data: PropTypes.arrayOf(PropTypes.shape({})).isRequired
}).isRequired,
HeaderComponent: PropTypes.element,
ColumnComponent: PropTypes.element,
RowComponent: PropTypes.element,
tableClass: PropTypes.string
};