Skip to content

Commit 38b3969

Browse files
authored
Merge pull request #12 from vishsanghishetty/feat/clickable-table-rows
feat: Add onRowClick handler for interactive table rows
2 parents 17d84fc + 837c14b commit 38b3969

File tree

2 files changed

+268
-3
lines changed

2 files changed

+268
-3
lines changed

src/components/TableWrapper.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ interface TableWrapperProps {
2323
id: string;
2424
fields: FieldData[];
2525
className?: string;
26+
onRowClick?: (rowData: Record<string, string | number | null>) => void;
2627
}
2728

2829
const TableWrapper = (props: TableWrapperProps) => {
29-
const { title, id, fields, className } = props;
30+
const { title, id, fields, className, onRowClick } = props;
3031

3132
// Check for missing or invalid data
3233
const hasNoFields = !fields || fields.length === 0;
@@ -102,7 +103,13 @@ const TableWrapper = (props: TableWrapperProps) => {
102103
</Thead>
103104
<Tbody>
104105
{rows.map((row, rowIndex) => (
105-
<Tr key={rowIndex} data-testid={`row-${rowIndex}`}>
106+
<Tr
107+
key={rowIndex}
108+
data-testid={`row-${rowIndex}`}
109+
onClick={() => onRowClick?.(row)}
110+
style={onRowClick ? { cursor: 'pointer' } : undefined}
111+
isHoverable={!!onRowClick}
112+
>
106113
{columns.map((col, colIndex) => (
107114
<Td key={colIndex}>{row[col.key]}</Td>
108115
))}

src/test/components/TableWrapper.test.tsx

Lines changed: 259 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from "@testing-library/react";
1+
import { render, screen, fireEvent } from "@testing-library/react";
22

33
import TableWrapper from "../../components/TableWrapper";
44

@@ -238,4 +238,262 @@ describe("TableWrapper Component", () => {
238238
expect(screen.getByText("123")).toBeInTheDocument();
239239
expect(screen.getByText("true")).toBeInTheDocument();
240240
});
241+
242+
// ========== onRowClick Feature Tests ==========
243+
244+
describe("onRowClick functionality", () => {
245+
const mockOnRowClick = vitest.fn();
246+
247+
beforeEach(() => {
248+
mockOnRowClick.mockClear();
249+
});
250+
251+
it("should call onRowClick handler when row is clicked", () => {
252+
render(<TableWrapper {...mockFieldsData} onRowClick={mockOnRowClick} />);
253+
254+
const firstRow = screen.getByTestId("row-0");
255+
fireEvent.click(firstRow);
256+
257+
expect(mockOnRowClick).toHaveBeenCalledTimes(1);
258+
expect(mockOnRowClick).toHaveBeenCalledWith({
259+
Title: "Toy Story",
260+
Year: "1995",
261+
Runtime: "81",
262+
"IMDB Rating": "8.3",
263+
Revenue: "373554033",
264+
Countries: "USA",
265+
});
266+
});
267+
268+
it("should call onRowClick with correct data for each row", () => {
269+
const multiRowData = {
270+
...mockFieldsData,
271+
fields: [
272+
{
273+
name: "Name",
274+
data_path: "user.name",
275+
data: ["Alice", "Bob", "Charlie"],
276+
},
277+
{
278+
name: "Age",
279+
data_path: "user.age",
280+
data: [25, 30, 35],
281+
},
282+
],
283+
};
284+
285+
render(<TableWrapper {...multiRowData} onRowClick={mockOnRowClick} />);
286+
287+
// Click second row
288+
const secondRow = screen.getByTestId("row-1");
289+
fireEvent.click(secondRow);
290+
291+
expect(mockOnRowClick).toHaveBeenCalledWith({
292+
Name: "Bob",
293+
Age: "30",
294+
});
295+
});
296+
297+
it("should apply pointer cursor style when onRowClick is provided", () => {
298+
render(<TableWrapper {...mockFieldsData} onRowClick={mockOnRowClick} />);
299+
300+
const firstRow = screen.getByTestId("row-0");
301+
expect(firstRow).toHaveStyle({ cursor: "pointer" });
302+
});
303+
304+
it("should not apply pointer cursor style when onRowClick is not provided", () => {
305+
render(<TableWrapper {...mockFieldsData} />);
306+
307+
const firstRow = screen.getByTestId("row-0");
308+
expect(firstRow).not.toHaveStyle({ cursor: "pointer" });
309+
});
310+
311+
it("should make rows hoverable when onRowClick is provided", () => {
312+
const { container } = render(
313+
<TableWrapper {...mockFieldsData} onRowClick={mockOnRowClick} />
314+
);
315+
316+
const firstRow = container.querySelector('[data-testid="row-0"]');
317+
// Check that isHoverable prop is applied (PatternFly adds hover class)
318+
expect(firstRow).toBeInTheDocument();
319+
});
320+
321+
it("should handle multiple row clicks", () => {
322+
const multiRowData = {
323+
...mockFieldsData,
324+
fields: [
325+
{
326+
name: "Item",
327+
data_path: "item",
328+
data: ["A", "B", "C"],
329+
},
330+
],
331+
};
332+
333+
render(<TableWrapper {...multiRowData} onRowClick={mockOnRowClick} />);
334+
335+
fireEvent.click(screen.getByTestId("row-0"));
336+
fireEvent.click(screen.getByTestId("row-1"));
337+
fireEvent.click(screen.getByTestId("row-2"));
338+
339+
expect(mockOnRowClick).toHaveBeenCalledTimes(3);
340+
expect(mockOnRowClick).toHaveBeenNthCalledWith(1, { Item: "A" });
341+
expect(mockOnRowClick).toHaveBeenNthCalledWith(2, { Item: "B" });
342+
expect(mockOnRowClick).toHaveBeenNthCalledWith(3, { Item: "C" });
343+
});
344+
345+
it("should handle row click with array data", () => {
346+
const fieldsWithArray = {
347+
...mockFieldsData,
348+
fields: [
349+
{
350+
name: "Name",
351+
data_path: "name",
352+
data: ["Test"],
353+
},
354+
{
355+
name: "Tags",
356+
data_path: "tags",
357+
data: [["tag1", "tag2", "tag3"]],
358+
},
359+
],
360+
};
361+
362+
render(<TableWrapper {...fieldsWithArray} onRowClick={mockOnRowClick} />);
363+
364+
fireEvent.click(screen.getByTestId("row-0"));
365+
366+
expect(mockOnRowClick).toHaveBeenCalledWith({
367+
Name: "Test",
368+
Tags: "tag1, tag2, tag3", // Array should be joined
369+
});
370+
});
371+
372+
it("should handle row click with null values", () => {
373+
const fieldsWithNull = {
374+
...mockFieldsData,
375+
fields: [
376+
{
377+
name: "Field1",
378+
data_path: "field1",
379+
data: ["value1"],
380+
},
381+
{
382+
name: "Field2",
383+
data_path: "field2",
384+
data: [null],
385+
},
386+
],
387+
};
388+
389+
render(<TableWrapper {...fieldsWithNull} onRowClick={mockOnRowClick} />);
390+
391+
fireEvent.click(screen.getByTestId("row-0"));
392+
393+
expect(mockOnRowClick).toHaveBeenCalledWith({
394+
Field1: "value1",
395+
Field2: "", // null should become empty string
396+
});
397+
});
398+
399+
it("should not break when onRowClick is undefined", () => {
400+
render(<TableWrapper {...mockFieldsData} />);
401+
402+
const firstRow = screen.getByTestId("row-0");
403+
404+
// Should not throw error when clicking without handler
405+
expect(() => fireEvent.click(firstRow)).not.toThrow();
406+
});
407+
408+
it("should work with single row tables", () => {
409+
render(<TableWrapper {...mockFieldsData} onRowClick={mockOnRowClick} />);
410+
411+
const row = screen.getByTestId("row-0");
412+
fireEvent.click(row);
413+
414+
expect(mockOnRowClick).toHaveBeenCalledTimes(1);
415+
});
416+
417+
it("should work with large tables (100+ rows)", () => {
418+
const largeData = {
419+
...mockFieldsData,
420+
fields: [
421+
{
422+
name: "ID",
423+
data_path: "id",
424+
data: Array.from({ length: 150 }, (_, i) => i + 1),
425+
},
426+
{
427+
name: "Value",
428+
data_path: "value",
429+
data: Array.from({ length: 150 }, (_, i) => `value-${i + 1}`),
430+
},
431+
],
432+
};
433+
434+
render(<TableWrapper {...largeData} onRowClick={mockOnRowClick} />);
435+
436+
// Click a row in the middle
437+
fireEvent.click(screen.getByTestId("row-75"));
438+
439+
expect(mockOnRowClick).toHaveBeenCalledWith({
440+
ID: "76",
441+
Value: "value-76",
442+
});
443+
});
444+
445+
it("should maintain backward compatibility with tables without onRowClick", () => {
446+
const { container } = render(<TableWrapper {...mockFieldsData} />);
447+
448+
const rows = container.querySelectorAll('[data-testid^="row-"]');
449+
expect(rows.length).toBeGreaterThan(0);
450+
451+
// Should render normally without onRowClick
452+
expect(screen.getByText("Toy Story")).toBeInTheDocument();
453+
});
454+
455+
it("should pass all row data fields to click handler", () => {
456+
const complexData = {
457+
...mockFieldsData,
458+
fields: [
459+
{ name: "Name", data_path: "name", data: ["Test"] },
460+
{ name: "Age", data_path: "age", data: [25] },
461+
{ name: "City", data_path: "city", data: ["NYC"] },
462+
{ name: "Active", data_path: "active", data: [true] },
463+
{ name: "Score", data_path: "score", data: [95.5] },
464+
],
465+
};
466+
467+
render(<TableWrapper {...complexData} onRowClick={mockOnRowClick} />);
468+
469+
fireEvent.click(screen.getByTestId("row-0"));
470+
471+
expect(mockOnRowClick).toHaveBeenCalledWith({
472+
Name: "Test",
473+
Age: "25",
474+
City: "NYC",
475+
Active: "true",
476+
Score: "95.5",
477+
});
478+
});
479+
480+
it("should have data-testid on all rows", () => {
481+
const multiRowData = {
482+
...mockFieldsData,
483+
fields: [
484+
{
485+
name: "Item",
486+
data_path: "item",
487+
data: ["A", "B", "C", "D", "E"],
488+
},
489+
],
490+
};
491+
492+
render(<TableWrapper {...multiRowData} onRowClick={mockOnRowClick} />);
493+
494+
for (let i = 0; i < 5; i++) {
495+
expect(screen.getByTestId(`row-${i}`)).toBeInTheDocument();
496+
}
497+
});
498+
});
241499
});

0 commit comments

Comments
 (0)