@@ -10,6 +10,7 @@ import {
1010 TableProps ,
1111 TableRow ,
1212 LinearProgress ,
13+ Checkbox ,
1314} from "@mui/material" ;
1415import TableSortLabel from "@mui/material/TableSortLabel" ;
1516import { useEffect , useMemo , useState , ReactNode , useCallback } from "react" ;
@@ -39,8 +40,12 @@ export type DataTableProps<T> = Readonly<{
3940 defaultSortKey ?: string ;
4041 defaultSortOrder ?: "asc" | "desc" ;
4142 onSortChange ?: ( sortKey : string , sortOrder : "asc" | "desc" ) => void ;
43+ paginated ?: boolean ;
4244 rowsPerPageOptions ?: readonly number [ ] ;
4345 tableProps ?: TableProps ;
46+ selectable ?: boolean ;
47+ selected ?: ( string | number ) [ ] ;
48+ onSelectionChange ?: ( selected : ( string | number ) [ ] ) => void ;
4449} > ;
4550
4651export function DataTable < T > ( {
@@ -55,8 +60,12 @@ export function DataTable<T>({
5560 defaultSortKey,
5661 defaultSortOrder = "asc" ,
5762 onSortChange,
63+ paginated = true ,
5864 rowsPerPageOptions = [ 10 , 25 ] ,
5965 tableProps,
66+ selectable = false ,
67+ selected = [ ] ,
68+ onSelectionChange,
6069} : DataTableProps < T > ) {
6170 const [ page , setPage ] = useState ( DEFAULT_PAGE ) ;
6271 const [ rowsPerPage , setRowsPerPage ] = useState ( DEFAULT_ROWS_PER_PAGE ) ;
@@ -117,6 +126,34 @@ export function DataTable<T>({
117126 return sortOrder === "asc" ? sorted : sorted . reverse ( ) ;
118127 } , [ data , sortKey , sortOrder , columns , sortable ] ) ;
119128
129+ const handleSelectAllClick = useCallback (
130+ ( event : React . ChangeEvent < HTMLInputElement > ) => {
131+ if ( event . target . checked ) {
132+ const newSelected = sortedData . map ( ( row ) => getRowId ( row ) ) ;
133+ onSelectionChange ?.( newSelected ) ;
134+ } else {
135+ onSelectionChange ?.( [ ] ) ;
136+ }
137+ } ,
138+ [ sortedData , getRowId , onSelectionChange ] ,
139+ ) ;
140+
141+ const handleRowClick = useCallback (
142+ ( rowId : string | number ) => {
143+ const selectedIndex = selected . indexOf ( rowId ) ;
144+ let newSelected : ( string | number ) [ ] = [ ] ;
145+
146+ if ( selectedIndex === - 1 ) {
147+ newSelected = [ ...selected , rowId ] ;
148+ } else {
149+ newSelected = selected . filter ( ( id ) => id !== rowId ) ;
150+ }
151+
152+ onSelectionChange ?.( newSelected ) ;
153+ } ,
154+ [ selected , onSelectionChange ] ,
155+ ) ;
156+
120157 useEffect ( ( ) => {
121158 const totalPages = Math . max ( 1 , Math . ceil ( sortedData . length / rowsPerPage ) ) ;
122159 if ( sortedData . length > 0 && page >= totalPages ) {
@@ -128,14 +165,25 @@ export function DataTable<T>({
128165 if ( sortedData . length === 0 ) {
129166 return [ ] ;
130167 }
168+ if ( ! paginated ) {
169+ return sortedData
170+ }
131171
132172 const startIndex = page * rowsPerPage ;
133173 const endIndex = startIndex + rowsPerPage ;
134174 return sortedData . slice ( startIndex , endIndex ) ;
135- } , [ sortedData , page , rowsPerPage ] ) ;
175+ } , [ sortedData , page , rowsPerPage , paginated ] ) ;
136176
137177 const emptyRows = Math . max ( 0 , rowsPerPage - paginatedData . length ) ;
138178
179+ const isSelected = useCallback (
180+ ( rowId : string | number ) => selected . indexOf ( rowId ) !== - 1 ,
181+ [ selected ] ,
182+ ) ;
183+
184+ const numSelected = selected . length ;
185+ const rowCount = sortedData . length ;
186+
139187 return (
140188 < Box >
141189 < TableContainer >
@@ -145,6 +193,20 @@ export function DataTable<T>({
145193 >
146194 < TableHead >
147195 < TableRow >
196+ { selectable && (
197+ < SubmitTableHeadCell padding = "checkbox" >
198+ < Checkbox
199+ color = "primary"
200+ indeterminate = { numSelected > 0 && numSelected < rowCount }
201+ checked = { rowCount > 0 && numSelected === rowCount }
202+ onChange = { handleSelectAllClick }
203+ inputProps = { {
204+ "aria-label" : "select all" ,
205+ } }
206+ sx = { { p : 0 , ml : 0.5 } }
207+ />
208+ </ SubmitTableHeadCell >
209+ ) }
148210 { columns . map ( ( column ) => (
149211 < SubmitTableHeadCell
150212 key = { column . id }
@@ -169,7 +231,7 @@ export function DataTable<T>({
169231 < TableBody key = { `table-body-${ page } -${ rowsPerPage } ` } >
170232 { isError && (
171233 < TableRow >
172- < TableCell colSpan = { columns . length } align = "center" >
234+ < TableCell colSpan = { columns . length + ( selectable ? 1 : 0 ) } align = "center" >
173235 { errorMessage }
174236 </ TableCell >
175237 </ TableRow >
@@ -179,7 +241,7 @@ export function DataTable<T>({
179241 < >
180242 < TableRow >
181243 < TableCell
182- colSpan = { columns . length }
244+ colSpan = { columns . length + ( selectable ? 1 : 0 ) }
183245 sx = { {
184246 border : "none" ,
185247 } }
@@ -189,7 +251,7 @@ export function DataTable<T>({
189251 </ TableCell >
190252 </ TableRow >
191253 < TableRow >
192- < TableCell colSpan = { columns . length } >
254+ < TableCell colSpan = { columns . length + ( selectable ? 1 : 0 ) } >
193255 < LinearProgress />
194256 </ TableCell >
195257 </ TableRow >
@@ -198,46 +260,80 @@ export function DataTable<T>({
198260
199261 { ! isLoading && ! isError && paginatedData . length === 0 && (
200262 < TableRow >
201- < TableCell colSpan = { columns . length } align = "center" >
263+ < TableCell colSpan = { columns . length + ( selectable ? 1 : 0 ) } align = "center" >
202264 { emptyMessage }
203265 </ TableCell >
204266 </ TableRow >
205267 ) }
206268
207269 { ! isLoading &&
208270 ! isError &&
209- paginatedData . map ( ( row ) => (
210- < TableRow key = { getRowId ( row ) } >
211- { columns . map ( ( column ) => {
212- const cellContent =
213- column . renderCell ?.( row ) ?? column . getValue ?.( row ) ?? null ;
214- return (
215- < PlainTableCell key = { column . id } >
216- { cellContent }
271+ paginatedData . map ( ( row ) => {
272+ const rowId = getRowId ( row ) ;
273+ const isItemSelected = isSelected ( rowId ) ;
274+
275+ return (
276+ < TableRow
277+ key = { rowId }
278+ hover = { selectable }
279+ onClick = { ( ) => selectable && handleRowClick ( rowId ) }
280+ role = { selectable ? "checkbox" : undefined }
281+ aria-checked = { selectable ? isItemSelected : undefined }
282+ selected = { isItemSelected }
283+ sx = { {
284+ cursor : selectable ? "pointer" : "default" ,
285+ "&.Mui-selected" : {
286+ backgroundColor : "transparent" ,
287+ } ,
288+ "&.Mui-selected:hover" : {
289+ backgroundColor : "rgba(0, 0, 0, 0.04)" ,
290+ } ,
291+ } }
292+ >
293+ { selectable && (
294+ < PlainTableCell padding = "checkbox" >
295+ < Checkbox
296+ color = "primary"
297+ checked = { isItemSelected }
298+ inputProps = { {
299+ "aria-labelledby" : `checkbox-${ rowId } ` ,
300+ } }
301+ sx = { { p : 0 } }
302+ />
217303 </ PlainTableCell >
218- ) ;
219- } ) }
220- </ TableRow >
221- ) ) }
304+ ) }
305+ { columns . map ( ( column ) => {
306+ const cellContent =
307+ column . renderCell ?.( row ) ?? column . getValue ?.( row ) ?? null ;
308+ return (
309+ < PlainTableCell key = { column . id } >
310+ { cellContent }
311+ </ PlainTableCell >
312+ ) ;
313+ } ) }
314+ </ TableRow >
315+ ) ;
316+ } ) }
222317
223- { ! isLoading && ! isError && emptyRows > 0 && (
318+ { paginated && ! isLoading && ! isError && emptyRows > 0 && (
224319 < TableRow style = { { height : ROW_HEIGHT * emptyRows } } >
225- < TableCell colSpan = { columns . length } sx = { { border : "none" } } />
320+ < TableCell colSpan = { columns . length + ( selectable ? 1 : 0 ) } sx = { { border : "none" } } />
226321 </ TableRow >
227322 ) }
228323 </ TableBody >
229324 </ Table >
230325 </ TableContainer >
231- < TablePagination
232- component = "div"
233- count = { sortedData . length }
234- page = { page }
235- rowsPerPage = { rowsPerPage }
236- onPageChange = { handleChangePage }
237- onRowsPerPageChange = { handleChangeRowsPerPage }
238- rowsPerPageOptions = { rowsPerPageOptions }
239- />
326+ { paginated && (
327+ < TablePagination
328+ component = "div"
329+ count = { sortedData . length }
330+ page = { page }
331+ rowsPerPage = { rowsPerPage }
332+ onPageChange = { handleChangePage }
333+ onRowsPerPageChange = { handleChangeRowsPerPage }
334+ rowsPerPageOptions = { rowsPerPageOptions }
335+ />
336+ ) }
240337 </ Box >
241338 ) ;
242339}
243-
0 commit comments