Skip to content

Commit 04869b5

Browse files
author
tiltom
authored
SOV-678: add address table component (#44)
* Initial commit * Add Jest tests * Add clickable and hover styles * Fix row borders to be 1px wide * Fix review remarks * Create empty-socks-grin.md * QA adjustments
1 parent 68346b6 commit 04869b5

9 files changed

Lines changed: 408 additions & 0 deletions

File tree

.changeset/empty-socks-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sovryn/ui": patch
3+
---
4+
5+
SOV-678: TableBase (AddressTable) component

packages/ui/src/1_atoms/Icon/Icon.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export enum IconNames {
99
ARROW_DOWN = 'arrow-down',
1010
ARROW_RIGHT = 'arrow-right',
1111
ARROW_FORWARD = 'arrow-forward',
12+
ARROW_BACK = 'arrow-back',
1213
DEPOSIT = 'deposit',
1314
DISCONNECT = 'disconnect',
1415
EDIT = 'edit',
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.table {
2+
@apply w-full min-w-auto h-full min-h-auto;
3+
4+
& th {
5+
@apply px-4;
6+
}
7+
}
8+
9+
.text {
10+
&Left {
11+
@apply text-left;
12+
}
13+
14+
&Center {
15+
@apply text-center;
16+
}
17+
18+
&Right {
19+
@apply text-right;
20+
}
21+
}
22+
23+
.noData {
24+
@apply relative px-4 py-4 text-center;
25+
}
26+
27+
.header {
28+
@apply text-xs font-medium leading-[0.875rem];
29+
}
30+
31+
.body {
32+
&:before {
33+
@apply content-['@'] block leading-3 invisible;
34+
}
35+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Story } from '@storybook/react';
2+
3+
import React, { ComponentProps } from 'react';
4+
5+
import { TransactionId } from '../TransactionId';
6+
import { TableBase } from './TableBase';
7+
import { Align } from './TableBase.types';
8+
9+
const columns = [
10+
{
11+
id: 'index',
12+
title: 'Index',
13+
align: Align.left,
14+
cellRenderer: row => `${row.index}.`,
15+
},
16+
{
17+
id: 'address',
18+
title: 'Address',
19+
align: Align.left,
20+
},
21+
{
22+
id: 'balance',
23+
title: 'Balance',
24+
align: Align.left,
25+
cellRenderer: row => `${row.balance} RBTC`,
26+
},
27+
];
28+
29+
// TODO: Change hardcoded addresses for TransactionId component once it's merged
30+
const rows = [
31+
{
32+
index: 1,
33+
address: (
34+
<TransactionId
35+
value="0xbcb5a190ACCbc80F4F2c130b5876521E4D5A2C0a"
36+
href="https://explorer.testnet.rsk.co/address/0xbcb5a190accbc80f4f2c130b5876521e4d5a2c0a"
37+
/>
38+
),
39+
balance: 0.2,
40+
},
41+
{
42+
index: 2,
43+
address: (
44+
<TransactionId
45+
value="0xop42490ACCbc50F4F9c130b5876521I1q7b3C0p"
46+
href="https://explorer.testnet.rsk.co/address/0xop42490ACCbc50F4F9c130b5876521I1q7b3C0p"
47+
/>
48+
),
49+
balance: 2,
50+
},
51+
];
52+
53+
export default {
54+
title: 'Molecule/TableBase',
55+
component: TableBase,
56+
};
57+
58+
const Template: Story<ComponentProps<typeof TableBase>> = args => (
59+
<div className="max-w-sm">
60+
<TableBase {...args} />
61+
</div>
62+
);
63+
64+
export const Basic = Template.bind({});
65+
Basic.args = {
66+
columns,
67+
rows,
68+
dataAttribute: 'addressTable',
69+
rowKey: row => `my-custom-key-${row.index}`,
70+
};
71+
72+
export const WithRowClickHandler = Template.bind({});
73+
WithRowClickHandler.args = {
74+
columns,
75+
rows,
76+
onRowClick: row =>
77+
alert(
78+
`Row with index ${row.index} and balance ${row.balance} RBTC was clicked`,
79+
),
80+
dataAttribute: 'addressTable',
81+
isClickable: true,
82+
};
83+
84+
export const NoData = Template.bind({});
85+
NoData.args = {
86+
columns,
87+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import React from 'react';
5+
6+
import { prettyTx } from '../../utils';
7+
import { TableBase } from './TableBase';
8+
import { Align } from './TableBase.types';
9+
10+
const testColumns = [
11+
{
12+
id: 'index',
13+
title: 'Index',
14+
align: Align.center,
15+
cellRenderer: row => `${row.index}.`,
16+
},
17+
{
18+
id: 'address',
19+
title: 'Address',
20+
align: Align.center,
21+
},
22+
{
23+
id: 'balance',
24+
title: 'Balance',
25+
align: Align.center,
26+
cellRenderer: row => `${row.balance} RBTC`,
27+
},
28+
];
29+
30+
const testRows = [
31+
{
32+
index: 1,
33+
address: prettyTx('0xbcb5a190ACCbc80F4F2c130b5876521E4D5A2C0a', 6, 4),
34+
balance: 0.2,
35+
},
36+
{
37+
index: 2,
38+
address: prettyTx('0xop42490ACCbc50F4F9c130b5876521I1q7b3C0p', 6, 4),
39+
balance: 2,
40+
},
41+
];
42+
43+
describe('TableBase', () => {
44+
test('should render a table with 2 items', () => {
45+
const { getAllByTestId } = render(
46+
<TableBase columns={testColumns} rows={testRows} dataAttribute="table" />,
47+
);
48+
49+
const rows = getAllByTestId('table-row', { exact: false });
50+
expect(rows.length).toBe(2);
51+
});
52+
53+
test('should click on an item if onRowClick is provided', () => {
54+
const handleClick = jest.fn();
55+
56+
const { getByTestId } = render(
57+
<TableBase
58+
columns={testColumns}
59+
rows={testRows}
60+
dataAttribute="table"
61+
onRowClick={handleClick}
62+
/>,
63+
);
64+
65+
const row = getByTestId('table-row-1');
66+
userEvent.click(row);
67+
68+
expect(handleClick).toBeCalledTimes(1);
69+
});
70+
71+
test('should contain an empty message if there are no rows', () => {
72+
const noDataText = 'You should see this as there are no rows';
73+
const { getByText } = render(
74+
<TableBase columns={testColumns} noData={noDataText} />,
75+
);
76+
77+
const emptyText = getByText(noDataText);
78+
expect(emptyText).toBeInTheDocument();
79+
});
80+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { ReactNode } from 'react';
2+
3+
import classNames from 'classnames';
4+
5+
import styles from './TableBase.module.css';
6+
import { ColumnOptions, RowObject } from './TableBase.types';
7+
import { TableRow } from './components/TableRow/TableRow';
8+
import rowStyles from './components/TableRow/TableRow.module.css';
9+
10+
export type TableBaseProps<RowType extends RowObject> = {
11+
className?: string;
12+
columns: ColumnOptions<RowType>[];
13+
rows?: RowType[];
14+
rowKey?: (row: RowType) => number | string;
15+
noData?: ReactNode;
16+
onRowClick?: (row: RowType) => void;
17+
dataAttribute?: string;
18+
isClickable?: boolean;
19+
};
20+
21+
// No React.FC, since doesn't support Generic PropTypes
22+
export const TableBase = <RowType extends RowObject>({
23+
className,
24+
columns,
25+
rows,
26+
rowKey,
27+
noData,
28+
onRowClick,
29+
dataAttribute,
30+
isClickable,
31+
}: TableBaseProps<RowType>) => {
32+
return (
33+
<table
34+
className={classNames(styles.table, className)}
35+
data-layout-id={dataAttribute}
36+
>
37+
<thead>
38+
<tr>
39+
{columns.map(column => (
40+
<th
41+
key={column.id.toString()}
42+
className={classNames(
43+
styles.header,
44+
column.align && [styles[`text${column.align}`]],
45+
column.className,
46+
)}
47+
>
48+
<>{column.title || column.id}</>
49+
</th>
50+
))}
51+
</tr>
52+
</thead>
53+
<tbody className={styles.body}>
54+
{rows && rows.length >= 1 ? (
55+
rows.map((row, index) => (
56+
<TableRow
57+
key={rowKey?.(row) || JSON.stringify(row)}
58+
columns={columns}
59+
row={row}
60+
index={index}
61+
onRowClick={onRowClick}
62+
dataAttribute={dataAttribute}
63+
isClickable={isClickable}
64+
/>
65+
))
66+
) : (
67+
<tr className={rowStyles.row}>
68+
<td className={styles.noData} colSpan={999}>
69+
{noData ? noData : 'No data'}
70+
</td>
71+
</tr>
72+
)}
73+
</tbody>
74+
</table>
75+
);
76+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ReactNode } from 'react';
2+
3+
export type RowObject = { [param: string]: any };
4+
5+
export enum Align {
6+
left = 'Left',
7+
center = 'Center',
8+
right = 'Right',
9+
}
10+
11+
export type ColumnOptions<RowType extends RowObject> = {
12+
id: keyof RowType | string;
13+
title?: ReactNode;
14+
align?: Align;
15+
className?: string;
16+
cellRenderer?: (
17+
row: RowType,
18+
columnId: ColumnOptions<RowType>['id'],
19+
) => ReactNode;
20+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.row {
2+
@apply cursor-pointer sm:cursor-auto outline outline-1 outline-gray-70 rounded bg-gray-90 text-xs font-medium;
3+
4+
& > td {
5+
@apply py-2.5 px-5;
6+
7+
&:first-child {
8+
@apply rounded-l;
9+
}
10+
11+
&:last-child {
12+
@apply rounded-r;
13+
}
14+
}
15+
}
16+
17+
.clickable {
18+
@apply cursor-pointer;
19+
20+
&:hover {
21+
@apply outline-gray-50;
22+
}
23+
}
24+
25+
.active {
26+
@apply outline-gray-50 bg-gray-70;
27+
}
28+
29+
.text {
30+
&Left {
31+
@apply text-left;
32+
}
33+
34+
&Center {
35+
@apply text-center;
36+
}
37+
38+
&Right {
39+
@apply text-right;
40+
}
41+
}
42+
43+
.spacer {
44+
@apply h-2;
45+
}

0 commit comments

Comments
 (0)