Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ const App: React.FC = () => {
sortable={true}
selectable={true}
onSelectionChange={handleSelectionChange}
onRowClick={(data) => console.log(data)}
/>
</div>
{selectedRows.size > 0 && (
Expand Down
9 changes: 7 additions & 2 deletions src/components/MultiLevelTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import "../styles/MultiLevelTable.css";
* @property {Column[]} columns - Array of column configurations
* @property {number} [pageSize=10] - Number of items per page
* @property {ThemeProps} theme - Theme properties
* @property {(row: DataItem) => void} [onRowClick] - Optional callback function when a parent row is clicked
*/
export interface MultiLevelTableProps {
data: DataItem[];
Expand All @@ -46,6 +47,7 @@ export interface MultiLevelTableProps {
expandIcon?: React.ReactNode;
selectable?: boolean;
onSelectionChange?: (selectedRows: Set<string | number>) => void;
onRowClick?: (row: DataItem) => void;
}

/**
Expand All @@ -66,6 +68,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
expandIcon,
selectable = false,
onSelectionChange,
onRowClick,
}) => {
const mergedTheme = mergeThemeProps(defaultTheme, theme);
const [filterInput, setFilterInput] = useState("");
Expand Down Expand Up @@ -113,7 +116,8 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
*/
const tableColumns = useMemo<TableColumn<DataItem>[]>(() => {
return columns.map((col) => ({
Header: col.title,
id: col.key,
Header: () => col.title,
accessor: (row: DataItem) => row[col.key as keyof DataItem],
disableSortBy: sortable ? col.sortable === false : true,
sortType: col.customSortFn ? SortType.Custom : SortType.Basic,
Expand All @@ -139,7 +143,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
setFilterInput(e.target.value);
column.setFilter(e.target.value);
}}
placeholder={`Filter ${col.title}...`}
placeholder={`Filter ${typeof col.title === 'string' ? col.title : col.key}...`}
/>
)
: undefined,
Expand Down Expand Up @@ -300,6 +304,7 @@ export const MultiLevelTable: React.FC<MultiLevelTableProps> = ({
selectable={true}
isRowSelected={selectionState.selectedRows.has(row.original.id)}
onRowSelect={handleRowSelect}
onRowClick={onRowClick}
/>
{renderNestedRows(parentId)}
</React.Fragment>
Expand Down
6 changes: 3 additions & 3 deletions src/components/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
Filter?: React.ComponentType<{ column: ColumnWithSorting }>;
id: string;
disableSortBy?: boolean;
title?: string;
title?: string | React.ReactNode;
filterValue?: string;
setFilter?: (value: string) => void;
};
Expand Down Expand Up @@ -104,9 +104,9 @@
)}
<span
style={{ display: 'inline-flex', alignItems: 'center', cursor: isColumnSortable ? 'pointer' : 'default', userSelect: 'none' }}
onClick={isColumnSortable ? (e => { e.stopPropagation(); (sortProps.onClick as any)?.(e); }) : undefined}

Check warning on line 107 in src/components/TableHeader.tsx

View workflow job for this annotation

GitHub Actions / test-and-build (18.x)

Unexpected any. Specify a different type
>
{column.title || column.id}
{column.render('Header')}
<span className="sort-icon" style={{ marginLeft: 4 }}>
{column.isSorted
? column.isSortedDesc
Expand All @@ -121,7 +121,7 @@
className="filter-input"
value={column.filterValue || ""}
onChange={(e) => column.setFilter?.(e.target.value)}
placeholder={`Filter ${column.title || column.id}...`}
placeholder={`Filter ${typeof column.title === 'string' ? column.title : column.id}...`}
style={{
color: theme.table?.filter?.textColor,
borderColor: theme.table?.filter?.borderColor,
Expand Down
23 changes: 20 additions & 3 deletions src/components/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import "../styles/TableRow.css";
* @property {boolean} [selectable=false] - Whether the row is selectable
* @property {boolean} [isRowSelected=false] - Whether the row is selected
* @property {(rowId: number) => void} [onRowSelect] - Function to select a row
* @property {(row: DataItem) => void} [onRowClick] - Optional callback function when a parent row is clicked
*/
interface TableRowProps {
row: Row<DataItem> | DataItem;
Expand All @@ -36,6 +37,7 @@ interface TableRowProps {
selectable?: boolean;
isRowSelected?: boolean;
onRowSelect?: (rowId: number) => void;
onRowClick?: (row: DataItem) => void;
}

/**
Expand All @@ -56,16 +58,18 @@ export const TableRow: React.FC<TableRowProps> = ({
selectable = false,
isRowSelected = false,
onRowSelect,
onRowClick,
}) => {
const getRowClassName = useMemo(() => {
const classes = ["table-row"];
const classes = [];

if (isExpanded) classes.push("table-row-expanded");
if (level === 0) classes.push("table-row-main");
if(onRowClick) classes.push("table-row-clickable");
else classes.push("table-row-nested");

return classes.join(" ");
}, [isExpanded, level]);
}, [isExpanded, level, onRowClick]);

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

const handleRowClick = () => {
if (onRowClick && level === 0) {
const dataItem = "original" in row ? row.original : row as DataItem;

onRowClick(dataItem);
}
};

// For nested rows that don't have getRowProps
if (!("getRowProps" in row)) {
const dataItem = row as DataItem;

return (
<tr className={getRowClassName} style={getRowStyle}>
<tr
className={getRowClassName}
style={getRowStyle}
onClick={handleRowClick}
>
{columns.map((column: Column, index: number) => {
const value = dataItem[column.key as keyof DataItem];
const displayValue =
Expand Down Expand Up @@ -139,6 +155,7 @@ export const TableRow: React.FC<TableRowProps> = ({
{...rowProps}
className={getRowClassName}
style={getRowStyle}
onClick={handleRowClick}
>
{tableRow.cells.map((cell: Cell<DataItem>, index: number) => (
<TableCell
Expand Down
2 changes: 1 addition & 1 deletion src/styles/TableRow.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.table-row {
.table-row-clickable {
cursor: pointer;
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Row, TableInstance, TableState } from 'react-table';

export interface Column {
key: string;
title: string;
title: string | React.ReactNode;
filterable?: boolean;
render?: (value: string | number, item: DataItem) => React.ReactNode;
sortable?: boolean;
Expand Down
13 changes: 12 additions & 1 deletion tests/components/MultiLevelTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';

import { fireEvent, render, screen, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { MultiLevelTable } from '../../src/components/MultiLevelTable';
import type { Column, DataItem } from '../../src/types/types';
// Mock data for testing
Expand Down Expand Up @@ -71,6 +73,7 @@ const mockColumns: Column[] = [
),
},
];

describe('MultiLevelTable', () => {
it('renders table with basic data', () => {
render(<MultiLevelTable data={mockData} columns={mockColumns} />);
Expand All @@ -96,6 +99,7 @@ describe('MultiLevelTable', () => {

// Click expand button for first parent
const expandButton = screen.getAllByRole('button')[0];

fireEvent.click(expandButton);

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

// Click name header to sort
const nameHeader = screen.getByText('Name');

fireEvent.click(nameHeader);

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

// Check if order is reversed
const updatedRows = screen.getAllByRole('row').slice(1);

expect(within(updatedRows[0]).getByText('Parent 2')).toBeInTheDocument();
expect(within(updatedRows[1]).getByText('Parent 1')).toBeInTheDocument();
});
Expand All @@ -145,6 +151,7 @@ describe('MultiLevelTable', () => {
// Check if pagination controls are present
const nextButton = screen.getByRole('button', { name: '>' });
const prevButton = screen.getByRole('button', { name: '<' });

expect(nextButton).toBeInTheDocument();
expect(prevButton).toBeInTheDocument();

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

const table = screen.getByRole('table');
const tableWrapper = table.closest('.table-wrapper');

expect(tableWrapper?.parentElement).toHaveStyle({ backgroundColor: '#f0f0f0' });
expect(table).toHaveStyle({ borderColor: '#ff0000' });
});
Expand All @@ -207,14 +215,15 @@ describe('MultiLevelTable', () => {

// Check if custom render is applied
const customElements = screen.getAllByTestId('custom-name');

expect(customElements).toHaveLength(2); // Two parent rows
expect(customElements[0]).toHaveTextContent('Parent 1');
});
it('handles filtering', () => {
render(<MultiLevelTable data={mockData} columns={mockColumns} />);

// Find filter input
const filterInput = screen.getByPlaceholderText('Filter Name...');
const filterInput = screen.getByPlaceholderText('Filter name...');

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

const statusCells = screen.getAllByTestId('status-cell');

expect(statusCells).toHaveLength(2); // Two parent rows

// Check if status cells have correct styles
const activeCell = statusCells.find(cell => cell.textContent === 'Active');

expect(activeCell).toHaveStyle({
backgroundColor: '#e6ffe6',
color: '#006600',
Expand Down
Loading