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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

A summary of notable changes per release. For the full commit history see the [repository on GitHub](https://github.com/jbetancur/react-data-table-component/commits/master).

## 8.8.1

### Bug fixes

- Controlled `selectedRows` is no longer dropped when a row is toggled in-table. Selection deltas are computed against the `selectedRows` prop instead of stale internal state, so a selection set from outside survives the next checkbox click and `onSelectedRowsChange` emits the correct set. → [Row selection](/docs/selection) ([#1374](https://github.com/jbetancur/react-data-table-component/issues/1374))

---

## 8.8.0

### New features
Expand Down
114 changes: 114 additions & 0 deletions apps/docs/src/components/demos/ControlledSelectionDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import DataTable from '../ThemedDataTable';
import { type TableColumn } from 'react-data-table-component';

interface Row {
id: number;
name: string;
role: string;
status: 'Active' | 'On Leave';
}

const data: Row[] = [
{ id: 1, name: 'Aria Chen', role: 'Engineering Lead', status: 'Active' },
{ id: 2, name: 'Marcus Webb', role: 'Product Manager', status: 'Active' },
{ id: 3, name: 'Priya Kapoor', role: 'Senior Designer', status: 'On Leave' },
{ id: 4, name: 'Jordan Ellis', role: 'Data Scientist', status: 'Active' },
{ id: 5, name: 'Sam Rivera', role: 'DevOps Engineer', status: 'On Leave' },
];

const columns: TableColumn<Row>[] = [
{ name: 'Name', selector: r => r.name, sortable: true },
{ name: 'Role', selector: r => r.role },
{ name: 'Status', selector: r => r.status },
];

export default function ControlledSelectionDemo() {
const [selected, setSelected] = useState<Row[]>([]);
const [events, setEvents] = useState<string[]>([]);

const setExternally = (rows: Row[], label: string) => {
setSelected(rows);
setEvents(prev => [`external: ${label}`, ...prev].slice(0, 6));
};

const selectWhere = (label: string, predicate: (row: Row) => boolean) =>
setExternally(data.filter(predicate), label);

const invert = () =>
setExternally(
data.filter(row => !selected.some(s => s.id === row.id)),
'invert selection',
);

return (
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm flex-wrap">
<button
onClick={() => selectWhere('status is Active', r => r.status === 'Active')}
className="px-2.5 py-1 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
>
Select Active
</button>
<button
onClick={() => selectWhere('role contains "Engineer"', r => r.role.includes('Engineer'))}
className="px-2.5 py-1 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
>
Select Engineers
</button>
<button
onClick={invert}
className="px-2.5 py-1 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
>
Invert
</button>
<button
onClick={() => setExternally([], 'clear')}
className="px-2.5 py-1 text-xs border border-gray-200 rounded-md text-gray-600 hover:border-gray-300 hover:text-gray-900"
>
Clear
</button>
</div>

<p className="text-xs text-gray-400">
The buttons write straight to the <code>selectedRows</code> state. Checking a box in the table
calls <code>onSelectedRowsChange</code>, which writes back to that same state. Both paths stay in sync.
</p>

<DataTable
columns={columns}
data={data}
keyField="id"
selectableRows
selectedRows={selected}
onSelectedRowsChange={({ selectedRows }) => {
setSelected(selectedRows);
setEvents(prev => [`table: [${selectedRows.map(r => r.id).join(', ')}]`, ...prev].slice(0, 6));
}}
highlightOnHover
/>

<div className="grid gap-3 sm:grid-cols-2 text-sm">
<div>
<span className="text-gray-500 text-xs font-medium">selectedRows state</span>
<div className="mt-1 font-mono text-xs text-brand-600 min-h-[1.25rem]">
{selected.length > 0 ? `[${selected.map(r => r.id).join(', ')}]` : '[]'}
{selected.length > 0 && (
<span className="text-gray-400"> — {selected.map(r => r.name).join(', ')}</span>
)}
</div>
</div>
<div>
<span className="text-gray-500 text-xs font-medium">recent updates</span>
<ul className="mt-1 font-mono text-xs text-gray-500 space-y-0.5 min-h-[1.25rem]">
{events.length === 0 ? (
<li className="text-gray-400">none yet</li>
) : (
events.map((e, i) => <li key={`${e}-${i}`}>{e}</li>)
)}
</ul>
</div>
</div>
</div>
);
}
171 changes: 171 additions & 0 deletions apps/docs/src/components/demos/CustomCheckboxDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useState, forwardRef } from 'react';
import DataTable from '../ThemedDataTable';
import { type TableColumn } from 'react-data-table-component';

interface Row {
id: number;
track: string;
artist: string;
length: string;
}

const data: Row[] = [
{ id: 1, track: 'Midnight Static', artist: 'Vela Nine', length: '3:42' },
{ id: 2, track: 'Paper Ghosts', artist: 'The Longwave', length: '4:15' },
{ id: 3, track: 'Neon Orchard', artist: 'Kite Parade', length: '2:58' },
{ id: 4, track: 'Slow Cartography', artist: 'Ansel Frame', length: '5:07' },
];

const columns: TableColumn<Row>[] = [
{ name: 'Track', selector: r => r.track, sortable: true },
{ name: 'Artist', selector: r => r.artist },
{ name: 'Length', selector: r => r.length, right: true },
];

type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
accent?: string;
};

/**
* The ref must land on a real <input>: DataTable sets `.indeterminate` on it directly.
* Everything visual is a sibling overlay driven by :checked, so the input itself stays
* a functioning checkbox for keyboard and assistive tech.
*/
const VinylCheckbox = forwardRef<HTMLInputElement, CheckboxProps>(
({ accent = '#7c3aed', checked, disabled, ...rest }, ref) => (
<label
className="vinyl-wrap"
style={{
['--vinyl-accent' as string]: accent,
opacity: disabled ? 0.4 : 1,
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
<input ref={ref} type="checkbox" checked={checked} disabled={disabled} {...rest} />
<span className="vinyl-disc" aria-hidden="true">
<span className="vinyl-label" />
</span>
</label>
),
);

VinylCheckbox.displayName = 'VinylCheckbox';

const styles = `
.vinyl-wrap {
position: relative;
display: inline-flex;
width: 22px;
height: 22px;
align-items: center;
justify-content: center;
}
.vinyl-wrap input {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: inherit;
}
.vinyl-disc {
width: 20px;
height: 20px;
border-radius: 999px;
background: repeating-radial-gradient(circle at 50% 50%, #1f2937 0 2px, #111827 2px 3px);
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.5);
display: grid;
place-items: center;
transition: transform 0.35s ease, box-shadow 0.2s ease;
}
.vinyl-label {
width: 7px;
height: 7px;
border-radius: 999px;
background: #9ca3af;
transition: background 0.2s ease, transform 0.2s ease;
}
.vinyl-wrap input:checked ~ .vinyl-disc {
transform: rotate(180deg);
box-shadow: 0 0 0 2px var(--vinyl-accent), 0 0 10px -1px var(--vinyl-accent);
}
.vinyl-wrap input:checked ~ .vinyl-disc .vinyl-label {
background: var(--vinyl-accent);
transform: scale(1.25);
}
/* Indeterminate: DataTable sets this directly on the input for the header checkbox. */
.vinyl-wrap input:indeterminate ~ .vinyl-disc {
box-shadow: 0 0 0 2px #f59e0b;
}
.vinyl-wrap input:indeterminate ~ .vinyl-disc .vinyl-label {
background: #f59e0b;
transform: scale(0.6);
}
.vinyl-wrap input:focus-visible ~ .vinyl-disc {
outline: 2px solid var(--vinyl-accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.vinyl-disc { transition: none; }
}
`;

export default function CustomCheckboxDemo() {
const [selected, setSelected] = useState<Row[]>([]);
const [accent, setAccent] = useState('#7c3aed');

return (
<div className="space-y-3">
<style dangerouslySetInnerHTML={{ __html: styles }} />

<div className="flex items-center gap-3 text-sm flex-wrap">
<span className="text-gray-500 text-xs">Accent, forwarded via selectableRowsComponentProps:</span>
{[
['#7c3aed', 'violet'],
['#0891b2', 'cyan'],
['#e11d48', 'rose'],
].map(([value, label]) => (
<button
key={value}
onClick={() => setAccent(value)}
className={`px-2.5 py-1 text-xs border rounded-md ${
accent === value ? 'border-gray-400 text-gray-900' : 'border-gray-200 text-gray-500'
}`}
>
<span
className="inline-block w-2 h-2 rounded-full mr-1.5 align-middle"
style={{ background: value }}
/>
{label}
</button>
))}
</div>

<DataTable
columns={columns}
data={data}
keyField="id"
selectableRows
selectableRowsComponent={VinylCheckbox}
selectableRowsComponentProps={{ accent }}
selectableRowDisabled={r => r.id === 4}
onSelectedRowsChange={({ selectedRows }) => setSelected(selectedRows)}
highlightOnHover
/>

<div className="text-sm min-h-[1.25rem]">
{selected.length > 0 ? (
<span className="text-brand-600 font-medium">
{selected.length} queued: {selected.map(r => r.track).join(', ')}
</span>
) : (
<span className="text-gray-400 text-xs">
Select a few rows. "Slow Cartography" is disabled, and the header disc turns amber when the
selection is partial.
</span>
)}
</div>
</div>
);
}
78 changes: 78 additions & 0 deletions apps/docs/src/components/demos/VisibleOnlyDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import DataTable from '../ThemedDataTable';
import { type TableColumn } from 'react-data-table-component';

interface Row {
id: number;
name: string;
region: string;
}

const regions = ['North', 'South', 'East', 'West'];

const data: Row[] = Array.from({ length: 9 }, (_, i) => ({
id: i + 1,
name: `Account ${String(i + 1).padStart(2, '0')}`,
region: regions[i % regions.length],
}));

const columns: TableColumn<Row>[] = [
{ name: 'Account', selector: r => r.name, sortable: true },
{ name: 'Region', selector: r => r.region },
];

function Pane({ visibleOnly, title, note }: { visibleOnly: boolean; title: string; note: string }) {
const [selected, setSelected] = useState<Row[]>([]);

return (
<div className="rounded-lg border border-gray-200 p-3 space-y-2">
<div>
<p className="text-sm font-semibold text-gray-800">{title}</p>
<p className="text-xs text-gray-400 mt-0.5">{note}</p>
</div>

<DataTable
columns={columns}
data={data}
keyField="id"
selectableRows
selectableRowsVisibleOnly={visibleOnly}
pagination
paginationPerPage={3}
paginationRowsPerPageOptions={[3]}
onSelectedRowsChange={({ selectedRows }) => setSelected(selectedRows)}
dense
/>

<div className="text-xs font-mono min-h-[1.25rem]">
<span className="text-gray-500">selected: </span>
<span className={selected.length > 0 ? 'text-brand-600 font-medium' : 'text-gray-400'}>
{selected.length > 0 ? `[${selected.map(r => r.id).join(', ')}]` : '[]'}
</span>
</div>
</div>
);
}

export default function VisibleOnlyDemo() {
return (
<div className="space-y-3">
<p className="text-xs text-gray-500">
Nine rows, three per page. In both tables: tick "select all" on page 1, then go to page 2.
</p>

<div className="grid gap-3 sm:grid-cols-2">
<Pane
visibleOnly={false}
title="Default"
note="Select all takes every row in the dataset, and the selection survives paging."
/>
<Pane
visibleOnly
title="selectableRowsVisibleOnly"
note="Select all takes only the current page, and changing page clears the selection."
/>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions apps/docs/src/pages/docs/export.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import DocsTable from '../../components/DocsTable.astro';
</p>

<p class="callout">
<strong>New in 8.1.0</strong> — <code>useTableExport</code> ships as part of the library's
headless surface alongside <code>useTableState</code>, <code>useTableData</code>, and friends.
<code>useTableExport</code> ships as part of the library's headless surface
alongside <code>useTableState</code>, <code>useTableData</code>, and friends.
</p>

<Demo
Expand Down
Loading