Skip to content

Commit b379a63

Browse files
authored
feat: add footer row support with customizable content (jbetancur#1317)
- Introduced a built-in footer row for totals, averages, and summaries in the DataTable component. - Added support for per-column footer definitions using a new `footer` field and a `footerComponent` prop for custom footer rendering. - Enhanced pagination button aria-labels for better accessibility. - Updated documentation to reflect new footer features and usage examples. - Implemented styling for the footer to align with existing table layouts. - Added tests to ensure footer functionality works as expected, including rendering conditions and custom components.
1 parent 5212514 commit b379a63

24 files changed

Lines changed: 953 additions & 28 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useState } from 'react';
2+
import DataTable from '../ThemedDataTable';
3+
import { useTableExport, type TableColumn } from 'react-data-table-component';
4+
5+
interface Employee {
6+
id: number;
7+
name: string;
8+
department: string;
9+
salary: number;
10+
}
11+
12+
const data: Employee[] = [
13+
{ id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000 },
14+
{ id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000 },
15+
{ id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000 },
16+
{ id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000 },
17+
{ id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000 },
18+
{ id: 6, name: 'Taylor Brooks', department: 'Sales', salary: 97000 },
19+
];
20+
21+
const columns: TableColumn<Employee>[] = [
22+
{ id: 'name', name: 'Name', selector: r => r.name, sortable: true },
23+
{ id: 'department', name: 'Department', selector: r => r.department, sortable: true },
24+
{ id: 'salary', name: 'Salary', selector: r => r.salary, right: true,
25+
format: r => `$${r.salary.toLocaleString()}` },
26+
];
27+
28+
export default function ExportDemo() {
29+
const [copied, setCopied] = useState(false);
30+
const { download, copy } = useTableExport({ columns, rows: data, valueSource: 'format' });
31+
32+
async function handleCopy(format: 'csv' | 'json') {
33+
await copy(format);
34+
setCopied(true);
35+
setTimeout(() => setCopied(false), 2000);
36+
}
37+
38+
return (
39+
<div className="space-y-3">
40+
<div className="flex items-center gap-2 text-sm flex-wrap">
41+
<button
42+
onClick={() => download('employees.csv')}
43+
className="px-3 py-1.5 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
44+
>
45+
Download CSV
46+
</button>
47+
<button
48+
onClick={() => download('employees.json', 'json')}
49+
className="px-3 py-1.5 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
50+
>
51+
Download JSON
52+
</button>
53+
<button
54+
onClick={() => handleCopy('csv')}
55+
className="px-3 py-1.5 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
56+
>
57+
{copied ? 'Copied!' : 'Copy CSV'}
58+
</button>
59+
</div>
60+
<DataTable columns={columns} data={data} />
61+
</div>
62+
);
63+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useState } from 'react';
2+
import DataTable from '../ThemedDataTable';
3+
import { type TableColumn } from 'react-data-table-component';
4+
5+
interface Row {
6+
id: number;
7+
name: string;
8+
department: string;
9+
salary: number;
10+
bonus: number;
11+
}
12+
13+
const ALL_DATA: Row[] = [
14+
{ id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000, bonus: 12000 },
15+
{ id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000, bonus: 9500 },
16+
{ id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000, bonus: 7800 },
17+
{ id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000, bonus: 11200 },
18+
{ id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000, bonus: 9000 },
19+
{ id: 6, name: 'Taylor Brooks', department: 'Engineering', salary: 122000, bonus: 8400 },
20+
{ id: 7, name: 'Casey Morgan', department: 'Product', salary: 108000, bonus: 6600 },
21+
{ id: 8, name: 'Alex Kim', department: 'Analytics', salary: 137000, bonus: 10100 },
22+
{ id: 9, name: 'Morgan Lee', department: 'Design', salary: 114000, bonus: 7200 },
23+
{ id: 10, name: 'Drew Park', department: 'Engineering', salary: 141000, bonus: 10800 },
24+
];
25+
26+
export default function FooterBasicDemo() {
27+
const [deptFilter, setDeptFilter] = useState('All');
28+
const departments = ['All', 'Engineering', 'Product', 'Design', 'Analytics'];
29+
const data = deptFilter === 'All' ? ALL_DATA : ALL_DATA.filter(r => r.department === deptFilter);
30+
31+
const columns: TableColumn<Row>[] = [
32+
{
33+
id: 'name',
34+
name: 'Name',
35+
selector: r => r.name,
36+
sortable: true,
37+
footer: rows => `${rows.length} employee${rows.length !== 1 ? 's' : ''}`,
38+
},
39+
{
40+
id: 'department',
41+
name: 'Department',
42+
selector: r => r.department,
43+
sortable: true,
44+
},
45+
{
46+
id: 'salary',
47+
name: 'Salary',
48+
selector: r => r.salary,
49+
sortable: true,
50+
right: true,
51+
format: r => `$${r.salary.toLocaleString()}`,
52+
footer: rows => `$${rows.reduce((s, r) => s + r.salary, 0).toLocaleString()}`,
53+
},
54+
{
55+
id: 'bonus',
56+
name: 'Bonus',
57+
selector: r => r.bonus,
58+
sortable: true,
59+
right: true,
60+
format: r => `$${r.bonus.toLocaleString()}`,
61+
footer: rows => `$${rows.reduce((s, r) => s + r.bonus, 0).toLocaleString()}`,
62+
},
63+
];
64+
65+
return (
66+
<div>
67+
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
68+
<span style={{ fontSize: 13, color: '#6b7280' }}>Filter by department:</span>
69+
{departments.map(d => (
70+
<button
71+
key={d}
72+
onClick={() => setDeptFilter(d)}
73+
style={{
74+
fontSize: 12,
75+
padding: '3px 10px',
76+
borderRadius: 9999,
77+
border: '1px solid',
78+
cursor: 'pointer',
79+
borderColor: deptFilter === d ? '#6366f1' : '#d1d5db',
80+
backgroundColor: deptFilter === d ? '#6366f1' : 'transparent',
81+
color: deptFilter === d ? '#fff' : '#374151',
82+
}}
83+
>
84+
{d}
85+
</button>
86+
))}
87+
</div>
88+
<DataTable
89+
columns={columns}
90+
data={data}
91+
highlightOnHover
92+
dense
93+
/>
94+
</div>
95+
);
96+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useState } from 'react';
2+
import DataTable from '../ThemedDataTable';
3+
import { type TableColumn, type FooterComponentProps } from 'react-data-table-component';
4+
5+
interface Row {
6+
id: number;
7+
name: string;
8+
department: string;
9+
salary: number;
10+
bonus: number;
11+
}
12+
13+
const ALL_DATA: Row[] = [
14+
{ id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000, bonus: 12000 },
15+
{ id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000, bonus: 9500 },
16+
{ id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000, bonus: 7800 },
17+
{ id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000, bonus: 11200 },
18+
{ id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000, bonus: 9000 },
19+
{ id: 6, name: 'Taylor Brooks', department: 'Engineering', salary: 122000, bonus: 8400 },
20+
{ id: 7, name: 'Casey Morgan', department: 'Product', salary: 108000, bonus: 6600 },
21+
{ id: 8, name: 'Alex Kim', department: 'Analytics', salary: 137000, bonus: 10100 },
22+
{ id: 9, name: 'Morgan Lee', department: 'Design', salary: 114000, bonus: 7200 },
23+
{ id: 10, name: 'Drew Park', department: 'Engineering', salary: 141000, bonus: 10800 },
24+
];
25+
26+
function SummaryFooter({ rows }: FooterComponentProps<Row>) {
27+
const totalSalary = rows.reduce((s, r) => s + r.salary, 0);
28+
const totalBonus = rows.reduce((s, r) => s + r.bonus, 0);
29+
const avgSalary = rows.length ? Math.round(totalSalary / rows.length) : 0;
30+
31+
return (
32+
<div
33+
style={{
34+
display: 'flex',
35+
flexWrap: 'wrap',
36+
gap: '16px 32px',
37+
padding: '10px 16px',
38+
borderTop: '1px solid #e5e7eb',
39+
backgroundColor: '#f8fafc',
40+
fontSize: 13,
41+
color: '#374151',
42+
}}
43+
>
44+
<span>
45+
<strong>{rows.length}</strong> {rows.length === 1 ? 'employee' : 'employees'}
46+
</span>
47+
<span>
48+
Salary total: <strong>${totalSalary.toLocaleString()}</strong>
49+
</span>
50+
<span>
51+
Salary avg: <strong>${avgSalary.toLocaleString()}</strong>
52+
</span>
53+
<span>
54+
Bonus total: <strong>${totalBonus.toLocaleString()}</strong>
55+
</span>
56+
<span>
57+
Total comp: <strong>${(totalSalary + totalBonus).toLocaleString()}</strong>
58+
</span>
59+
</div>
60+
);
61+
}
62+
63+
const columns: TableColumn<Row>[] = [
64+
{ id: 'name', name: 'Name', selector: r => r.name, sortable: true },
65+
{ id: 'department', name: 'Department', selector: r => r.department, sortable: true },
66+
{
67+
id: 'salary',
68+
name: 'Salary',
69+
selector: r => r.salary,
70+
sortable: true,
71+
right: true,
72+
format: r => `$${r.salary.toLocaleString()}`,
73+
},
74+
{
75+
id: 'bonus',
76+
name: 'Bonus',
77+
selector: r => r.bonus,
78+
sortable: true,
79+
right: true,
80+
format: r => `$${r.bonus.toLocaleString()}`,
81+
},
82+
];
83+
84+
export default function FooterCustomDemo() {
85+
const [deptFilter, setDeptFilter] = useState('All');
86+
const departments = ['All', 'Engineering', 'Product', 'Design', 'Analytics'];
87+
const data = deptFilter === 'All' ? ALL_DATA : ALL_DATA.filter(r => r.department === deptFilter);
88+
89+
return (
90+
<div>
91+
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
92+
<span style={{ fontSize: 13, color: '#6b7280' }}>Filter by department:</span>
93+
{departments.map(d => (
94+
<button
95+
key={d}
96+
onClick={() => setDeptFilter(d)}
97+
style={{
98+
fontSize: 12,
99+
padding: '3px 10px',
100+
borderRadius: 9999,
101+
border: '1px solid',
102+
cursor: 'pointer',
103+
borderColor: deptFilter === d ? '#6366f1' : '#d1d5db',
104+
backgroundColor: deptFilter === d ? '#6366f1' : 'transparent',
105+
color: deptFilter === d ? '#fff' : '#374151',
106+
}}
107+
>
108+
{d}
109+
</button>
110+
))}
111+
</div>
112+
<DataTable
113+
columns={columns}
114+
data={data}
115+
highlightOnHover
116+
dense
117+
footerComponent={SummaryFooter}
118+
/>
119+
</div>
120+
);
121+
}

