Add onRowClick event + docs improvements

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-28 17:59:13 +01:00
parent 79335df33e
commit c33c6936ed
7 changed files with 567 additions and 7 deletions
File diff suppressed because one or more lines are too long
+200 -1
View File
@@ -1,6 +1,205 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { TableSnippet } from '@/snippets/stories-snippets';
import {
tablePropDefs,
tablePaginationPropDefs,
tableUsageSnippet,
tableBasicSnippet,
tableRowClickSnippet,
tableHybridSnippet,
tableCellInteractionsSnippet,
tablePaginationSnippet,
tableSelectionSnippet,
tableSortingSnippet,
} from './table.props';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
<PageTitle
title="Table"
description="A table component that can be used to display data in a grid. (Coming soon)"
description="A flexible table component built on top of TanStack Table with built-in styling, interactions, and pagination support."
/>
<Snippet
py={4}
preview={<TableSnippet story="Uncontrolled" />}
code={tableBasicSnippet}
/>
## Usage
<CodeBlock code={tableUsageSnippet} />
## API reference
### Table
The main table component that renders data in a structured grid format.
<PropsTable data={tablePropDefs} />
### TablePagination
A pagination component designed to work with the Table component.
<PropsTable data={tablePaginationPropDefs} />
## Examples
### Basic Table
A simple table with data display.
<Snippet
py={4}
open
preview={<TableSnippet story="Uncontrolled" />}
code={tableBasicSnippet}
/>
### Row Clicks
The Table supports a hybrid interaction model where you can add row-level click handlers while still allowing cell-level interactions to take precedence.
<Snippet
py={4}
open
preview={<TableSnippet story="WithRowClick" />}
code={tableRowClickSnippet}
/>
### Hybrid Interactions
This demonstrates the most common pattern: specific cells (like names) have links for navigation, while clicking empty row areas triggers a different action like selection or quick preview.
<Snippet
py={4}
open
preview={<TableSnippet story="WithNavigationLinks" />}
code={tableHybridSnippet}
/>
### Cell-Level Interactions
Different cells can have completely different interaction behaviors. Links navigate, buttons trigger actions, and read-only cells allow row clicks.
<Snippet
py={4}
open
preview={<TableSnippet story="WithMixedInteractions" />}
code={tableCellInteractionsSnippet}
/>
### Pagination
Enable pagination by adding the pagination row model and using the TablePagination component.
<Snippet
py={4}
open
preview={<TableSnippet story="Controlled" />}
code={tablePaginationSnippet}
/>
## Advanced Features
### Row Selection
Enable row selection using TanStack Table's built-in selection features.
<CodeBlock code={tableSelectionSnippet} />
### Sorting
Enable column sorting by adding the sorting row model.
<CodeBlock code={tableSortingSnippet} />
## Interaction Patterns
The Table component supports a **two-level interaction model**:
1. **Primary**: Cell-level interactions (Links, buttons, etc.) - handled in column definitions
2. **Secondary**: Row-level background handler - handles clicks on empty row areas
### When to Use Each Pattern
**Use `onRowClick` only:**
- Simple tables where entire rows navigate to the same place
- Selection-based interactions
- Quick actions that apply to the whole row
**Use cell-level interactions only:**
- Complex tables with many different interactive elements
- When you need precise control over each interaction
- When different cells need different behaviors
**Use hybrid approach:**
- **Most common case** - Some cells have specific actions, others are background-clickable
- Data tables with primary actions (links) and secondary actions (selection/preview)
- When you want the convenience of row clicking but flexibility of cell control
### Event Handling
Cell-level interactions should call `e.stopPropagation()` to prevent row clicks:
```tsx
// ✅ Correct - prevents row click
<Link
to="/somewhere"
onClick={(e) => e.stopPropagation()}
>
Navigate
</Link>
// ✅ Also correct for custom actions
<button onClick={(e) => {
e.stopPropagation();
handleAction();
}}>
Action
</button>
```
## Best Practices
1. **Use Link components for navigation** - Better UX and accessibility
2. **Handle keyboard modifiers** - Support Cmd/Ctrl+click for new tabs
3. **Use `stopPropagation()`** in cell interactions to prevent row clicks
4. **Provide visual feedback** - Use appropriate cursor styles
5. **Consider accessibility** - Screen readers understand Links and buttons in context
## TypeScript Support
The Table component is fully typed with generic support:
```tsx
interface MyDataType {
id: string;
name: string;
status: string;
}
const table = useReactTable<MyDataType>({
data: myData, // ✅ Typed as MyDataType[]
columns: myColumns, // ✅ Typed as ColumnDef<MyDataType>[]
getCoreRowModel: getCoreRowModel(),
});
const handleRowClick = (row: MyDataType) => {
// ✅ row is fully typed as MyDataType
console.log(row.name);
};
return <Table table={table} onRowClick={handleRowClick} />;
```
<Theming component="Table" />
<ChangelogComponent component="table" />
@@ -0,0 +1,278 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '../../utils/propDefs';
export const tablePropDefs: Record<string, PropDef> = {
table: {
type: 'complex',
complexType: {
name: 'Table<TData>',
properties: {
'TanStack Table instance': {
type: 'object',
required: true,
description:
'The TanStack Table instance created with useReactTable()',
},
},
},
required: true,
},
onRowClick: {
type: 'complex',
complexType: {
name: '(row: TData, event: React.MouseEvent<HTMLTableRowElement>) => void',
properties: {
row: {
type: 'TData',
required: true,
description: 'The row data object',
},
event: {
type: 'React.MouseEvent<HTMLTableRowElement>',
required: true,
description: 'The mouse event object',
},
},
},
required: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const tablePaginationPropDefs: Record<string, PropDef> = {
table: {
type: 'complex',
complexType: {
name: 'Table<TData>',
properties: {
'TanStack Table instance': {
type: 'object',
required: true,
description:
'The TanStack Table instance (same instance passed to Table component)',
},
},
},
required: true,
},
};
export const tableUsageSnippet = `import { useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table';
import { Table, TablePagination } from '@backstage/ui/components/Table';
interface Person {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
}
const data: Person[] = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 24,
visits: 100,
status: 'In Relationship',
},
// ... more data
];
const columns: ColumnDef<Person>[] = [
{
accessorKey: 'firstName',
header: 'First Name',
},
{
accessorKey: 'lastName',
header: 'Last Name',
},
{
accessorKey: 'age',
header: 'Age',
},
{
accessorKey: 'visits',
header: 'Visits',
},
{
accessorKey: 'status',
header: 'Status',
},
];
function MyTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<>
<Table table={table} />
<TablePagination table={table} />
</>
);
}`;
export const tableBasicSnippet = `const table = useReactTable({
data: myData,
columns: myColumns,
getCoreRowModel: getCoreRowModel(),
});
return <Table table={table} />;`;
export const tableRowClickSnippet = `const table = useReactTable({
data: myData,
columns: myColumns,
getCoreRowModel: getCoreRowModel(),
});
const handleRowClick = (rowData: MyDataType) => {
console.log('Row clicked:', rowData);
navigate(\`/details/\${rowData.id}\`);
};
return <Table table={table} onRowClick={handleRowClick} />;`;
export const tableHybridSnippet = `const columns: ColumnDef<MyDataType>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<Link
to={\`/items/\${row.original.id}\`}
onClick={(e) => e.stopPropagation()} // Prevent row click
>
<TableCellText title={row.getValue('name')} />
</Link>
),
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => <TableCellText title={row.getValue('status')} />,
},
];
const table = useReactTable({
data: myData,
columns,
getCoreRowModel: getCoreRowModel(),
});
const handleRowClick = (rowData: MyDataType) => {
// Called when clicking empty row areas (not the name link)
showQuickPreview(rowData);
};
return <Table table={table} onRowClick={handleRowClick} />;`;
export const tableCellInteractionsSnippet = `const columns: ColumnDef<MyDataType>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<Link to={\`/items/\${row.original.id}\`}>
<TableCellText title={row.getValue('name')} />
</Link>
),
},
{
accessorKey: 'owner',
header: 'Owner',
cell: ({ row }) => (
<div
onClick={(e) => {
e.stopPropagation();
openContactModal(row.original.owner);
}}
style={{ cursor: 'pointer' }}
>
<TableCellText title={row.original.owner.name} />
</div>
),
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button
onClick={(e) => {
e.stopPropagation();
handleEdit(row.original);
}}
>
Edit
</button>
),
},
];`;
export const tablePaginationSnippet = `import {
useReactTable,
getCoreRowModel,
getPaginationRowModel
} from '@tanstack/react-table';
import { Table, TablePagination } from '@backstage/ui/components/Table';
const table = useReactTable({
data: myData,
columns: myColumns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), // Enable pagination
});
return (
<>
<Table table={table} />
<TablePagination table={table} />
</>
);`;
export const tableSelectionSnippet = `import { useState } from 'react';
import {
useReactTable,
getCoreRowModel,
RowSelectionState
} from '@tanstack/react-table';
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const table = useReactTable({
data: myData,
columns: myColumns,
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
state: {
rowSelection,
},
onRowSelectionChange: setRowSelection,
});
// Access selected rows
const selectedRows = table.getFilteredSelectedRowModel().rows;
return <Table table={table} />;`;
export const tableSortingSnippet = `import {
useReactTable,
getCoreRowModel,
getSortedRowModel
} from '@tanstack/react-table';
const table = useReactTable({
data: myData,
columns: myColumns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), // Enable sorting
});
return <Table table={table} />;`;
@@ -26,6 +26,7 @@ import * as SkeletonStories from '../../../packages/ui/src/components/Skeleton/S
import * as CardStories from '../../../packages/ui/src/components/Card/Card.stories';
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
// Helper function to create snippet components
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -67,3 +68,4 @@ export const SkeletonSnippet = createSnippetComponent(SkeletonStories);
export const CardSnippet = createSnippetComponent(CardStories);
export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
export const TableSnippet = createSnippetComponent(TableStories);
@@ -24,9 +24,11 @@ import {
getSortedRowModel,
useReactTable,
PaginationState,
ColumnDef,
} from '@tanstack/react-table';
import { useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { TableCellText } from './TableCellText/TableCellText';
const meta = {
title: 'Components/Table',
@@ -89,3 +91,73 @@ export const Controlled: Story = {
);
},
};
export const WithRowClick: Story = {
render: () => {
const table = useReactTable<DataProps>({
data,
columns, // Use default columns with no custom cell interactions
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const handleRowClick = (rowData: DataProps) => {
console.log('Pure row click:', rowData.name);
alert(`Navigating to: ${rowData.name}`);
};
return (
<>
<Table table={table} onRowClick={handleRowClick} />
<TablePagination table={table} />
</>
);
},
};
export const WithClickableCells: Story = {
render: () => {
// Create columns with clickable name cells
const clickableColumns: ColumnDef<DataProps>[] = [
...columns.slice(0, 1), // Keep select and name columns, but modify name
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => (
<div
onClick={e => {
e.stopPropagation(); // Prevent row click
alert(`Clicked on: ${row.original.name}`);
console.log('Cell clicked:', row.original);
}}
style={{ cursor: 'pointer' }}
>
<TableCellText
title={row.getValue('name')}
description={row.original.description}
/>
</div>
),
size: 450,
enableSorting: false,
},
...columns.slice(2), // Keep remaining columns
];
const table = useReactTable<DataProps>({
data,
columns: clickableColumns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<>
<Table table={table} />
<TablePagination table={table} />
</>
);
},
};
+5 -5
View File
@@ -44,7 +44,7 @@ function getAriaSort(sortDirection: string | false) {
export function Table<TData>(
props: TableProps<TData> & { ref?: React.ForwardedRef<HTMLTableElement> },
) {
const { className, table, ref, ...rest } = props;
const { className, table, onRowClick, ref, ...rest } = props;
return (
<RawTable
@@ -76,11 +76,11 @@ export function Table<TData>(
<RawTableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => {
const rowData = row.original as TData & { onClick?: () => void };
const handleRowClick = rowData.onClick
const handleRowClick = onRowClick
? (e: React.MouseEvent<HTMLTableRowElement>) => {
// Only call onRowClick if the event hasn't been handled by a child element
if (!e.isPropagationStopped()) {
rowData.onClick!();
onRowClick(row.original, e);
}
}
: undefined;
@@ -89,7 +89,7 @@ export function Table<TData>(
<RawTableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
data-clickable={!!rowData.onClick}
data-clickable={!!onRowClick}
onClick={handleRowClick}
>
{row.getVisibleCells().map(cell => (
@@ -20,4 +20,13 @@ import { Table } from '@tanstack/react-table';
export interface TableProps<TData>
extends React.HTMLAttributes<HTMLTableElement> {
table: Table<TData>;
/**
* Background click handler for rows. This will be called when clicking on empty
* areas of a row that don't have their own click handlers. Cell-level interactions
* (like Links or buttons) will automatically prevent this from firing.
*/
onRowClick?: (
row: TData,
event: React.MouseEvent<HTMLTableRowElement>,
) => void;
}