Skip to content

Commit 362dba5

Browse files
authored
feat(table): add sortable table components and hook
Add a component and a hook for making tables sortable.
1 parent ec00614 commit 362dba5

14 files changed

+715
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { Colors } from '../../../essentials';
3+
import { IconProps } from '../../../icons';
4+
5+
type Props = IconProps & {
6+
direction: 'ASC' | 'DESC' | 'NONE';
7+
};
8+
9+
const SortingIndicator: React.FC<Props> = ({
10+
size = 20,
11+
color = Colors.AUTHENTIC_BLUE_900,
12+
direction,
13+
...props
14+
}: IconProps) => (
15+
<svg color={color} width={size} height={size} viewBox="0 0 20 20" fill="none" {...props}>
16+
<path
17+
d="M10 15.8334L7.83494 13.3334L5.66987 10.8334L10 10.8334L14.3301 10.8334L12.1651 13.3334L10 15.8334Z"
18+
fill={direction === 'DESC' ? 'currentColor' : Colors.AUTHENTIC_BLUE_200}
19+
/>
20+
<path
21+
d="M10 4.16663L7.83494 6.66663L5.66987 9.16663L10 9.16663L14.3301 9.16663L12.1651 6.66663L10 4.16663Z"
22+
fill={direction === 'ASC' ? 'currentColor' : Colors.AUTHENTIC_BLUE_200}
23+
/>
24+
</svg>
25+
);
26+
27+
export { SortingIndicator };