apps/docs/src/components/demos/SelectionDemo.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ export default function SelectionDemo() {
5757
>
5858
Clear selection
5959
</button>
60+
</div>
61+
<div className="text-sm min-h-[1.25rem]">
6062
{selectedRows.length > 0 && (
61-
<span className="text-brand-600 font-medium ml-auto">
63+
<span className="text-brand-600 font-medium">
6264
{selectedRows.length} selected: {selectedRows.map(r => r.name).join(', ')}
6365
</span>
6466
)}

apps/docs/src/layouts/DocsLayout.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const nav = [
5050
{ label: 'Conditional Styles', href: '/docs/conditional-styles' },
5151
{ label: 'Fixed Header', href: '/docs/fixed-header' },
5252
{ label: 'Pagination', href: '/docs/pagination' },
53+
{ label: 'Footer', href: '/docs/footer' },
5354
{ label: 'Loading State', href: '/docs/loading' },
5455
],
5556
},

apps/docs/src/pages/docs/api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ Complete reference for every prop, type, and export in `react-data-table-compone
9797
| `onChangePage` | `(page, totalRows) => void` | - | Called when the active page changes. |
9898
| `onChangeRowsPerPage` | `(rowsPerPage, page) => void` | - | Called when rows-per-page selection changes. |
9999

100+
### Footer
101+
102+
| Prop | Type | Default | Description |
103+
|---|---|---|---|
104+
| `footerComponent` | `ComponentType<FooterComponentProps<T>>` | - | Replace the footer row with a custom component. Receives `{ rows, columns }`. Takes precedence over column-level `footer` fields. |
105+
| `showFooter` | `boolean` | - | Force the footer row on or off. By default the footer renders when `footerComponent` is set or any visible column declares a `footer`. Set to `false` to suppress, `true` to render an empty footer row. |
106+
107+
Column-level footers live on each [`TableColumn<T>`](#tablecolumnt) as the `footer` field. See [Footer](/docs/footer) for the full walkthrough.
108+
100109
### Row selection
101110

102111
| Prop | Type | Default | Description |
@@ -219,6 +228,7 @@ const columns: TableColumn<MyRow>[] = [
219228
| `reorder` | `boolean` | Allow drag-to-reorder for this column (requires `reorder` on at least two columns). |
220229
| `style` | `CSSProperties` | Inline styles applied to every cell in this column. |
221230
| `conditionalCellStyles` | `ConditionalStyles<T>[]` | Per-cell conditional styles. |
231+
| `footer` | `ReactNode \| (rows: T[]) => ReactNode` | Footer cell for this column. Static node or a function receiving the filtered+sorted rows (typically used to render aggregates like sums or averages). When any visible column has a `footer`, a footer row renders below the body. See [Footer](/docs/footer). |
222232

223233
### Inline editing
224234

@@ -361,6 +371,8 @@ const customStyles: TableStyles = {
361371
| `expanderCell` | - | Cell containing the expand/collapse button. |
362372
| `expanderButton` | - | The expand/collapse button itself. |
363373
| `pagination` | `pageButtonsStyle` | Pagination bar and page buttons. |
374+
| `footer` | - | The footer row container. |
375+
| `footerCells` | - | Individual footer cells. |
364376
| `noData` | - | Empty-state container. |
365377
| `progress` | - | Loading indicator container. |
366378

apps/docs/src/pages/docs/changelog.astro

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ import CodeBlock from '../../components/CodeBlock.astro';
2121
<code>onChangePage</code> to keep them in sync.
2222
→ <a href="/docs/api#pagination">API reference</a>
2323
</li>
24+
<li>
25+
Built-in <strong>footer row</strong> for totals, averages, and other summary cells.
26+
Declare per-column with the new <code>footer</code> field
27+
(<code>ReactNode</code> or <code>(rows) =&gt; ReactNode</code>) or replace the whole
28+
row with the <code>footerComponent</code> prop. Footer cells respect column widths,
29+
alignment, and pinning automatically.
30+
→ <a href="/docs/footer">Footer docs</a>
31+
</li>
32+
<li>
33+
<strong>Pagination button aria-labels</strong> — "First Page", "Previous Page", "Next Page",
34+
and "Last Page" are now configurable via <code>paginationComponentOptions</code>, enabling
35+
proper i18n for screen readers.
36+
</li>
37+
</ul>
38+
39+
<h3>Bug fixes</h3>
40+
<ul>
41+
<li>
42+
Fixed column reordering bypassing <code>reorder=&#123;false&#125;</code> when a cell's text
43+
was selected via double-click and then dragged. The cell now correctly gates all drag
44+
handlers on <code>column.reorder</code>.
45+
</li>
2446
</ul>
2547

2648
<h2>8.1.0</h2>
@@ -46,7 +68,8 @@ import CodeBlock from '../../components/CodeBlock.astro';
4668
</li>
4769
<li>
4870
New headless export hook <code>useTableExport</code>: build CSV/JSON, trigger a
49-
download, or copy to clipboard.
71+
download, or copy to clipboard. Import directly from the package:
72+
<code>import &#123; useTableExport &#125; from 'react-data-table-component'</code>.
5073
→ <a href="/docs/export">Export</a>
5174
</li>
5275
</ul>

0 commit comments

Comments
 (0)