Skip to content

Commit 4a3d29c

Browse files
authored
feat: Implement sortable functionality in MultiLevelTable component (#8)
* feat: Implement sortable functionality in MultiLevelTable component - Added `sortable` prop to MultiLevelTable and TableHeader components. - Introduced custom sorting logic for the 'Status' column. - Updated styles to indicate sortable columns with a pointer cursor. - Enhanced table header to conditionally render sorting indicators based on column state. * refactor: Move sort types to constant * fix: Fix lint errors * feat: Accept custom sort icons
1 parent af588ec commit 4a3d29c

File tree

6 files changed

+90
-17
lines changed

6 files changed

+90
-17
lines changed

src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,14 @@ const columns = [
373373
key: "status",
374374
title: "Status",
375375
filterable: true,
376+
sortable: true,
377+
customSortFn: (rowA: any, rowB: any, columnId: string) => {
378+
const statusOrder = { 'Active': 0, 'Pending': 1, 'Inactive': 2 };
379+
const statusA = String(rowA[columnId]);
380+
const statusB = String(rowB[columnId]);
381+
382+
return (statusOrder[statusA as keyof typeof statusOrder] || 0) - (statusOrder[statusB as keyof typeof statusOrder] || 0);
383+
},
376384
render: (value: unknown) => (
377385
<span
378386
style={{
@@ -411,6 +419,9 @@ function App() {
411419
data={data}
412420
columns={columns}
413421
pageSize={5}
422+
sortable={true}
423+
ascendingIcon={<div></div>}
424+
descendingIcon={<div></div>}
414425
renderCustomPagination={renderCustomPagination}
415426
/>
416427
</div>

src/components/MultiLevelTable.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Pagination } from "./Pagination";
77
import type { PaginationProps } from "./Pagination";
88
import { TableHeader } from "./TableHeader";
99
import { TableRow } from "./TableRow";
10+
import { SortType } from '../constants/sort';
1011
import type {
1112
Column,
1213
DataItem,
@@ -30,6 +31,9 @@ export interface MultiLevelTableProps {
3031
childrenKey?: string;
3132
pageSize?: number;
3233
renderCustomPagination?: (props?: PaginationProps) => React.ReactNode;
34+
sortable?: boolean;
35+
ascendingIcon?: React.ReactNode;
36+
descendingIcon?: React.ReactNode;
3337
}
3438

3539
/**
@@ -44,6 +48,9 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
4448
childrenKey = "children",
4549
pageSize = 10,
4650
renderCustomPagination = null,
51+
sortable = false,
52+
ascendingIcon,
53+
descendingIcon,
4754
}) => {
4855
const [filterInput, setFilterInput] = useState("");
4956
const [expandedRows, setExpandedRows] = useState<Set<string | number>>(
@@ -80,6 +87,9 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
8087
return columns.map((col) => ({
8188
Header: col.title,
8289
accessor: col.key,
90+
disableSortBy: sortable ? col.sortable === false : true,
91+
sortType: col.customSortFn ? SortType.Custom : SortType.Basic,
92+
sortFn: col.customSortFn,
8393
Cell: ({ row, value }: { row: Row<DataItem>; value: unknown }) => {
8494
const item = row.original;
8595

@@ -102,7 +112,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
102112
)
103113
: undefined,
104114
}));
105-
}, [columns, filterInput]);
115+
}, [columns, filterInput, sortable]);
106116

107117
// Initialize table with react-table hooks
108118
const {
@@ -125,9 +135,20 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
125135
columns: tableColumns,
126136
data,
127137
initialState: { pageSize } as TableStateWithPagination<DataItem>,
138+
// @ts-expect-error - sortTypes is not included in the type definition but is supported by react-table
139+
sortTypes: {
140+
custom: (rowA: Row<DataItem>, rowB: Row<DataItem>, columnId: string) => {
141+
const column = columns.find(col => col.key === columnId);
142+
143+
if (column?.customSortFn)
144+
return column.customSortFn(rowA.original, rowB.original, columnId);
145+
146+
return 0;
147+
},
148+
},
128149
},
129150
useFilters,
130-
useSortBy,
151+
...(sortable ? [useSortBy] : []),
131152
usePagination
132153
) as TableInstanceWithHooks<DataItem>;
133154

@@ -179,7 +200,12 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
179200
return (
180201
<div>
181202
<table {...getTableProps()} className="table-container">
182-
<TableHeader headerGroups={headerGroups} />
203+
<TableHeader
204+
headerGroups={headerGroups}
205+
sortable={sortable}
206+
ascendingIcon={ascendingIcon}
207+
descendingIcon={descendingIcon}
208+
/>
183209
<tbody {...getTableBodyProps()}>
184210
{page.map((row) => {
185211
prepareRow(row);

src/components/TableHeader.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import '../styles/TableHeader.css';
99
* Props for the TableHeader component
1010
* @interface TableHeaderProps
1111
* @property {HeaderGroup<DataItem>[]} headerGroups - Array of header groups from react-table
12+
* @property {boolean} [sortable=false] - Whether the table is sortable
13+
* @property {React.ReactNode} [ascendingIcon] - Custom icon for ascending sort
14+
* @property {React.ReactNode} [descendingIcon] - Custom icon for descending sort
1215
*/
1316
interface TableHeaderProps {
1417
headerGroups: HeaderGroup<DataItem>[];
18+
sortable?: boolean;
19+
ascendingIcon?: React.ReactNode;
20+
descendingIcon?: React.ReactNode;
1521
}
1622

1723
type ColumnWithSorting = {
@@ -22,6 +28,7 @@ type ColumnWithSorting = {
2228
isSortedDesc?: boolean;
2329
Filter?: React.ComponentType<{ column: ColumnWithSorting }>;
2430
id: string;
31+
disableSortBy?: boolean;
2532
}
2633

2734
/**
@@ -30,30 +37,43 @@ type ColumnWithSorting = {
3037
* @param {TableHeaderProps} props - Component props
3138
* @returns {JSX.Element} Rendered table header
3239
*/
33-
export const TableHeader: React.FC<TableHeaderProps> = ({ headerGroups }) => (
40+
export const TableHeader: React.FC<TableHeaderProps> = ({
41+
headerGroups,
42+
sortable = false,
43+
ascendingIcon,
44+
descendingIcon
45+
}) => (
3446
<thead className="table-header">
3547
{headerGroups.map(headerGroup => {
3648
const { key: headerGroupKey, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
3749

3850
return (
3951
<tr key={headerGroupKey} {...headerGroupProps}>
4052
{(headerGroup.headers as unknown as ColumnWithSorting[]).map((column) => {
41-
const { key: columnKey, ...columnProps } = column.getHeaderProps(column.getSortByToggleProps());
42-
53+
const isColumnSortable = sortable && !column.disableSortBy;
54+
const { key: columnKey, ...columnProps } = isColumnSortable
55+
? column.getHeaderProps(column.getSortByToggleProps())
56+
: column.getHeaderProps();
57+
4358
return (
4459
<th
4560
key={columnKey}
4661
{...columnProps}
62+
className={isColumnSortable ? 'sortable' : ''}
4763
>
48-
{column.render('Header')}
49-
<span>
50-
{column.isSorted
51-
? column.isSortedDesc
52-
? ' 🔽 '
53-
: ' 🔼 '
54-
: ' '}
55-
</span>
56-
{column.Filter ? column.render('Filter') : null}
64+
<div className='table-header-cell'>
65+
{column.render('Header')}
66+
{isColumnSortable && (
67+
<span>
68+
{column.isSorted
69+
? column.isSortedDesc
70+
? descendingIcon || ' 🔽 '
71+
: ascendingIcon || ' 🔼 '
72+
: ' '}
73+
</span>
74+
)}
75+
{column.Filter ? column.render('Filter') : null}
76+
</div>
5777
</th>
5878
);
5979
})}

src/constants/sort.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum SortType {
2+
Custom = 'custom',
3+
Basic = 'basic',
4+
}

src/styles/TableHeader.css

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
border-bottom: 2px solid #e0e0e0;
44
background-color: #2c3e50;
55
color: #ffffff;
6-
cursor: pointer;
76
font-weight: 600;
8-
}
7+
}
8+
9+
.table-header th.sortable {
10+
cursor: pointer;
11+
}
12+
13+
.table-header-cell{
14+
display: flex;
15+
flex-wrap: wrap;
16+
align-items: center;
17+
gap: 16px;
18+
}

src/types/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface Column {
77
title: string;
88
render?: (value: unknown, record: DataItem) => React.ReactNode;
99
filterable?: boolean;
10+
sortable?: boolean;
11+
customSortFn?: (rowA: DataItem, rowB: DataItem, columnId: string) => number;
1012
}
1113

1214
export interface DataItem {

0 commit comments

Comments
 (0)