Skip to content

Commit 90fe629

Browse files
committed
feat: Accept react node as column header
1 parent e6d71f6 commit 90fe629

File tree

7 files changed

+154
-30
lines changed

7 files changed

+154
-30
lines changed

src/components/MultiLevelTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
116116
*/
117117
const tableColumns = useMemo<TableColumn<DataItem>[]>(() => {
118118
return columns.map((col) => ({
119-
Header: col.title,
119+
id: col.key,
120+
Header: () => col.title,
120121
accessor: (row: DataItem) => row[col.key as keyof DataItem],
121122
disableSortBy: sortable ? col.sortable === false : true,
122123
sortType: col.customSortFn ? SortType.Custom : SortType.Basic,
@@ -142,7 +143,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
142143
setFilterInput(e.target.value);
143144
column.setFilter(e.target.value);
144145
}}
145-
placeholder={`Filter ${col.title}...`}
146+
placeholder={`Filter ${typeof col.title === 'string' ? col.title : col.key}...`}
146147
/>
147148
)
148149
: undefined,

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const TableRow: React.FC<TableRowProps> = ({
8585
};
8686

8787
const handleRowClick = () => {
88-
if (onRowClick) {
88+
if (onRowClick && level === 0) {
8989
const dataItem = "original" in row ? row.original : row as DataItem;
9090

9191
onRowClick(dataItem);

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',

tests/components/TableHeader.test.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import React from 'react';
2-
import { render, screen, fireEvent } from '@testing-library/react';
2+
3+
import { fireEvent, render, screen } from '@testing-library/react';
34
import '@testing-library/jest-dom';
4-
import { describe, it, expect, vi } from 'vitest';
5-
import { TableHeader } from '../../src/components/TableHeader';
65
import type { HeaderGroup } from 'react-table';
7-
import type { DataItem } from '../../src/types/types';
6+
import { describe, expect, it, vi } from 'vitest';
7+
8+
import { TableHeader } from '../../src/components/TableHeader';
89
import type { ThemeProps } from '../../src/types/theme';
10+
import type { DataItem } from '../../src/types/types';
911

1012
// Mock theme
1113
const mockTheme: ThemeProps = {
@@ -28,7 +30,7 @@ const mockTheme: ThemeProps = {
2830
// Mock header groups
2931
const createMockHeaderGroup = (
3032
id: string,
31-
title: string,
33+
title: string | React.ReactNode,
3234
isSorted = false,
3335
isSortedDesc = false,
3436
hasFilter = false
@@ -42,21 +44,22 @@ const createMockHeaderGroup = (
4244
getSortByToggleProps: () => ({
4345
onClick: vi.fn(),
4446
}),
45-
render: (type: string) => (type === 'Header' ? title : null),
47+
render: (type: string) => (type === 'Header' ? (title || id) : null),
4648
isSorted,
4749
isSortedDesc,
4850
Filter: hasFilter
4951
? ({ column }: { column: any }) => (
50-
<input
51-
data-testid={`filter-${id}`}
52-
value={column.filterValue || ''}
53-
onChange={(e) => column.setFilter?.(e.target.value)}
54-
/>
55-
)
52+
<input
53+
data-testid={`filter-${id}`}
54+
value={column.filterValue || ''}
55+
onChange={(e) => column.setFilter?.(e.target.value)}
56+
placeholder={`Filter ${column.title || column.id}...`}
57+
/>
58+
)
5659
: undefined,
5760
setFilter: hasFilter ? vi.fn() : undefined,
5861
disableSortBy: false,
59-
title,
62+
title: title || id,
6063
filterValue: '',
6164
};
6265

@@ -119,9 +122,11 @@ describe('TableHeader', () => {
119122

120123
it('applies theme styles correctly', () => {
121124
const headerGroups = [createMockHeaderGroup('name', 'Name')];
125+
122126
renderTableHeader({ headerGroups });
123127

124128
const headerCell = screen.getByRole('columnheader', { name: 'Name' });
129+
125130
expect(headerCell).toHaveStyle({
126131
backgroundColor: mockTheme.table?.header?.background,
127132
color: mockTheme.table?.header?.textColor,
@@ -160,22 +165,27 @@ describe('TableHeader', () => {
160165

161166
it('uses column id when title is not provided', () => {
162167
const headerGroups = [createMockHeaderGroup('name', '')];
168+
163169
renderTableHeader({ headerGroups });
164170
expect(screen.getByText('name')).toBeInTheDocument();
165171
});
166172

167173
it('renders filter input when column has Filter component', () => {
168174
const headerGroups = [createMockHeaderGroup('name', 'Name', false, false, true)];
175+
169176
renderTableHeader({ headerGroups });
170177
const filterInput = screen.getByPlaceholderText('Filter Name...');
178+
171179
expect(filterInput).toBeInTheDocument();
172180
expect(filterInput).toHaveClass('filter-input');
173181
});
174182

175183
it('applies filter theme styles correctly', () => {
176184
const headerGroups = [createMockHeaderGroup('name', 'Name', false, false, true)];
185+
177186
renderTableHeader({ headerGroups });
178187
const filterInput = screen.getByPlaceholderText('Filter Name...');
188+
179189
expect(filterInput).toHaveStyle({
180190
color: mockTheme.table?.filter?.textColor,
181191
borderColor: mockTheme.table?.filter?.borderColor,
@@ -185,24 +195,43 @@ describe('TableHeader', () => {
185195

186196
it('handles filter input change correctly', () => {
187197
const headerGroups = [createMockHeaderGroup('name', 'Name', false, false, true)];
198+
188199
renderTableHeader({ headerGroups });
189200
const filterInput = screen.getByPlaceholderText('Filter Name...');
201+
190202
fireEvent.change(filterInput, { target: { value: 'test' } });
191203
const mockColumn = (headerGroups[0].headers[0] as any).column;
204+
192205
expect(mockColumn.setFilter).toHaveBeenCalledWith('test');
193206
});
194207

195208
it('renders filter input with column id when title is not provided', () => {
196209
const headerGroups = [createMockHeaderGroup('name', '', false, false, true)];
210+
197211
renderTableHeader({ headerGroups });
198212
const filterInput = screen.getByPlaceholderText('Filter name...');
213+
199214
expect(filterInput).toBeInTheDocument();
200215
});
201216

202217
it('does not render filter input when column has no Filter component', () => {
203218
const headerGroups = [createMockHeaderGroup('name', 'Name', false, false, false)];
219+
204220
renderTableHeader({ headerGroups });
205221
const filterInput = screen.queryByPlaceholderText('Filter Name...');
222+
206223
expect(filterInput).not.toBeInTheDocument();
207224
});
225+
226+
it('renders a React node as column header', () => {
227+
const customHeader = <span data-testid="custom-header">Custom</span>;
228+
const headerGroups = [
229+
createMockHeaderGroup('custom', customHeader as any),
230+
];
231+
232+
// Patch the render function to return the React node for 'Header'
233+
(headerGroups[0].headers[0] as any).render = (type: string) => type === 'Header' ? customHeader : null;
234+
renderTableHeader({ headerGroups });
235+
expect(screen.getByTestId('custom-header')).toBeInTheDocument();
236+
});
208237
});

0 commit comments

Comments
 (0)