Merged in feature/sortable_table (pull request #367)

Feature/sortable table
This commit is contained in:
Eduard Heimbuch
2019-12-03 15:08:13 +00:00
9 changed files with 417 additions and 4 deletions

View File

@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
exports[`Storyshots Forms|Checkbox Default 1`] = `
<div
className="sc-hzDkRC itNtPz"
className="sc-fBuWsC ldmpJA"
>
<div
className="field"
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
<div
className="sc-hzDkRC itNtPz"
className="sc-fBuWsC ldmpJA"
>
<div
className="field"
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
exports[`Storyshots Forms|Radio Default 1`] = `
<div
className="sc-jhAzac edPAWD"
className="sc-fMiknA keSQNk"
>
<label
className="radio"
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
exports[`Storyshots Forms|Radio Disabled 1`] = `
<div
className="sc-jhAzac edPAWD"
className="sc-fMiknA keSQNk"
>
<label
className="radio"
@@ -2311,3 +2311,164 @@ PORT_NUMBER =
</pre>
</div>
`;
exports[`Storyshots Table|Table Default 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
First Name
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Last Name
<i
className="fas fa-sort-amount-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
E-Mail
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4>
Tricia
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
McMillan
</b>
</td>
<td>
<a>
tricia@hitchhiker.com
</a>
</td>
</tr>
<tr>
<td>
<h4>
Arthur
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
Dent
</b>
</td>
<td>
<a>
arthur@hitchhiker.com
</a>
</td>
</tr>
</tbody>
</table>
`;
exports[`Storyshots Table|Table TextColumn 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Id
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Name
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Description
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
21
</td>
<td>
Pommes
</td>
<td>
Fried potato sticks
</td>
</tr>
<tr>
<td>
42
</td>
<td>
Quarter-Pounder
</td>
<td>
Big burger
</td>
</tr>
<tr>
<td>
-84
</td>
<td>
Icecream
</td>
<td>
Cold dessert
</td>
</tr>
</tbody>
</table>
`;

View File

@@ -63,6 +63,7 @@ export * from "./layout";
export * from "./modals";
export * from "./navigation";
export * from "./repos";
export * from "./table";
export {
File,

View File

@@ -0,0 +1,18 @@
import React, { FC, ReactNode } from "react";
import { ColumnProps } from "./table";
type Props = ColumnProps & {
children: (row: any, columnIndex: number) => ReactNode;
};
const Column: FC<Props> = ({ row, columnIndex, children }) => {
if (row === undefined) {
throw new Error("missing row, use column only as child of Table");
}
if (columnIndex === undefined) {
throw new Error("missing row, use column only as child of Table");
}
return <>{children(row, columnIndex)}</>;
};
export default Column;

View File

@@ -0,0 +1,19 @@
import React, { FC } from "react";
import styled from "styled-components";
import Icon from "../Icon";
type Props = {
name: string;
isVisible: boolean;
};
const IconWithMarginLeft = styled(Icon)`
visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
margin-left: 0.25em;
`;
const SortIcon: FC<Props> = (props: Props) => {
return <IconWithMarginLeft name={props.name} isVisible={props.isVisible} />;
};
export default SortIcon;

View File

@@ -0,0 +1,53 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import Table from "./Table";
import Column from "./Column";
import TextColumn from "./TextColumn";
storiesOf("Table|Table", module)
.add("Default", () => (
<Table
data={[
{ firstname: "Tricia", lastname: "McMillan", email: "tricia@hitchhiker.com" },
{ firstname: "Arthur", lastname: "Dent", email: "arthur@hitchhiker.com" }
]}
>
<Column header={"First Name"}>{(row: any) => <h4>{row.firstname}</h4>}</Column>
<Column
header={"Last Name"}
createComparator={() => {
return (a: any, b: any) => {
if (a.lastname > b.lastname) {
return -1;
} else if (a.lastname < b.lastname) {
return 1;
} else {
return 0;
}
};
}}
>
{(row: any) => <b style={{ color: "red" }}>{row.lastname}</b>}
</Column>
<Column header={"E-Mail"}>{(row: any) => <a>{row.email}</a>}</Column>
</Table>
))
.add("TextColumn", () => (
<Table
data={[
{ id: "21", title: "Pommes", desc: "Fried potato sticks" },
{ id: "42", title: "Quarter-Pounder", desc: "Big burger" },
{ id: "-84", title: "Icecream", desc: "Cold dessert" }
]}
>
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="title" />
<TextColumn header="Description" dataKey="desc" />
</Table>
))
.add("Empty", () => (
<Table data={[]} emptyMessage="No data found.">
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="name" />
</Table>
));

View File

@@ -0,0 +1,117 @@
import React, { FC, ReactElement, useState } from "react";
import styled from "styled-components";
import { Comparator } from "./table";
import SortIcon from "./SortIcon";
import Notification from "../Notification";
const StyledTable = styled.table.attrs(() => ({
className: "table content is-hoverable"
}))``;
type Props = {
data: any[];
sortable?: boolean;
emptyMessage?: string;
children: Array<ReactElement>;
};
const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
const [tableData, setTableData] = useState(data);
const [ascending, setAscending] = useState(false);
const [lastSortBy, setlastSortBy] = useState<number | undefined>();
const [hoveredColumnIndex, setHoveredColumnIndex] = useState<number | undefined>();
const isSortable = (child: ReactElement) => {
return sortable && child.props.createComparator;
};
const sortFunctions: Comparator | undefined[] = [];
React.Children.forEach(children, (child, index) => {
if (child && isSortable(child)) {
sortFunctions.push(child.props.createComparator(child.props, index));
} else {
sortFunctions.push(undefined);
}
});
const mapDataToColumns = (row: any) => {
return (
<tr>
{React.Children.map(children, (child, columnIndex) => {
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
})}
</tr>
);
};
const sortDescending = (sortAscending: (a: any, b: any) => number) => {
return (a: any, b: any) => {
return sortAscending(a, b) * -1;
};
};
const tableSort = (index: number) => {
const sortFn = sortFunctions[index];
if (!sortFn) {
throw new Error(`column with index ${index} is not sortable`);
}
const sortableData = [...tableData];
let sortOrder = ascending;
if (lastSortBy !== index) {
setAscending(true);
sortOrder = true;
}
const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
sortableData.sort(sortFunction);
setTableData(sortableData);
setAscending(!sortOrder);
setlastSortBy(index);
};
const shouldShowIcon = (index: number) => {
return index === lastSortBy || index === hoveredColumnIndex;
};
if (!tableData || tableData.length <= 0) {
if (emptyMessage) {
return <Notification type="info">{emptyMessage}</Notification>;
} else {
return null;
}
}
return (
<StyledTable>
<thead>
<tr>
{React.Children.map(children, (child, index) => (
<th
className={isSortable(child) && "has-cursor-pointer"}
onClick={isSortable(child) ? () => tableSort(index) : undefined}
onMouseEnter={() => setHoveredColumnIndex(index)}
onMouseLeave={() => setHoveredColumnIndex(undefined)}
>
{child.props.header}
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
</th>
))}
</tr>
</thead>
<tbody>{tableData.map(mapDataToColumns)}</tbody>
</StyledTable>
);
};
Table.defaultProps = {
sortable: true
};
const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
if (child.props.ascendingIcon && child.props.descendingIcon) {
return <SortIcon name={ascending ? child.props.ascendingIcon : child.props.descendingIcon} isVisible={showIcon} />;
} else {
return <SortIcon name={ascending ? "sort-amount-down-alt" : "sort-amount-down"} isVisible={showIcon} />;
}
};
export default Table;

View File

@@ -0,0 +1,28 @@
import React, { FC } from "react";
import { ColumnProps } from "./table";
type Props = ColumnProps & {
dataKey: string;
};
const TextColumn: FC<Props> = ({ row, dataKey }) => {
return row[dataKey];
};
TextColumn.defaultProps = {
createComparator: (props: Props) => {
return (a: any, b: any) => {
if (a[props.dataKey] < b[props.dataKey]) {
return -1;
} else if (a[props.dataKey] > b[props.dataKey]) {
return 1;
} else {
return 0;
}
};
},
ascendingIcon: "sort-alpha-down-alt",
descendingIcon: "sort-alpha-down"
};
export default TextColumn;

View File

@@ -0,0 +1,4 @@
export { default as Table } from "./Table";
export { default as Column } from "./Column";
export { default as TextColumn } from "./TextColumn";
export { default as SortIcon } from "./SortIcon";

View File

@@ -0,0 +1,12 @@
import { ReactNode } from "react";
export type Comparator = (a: any, b: any) => number;
export type ColumnProps = {
header: ReactNode;
row?: any;
columnIndex?: number;
createComparator?: (props: any, columnIndex: number) => Comparator;
ascendingIcon?: string;
descendingIcon?: string;
};