Skip to content

Commit 64ed321

Browse files
authored
feat: Add row click functionality
* feat: Add onRowClick feature * feat: Accept react node as column header
1 parent c69a2e7 commit 64ed321

File tree

9 files changed

+236
-34
lines changed

9 files changed

+236
-34
lines changed

src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ const App: React.FC = () => {
455455
sortable={true}
456456
selectable={true}
457457
onSelectionChange={handleSelectionChange}
458+
onRowClick={(data) => console.log(data)}
458459
/>
459460
</div>
460461
{selectedRows.size > 0 && (

src/components/MultiLevelTable.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import "../styles/MultiLevelTable.css";
3333
* @property {Column[]} columns - Array of column configurations
3434
* @property {number} [pageSize=10] - Number of items per page
3535
* @property {ThemeProps} theme - Theme properties
36+
* @property {(row: DataItem) => void} [onRowClick] - Optional callback function when a parent row is clicked
3637
*/
3738
export interface MultiLevelTableProps {
3839
data: DataItem[];
@@ -46,6 +47,7 @@ export interface MultiLevelTableProps {
4647
expandIcon?: React.ReactNode;
4748
selectable?: boolean;
4849
onSelectionChange?: (selectedRows: Set<string | number>) => void;
50+
onRowClick?: (row: DataItem) => void;
4951
}
5052

5153
/**
@@ -66,6 +68,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
6668
expandIcon,
6769
selectable = false,
6870
onSelectionChange,
71+
onRowClick,
6972
}) => {
7073
const mergedTheme = mergeThemeProps(defaultTheme, theme);
7174
const [filterInput, setFilterInput] = useState("");
@@ -113,7 +116,8 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
113116
*/
114117
const tableColumns = useMemo<TableColumn<DataItem>[]>(() => {
115118
return columns.map((col) => ({
116-
Header: col.title,
119+
id: col.key,
120+
Header: () => col.title,
117121
accessor: (row: DataItem) => row[col.key as keyof DataItem],
118122
disableSortBy: sortable ? col.sortable === false : true,
119123
sortType: col.customSortFn ? SortType.Custom : SortType.Basic,
@@ -139,7 +143,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
139143
setFilterInput(e.target.value);
140144
column.setFilter(e.target.value);
141145
}}
142-
placeholder={`Filter ${col.title}...`}
146+
placeholder={`Filter ${typeof col.title === 'string' ? col.title : col.key}...`}
143147
/>
144148
)
145149
: undefined,
@@ -300,6 +304,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
300304
selectable={true}
301305
isRowSelected={selectionState.selectedRows.has(row.original.id)}
302306
onRowSelect={handleRowSelect}
307+
onRowClick={onRowClick}
303308
/>
304309
{renderNestedRows(parentId)}
305310
</React.Fragment>

src/components/TableHeader.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type ColumnWithSorting = {
4646
Filter?: React.ComponentType<{ column: ColumnWithSorting }>;
4747
id: string;
4848
disableSortBy?: boolean;
49-
title?: string;
49+
title?: string | React.ReactNode;
5050
filterValue?: string;
5151
setFilter?: (value: string) => void;
5252
};
@@ -106,7 +106,7 @@ export const TableHeader: React.FC<TableHeaderProps> = ({
106106
style={{ display: 'inline-flex', alignItems: 'center', cursor: isColumnSortable ? 'pointer' : 'default', userSelect: 'none' }}
107107
onClick={isColumnSortable ? (e => { e.stopPropagation(); (sortProps.onClick as any)?.(e); }) : undefined}
108108
>
109-
{column.title || column.id}
109+
{column.render('Header')}
110110
<span className="sort-icon" style={{ marginLeft: 4 }}>
111111
{column.isSorted
112112
? column.isSortedDesc
@@ -121,7 +121,7 @@ export const TableHeader: React.FC<TableHeaderProps> = ({
121121
className="filter-input"
122122
value={column.filterValue || ""}
123123
onChange={(e) => column.setFilter?.(e.target.value)}
124-
placeholder={`Filter ${column.title || column.id}...`}
124+
placeholder={`Filter ${typeof column.title === 'string' ? column.title : column.id}...`}
125125
style={{
126126
color: theme.table?.filter?.textColor,
127127
borderColor: theme.table?.filter?.borderColor,

src/components/TableRow.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import "../styles/TableRow.css";
2323
* @property {boolean} [selectable=false] - Whether the row is selectable
2424
* @property {boolean} [isRowSelected=false] - Whether the row is selected
2525
* @property {(rowId: number) => void} [onRowSelect] - Function to select a row
26+
* @property {(row: DataItem) => void} [onRowClick] - Optional callback function when a parent row is clicked
2627
*/
2728
interface TableRowProps {
2829
row: Row<DataItem> | DataItem;
@@ -36,6 +37,7 @@ interface TableRowProps {
3637
selectable?: boolean;
3738
isRowSelected?: boolean;
3839
onRowSelect?: (rowId: number) => void;
40+
onRowClick?: (row: DataItem) => void;
3941
}
4042

4143
/**
@@ -56,16 +58,18 @@ export const TableRow: React.FC<TableRowProps> = ({
5658
selectable = false,
5759
isRowSelected = false,
5860
onRowSelect,
61+
onRowClick,
5962
}) => {
6063
const getRowClassName = useMemo(() => {
61-
const classes = ["table-row"];
64+
const classes = [];
6265

6366
if (isExpanded) classes.push("table-row-expanded");
6467
if (level === 0) classes.push("table-row-main");
68+
if(onRowClick) classes.push("table-row-clickable");
6569
else classes.push("table-row-nested");
6670

6771
return classes.join(" ");
68-
}, [isExpanded, level]);
72+
}, [isExpanded, level, onRowClick]);
6973

7074
const getRowStyle = useMemo(() => {
7175
const rowShades = theme.table?.row?.levelColors || [];
@@ -80,12 +84,24 @@ export const TableRow: React.FC<TableRowProps> = ({
8084
onToggle();
8185
};
8286

87+
const handleRowClick = () => {
88+
if (onRowClick && level === 0) {
89+
const dataItem = "original" in row ? row.original : row as DataItem;
90+
91+
onRowClick(dataItem);
92+
}
93+
};
94+
8395
// For nested rows that don't have getRowProps
8496
if (!("getRowProps" in row)) {
8597
const dataItem = row as DataItem;
8698

8799
return (
88-
<tr className={getRowClassName} style={getRowStyle}>
100+
<tr
101+
className={getRowClassName}
102+
style={getRowStyle}
103+
onClick={handleRowClick}
104+
>
89105
{columns.map((column: Column, index: number) => {
90106
const value = dataItem[column.key as keyof DataItem];
91107
const displayValue =
@@ -139,6 +155,7 @@ export const TableRow: React.FC<TableRowProps> = ({
139155
{...rowProps}
140156
className={getRowClassName}
141157
style={getRowStyle}
158+
onClick={handleRowClick}
142159
>
143160
{tableRow.cells.map((cell: Cell<DataItem>, index: number) => (
144161
<TableCell

src/styles/TableRow.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.table-row {
1+
.table-row-clickable {
22
cursor: pointer;
33
}
44

src/types/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Row, TableInstance, TableState } from 'react-table';
44

55
export interface Column {
66
key: string;
7-
title: string;
7+
title: string | React.ReactNode;
88
filterable?: boolean;
99
render?: (value: string | number, item: DataItem) => React.ReactNode;
1010
sortable?: boolean;

tests/components/MultiLevelTable.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
2+
23
import { fireEvent, render, screen, within } from '@testing-library/react';
34
import { describe, expect, it, vi } from 'vitest';
5+
46
import { MultiLevelTable } from '../../src/components/MultiLevelTable';
57
import type { Column, DataItem } from '../../src/types/types';
68
// Mock data for testing
@@ -71,6 +73,7 @@ const mockColumns: Column[] = [
7173
),
7274
},
7375
];
76+
7477
describe('MultiLevelTable', () => {
7578
it('renders table with basic data', () => {
7679
render(<MultiLevelTable data={mockData} columns={mockColumns} />);
@@ -96,6 +99,7 @@ describe('MultiLevelTable', () => {
9699

97100
// Click expand button for first parent
98101
const expandButton = screen.getAllByRole('button')[0];
102+
99103
fireEvent.click(expandButton);
100104

101105
// Children should now be visible
@@ -113,6 +117,7 @@ describe('MultiLevelTable', () => {
113117

114118
// Click name header to sort
115119
const nameHeader = screen.getByText('Name');
120+
116121
fireEvent.click(nameHeader);
117122

118123
// Get all rows and check order
@@ -129,6 +134,7 @@ describe('MultiLevelTable', () => {
129134

130135
// Check if order is reversed
131136
const updatedRows = screen.getAllByRole('row').slice(1);
137+
132138
expect(within(updatedRows[0]).getByText('Parent 2')).toBeInTheDocument();
133139
expect(within(updatedRows[1]).getByText('Parent 1')).toBeInTheDocument();
134140
});
@@ -145,6 +151,7 @@ describe('MultiLevelTable', () => {
145151
// Check if pagination controls are present
146152
const nextButton = screen.getByRole('button', { name: '>' });
147153
const prevButton = screen.getByRole('button', { name: '<' });
154+
148155
expect(nextButton).toBeInTheDocument();
149156
expect(prevButton).toBeInTheDocument();
150157

@@ -190,6 +197,7 @@ describe('MultiLevelTable', () => {
190197

191198
const table = screen.getByRole('table');
192199
const tableWrapper = table.closest('.table-wrapper');
200+
193201
expect(tableWrapper?.parentElement).toHaveStyle({ backgroundColor: '#f0f0f0' });
194202
expect(table).toHaveStyle({ borderColor: '#ff0000' });
195203
});
@@ -207,14 +215,15 @@ describe('MultiLevelTable', () => {
207215

208216
// Check if custom render is applied
209217
const customElements = screen.getAllByTestId('custom-name');
218+
210219
expect(customElements).toHaveLength(2); // Two parent rows
211220
expect(customElements[0]).toHaveTextContent('Parent 1');
212221
});
213222
it('handles filtering', () => {
214223
render(<MultiLevelTable data={mockData} columns={mockColumns} />);
215224

216225
// Find filter input
217-
const filterInput = screen.getByPlaceholderText('Filter Name...');
226+
const filterInput = screen.getByPlaceholderText('Filter name...');
218227

219228
// Type in filter
220229
fireEvent.change(filterInput, { target: { value: 'Parent 1' } });
@@ -241,10 +250,12 @@ describe('MultiLevelTable', () => {
241250
render(<MultiLevelTable data={mockData} columns={mockColumns} />);
242251

243252
const statusCells = screen.getAllByTestId('status-cell');
253+
244254
expect(statusCells).toHaveLength(2); // Two parent rows
245255

246256
// Check if status cells have correct styles
247257
const activeCell = statusCells.find(cell => cell.textContent === 'Active');
258+
248259
expect(activeCell).toHaveStyle({
249260
backgroundColor: '#e6ffe6',
250261
color: '#006600',

0 commit comments

Comments
 (0)