react-live-data-table 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-live-data-table",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Your React component package with Tailwind",
5
5
  "main": "src/index.js",
6
6
  "module": "dist/index.esm.js",
package/readme.md CHANGED
@@ -8,7 +8,7 @@ A highly customizable and efficient data grid table for React. It supports featu
8
8
 
9
9
  To install the package, run one of the following commands:
10
10
 
11
- ```bash
11
+ bash
12
12
  npm install react-data-grid-table
13
13
 
14
14
  # React Data Grid
@@ -41,40 +41,4 @@ A flexible React data grid component with support for pagination, row selection,
41
41
  | `rowHeights` | `number` | `40` | Height of each row |
42
42
  | `headerProps` | `object` | `{}` | Custom header properties |
43
43
 
44
-
45
-
46
-
47
- ### Column Definition
48
-
49
- ```typescript
50
- interface ColumnDefinition {
51
- field: string; // Key of the data field to display
52
- headerName: string; // Text to show in column header
53
- width?: number; // Column width in pixels
54
- sortable?: boolean; // Enable column sorting
55
- render?: (value: any, rowData: Object) => React.ReactNode; // Custom cell renderer
56
-
57
-
58
44
  ## Basic Usage
59
-
60
- ```jsx
61
- import { ReactDataTable } from 'react-data-grid';
62
-
63
- const App = () => {
64
- const columns = [
65
- { field: 'id', headerName: 'ID' },
66
- { field: 'name', headerName: 'Name' }
67
- ];
68
-
69
- const data = [
70
- { id: 1, name: 'John' },
71
- { id: 2, name: 'Jane' }
72
- ];
73
-
74
- return (
75
- <ReactDataTable
76
- columns={columns}
77
- dataSource={data}
78
- />
79
- );
80
- };
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
2
  import "./index.css";
3
3
 
4
4
  function ReactDataTable({
@@ -15,14 +15,122 @@ function ReactDataTable({
15
15
  staticData = null,
16
16
  emptyText,
17
17
  rowHeights = 40,
18
- headerProps = {} // Added default value
18
+ headerProps = {},
19
+ selected = {},
20
+ showSelectAllCheckbox = true,
21
+ rowStyle = {},
22
+ rowClassName = ""
19
23
  }) {
20
24
  const tableContainerRef = React.useRef(null);
21
25
  const [data, setData] = React.useState({ pages: [], meta: { totalPages: 1 } });
22
26
  const [isFetching, setIsFetching] = React.useState(false);
23
27
  const [pageParam, setPageParam] = React.useState(1);
24
- const [selectedRows, setSelectedRows] = React.useState({});
28
+ const [selectedRows, setSelectedRows] = React.useState(selected);
29
+ const previousSelected = React.useRef(selected);
30
+ const [resizingIndex, setResizingIndex] = useState(null);
31
+ const [columnWidths, setColumnWidths] = useState([]);
32
+ const [startX, setStartX] = useState(null);
33
+ const [initialWidth, setInitialWidth] = useState(null);
34
+ const [tableWidth, setTableWidth] = useState(0);
35
+ // Ref to store column widths to ensure persistence during data loading
36
+ const persistedColumnWidthsRef = React.useRef([]);
25
37
 
38
+ const flatData = data.pages.flatMap(page => page.data);
39
+
40
+ const checkboxColumn = {
41
+ id: 'select',
42
+ size: 50,
43
+ minWidth: 50,
44
+ resizable: false,
45
+ textAlign: "center",
46
+ header: ({ data }) => {
47
+ const allSelected = flatData.length > 0 && flatData.every(row => selectedRows[row.id]);
48
+ const someSelected = flatData.some(row => selectedRows[row.id]) && !allSelected;
49
+
50
+ return (
51
+ <div className="flex items-center justify-center h-[40px]">
52
+ {showSelectAllCheckbox && (
53
+ <div className="relative">
54
+ <input
55
+ id={data.id}
56
+ type="checkbox"
57
+ className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
58
+ checked={allSelected}
59
+ onChange={(e) => handleSelectAll(e.target.checked, flatData)}
60
+ />
61
+ {allSelected ? (
62
+ <svg
63
+ className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none text-white"
64
+ viewBox="0 0 20 20"
65
+ fill="currentColor"
66
+ >
67
+ <path
68
+ fillRule="evenodd"
69
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
70
+ clipRule="evenodd"
71
+ />
72
+ </svg>
73
+ ) : (
74
+ someSelected &&
75
+ <svg
76
+ className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none "
77
+ viewBox="0 0 20 20"
78
+ fill="currentColor"
79
+ >
80
+ <path
81
+ fillRule="evenodd"
82
+ d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
83
+ clipRule="evenodd"
84
+ />
85
+ </svg>
86
+ )}
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ },
92
+ cell: ({ row }) => {
93
+ return (
94
+ <div className="flex items-center justify-center h-[40px]" onClick={(e) => e.stopPropagation()}>
95
+ <input
96
+ id={row.id}
97
+ type="checkbox"
98
+ className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
99
+ checked={!!selectedRows[row.id]}
100
+ onClick={(e) => e.stopPropagation()}
101
+ onChange={(e) => { e.stopPropagation(); handleSelectRow(e.target.checked, row, flatData) }}
102
+ />
103
+ </div>
104
+ )
105
+ }
106
+ };
107
+
108
+ const enhancedColumns = showCheckbox ? [checkboxColumn, ...columns] : columns;
109
+
110
+ useEffect(() => {
111
+ if (JSON.stringify(previousSelected.current) !== JSON.stringify(selected)) {
112
+ setSelectedRows({ ...selected });
113
+ previousSelected.current = selected;
114
+ }
115
+ }, [selected]);
116
+
117
+ // Initialize column widths array only once when component mounts or columns change
118
+ useEffect(() => {
119
+ const allColumns = showCheckbox ? [{ id: 'select', size: 50 }, ...columns] : columns;
120
+
121
+ // If we have persisted widths and the number of columns matches, use those
122
+ if (persistedColumnWidthsRef.current.length === allColumns.length) {
123
+ setColumnWidths([...persistedColumnWidthsRef.current]);
124
+ setTableWidth(persistedColumnWidthsRef.current.reduce((sum, width) => sum + width, 0));
125
+ } else {
126
+ // Otherwise initialize with default widths
127
+ const initialWidths = allColumns.map(column => column.size || column.minWidth || 150);
128
+ setColumnWidths(initialWidths);
129
+ setTableWidth(initialWidths.reduce((sum, width) => sum + width, 0));
130
+ // Store in our ref for persistence
131
+ persistedColumnWidthsRef.current = [...initialWidths];
132
+ }
133
+ }, [columns, showCheckbox]);
26
134
 
27
135
  useEffect(() => {
28
136
  setData({ pages: [], meta: { totalPages: 1 } });
@@ -47,6 +155,57 @@ function ReactDataTable({
47
155
  }
48
156
  }, [dataSource, staticData]);
49
157
 
158
+ const handleMouseMove = useCallback((e) => {
159
+ if (resizingIndex === null || startX === null || initialWidth === null) return;
160
+
161
+ const delta = e.clientX - startX;
162
+ const newWidth = Math.max(enhancedColumns[resizingIndex]?.minWidth || 80, initialWidth + delta);
163
+
164
+ // Create a new array of column widths with the updated width
165
+ const newColumnWidths = [...columnWidths];
166
+ newColumnWidths[resizingIndex] = newWidth;
167
+
168
+ // Update the column widths state
169
+ setColumnWidths(newColumnWidths);
170
+
171
+ // Update our persisted ref to maintain widths during pagination
172
+ persistedColumnWidthsRef.current = newColumnWidths;
173
+
174
+ // Recalculate table width based on the new column widths
175
+ const newTableWidth = newColumnWidths.reduce((sum, width) => sum + width, 0);
176
+ setTableWidth(newTableWidth);
177
+ }, [resizingIndex, startX, initialWidth, enhancedColumns, columnWidths]);
178
+
179
+ const handleMouseUp = useCallback(() => {
180
+ setResizingIndex(null);
181
+ setStartX(null);
182
+ setInitialWidth(null);
183
+ }, []);
184
+
185
+ // Add resize event listeners
186
+ useEffect(() => {
187
+ if (resizingIndex !== null) {
188
+ document.addEventListener('mousemove', handleMouseMove);
189
+ document.addEventListener('mouseup', handleMouseUp);
190
+ return () => {
191
+ document.removeEventListener('mousemove', handleMouseMove);
192
+ document.removeEventListener('mouseup', handleMouseUp);
193
+ };
194
+ }
195
+ }, [resizingIndex, handleMouseMove, handleMouseUp]);
196
+
197
+ const handleResizeStart = (e, index) => {
198
+ const column = enhancedColumns[index];
199
+ if (!column.resizable) return;
200
+
201
+ e.preventDefault();
202
+ e.stopPropagation();
203
+
204
+ setResizingIndex(index);
205
+ setStartX(e.clientX);
206
+ setInitialWidth(columnWidths[index] || column.size || column.minWidth || 150);
207
+ };
208
+
50
209
  const loadInitialData = async () => {
51
210
  if (!dataSource) return;
52
211
 
@@ -71,10 +230,13 @@ function ReactDataTable({
71
230
  skip: pageParam * defaultLimit,
72
231
  limit: defaultLimit
73
232
  });
233
+
234
+ // Update the data but maintain our column widths
74
235
  setData(prev => ({
75
236
  pages: [...prev.pages, nextData],
76
237
  meta: nextData.meta
77
238
  }));
239
+
78
240
  setPageParam(prev => prev + 1);
79
241
  } catch (error) {
80
242
  console.error('Error fetching next page:', error);
@@ -157,122 +319,173 @@ function ReactDataTable({
157
319
  handleScroll(tableContainerRef.current);
158
320
  }, [data]);
159
321
 
160
- const flatData = data.pages.flatMap(page => page.data);
161
-
162
- const checkboxColumn = {
163
- id: 'select',
164
- size: 50,
165
- minWidth: 50,
166
- textAlign: "center",
167
- header: ({ data }) => (
168
- <div className="flex items-center justify-center h-[40px]">
169
- <input
170
- type="checkbox"
171
- checked={Object.keys(selectedRows).length > 0 && data.every(row => selectedRows[row.id])}
172
- onChange={(e) => handleSelectAll(e.target.checked, flatData)}
173
- />
174
- </div>
175
- ),
176
- cell: ({ row }) => (
177
- <div className="flex items-center justify-center h-[40px]">
178
- <input
179
- type="checkbox"
180
- checked={!!selectedRows[row.id]}
181
- onChange={(e) => handleSelectRow(e.target.checked, row, flatData)}
182
- />
183
- </div>
184
- ),
185
- };
186
-
187
- const enhancedColumns = showCheckbox ? [checkboxColumn, ...columns] : columns;
188
-
189
322
  return (
190
- <div className="bg-white relative w-full">
191
- {loading && <div className="absolute inset-0 bg-white/50 z-20 flex items-center justify-center">
192
- <div data-testid="loading-spinner" role="status" className="p-2">
193
- <svg
194
- aria-hidden="true"
195
- className="inline w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-green-500"
196
- viewBox="0 0 100 101"
197
- fill="none"
198
- xmlns="http://www.w3.org/2000/svg"
199
- >
200
- <path
201
- d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908Z"
202
- fill="currentColor"
203
- />
204
- </svg>
205
- </div>
206
- </div>}
207
- {flatData.length === 0 && !loading ? (
208
- <div className="flex items-center justify-center" style={{ height }}>
209
- <div className="text-gray-500">
210
- {emptyText || 'No data available'}
211
- </div>
212
- </div>
213
- ) : (
214
- <div className="overflow-hidden">
215
- <div
216
- ref={tableContainerRef}
217
- className="overflow-auto w-full"
218
- style={{ maxHeight, height }}
219
- onScroll={(e) => handleScroll(e.currentTarget)}
323
+ <div className="bg-white relative w-full react-live-data-table" >
324
+ {loading && (
325
+ <div className="absolute inset-0 bg-white/50 z-20 flex items-center justify-center">
326
+ <svg
327
+ style={{
328
+ animation: 'spin 1s linear infinite',
329
+ width: '24px',
330
+ height: '24px'
331
+ }}
332
+ viewBox="0 0 24 24"
333
+ fill="none"
334
+ xmlns="http://www.w3.org/2000/svg"
220
335
  >
221
- <table className="w-full border-collapse">
222
- <thead
223
- className="sticky top-0 z-1 bg-blue-300"
224
- style={{ ...headerProps.style }}
336
+ <style>
337
+ {`@keyframes spin {from {transform: rotate(0deg)} to {transform: rotate(360deg)}}`}
338
+ </style>
339
+ <circle
340
+ style={{ opacity: 0.25 }}
341
+ cx="12"
342
+ cy="12"
343
+ r="10"
344
+ stroke="currentColor"
345
+ strokeWidth="4"
346
+ />
347
+ <path
348
+ style={{ opacity: 0.75 }}
349
+ fill="currentColor"
350
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
351
+ />
352
+ </svg>
353
+ </div>
354
+ )}
355
+
356
+ {
357
+ flatData.length === 0 && !loading ? (
358
+ <div className="flex items-center justify-center" style={{ height }}>
359
+ <div className="text-gray-500">
360
+ {emptyText || 'No data available'}
361
+ </div>
362
+ </div>
363
+ ) : (
364
+ <div className="overflow-hidden">
365
+ <div
366
+ ref={tableContainerRef}
367
+ className="overflow-auto w-full"
368
+ style={{ maxHeight, height }}
369
+ onScroll={(e) => handleScroll(e.currentTarget)}
370
+ >
371
+ <table
372
+ className="w-full border-collapse"
373
+ style={{
374
+ tableLayout: 'fixed',
375
+ }}
225
376
  >
226
- <tr>
227
- {enhancedColumns.map(column => (
228
- <th
229
- key={column.accessorKey || column.id}
230
- className="text-left font-normal h-[40px]"
231
- style={{
232
- width: column.size,
233
- minWidth: column.minWidth,
234
- textAlign: column.textAlign,
235
- }}
236
- >
237
- {typeof column.header === 'function' ? column.header({ data: flatData }) : column.header}
238
- </th>
239
- ))}
240
- </tr>
241
- </thead>
242
- <tbody>
243
- {flatData.length > 0 ? (
244
- flatData.map((row, index) => (
245
- <tr
246
- key={row.id}
247
- className={`border-t border-x border-gray-200 hover:bg-[#dee1f2] h-[${rowHeights}px] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''}`}
248
- onClick={() => handleRowClick(row, index, flatData)}
249
- >
250
- {enhancedColumns.map(column => (
251
- <td
377
+ <thead
378
+ className="sticky top-0 z-10 bg-blue-300"
379
+ style={{ ...headerProps.style }}
380
+ >
381
+ <tr>
382
+ {enhancedColumns.map((column, columnIndex) => {
383
+ // Use persisted column widths to ensure consistency
384
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
385
+
386
+ return (
387
+ <th
252
388
  key={column.accessorKey || column.id}
253
- className={`text-left font-normal border-r ${column?.cellProps?.className || ''}`}
389
+ className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
390
+ }`}
254
391
  style={{
255
- minWidth: `${column.minWidth}px`,
256
- textAlign: column?.textAlign,
257
- ...column?.cellProps?.style,
392
+ width: `${width}px`,
393
+ minWidth: `${width}px`,
394
+ maxWidth: `${width}px`,
395
+ textAlign: column.textAlign,
258
396
  }}
259
397
  >
260
- {typeof column.cell === 'function' ? column.cell({ row }) : null}
261
- </td>
262
- ))}
263
- </tr>
264
- ))
265
- ) : (
266
- <tr>
267
- <td colSpan={enhancedColumns.length} className="text-center py-4">
268
- {emptyText || 'No data available'}
269
- </td>
398
+ <div className="flex items-center h-full overflow-hidden justify-center pl-[17px]">
399
+ <span className="truncate">
400
+ {typeof column.header === 'function' ? column.header({ data: flatData }) : column.header}
401
+ </span>
402
+ </div>
403
+
404
+ {/* Resize handle - Only show if column is resizable */}
405
+ {column.resizable !== false && (
406
+ <div
407
+ className={`absolute top-0 right-0 h-full w-4 flex items-center justify-center group ${resizingIndex === columnIndex ? 'bg-blue-100' : 'hover:bg-blue-100'
408
+ } transition-colors duration-200`}
409
+ onMouseDown={(e) => handleResizeStart(e, columnIndex)}
410
+ style={{
411
+ touchAction: 'none',
412
+ userSelect: 'none',
413
+ cursor: 'col-resize'
414
+ }}
415
+ >
416
+ </div>
417
+ )}
418
+ </th>
419
+ );
420
+ })}
270
421
  </tr>
271
- )}
272
- </tbody>
273
- </table>
422
+ </thead>
423
+ <tbody>
424
+ {flatData.length > 0 ? (
425
+ flatData.map((row, rowIndex) => {
426
+ const isLastRow = rowIndex === flatData.length - 1;
427
+ return (
428
+ <tr
429
+ key={row.id}
430
+ className={`border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName} cursor-pointer`}
431
+ style={{
432
+ height: `${rowHeights}px`,
433
+ ...rowStyle,
434
+ ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
435
+ }}
436
+ onClick={() => handleRowClick(row, rowIndex, flatData)}
437
+ >
438
+ {enhancedColumns.map((column, columnIndex) => {
439
+ // Use persisted column widths for cells as well
440
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
441
+
442
+ return (
443
+ <td
444
+ key={column.accessorKey || column.id}
445
+ className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
446
+ } ${column?.cellProps?.className || ''}`}
447
+ style={{
448
+ width: `${width}px`,
449
+ minWidth: `${width}px`,
450
+ maxWidth: `${width}px`,
451
+ textAlign: column?.textAlign,
452
+ ...column?.cellProps?.style,
453
+ overflow: 'hidden',
454
+ textOverflow: 'ellipsis',
455
+ whiteSpace: 'nowrap'
456
+ }}
457
+ >
458
+ {typeof column.cell === 'function' ? column.cell({ row }) : null}
459
+ </td>
460
+ );
461
+ })}
462
+ </tr>
463
+ );
464
+ })
465
+ ) : (
466
+ <tr>
467
+ <td colSpan={enhancedColumns.length} className="text-center py-4">
468
+ {emptyText || 'No data available'}
469
+ </td>
470
+ </tr>
471
+ )}
472
+ </tbody>
473
+ </table>
474
+ </div>
274
475
  </div>
275
- </div>
476
+ )
477
+ }
478
+
479
+ {/* Resize overlay */}
480
+ {resizingIndex !== null && (
481
+ <div
482
+ className="fixed inset-0 z-40 bg-blue-50/5"
483
+ style={{
484
+ pointerEvents: 'none',
485
+ userSelect: 'none',
486
+ cursor: 'col-resize'
487
+ }}
488
+ />
276
489
  )}
277
490
  </div>
278
491
  );