diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ebed3f..245013a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/docs/src/components/demos/ControlledSelectionDemo.tsx b/apps/docs/src/components/demos/ControlledSelectionDemo.tsx new file mode 100644 index 00000000..86337f06 --- /dev/null +++ b/apps/docs/src/components/demos/ControlledSelectionDemo.tsx @@ -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[] = [ + { 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([]); + const [events, setEvents] = useState([]); + + 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 ( +
+
+ + + + +
+ +

+ The buttons write straight to the selectedRows state. Checking a box in the table + calls onSelectedRowsChange, which writes back to that same state. Both paths stay in sync. +

+ + { + setSelected(selectedRows); + setEvents(prev => [`table: [${selectedRows.map(r => r.id).join(', ')}]`, ...prev].slice(0, 6)); + }} + highlightOnHover + /> + +
+
+ selectedRows state +
+ {selected.length > 0 ? `[${selected.map(r => r.id).join(', ')}]` : '[]'} + {selected.length > 0 && ( + — {selected.map(r => r.name).join(', ')} + )} +
+
+
+ recent updates +
    + {events.length === 0 ? ( +
  • none yet
  • + ) : ( + events.map((e, i) =>
  • {e}
  • ) + )} +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/demos/CustomCheckboxDemo.tsx b/apps/docs/src/components/demos/CustomCheckboxDemo.tsx new file mode 100644 index 00000000..26f7b09a --- /dev/null +++ b/apps/docs/src/components/demos/CustomCheckboxDemo.tsx @@ -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[] = [ + { 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 & { + accent?: string; +}; + +/** + * The ref must land on a real : 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( + ({ accent = '#7c3aed', checked, disabled, ...rest }, ref) => ( + + ), +); + +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([]); + const [accent, setAccent] = useState('#7c3aed'); + + return ( +
+