src/components/Table/components/Table.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ const TableElement: StyledComponent<FC<TableElementProps>, typeof theme> = style
3636
${compose(margin, width, height)}
3737
`;
3838

39-
const Table: FC<TableProps> = ({ children, rowStyle, rowSize = 'normal', columnSpace = 'normal', ...props }) => {
39+
const Table: FC<TableProps> = ({
40+
children,
41+
rowStyle,
42+
rowSize = 'normal',
43+
columnSpace = 'normal',
44+
...props
45+
}: TableProps) => {
4046
const context = {
4147
columnSpace: getColumnSpace(columnSpace),
4248
rowSize: getRowSize(rowSize),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
import { SortingDirection } from '../types';
5+
import { TableSortableHeaderCell } from './TableSortableHeaderCell';
6+
7+
const handleSortChange = jest.fn();
8+
9+
const renderComponent = (active = false, direction = 'ASC') =>
10+
render(
11+
<table>
12+
<thead>
13+
<tr>
14+
<TableSortableHeaderCell
15+
active={active}
16+
field="address"
17+
direction={direction as SortingDirection}
18+
onSortChange={handleSortChange}
19+
>
20+
Address
21+
</TableSortableHeaderCell>
22+
</tr>
23+
</thead>
24+
</table>
25+
);
26+
27+
describe('TableSortableHeaderCell', () => {
28+
it('should indicate the ASC direction', () => {
29+
renderComponent(true, 'ASC');
30+
31+
expect(screen.getByText('Address', { exact: false })).toMatchSnapshot();
32+
});
33+
34+
it('should indicate the DESC direction', () => {
35+
renderComponent(true, 'DESC');
36+
37+
expect(screen.getByText('Address', { exact: false })).toMatchSnapshot();
38+
});
39+
40+
it('should have aria-sort none when inactive', () => {
41+
renderComponent();
42+
43+
expect(screen.getByText('Address', { exact: false }).parentElement).toHaveAttribute('aria-sort', 'none');
44+
});
45+
46+
it('should have aria-sort ascending when active and direction is ASC', () => {
47+
renderComponent(true, 'ASC');
48+
49+
expect(screen.getByText('Address', { exact: false }).parentElement).toHaveAttribute('aria-sort', 'ascending');
50+
});
51+
52+
it('should have aria-sort descending when active and direction is DESC', () => {
53+
renderComponent(true, 'DESC');
54+
55+
expect(screen.getByText('Address', { exact: false }).parentElement).toHaveAttribute('aria-sort', 'descending');
56+
});
57+
58+
it('should call the callback with the right arguments', () => {
59+
renderComponent(true, 'ASC');
60+
61+
userEvent.click(screen.getByText('Address', { exact: false }));
62+
63+
expect(handleSortChange).toHaveBeenCalledWith('address', 'ASC');
64+
});
65+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { FC } from 'react';
2+
import styled from 'styled-components';
3+
import { Box } from '../../Box/Box';
4+
import { SortingIndicator } from './SortingIndicator';
5+
import { TableHeaderCell, TableHeaderCellProps } from './TableHeaderCell';
6+
import type { SortingDirection } from '../types';
7+
8+
const TableHeaderCellWithPointer = styled(TableHeaderCell)`
9+
cursor: pointer;
10+
user-select: none;
11+
`;
12+
13+
type TableSortableHeaderCellProps = TableHeaderCellProps & {
14+
active: boolean;
15+
field: string;
16+
direction: SortingDirection;
17+
onSortChange: (field: string, direction: string) => void;
18+
};
19+
20+
const TableSortableHeaderCell: FC<TableSortableHeaderCellProps> = ({
21+
active,
22+
direction,
23+
children,
24+
field,
25+
onSortChange,
26+
...rest
27+
}: TableSortableHeaderCellProps) => (
28+
<TableHeaderCellWithPointer
29+
onClick={() => onSortChange(field, direction)}
30+
aria-sort={active ? (direction === 'ASC' ? 'ascending' : 'descending') : 'none'}
31+
{...rest}
32+
>
33+
<Box display="inline-flex" alignItems="center">
34+
{children}
35+
<Box mr="0.25rem" />
36+
<Box flexShrink="0" display="inline-flex" alignItems="center">
37+
<SortingIndicator direction={active ? direction : 'NONE'} />
38+
</Box>
39+
</Box>
40+
</TableHeaderCellWithPointer>
41+
);
42+
43+
export { TableSortableHeaderCell, TableSortableHeaderCellProps };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`TableSortableHeaderCell should indicate the ASC direction 1`] = `
4+
.c0 {
5+
display: -webkit-inline-box;
6+
display: -webkit-inline-flex;
7+
display: -ms-inline-flexbox;
8+
display: inline-flex;
9+
-webkit-align-items: center;
10+
-webkit-box-align: center;
11+
-ms-flex-align: center;
12+
align-items: center;
13+
}
14+
15+
.c1 {
16+
margin-right: 0.25rem;
17+
}
18+
19+
.c2 {
20+
-webkit-flex-shrink: 0;
21+
-ms-flex-negative: 0;
22+
flex-shrink: 0;
23+
display: -webkit-inline-box;
24+
display: -webkit-inline-flex;
25+
display: -ms-inline-flexbox;
26+
display: inline-flex;
27+
-webkit-align-items: center;
28+
-webkit-box-align: center;
29+
-ms-flex-align: center;
30+
align-items: center;
31+
}
32+
33+
<div
34+
class="c0"
35+
display="inline-flex"
36+
>
37+
Address
38+
<div
39+
class="c1"
40+
/>
41+
<div
42+
class="c2"
43+
display="inline-flex"
44+
>
45+
<svg
46+
color="#001E3E"
47+
fill="none"
48+
height="20"
49+
viewBox="0 0 20 20"
50+
width="20"
51+
>
52+
<path
53+
d="M10 15.8334L7.83494 13.3334L5.66987 10.8334L10 10.8334L14.3301 10.8334L12.1651 13.3334L10 15.8334Z"
54+
fill="#C6CDD4"
55+
/>
56+
<path
57+
d="M10 4.16663L7.83494 6.66663L5.66987 9.16663L10 9.16663L14.3301 9.16663L12.1651 6.66663L10 4.16663Z"
58+
fill="currentColor"
59+
/>
60+
</svg>
61+
</div>
62+
</div>
63+
`;
64+
65+
exports[`TableSortableHeaderCell should indicate the DESC direction 1`] = `
66+
.c0 {
67+
display: -webkit-inline-box;
68+
display: -webkit-inline-flex;
69+
display: -ms-inline-flexbox;
70+
display: inline-flex;
71+
-webkit-align-items: center;
72+
-webkit-box-align: center;
73+
-ms-flex-align: center;
74+
align-items: center;
75+
}
76+
77+
.c1 {
78+
margin-right: 0.25rem;
79+
}
80+
81+
.c2 {
82+
-webkit-flex-shrink: 0;
83+
-ms-flex-negative: 0;
84+
flex-shrink: 0;
85+
display: -webkit-inline-box;
86+
display: -webkit-inline-flex;
87+
display: -ms-inline-flexbox;
88+
display: inline-flex;
89+
-webkit-align-items: center;
90+
-webkit-box-align: center;
91+
-ms-flex-align: center;
92+
align-items: center;
93+
}
94+
95+
<div
96+
class="c0"
97+
display="inline-flex"
98+
>
99+
Address
100+
<div
101+
class="c1"
102+
/>
103+
<div
104+
class="c2"
105+
display="inline-flex"
106+
>
107+
<svg
108+
color="#001E3E"
109+
fill="none"
110+
height="20"
111+
viewBox="0 0 20 20"
112+
width="20"
113+
>
114+
<path
115+
d="M10 15.8334L7.83494 13.3334L5.66987 10.8334L10 10.8334L14.3301 10.8334L12.1651 13.3334L10 15.8334Z"
116+
fill="currentColor"
117+
/>
118+
<path
119+
d="M10 4.16663L7.83494 6.66663L5.66987 9.16663L10 9.16663L14.3301 9.16663L12.1651 6.66663L10 4.16663Z"
120+
fill="#C6CDD4"
121+
/>
122+
</svg>
123+
</div>
124+
</div>
125+
`;
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { FC, useCallback } from 'react';
2+
import { orderBy } from 'lodash';
3+
import { Table, TableCell, TableSortableHeaderCell, TableRow } from '..';
4+
import { Text } from '../..';
5+
import { useSortBy } from '../hooks/useSortBy';
6+
7+
const data = [
8+
{
9+
id: 1,
10+
name: 'Zaha',
11+
emailAddress: '[email protected]',
12+
role: 'Admin',
13+
costCenter: 'Design System'
14+
},
15+
{
16+
id: 2,
17+
name: 'Alex',
18+
emailAddress: '[email protected]',
19+
role: 'Booker',
20+
costCenter: 'Product'
21+
},
22+
{
23+
id: 3,
24+
name: 'Britta',
25+
emailAddress: '[email protected]',
26+
role: 'Passenger',
27+
costCenter: 'Customer'
28+
},
29+
{
30+
id: 4,
31+
name: 'Caio',
32+
emailAddress: '[email protected]',
33+
role: 'Admin',
34+
costCenter: 'Design System'
35+
}
36+
];
37+
38+
export const SortableTable: FC = () => {
39+
const { sortBy, setSortBy } = useSortBy();
40+
const handleSortingChange = useCallback(
41+
field => {
42+
setSortBy(field);
43+
},
44+
[setSortBy]
45+
);
46+
47+
const direction = sortBy.direction?.toLowerCase() || 'asc';
48+
// @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'Many<boolean | "asc" | "desc"> | undefined'.ts(2769)
49+
const sortedData = orderBy(data, sortBy.field, direction);
50+
51+
return (
52+
<Table rowStyle="zebra" rowSize="small">
53+
<thead>
54+
<TableRow>
55+
<TableSortableHeaderCell
56+
field="name"
57+
active={sortBy.field === 'name'}
58+
direction={sortBy.direction}
59+
onSortChange={handleSortingChange}
60+
>
61+
Name
62+
</TableSortableHeaderCell>
63+
<TableSortableHeaderCell
64+
field="emailAddress"
65+
active={sortBy.field === 'emailAddress'}
66+
direction={sortBy.direction}
67+
onSortChange={handleSortingChange}
68+
>
69+
E-Mail
70+
</TableSortableHeaderCell>
71+
<TableSortableHeaderCell
72+
field="role"
73+
active={sortBy.field === 'role'}
74+
direction={sortBy.direction}
75+
onSortChange={handleSortingChange}
76+
>
77+
Role
78+
</TableSortableHeaderCell>
79+
<TableSortableHeaderCell
80+
field="costCenter"
81+
active={sortBy.field === 'costCenter'}
82+
direction={sortBy.direction}
83+
onSortChange={handleSortingChange}
84+
>
85+
Cost Center
86+
</TableSortableHeaderCell>
87+
</TableRow>
88+
</thead>
89+
<tbody>
90+
{sortedData.map(entry => (
91+
<TableRow key={entry.id}>
92+
<TableCell>
93+
<Text fontWeight="semibold" fontSize={1}>
94+
{entry.name}
95+
</Text>
96+
</TableCell>
97+
<TableCell>{entry.emailAddress}</TableCell>
98+
<TableCell>{entry.role}</TableCell>
99+
<TableCell>{entry.costCenter}</TableCell>
100+
</TableRow>
101+
))}
102+
</tbody>
103+
</Table>
104+
);
105+
};

0 commit comments

Comments
 (0)