@papernote/ui 2.0.3 → 2.0.5
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/dist/components/DataTable.d.ts +29 -11
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/FilterBar.d.ts +1 -1
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/index.d.ts +28 -10
- package/dist/index.esm.js +433 -270
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +433 -270
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -0
- package/package.json +1 -1
- package/src/components/DataTable.tsx +993 -647
- package/src/components/FilterBar.tsx +130 -53
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import {
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
MoreVertical,
|
|
7
|
+
Edit,
|
|
8
|
+
Trash,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import Menu, { MenuItem } from "./Menu";
|
|
11
|
+
import Pagination from "./Pagination";
|
|
12
|
+
import Select from "./Select";
|
|
13
|
+
import DataTableCardView, { CardViewConfig } from "./DataTableCardView";
|
|
14
|
+
import { useIsMobile } from "../hooks/useResponsive";
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Base data item interface - all data items must have an id
|
|
13
|
-
*
|
|
18
|
+
*
|
|
14
19
|
* All data passed to DataTable must implement this interface to ensure
|
|
15
20
|
* proper row identification and selection handling.
|
|
16
21
|
*/
|
|
@@ -21,7 +26,7 @@ export interface BaseDataItem {
|
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Column configuration for DataTable
|
|
24
|
-
*
|
|
29
|
+
*
|
|
25
30
|
* Defines how each column should be displayed, including width constraints,
|
|
26
31
|
* custom rendering, sorting behavior, and alignment.
|
|
27
32
|
*/
|
|
@@ -42,26 +47,44 @@ export interface DataTableColumn<T> {
|
|
|
42
47
|
render?: (item: T, value: any) => React.ReactNode;
|
|
43
48
|
/** Secondary line content (smaller, muted text below primary) */
|
|
44
49
|
renderSecondary?: (item: T, value: any) => React.ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Header text for the secondary line. Surfaced as the cell's native
|
|
52
|
+
* tooltip on hover so users can identify the field even though the
|
|
53
|
+
* column header above only labels the primary row.
|
|
54
|
+
*/
|
|
55
|
+
secondaryHeader?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Custom tooltip resolver for the primary cell. Receives item + value;
|
|
58
|
+
* return a plain string. Falls back to `String(value)` when omitted so
|
|
59
|
+
* users hovering a truncated cell can still read the full value.
|
|
60
|
+
*/
|
|
61
|
+
tooltip?: (item: T, value: any) => string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Custom tooltip resolver for the secondary cell. When omitted, the
|
|
64
|
+
* tooltip becomes `${secondaryHeader}: ${value}` so the otherwise-
|
|
65
|
+
* unlabeled second row stays self-describing.
|
|
66
|
+
*/
|
|
67
|
+
secondaryTooltip?: (item: T, value: any) => string | undefined;
|
|
45
68
|
/** Enable sorting for this column */
|
|
46
69
|
sortable?: boolean;
|
|
47
70
|
/** Additional CSS classes for column cells */
|
|
48
71
|
className?: string;
|
|
49
72
|
/** Horizontal text alignment in column */
|
|
50
|
-
align?:
|
|
73
|
+
align?: "left" | "center" | "right";
|
|
51
74
|
/** Vertical alignment of cell content - useful when rows have varying heights */
|
|
52
|
-
verticalAlign?:
|
|
75
|
+
verticalAlign?: "top" | "middle" | "bottom";
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
/**
|
|
56
79
|
* Sort configuration
|
|
57
|
-
*
|
|
80
|
+
*
|
|
58
81
|
* Describes the current sort state for the table.
|
|
59
82
|
*/
|
|
60
83
|
export interface SortConfig {
|
|
61
84
|
/** Column key being sorted */
|
|
62
85
|
key: string;
|
|
63
86
|
/** Sort direction */
|
|
64
|
-
direction:
|
|
87
|
+
direction: "asc" | "desc";
|
|
65
88
|
/** Optional display label for sort indicator */
|
|
66
89
|
label?: string;
|
|
67
90
|
}
|
|
@@ -78,7 +101,7 @@ export interface DataTableAction<T> {
|
|
|
78
101
|
/** Click handler receives the row item */
|
|
79
102
|
onClick: (item: T) => void;
|
|
80
103
|
/** Button styling variant */
|
|
81
|
-
variant?:
|
|
104
|
+
variant?: "primary" | "secondary" | "ghost" | "danger";
|
|
82
105
|
/** Optional conditional visibility */
|
|
83
106
|
show?: (item: T) => boolean;
|
|
84
107
|
/** Optional tooltip text */
|
|
@@ -88,7 +111,7 @@ export interface DataTableAction<T> {
|
|
|
88
111
|
/**
|
|
89
112
|
* Expansion mode types
|
|
90
113
|
*/
|
|
91
|
-
export type ExpansionMode =
|
|
114
|
+
export type ExpansionMode = "edit" | "details" | string; // string allows 'addRelated-[key]' | 'manageRelated-[key]'
|
|
92
115
|
|
|
93
116
|
/**
|
|
94
117
|
* Configuration for different expansion modes
|
|
@@ -96,12 +119,16 @@ export type ExpansionMode = 'edit' | 'details' | string; // string allows 'addRe
|
|
|
96
119
|
export interface ExpandedRowConfig<T> {
|
|
97
120
|
/** Edit mode - inline editing of the record */
|
|
98
121
|
edit?: {
|
|
99
|
-
render: (
|
|
122
|
+
render: (
|
|
123
|
+
item: T,
|
|
124
|
+
onSave: (updated: T) => Promise<void>,
|
|
125
|
+
onCancel: () => void,
|
|
126
|
+
) => React.ReactNode;
|
|
100
127
|
triggerOnDoubleClick?: boolean; // Default: true
|
|
101
128
|
menuLabel?: string; // Default: 'Edit'
|
|
102
129
|
menuIcon?: React.ComponentType<any>;
|
|
103
130
|
};
|
|
104
|
-
|
|
131
|
+
|
|
105
132
|
/** View details mode - read-only expanded view */
|
|
106
133
|
details?: {
|
|
107
134
|
render: (item: T) => React.ReactNode;
|
|
@@ -110,16 +137,20 @@ export interface ExpandedRowConfig<T> {
|
|
|
110
137
|
menuLabel?: string; // Default: 'View Details'
|
|
111
138
|
menuIcon?: React.ComponentType<any>;
|
|
112
139
|
};
|
|
113
|
-
|
|
140
|
+
|
|
114
141
|
/** Add related modes - creating related records */
|
|
115
142
|
addRelated?: Array<{
|
|
116
143
|
key: string; // Unique identifier for this add related mode
|
|
117
144
|
label: string; // e.g., 'Add Line Item', 'Add Contact'
|
|
118
145
|
icon?: React.ComponentType<any>;
|
|
119
|
-
render: (
|
|
146
|
+
render: (
|
|
147
|
+
parentItem: T,
|
|
148
|
+
onSave: (newItem: any) => Promise<void>,
|
|
149
|
+
onCancel: () => void,
|
|
150
|
+
) => React.ReactNode;
|
|
120
151
|
showInMenu?: boolean; // Default: true
|
|
121
152
|
}>;
|
|
122
|
-
|
|
153
|
+
|
|
123
154
|
/** Manage related modes - viewing/editing related records */
|
|
124
155
|
manageRelated?: Array<{
|
|
125
156
|
key: string; // Unique identifier for this manage related mode
|
|
@@ -140,7 +171,7 @@ interface ExpansionState {
|
|
|
140
171
|
|
|
141
172
|
/**
|
|
142
173
|
* DataTable component props
|
|
143
|
-
*
|
|
174
|
+
*
|
|
144
175
|
* Feature-rich data table with sorting, filtering, selection, expansion,
|
|
145
176
|
* row actions, and virtual scrolling support.
|
|
146
177
|
*/
|
|
@@ -196,11 +227,11 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
|
|
|
196
227
|
|
|
197
228
|
// Visual customization props
|
|
198
229
|
/** Enable zebra striping - true for default, 'odd' or 'even' for specific rows */
|
|
199
|
-
striped?: boolean |
|
|
230
|
+
striped?: boolean | "odd" | "even";
|
|
200
231
|
/** Custom color for striped rows (Tailwind class like 'bg-primary-50' or 'bg-accent-50') */
|
|
201
232
|
stripedColor?: string;
|
|
202
233
|
/** Row density - affects padding and text size */
|
|
203
|
-
density?:
|
|
234
|
+
density?: "compact" | "normal" | "comfortable";
|
|
204
235
|
/** Custom class name for rows - static string or function returning class per row */
|
|
205
236
|
rowClassName?: string | ((item: T, index: number) => string);
|
|
206
237
|
/** Conditional row highlighting - returns color class (e.g., 'bg-warning-50') */
|
|
@@ -258,11 +289,11 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
|
|
|
258
289
|
|
|
259
290
|
// Mobile view props
|
|
260
291
|
/** Mobile view mode: 'auto' (detect viewport), 'card' (always cards), 'table' (always table) */
|
|
261
|
-
mobileView?:
|
|
292
|
+
mobileView?: "auto" | "card" | "table";
|
|
262
293
|
/** Configuration for card view layout */
|
|
263
294
|
cardConfig?: CardViewConfig<T>;
|
|
264
295
|
/** Gap between cards in card view */
|
|
265
|
-
cardGap?:
|
|
296
|
+
cardGap?: "sm" | "md" | "lg";
|
|
266
297
|
/** Custom class name for cards */
|
|
267
298
|
cardClassName?: string;
|
|
268
299
|
}
|
|
@@ -315,34 +346,38 @@ function ActionMenu<T>({
|
|
|
315
346
|
useEffect(() => {
|
|
316
347
|
const handleClickOutside = (event: MouseEvent) => {
|
|
317
348
|
if (
|
|
318
|
-
menuRef.current &&
|
|
319
|
-
|
|
349
|
+
menuRef.current &&
|
|
350
|
+
!menuRef.current.contains(event.target as Node) &&
|
|
351
|
+
buttonRef.current &&
|
|
352
|
+
!buttonRef.current.contains(event.target as Node)
|
|
320
353
|
) {
|
|
321
354
|
setIsOpen(false);
|
|
322
355
|
}
|
|
323
356
|
};
|
|
324
357
|
|
|
325
358
|
if (isOpen) {
|
|
326
|
-
document.addEventListener(
|
|
359
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
327
360
|
}
|
|
328
361
|
|
|
329
362
|
return () => {
|
|
330
|
-
document.removeEventListener(
|
|
363
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
331
364
|
};
|
|
332
365
|
}, [isOpen]);
|
|
333
366
|
|
|
334
|
-
const visibleActions = actions.filter(
|
|
367
|
+
const visibleActions = actions.filter(
|
|
368
|
+
(action) => !action.show || action.show(item),
|
|
369
|
+
);
|
|
335
370
|
|
|
336
371
|
if (visibleActions.length === 0) return null;
|
|
337
372
|
|
|
338
373
|
const dropdownContent = isOpen && (
|
|
339
|
-
<div
|
|
374
|
+
<div
|
|
340
375
|
ref={menuRef}
|
|
341
|
-
className="fixed w-56 bg-white rounded-lg shadow-lg border border-paper-300 py-1"
|
|
376
|
+
className="fixed w-56 bg-white rounded-lg shadow-lg border border-paper-300 py-1"
|
|
342
377
|
style={{
|
|
343
378
|
zIndex: 999999,
|
|
344
379
|
top: `${position.top}px`,
|
|
345
|
-
left: `${position.left}px
|
|
380
|
+
left: `${position.left}px`,
|
|
346
381
|
}}
|
|
347
382
|
>
|
|
348
383
|
{visibleActions.map((action, idx) => {
|
|
@@ -351,10 +386,12 @@ function ActionMenu<T>({
|
|
|
351
386
|
if (React.isValidElement(action.icon)) {
|
|
352
387
|
iconElement = action.icon;
|
|
353
388
|
} else {
|
|
354
|
-
iconElement = React.createElement(action.icon as any, {
|
|
389
|
+
iconElement = React.createElement(action.icon as any, {
|
|
390
|
+
className: "h-4 w-4 flex-shrink-0",
|
|
391
|
+
});
|
|
355
392
|
}
|
|
356
393
|
}
|
|
357
|
-
|
|
394
|
+
|
|
358
395
|
return (
|
|
359
396
|
<button
|
|
360
397
|
key={idx}
|
|
@@ -365,14 +402,14 @@ function ActionMenu<T>({
|
|
|
365
402
|
try {
|
|
366
403
|
await action.onClick(item);
|
|
367
404
|
} catch (error) {
|
|
368
|
-
console.error(
|
|
405
|
+
console.error("DataTable action error:", error);
|
|
369
406
|
}
|
|
370
407
|
setIsOpen(false);
|
|
371
408
|
}}
|
|
372
409
|
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
|
373
|
-
action.variant ===
|
|
374
|
-
?
|
|
375
|
-
:
|
|
410
|
+
action.variant === "danger"
|
|
411
|
+
? "text-error-600 hover:bg-error-50 hover:text-error-700"
|
|
412
|
+
: "text-ink-700 hover:bg-paper-50"
|
|
376
413
|
}`}
|
|
377
414
|
title={action.tooltip}
|
|
378
415
|
>
|
|
@@ -406,44 +443,54 @@ function ActionMenu<T>({
|
|
|
406
443
|
/**
|
|
407
444
|
* Helper function to generate column styles from width configuration
|
|
408
445
|
*/
|
|
409
|
-
function getColumnStyle<T>(
|
|
446
|
+
function getColumnStyle<T>(
|
|
447
|
+
column: DataTableColumn<T>,
|
|
448
|
+
dynamicWidth?: number,
|
|
449
|
+
): React.CSSProperties {
|
|
410
450
|
const style: React.CSSProperties = {};
|
|
411
|
-
|
|
451
|
+
|
|
412
452
|
// Use dynamic width if provided (from resizing)
|
|
413
453
|
if (dynamicWidth !== undefined) {
|
|
414
454
|
style.width = `${dynamicWidth}px`;
|
|
415
455
|
} else if (column.width !== undefined) {
|
|
416
|
-
style.width =
|
|
456
|
+
style.width =
|
|
457
|
+
typeof column.width === "number" ? `${column.width}px` : column.width;
|
|
417
458
|
}
|
|
418
|
-
|
|
459
|
+
|
|
419
460
|
if (column.minWidth !== undefined) {
|
|
420
|
-
style.minWidth =
|
|
461
|
+
style.minWidth =
|
|
462
|
+
typeof column.minWidth === "number"
|
|
463
|
+
? `${column.minWidth}px`
|
|
464
|
+
: column.minWidth;
|
|
421
465
|
}
|
|
422
|
-
|
|
466
|
+
|
|
423
467
|
if (column.maxWidth !== undefined) {
|
|
424
|
-
style.maxWidth =
|
|
468
|
+
style.maxWidth =
|
|
469
|
+
typeof column.maxWidth === "number"
|
|
470
|
+
? `${column.maxWidth}px`
|
|
471
|
+
: column.maxWidth;
|
|
425
472
|
}
|
|
426
|
-
|
|
473
|
+
|
|
427
474
|
if (column.flex !== undefined) {
|
|
428
475
|
style.flexGrow = column.flex;
|
|
429
476
|
style.flexShrink = 1;
|
|
430
477
|
style.flexBasis = 0;
|
|
431
478
|
}
|
|
432
|
-
|
|
479
|
+
|
|
433
480
|
if (column.align) {
|
|
434
481
|
style.textAlign = column.align;
|
|
435
482
|
}
|
|
436
|
-
|
|
483
|
+
|
|
437
484
|
if (column.verticalAlign) {
|
|
438
485
|
style.verticalAlign = column.verticalAlign;
|
|
439
486
|
}
|
|
440
|
-
|
|
487
|
+
|
|
441
488
|
return style;
|
|
442
489
|
}
|
|
443
490
|
|
|
444
491
|
/**
|
|
445
492
|
* DataTable - Feature-rich data table component
|
|
446
|
-
*
|
|
493
|
+
*
|
|
447
494
|
* Features:
|
|
448
495
|
* - Column sorting with visual indicators (3-state: asc → desc → none)
|
|
449
496
|
* - Loading states (skeleton + overlay)
|
|
@@ -457,7 +504,7 @@ function getColumnStyle<T>(column: DataTableColumn<T>, dynamicWidth?: number): R
|
|
|
457
504
|
* - Custom cell rendering
|
|
458
505
|
* - Controlled or uncontrolled selection and expansion
|
|
459
506
|
* - Column width configuration (width, minWidth, maxWidth, flex)
|
|
460
|
-
*
|
|
507
|
+
*
|
|
461
508
|
* @example
|
|
462
509
|
* ```tsx
|
|
463
510
|
* const columns: DataTableColumn<User>[] = [
|
|
@@ -465,12 +512,12 @@ function getColumnStyle<T>(column: DataTableColumn<T>, dynamicWidth?: number): R
|
|
|
465
512
|
* { key: 'email', header: 'Email', sortable: true, width: '250px' },
|
|
466
513
|
* { key: 'role', header: 'Role', width: '120px' },
|
|
467
514
|
* ];
|
|
468
|
-
*
|
|
515
|
+
*
|
|
469
516
|
* const actions: DataTableAction<User>[] = [
|
|
470
517
|
* { label: 'Edit', icon: Edit, onClick: handleEdit },
|
|
471
518
|
* { label: 'Delete', icon: Trash, onClick: handleDelete, variant: 'danger' },
|
|
472
519
|
* ];
|
|
473
|
-
*
|
|
520
|
+
*
|
|
474
521
|
* <DataTable
|
|
475
522
|
* data={users}
|
|
476
523
|
* columns={columns}
|
|
@@ -490,9 +537,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
490
537
|
columns,
|
|
491
538
|
loading = false,
|
|
492
539
|
error = null,
|
|
493
|
-
emptyMessage =
|
|
540
|
+
emptyMessage = "No data available",
|
|
494
541
|
loadingRows = 5,
|
|
495
|
-
className =
|
|
542
|
+
className = "",
|
|
496
543
|
onSortChange,
|
|
497
544
|
currentSort = null,
|
|
498
545
|
onEdit,
|
|
@@ -513,24 +560,24 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
513
560
|
// Visual customization props
|
|
514
561
|
striped = false,
|
|
515
562
|
stripedColor,
|
|
516
|
-
density =
|
|
563
|
+
density = "normal",
|
|
517
564
|
rowClassName,
|
|
518
565
|
rowHighlight,
|
|
519
566
|
highlightedRowId,
|
|
520
567
|
highlightedRows = [],
|
|
521
568
|
highlightDuration = 2000,
|
|
522
569
|
bordered = false,
|
|
523
|
-
borderColor =
|
|
570
|
+
borderColor = "border-paper-200",
|
|
524
571
|
disableHover = false,
|
|
525
572
|
hiddenColumns = [],
|
|
526
|
-
headerClassName =
|
|
573
|
+
headerClassName = "",
|
|
527
574
|
renderEmptyState: customRenderEmptyState,
|
|
528
575
|
resizable = false,
|
|
529
576
|
onColumnResize,
|
|
530
577
|
reorderable = false,
|
|
531
578
|
onColumnReorder,
|
|
532
579
|
virtualized = false,
|
|
533
|
-
virtualHeight =
|
|
580
|
+
virtualHeight = "600px",
|
|
534
581
|
virtualRowHeight = 60,
|
|
535
582
|
// Pagination props
|
|
536
583
|
paginated = false,
|
|
@@ -542,9 +589,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
542
589
|
onPageSizeChange,
|
|
543
590
|
showPageSizeSelector = true,
|
|
544
591
|
// Mobile view props
|
|
545
|
-
mobileView =
|
|
592
|
+
mobileView = "auto",
|
|
546
593
|
cardConfig,
|
|
547
|
-
cardGap =
|
|
594
|
+
cardGap = "md",
|
|
548
595
|
cardClassName,
|
|
549
596
|
}: DataTableProps<T>) {
|
|
550
597
|
// Mobile detection for auto mode
|
|
@@ -568,8 +615,11 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
568
615
|
const [hoveredRowKey, setHoveredRowKey] = useState<string | null>(null);
|
|
569
616
|
|
|
570
617
|
// Keyboard navigation state
|
|
571
|
-
const [focusedCell, setFocusedCell] = useState<{
|
|
572
|
-
|
|
618
|
+
const [focusedCell, setFocusedCell] = useState<{
|
|
619
|
+
row: number;
|
|
620
|
+
col: number;
|
|
621
|
+
} | null>(null);
|
|
622
|
+
const [announcement, setAnnouncement] = useState<string>("");
|
|
573
623
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
574
624
|
|
|
575
625
|
// Temporary row highlight state (for flash animation)
|
|
@@ -589,13 +639,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
589
639
|
|
|
590
640
|
// Filter columns based on hiddenColumns
|
|
591
641
|
const baseVisibleColumns = columns.filter(
|
|
592
|
-
col => !hiddenColumns.includes(String(col.key))
|
|
642
|
+
(col) => !hiddenColumns.includes(String(col.key)),
|
|
593
643
|
);
|
|
594
644
|
|
|
595
645
|
// Initialize column order on mount or when columns change
|
|
596
646
|
useEffect(() => {
|
|
597
647
|
if (columnOrder.length === 0) {
|
|
598
|
-
setColumnOrder(baseVisibleColumns.map(col => String(col.key)));
|
|
648
|
+
setColumnOrder(baseVisibleColumns.map((col) => String(col.key)));
|
|
599
649
|
}
|
|
600
650
|
}, [baseVisibleColumns, columnOrder.length]);
|
|
601
651
|
|
|
@@ -603,7 +653,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
603
653
|
useEffect(() => {
|
|
604
654
|
if (highlightedRows.length > 0) {
|
|
605
655
|
// Add new highlighted rows to flashing set
|
|
606
|
-
const newFlashingRows = new Set(highlightedRows.map(id => String(id)));
|
|
656
|
+
const newFlashingRows = new Set(highlightedRows.map((id) => String(id)));
|
|
607
657
|
setFlashingRows(newFlashingRows);
|
|
608
658
|
|
|
609
659
|
// Clear any existing timeout
|
|
@@ -625,28 +675,31 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
625
675
|
}, [highlightedRows, highlightDuration]);
|
|
626
676
|
|
|
627
677
|
// Apply column order
|
|
628
|
-
const visibleColumns =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
678
|
+
const visibleColumns =
|
|
679
|
+
reorderable && columnOrder.length > 0
|
|
680
|
+
? columnOrder
|
|
681
|
+
.map((key) =>
|
|
682
|
+
baseVisibleColumns.find((col) => String(col.key) === key),
|
|
683
|
+
)
|
|
684
|
+
.filter((col): col is DataTableColumn<T> => col !== undefined)
|
|
685
|
+
: baseVisibleColumns;
|
|
633
686
|
|
|
634
687
|
// Density classes
|
|
635
688
|
const densityClasses = {
|
|
636
689
|
compact: {
|
|
637
|
-
cell:
|
|
638
|
-
text:
|
|
639
|
-
header:
|
|
690
|
+
cell: "px-3 py-1",
|
|
691
|
+
text: "text-xs",
|
|
692
|
+
header: "px-3 py-2",
|
|
640
693
|
},
|
|
641
694
|
normal: {
|
|
642
|
-
cell:
|
|
643
|
-
text:
|
|
644
|
-
header:
|
|
695
|
+
cell: "px-6 py-1.5",
|
|
696
|
+
text: "text-sm",
|
|
697
|
+
header: "px-6 py-3",
|
|
645
698
|
},
|
|
646
699
|
comfortable: {
|
|
647
|
-
cell:
|
|
648
|
-
text:
|
|
649
|
-
header:
|
|
700
|
+
cell: "px-6 py-3",
|
|
701
|
+
text: "text-base",
|
|
702
|
+
header: "px-6 py-4",
|
|
650
703
|
},
|
|
651
704
|
};
|
|
652
705
|
|
|
@@ -657,9 +710,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
657
710
|
|
|
658
711
|
// Calculate if there are any actions (for keyboard navigation column calculation)
|
|
659
712
|
// This is computed early so it can be used in keyboard handlers
|
|
660
|
-
const hasAnyActions = !!(
|
|
661
|
-
|
|
662
|
-
|
|
713
|
+
const hasAnyActions = !!(
|
|
714
|
+
onEdit ||
|
|
715
|
+
onDelete ||
|
|
716
|
+
actions.length > 0 ||
|
|
717
|
+
expandedRowConfig?.edit ||
|
|
718
|
+
expandedRowConfig?.details ||
|
|
719
|
+
expandedRowConfig?.addRelated?.length ||
|
|
720
|
+
expandedRowConfig?.manageRelated?.length
|
|
721
|
+
);
|
|
663
722
|
|
|
664
723
|
// Get row background class based on striping and highlighting
|
|
665
724
|
const getRowBackgroundClass = (item: T, index: number): string => {
|
|
@@ -668,11 +727,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
668
727
|
|
|
669
728
|
// Check for temporary flash highlight (takes priority)
|
|
670
729
|
if (flashingRows.has(rowKey)) {
|
|
671
|
-
classes.push(
|
|
730
|
+
classes.push("animate-row-flash");
|
|
672
731
|
}
|
|
673
732
|
// Check for highlighted row
|
|
674
|
-
else if (
|
|
675
|
-
|
|
733
|
+
else if (
|
|
734
|
+
highlightedRowId !== undefined &&
|
|
735
|
+
rowKey === String(highlightedRowId)
|
|
736
|
+
) {
|
|
737
|
+
classes.push("bg-accent-100");
|
|
676
738
|
}
|
|
677
739
|
// Check for custom row highlight
|
|
678
740
|
else if (rowHighlight) {
|
|
@@ -685,32 +747,41 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
685
747
|
else if (striped) {
|
|
686
748
|
const isOdd = index % 2 === 0; // 0-indexed, so even index = odd row
|
|
687
749
|
const shouldStripe =
|
|
688
|
-
striped === true
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
750
|
+
striped === true
|
|
751
|
+
? isOdd
|
|
752
|
+
: striped === "odd"
|
|
753
|
+
? isOdd
|
|
754
|
+
: striped === "even"
|
|
755
|
+
? !isOdd
|
|
756
|
+
: false;
|
|
692
757
|
|
|
693
758
|
if (shouldStripe) {
|
|
694
|
-
classes.push(stripedColor ||
|
|
759
|
+
classes.push(stripedColor || "bg-paper-50");
|
|
695
760
|
}
|
|
696
761
|
}
|
|
697
762
|
|
|
698
763
|
// Add custom row class
|
|
699
764
|
if (rowClassName) {
|
|
700
|
-
if (typeof rowClassName ===
|
|
765
|
+
if (typeof rowClassName === "string") {
|
|
701
766
|
classes.push(rowClassName);
|
|
702
767
|
} else {
|
|
703
768
|
classes.push(rowClassName(item, index));
|
|
704
769
|
}
|
|
705
770
|
}
|
|
706
771
|
|
|
707
|
-
return classes.join(
|
|
772
|
+
return classes.join(" ");
|
|
708
773
|
};
|
|
709
774
|
// NEW: Expansion mode state management (for expandedRowConfig)
|
|
710
|
-
const [expansionState, setExpansionState] = useState<ExpansionState | null>(
|
|
775
|
+
const [expansionState, setExpansionState] = useState<ExpansionState | null>(
|
|
776
|
+
null,
|
|
777
|
+
);
|
|
711
778
|
|
|
712
779
|
// Column resize handlers
|
|
713
|
-
const handleResizeStart = (
|
|
780
|
+
const handleResizeStart = (
|
|
781
|
+
e: React.MouseEvent,
|
|
782
|
+
columnKey: string,
|
|
783
|
+
currentWidth: number,
|
|
784
|
+
) => {
|
|
714
785
|
e.preventDefault();
|
|
715
786
|
e.stopPropagation();
|
|
716
787
|
setResizingColumn(columnKey);
|
|
@@ -721,13 +792,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
721
792
|
// Column reorder handlers
|
|
722
793
|
const handleDragStart = (e: React.DragEvent, columnKey: string) => {
|
|
723
794
|
setDraggingColumn(columnKey);
|
|
724
|
-
e.dataTransfer.effectAllowed =
|
|
725
|
-
e.dataTransfer.setData(
|
|
795
|
+
e.dataTransfer.effectAllowed = "move";
|
|
796
|
+
e.dataTransfer.setData("text/html", columnKey);
|
|
726
797
|
};
|
|
727
798
|
|
|
728
799
|
const handleDragOver = (e: React.DragEvent, columnKey: string) => {
|
|
729
800
|
e.preventDefault();
|
|
730
|
-
e.dataTransfer.dropEffect =
|
|
801
|
+
e.dataTransfer.dropEffect = "move";
|
|
731
802
|
if (draggingColumn && draggingColumn !== columnKey) {
|
|
732
803
|
setDragOverColumn(columnKey);
|
|
733
804
|
}
|
|
@@ -763,7 +834,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
763
834
|
const handleMouseMove = (e: MouseEvent) => {
|
|
764
835
|
const delta = e.clientX - resizeStartX;
|
|
765
836
|
const newWidth = Math.max(50, resizeStartWidth + delta); // Min width 50px
|
|
766
|
-
setColumnWidths(prev => ({
|
|
837
|
+
setColumnWidths((prev) => ({
|
|
767
838
|
...prev,
|
|
768
839
|
[resizingColumn]: newWidth,
|
|
769
840
|
}));
|
|
@@ -776,62 +847,68 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
776
847
|
setResizingColumn(null);
|
|
777
848
|
};
|
|
778
849
|
|
|
779
|
-
document.addEventListener(
|
|
780
|
-
document.addEventListener(
|
|
850
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
851
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
781
852
|
|
|
782
853
|
return () => {
|
|
783
|
-
document.removeEventListener(
|
|
784
|
-
document.removeEventListener(
|
|
854
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
855
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
785
856
|
};
|
|
786
|
-
}, [
|
|
857
|
+
}, [
|
|
858
|
+
resizingColumn,
|
|
859
|
+
resizeStartX,
|
|
860
|
+
resizeStartWidth,
|
|
861
|
+
columnWidths,
|
|
862
|
+
onColumnResize,
|
|
863
|
+
]);
|
|
787
864
|
|
|
788
865
|
// Build combined actions: built-in edit/delete + custom actions + expansion mode actions
|
|
789
866
|
const builtInActions: DataTableAction<T>[] = [];
|
|
790
|
-
|
|
867
|
+
|
|
791
868
|
// Legacy onEdit (still supported)
|
|
792
869
|
if (onEdit) {
|
|
793
870
|
builtInActions.push({
|
|
794
|
-
label:
|
|
871
|
+
label: "Edit",
|
|
795
872
|
icon: Edit,
|
|
796
873
|
onClick: onEdit,
|
|
797
|
-
variant:
|
|
798
|
-
tooltip:
|
|
874
|
+
variant: "secondary",
|
|
875
|
+
tooltip: "Edit item",
|
|
799
876
|
});
|
|
800
877
|
}
|
|
801
|
-
|
|
878
|
+
|
|
802
879
|
// NEW: Edit mode from expandedRowConfig
|
|
803
880
|
if (expandedRowConfig?.edit && !onEdit) {
|
|
804
881
|
const editConfig = expandedRowConfig.edit;
|
|
805
882
|
builtInActions.push({
|
|
806
|
-
label: editConfig.menuLabel ||
|
|
883
|
+
label: editConfig.menuLabel || "Edit",
|
|
807
884
|
icon: editConfig.menuIcon || Edit,
|
|
808
885
|
onClick: (item: T) => {
|
|
809
886
|
const rowKey = getRowKey(item);
|
|
810
|
-
handleExpansionWithMode(rowKey,
|
|
887
|
+
handleExpansionWithMode(rowKey, "edit");
|
|
811
888
|
},
|
|
812
|
-
variant:
|
|
813
|
-
tooltip:
|
|
889
|
+
variant: "secondary",
|
|
890
|
+
tooltip: "Edit inline",
|
|
814
891
|
});
|
|
815
892
|
}
|
|
816
|
-
|
|
893
|
+
|
|
817
894
|
// NEW: View details mode from expandedRowConfig
|
|
818
895
|
if (expandedRowConfig?.details) {
|
|
819
896
|
const detailsConfig = expandedRowConfig.details;
|
|
820
897
|
builtInActions.push({
|
|
821
|
-
label: detailsConfig.menuLabel ||
|
|
898
|
+
label: detailsConfig.menuLabel || "View Details",
|
|
822
899
|
icon: detailsConfig.menuIcon,
|
|
823
900
|
onClick: (item: T) => {
|
|
824
901
|
const rowKey = getRowKey(item);
|
|
825
|
-
handleExpansionWithMode(rowKey,
|
|
902
|
+
handleExpansionWithMode(rowKey, "details");
|
|
826
903
|
},
|
|
827
|
-
variant:
|
|
828
|
-
tooltip:
|
|
904
|
+
variant: "ghost",
|
|
905
|
+
tooltip: "View details",
|
|
829
906
|
});
|
|
830
907
|
}
|
|
831
|
-
|
|
908
|
+
|
|
832
909
|
// NEW: Add related modes from expandedRowConfig
|
|
833
910
|
if (expandedRowConfig?.addRelated) {
|
|
834
|
-
expandedRowConfig.addRelated.forEach(config => {
|
|
911
|
+
expandedRowConfig.addRelated.forEach((config) => {
|
|
835
912
|
if (config.showInMenu !== false) {
|
|
836
913
|
builtInActions.push({
|
|
837
914
|
label: config.label,
|
|
@@ -840,16 +917,16 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
840
917
|
const rowKey = getRowKey(item);
|
|
841
918
|
handleExpansionWithMode(rowKey, `addRelated-${config.key}`);
|
|
842
919
|
},
|
|
843
|
-
variant:
|
|
844
|
-
tooltip: config.label
|
|
920
|
+
variant: "secondary",
|
|
921
|
+
tooltip: config.label,
|
|
845
922
|
});
|
|
846
923
|
}
|
|
847
924
|
});
|
|
848
925
|
}
|
|
849
|
-
|
|
926
|
+
|
|
850
927
|
// NEW: Manage related modes from expandedRowConfig
|
|
851
928
|
if (expandedRowConfig?.manageRelated) {
|
|
852
|
-
expandedRowConfig.manageRelated.forEach(config => {
|
|
929
|
+
expandedRowConfig.manageRelated.forEach((config) => {
|
|
853
930
|
if (config.showInMenu !== false) {
|
|
854
931
|
builtInActions.push({
|
|
855
932
|
label: config.label,
|
|
@@ -858,26 +935,26 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
858
935
|
const rowKey = getRowKey(item);
|
|
859
936
|
handleExpansionWithMode(rowKey, `manageRelated-${config.key}`);
|
|
860
937
|
},
|
|
861
|
-
variant:
|
|
862
|
-
tooltip: config.label
|
|
938
|
+
variant: "ghost",
|
|
939
|
+
tooltip: config.label,
|
|
863
940
|
});
|
|
864
941
|
}
|
|
865
942
|
});
|
|
866
943
|
}
|
|
867
|
-
|
|
944
|
+
|
|
868
945
|
// Combine all actions: built-in first, then custom actions, then delete last
|
|
869
946
|
// Delete is stored separately to ensure it's always last
|
|
870
947
|
let deleteAction: DataTableAction<T> | null = null;
|
|
871
948
|
if (onDelete) {
|
|
872
949
|
deleteAction = {
|
|
873
|
-
label:
|
|
950
|
+
label: "Delete",
|
|
874
951
|
icon: Trash,
|
|
875
952
|
onClick: onDelete,
|
|
876
|
-
variant:
|
|
877
|
-
tooltip:
|
|
953
|
+
variant: "danger",
|
|
954
|
+
tooltip: "Delete item",
|
|
878
955
|
};
|
|
879
956
|
}
|
|
880
|
-
|
|
957
|
+
|
|
881
958
|
// Build final actions array with consistent ordering:
|
|
882
959
|
// 1. Edit (first - most common action)
|
|
883
960
|
// 2. View Details
|
|
@@ -888,12 +965,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
888
965
|
const allActions: DataTableAction<T>[] = [
|
|
889
966
|
...builtInActions,
|
|
890
967
|
...actions,
|
|
891
|
-
...(deleteAction ? [deleteAction] : [])
|
|
968
|
+
...(deleteAction ? [deleteAction] : []),
|
|
892
969
|
];
|
|
893
970
|
|
|
894
971
|
// Convert actions to menu items for context menu
|
|
895
972
|
const convertActionsToMenuItems = (item: T): MenuItem[] => {
|
|
896
|
-
const visibleActions = allActions.filter(
|
|
973
|
+
const visibleActions = allActions.filter(
|
|
974
|
+
(action) => !action.show || action.show(item),
|
|
975
|
+
);
|
|
897
976
|
|
|
898
977
|
return visibleActions.map((action, idx) => {
|
|
899
978
|
let iconElement: React.ReactNode = null;
|
|
@@ -901,7 +980,9 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
901
980
|
if (React.isValidElement(action.icon)) {
|
|
902
981
|
iconElement = action.icon;
|
|
903
982
|
} else {
|
|
904
|
-
iconElement = React.createElement(action.icon as any, {
|
|
983
|
+
iconElement = React.createElement(action.icon as any, {
|
|
984
|
+
className: "h-4 w-4",
|
|
985
|
+
});
|
|
905
986
|
}
|
|
906
987
|
}
|
|
907
988
|
|
|
@@ -910,19 +991,26 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
910
991
|
label: action.label,
|
|
911
992
|
icon: iconElement,
|
|
912
993
|
onClick: () => action.onClick(item),
|
|
913
|
-
danger: action.variant ===
|
|
994
|
+
danger: action.variant === "danger",
|
|
914
995
|
};
|
|
915
996
|
});
|
|
916
997
|
};
|
|
917
998
|
|
|
918
999
|
// Selection state management
|
|
919
|
-
const [internalSelectedRows, setInternalSelectedRows] = useState<Set<string>>(
|
|
920
|
-
|
|
1000
|
+
const [internalSelectedRows, setInternalSelectedRows] = useState<Set<string>>(
|
|
1001
|
+
new Set(),
|
|
1002
|
+
);
|
|
1003
|
+
|
|
921
1004
|
// Expansion state management
|
|
922
|
-
const [internalExpandedRows, setInternalExpandedRows] = useState<Set<string>>(
|
|
923
|
-
|
|
1005
|
+
const [internalExpandedRows, setInternalExpandedRows] = useState<Set<string>>(
|
|
1006
|
+
new Set(),
|
|
1007
|
+
);
|
|
1008
|
+
|
|
924
1009
|
// Use external selection if provided, otherwise internal
|
|
925
|
-
const selectedRowsSet =
|
|
1010
|
+
const selectedRowsSet =
|
|
1011
|
+
externalSelectedRows !== undefined
|
|
1012
|
+
? externalSelectedRows
|
|
1013
|
+
: internalSelectedRows;
|
|
926
1014
|
const setSelectedRows = (newSet: Set<string>) => {
|
|
927
1015
|
if (externalSelectedRows !== undefined) {
|
|
928
1016
|
// Controlled component - notify parent
|
|
@@ -944,7 +1032,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
944
1032
|
}
|
|
945
1033
|
setSelectedRows(newSelected);
|
|
946
1034
|
};
|
|
947
|
-
|
|
1035
|
+
|
|
948
1036
|
// Handle select all
|
|
949
1037
|
const handleSelectAll = () => {
|
|
950
1038
|
if (selectedRowsSet.size === data.length && data.length > 0) {
|
|
@@ -954,9 +1042,12 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
954
1042
|
setSelectedRows(allKeys);
|
|
955
1043
|
}
|
|
956
1044
|
};
|
|
957
|
-
|
|
1045
|
+
|
|
958
1046
|
// Use external expansion if provided, otherwise internal
|
|
959
|
-
const expandedRowsSet =
|
|
1047
|
+
const expandedRowsSet =
|
|
1048
|
+
externalExpandedRows !== undefined
|
|
1049
|
+
? externalExpandedRows
|
|
1050
|
+
: internalExpandedRows;
|
|
960
1051
|
const setExpandedRows = (newSet: Set<string>) => {
|
|
961
1052
|
if (externalExpandedRows !== undefined) {
|
|
962
1053
|
// Controlled component - parent manages state
|
|
@@ -966,7 +1057,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
966
1057
|
setInternalExpandedRows(newSet);
|
|
967
1058
|
}
|
|
968
1059
|
};
|
|
969
|
-
|
|
1060
|
+
|
|
970
1061
|
// Handle row expansion
|
|
971
1062
|
const handleRowExpand = (rowKey: string) => {
|
|
972
1063
|
const newExpanded = new Set(expandedRowsSet);
|
|
@@ -995,219 +1086,272 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
995
1086
|
};
|
|
996
1087
|
|
|
997
1088
|
// Keyboard navigation handler
|
|
998
|
-
const handleKeyboardNavigation = useCallback(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1089
|
+
const handleKeyboardNavigation = useCallback(
|
|
1090
|
+
(e: React.KeyboardEvent<HTMLTableSectionElement>) => {
|
|
1091
|
+
if (!data.length) return;
|
|
1092
|
+
|
|
1093
|
+
const totalRows = data.length;
|
|
1094
|
+
const totalCols = visibleColumns.length;
|
|
1095
|
+
|
|
1096
|
+
// If no cell is focused, focus first data cell on first arrow key
|
|
1097
|
+
if (!focusedCell) {
|
|
1098
|
+
if (
|
|
1099
|
+
["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(e.key)
|
|
1100
|
+
) {
|
|
1101
|
+
e.preventDefault();
|
|
1102
|
+
setFocusedCell({ row: 0, col: 0 });
|
|
1103
|
+
const colHeader = visibleColumns[0]?.header || "first column";
|
|
1104
|
+
setAnnouncement(`Row 1, ${colHeader}`);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1011
1107
|
return;
|
|
1012
1108
|
}
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
1109
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
switch (e.key) {
|
|
1019
|
-
case 'ArrowDown':
|
|
1020
|
-
e.preventDefault();
|
|
1021
|
-
if (row < totalRows - 1) {
|
|
1022
|
-
const newRow = row + 1;
|
|
1023
|
-
setFocusedCell({ row: newRow, col });
|
|
1024
|
-
const rowItem = data[newRow];
|
|
1025
|
-
const colHeader = visibleColumns[col]?.header || '';
|
|
1026
|
-
const cellValue = rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
|
|
1027
|
-
setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
|
|
1028
|
-
}
|
|
1029
|
-
break;
|
|
1030
|
-
|
|
1031
|
-
case 'ArrowUp':
|
|
1032
|
-
e.preventDefault();
|
|
1033
|
-
if (row > 0) {
|
|
1034
|
-
const newRow = row - 1;
|
|
1035
|
-
setFocusedCell({ row: newRow, col });
|
|
1036
|
-
const rowItem = data[newRow];
|
|
1037
|
-
const colHeader = visibleColumns[col]?.header || '';
|
|
1038
|
-
const cellValue = rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
|
|
1039
|
-
setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
|
|
1040
|
-
}
|
|
1041
|
-
break;
|
|
1042
|
-
|
|
1043
|
-
case 'ArrowRight':
|
|
1044
|
-
e.preventDefault();
|
|
1045
|
-
if (col < totalCols - 1) {
|
|
1046
|
-
const newCol = col + 1;
|
|
1047
|
-
setFocusedCell({ row, col: newCol });
|
|
1048
|
-
const rowItem = data[row];
|
|
1049
|
-
const colHeader = visibleColumns[newCol]?.header || '';
|
|
1050
|
-
const cellValue = rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
|
|
1051
|
-
setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
|
|
1052
|
-
}
|
|
1053
|
-
break;
|
|
1054
|
-
|
|
1055
|
-
case 'ArrowLeft':
|
|
1056
|
-
e.preventDefault();
|
|
1057
|
-
if (col > 0) {
|
|
1058
|
-
const newCol = col - 1;
|
|
1059
|
-
setFocusedCell({ row, col: newCol });
|
|
1060
|
-
const rowItem = data[row];
|
|
1061
|
-
const colHeader = visibleColumns[newCol]?.header || '';
|
|
1062
|
-
const cellValue = rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
|
|
1063
|
-
setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
|
|
1064
|
-
}
|
|
1065
|
-
break;
|
|
1110
|
+
const { row, col } = focusedCell;
|
|
1066
1111
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
break;
|
|
1081
|
-
|
|
1082
|
-
case 'End':
|
|
1083
|
-
e.preventDefault();
|
|
1084
|
-
if (e.ctrlKey) {
|
|
1085
|
-
// Ctrl+End: Go to last cell
|
|
1086
|
-
const lastRow = totalRows - 1;
|
|
1087
|
-
const lastCol = totalCols - 1;
|
|
1088
|
-
setFocusedCell({ row: lastRow, col: lastCol });
|
|
1089
|
-
setAnnouncement(`Last cell, Row ${lastRow + 1}, ${visibleColumns[lastCol]?.header || ''}`);
|
|
1090
|
-
} else {
|
|
1091
|
-
// End: Go to last cell in current row
|
|
1092
|
-
const lastCol = totalCols - 1;
|
|
1093
|
-
setFocusedCell({ row, col: lastCol });
|
|
1094
|
-
const rowItem = data[row];
|
|
1095
|
-
const cellValue = rowItem[visibleColumns[lastCol]?.key as keyof typeof rowItem];
|
|
1096
|
-
setAnnouncement(`${visibleColumns[lastCol]?.header || ''}: ${cellValue || 'empty'}`);
|
|
1097
|
-
}
|
|
1098
|
-
break;
|
|
1099
|
-
|
|
1100
|
-
case 'Enter':
|
|
1101
|
-
e.preventDefault();
|
|
1102
|
-
{
|
|
1103
|
-
const rowItem = data[row];
|
|
1104
|
-
const rowKey = getRowKey(rowItem);
|
|
1105
|
-
|
|
1106
|
-
// Priority: Edit mode > Details mode > Row double-click handler
|
|
1107
|
-
if (onEdit) {
|
|
1108
|
-
onEdit(rowItem);
|
|
1109
|
-
setAnnouncement('Opening edit mode');
|
|
1110
|
-
} else if (expandedRowConfig?.edit) {
|
|
1111
|
-
handleExpansionWithMode(rowKey, 'edit');
|
|
1112
|
-
setAnnouncement('Opening inline edit');
|
|
1113
|
-
} else if (expandedRowConfig?.details) {
|
|
1114
|
-
handleExpansionWithMode(rowKey, 'details');
|
|
1115
|
-
setAnnouncement('Opening details view');
|
|
1116
|
-
} else if (onRowDoubleClick) {
|
|
1117
|
-
onRowDoubleClick(rowItem);
|
|
1118
|
-
setAnnouncement('Activating row');
|
|
1119
|
-
} else if (onRowClick) {
|
|
1120
|
-
onRowClick(rowItem);
|
|
1121
|
-
setAnnouncement('Row selected');
|
|
1112
|
+
switch (e.key) {
|
|
1113
|
+
case "ArrowDown":
|
|
1114
|
+
e.preventDefault();
|
|
1115
|
+
if (row < totalRows - 1) {
|
|
1116
|
+
const newRow = row + 1;
|
|
1117
|
+
setFocusedCell({ row: newRow, col });
|
|
1118
|
+
const rowItem = data[newRow];
|
|
1119
|
+
const colHeader = visibleColumns[col]?.header || "";
|
|
1120
|
+
const cellValue =
|
|
1121
|
+
rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
|
|
1122
|
+
setAnnouncement(
|
|
1123
|
+
`Row ${newRow + 1}, ${colHeader}: ${cellValue || "empty"}`,
|
|
1124
|
+
);
|
|
1122
1125
|
}
|
|
1123
|
-
|
|
1124
|
-
break;
|
|
1126
|
+
break;
|
|
1125
1127
|
|
|
1126
|
-
|
|
1127
|
-
// Space: Toggle selection if selectable
|
|
1128
|
-
if (selectable) {
|
|
1128
|
+
case "ArrowUp":
|
|
1129
1129
|
e.preventDefault();
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1130
|
+
if (row > 0) {
|
|
1131
|
+
const newRow = row - 1;
|
|
1132
|
+
setFocusedCell({ row: newRow, col });
|
|
1133
|
+
const rowItem = data[newRow];
|
|
1134
|
+
const colHeader = visibleColumns[col]?.header || "";
|
|
1135
|
+
const cellValue =
|
|
1136
|
+
rowItem[visibleColumns[col]?.key as keyof typeof rowItem];
|
|
1137
|
+
setAnnouncement(
|
|
1138
|
+
`Row ${newRow + 1}, ${colHeader}: ${cellValue || "empty"}`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
break;
|
|
1142
|
+
|
|
1143
|
+
case "ArrowRight":
|
|
1144
|
+
e.preventDefault();
|
|
1145
|
+
if (col < totalCols - 1) {
|
|
1146
|
+
const newCol = col + 1;
|
|
1147
|
+
setFocusedCell({ row, col: newCol });
|
|
1148
|
+
const rowItem = data[row];
|
|
1149
|
+
const colHeader = visibleColumns[newCol]?.header || "";
|
|
1150
|
+
const cellValue =
|
|
1151
|
+
rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
|
|
1152
|
+
setAnnouncement(`${colHeader}: ${cellValue || "empty"}`);
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
|
|
1156
|
+
case "ArrowLeft":
|
|
1157
|
+
e.preventDefault();
|
|
1158
|
+
if (col > 0) {
|
|
1159
|
+
const newCol = col - 1;
|
|
1160
|
+
setFocusedCell({ row, col: newCol });
|
|
1161
|
+
const rowItem = data[row];
|
|
1162
|
+
const colHeader = visibleColumns[newCol]?.header || "";
|
|
1163
|
+
const cellValue =
|
|
1164
|
+
rowItem[visibleColumns[newCol]?.key as keyof typeof rowItem];
|
|
1165
|
+
setAnnouncement(`${colHeader}: ${cellValue || "empty"}`);
|
|
1166
|
+
}
|
|
1167
|
+
break;
|
|
1168
|
+
|
|
1169
|
+
case "Home":
|
|
1170
|
+
e.preventDefault();
|
|
1171
|
+
if (e.ctrlKey) {
|
|
1172
|
+
// Ctrl+Home: Go to first cell
|
|
1173
|
+
setFocusedCell({ row: 0, col: 0 });
|
|
1174
|
+
setAnnouncement(
|
|
1175
|
+
`First cell, Row 1, ${visibleColumns[0]?.header || ""}`,
|
|
1176
|
+
);
|
|
1177
|
+
} else {
|
|
1178
|
+
// Home: Go to first cell in current row
|
|
1179
|
+
setFocusedCell({ row, col: 0 });
|
|
1180
|
+
const rowItem = data[row];
|
|
1181
|
+
const cellValue =
|
|
1182
|
+
rowItem[visibleColumns[0]?.key as keyof typeof rowItem];
|
|
1183
|
+
setAnnouncement(
|
|
1184
|
+
`${visibleColumns[0]?.header || ""}: ${cellValue || "empty"}`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
|
|
1189
|
+
case "End":
|
|
1190
|
+
e.preventDefault();
|
|
1191
|
+
if (e.ctrlKey) {
|
|
1192
|
+
// Ctrl+End: Go to last cell
|
|
1193
|
+
const lastRow = totalRows - 1;
|
|
1194
|
+
const lastCol = totalCols - 1;
|
|
1195
|
+
setFocusedCell({ row: lastRow, col: lastCol });
|
|
1196
|
+
setAnnouncement(
|
|
1197
|
+
`Last cell, Row ${lastRow + 1}, ${visibleColumns[lastCol]?.header || ""}`,
|
|
1198
|
+
);
|
|
1199
|
+
} else {
|
|
1200
|
+
// End: Go to last cell in current row
|
|
1201
|
+
const lastCol = totalCols - 1;
|
|
1202
|
+
setFocusedCell({ row, col: lastCol });
|
|
1203
|
+
const rowItem = data[row];
|
|
1204
|
+
const cellValue =
|
|
1205
|
+
rowItem[visibleColumns[lastCol]?.key as keyof typeof rowItem];
|
|
1206
|
+
setAnnouncement(
|
|
1207
|
+
`${visibleColumns[lastCol]?.header || ""}: ${cellValue || "empty"}`,
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
break;
|
|
1211
|
+
|
|
1212
|
+
case "Enter":
|
|
1213
|
+
e.preventDefault();
|
|
1214
|
+
{
|
|
1215
|
+
const rowItem = data[row];
|
|
1216
|
+
const rowKey = getRowKey(rowItem);
|
|
1217
|
+
|
|
1218
|
+
// Priority: Edit mode > Details mode > Row double-click handler
|
|
1219
|
+
if (onEdit) {
|
|
1220
|
+
onEdit(rowItem);
|
|
1221
|
+
setAnnouncement("Opening edit mode");
|
|
1222
|
+
} else if (expandedRowConfig?.edit) {
|
|
1223
|
+
handleExpansionWithMode(rowKey, "edit");
|
|
1224
|
+
setAnnouncement("Opening inline edit");
|
|
1225
|
+
} else if (expandedRowConfig?.details) {
|
|
1226
|
+
handleExpansionWithMode(rowKey, "details");
|
|
1227
|
+
setAnnouncement("Opening details view");
|
|
1228
|
+
} else if (onRowDoubleClick) {
|
|
1229
|
+
onRowDoubleClick(rowItem);
|
|
1230
|
+
setAnnouncement("Activating row");
|
|
1231
|
+
} else if (onRowClick) {
|
|
1232
|
+
onRowClick(rowItem);
|
|
1233
|
+
setAnnouncement("Row selected");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
|
|
1238
|
+
case " ":
|
|
1239
|
+
// Space: Toggle selection if selectable
|
|
1240
|
+
if (selectable) {
|
|
1241
|
+
e.preventDefault();
|
|
1242
|
+
const rowItem = data[row];
|
|
1243
|
+
const rowKey = getRowKey(rowItem);
|
|
1244
|
+
handleRowSelect(rowKey);
|
|
1245
|
+
const isNowSelected = !selectedRowsSet.has(rowKey);
|
|
1246
|
+
setAnnouncement(isNowSelected ? "Row selected" : "Row deselected");
|
|
1247
|
+
}
|
|
1248
|
+
break;
|
|
1249
|
+
|
|
1250
|
+
case "Escape":
|
|
1251
|
+
e.preventDefault();
|
|
1252
|
+
setFocusedCell(null);
|
|
1253
|
+
setAnnouncement("Table navigation exited");
|
|
1254
|
+
// Return focus to table container
|
|
1255
|
+
tableBodyRef.current?.closest("table")?.focus();
|
|
1256
|
+
break;
|
|
1257
|
+
|
|
1258
|
+
case "PageDown":
|
|
1259
|
+
e.preventDefault();
|
|
1260
|
+
{
|
|
1261
|
+
const jumpSize = 10;
|
|
1262
|
+
const newRow = Math.min(row + jumpSize, totalRows - 1);
|
|
1263
|
+
setFocusedCell({ row: newRow, col });
|
|
1264
|
+
const colHeader = visibleColumns[col]?.header || "";
|
|
1265
|
+
setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
|
|
1269
|
+
case "PageUp":
|
|
1270
|
+
e.preventDefault();
|
|
1271
|
+
{
|
|
1272
|
+
const jumpSize = 10;
|
|
1273
|
+
const newRow = Math.max(row - jumpSize, 0);
|
|
1274
|
+
setFocusedCell({ row: newRow, col });
|
|
1275
|
+
const colHeader = visibleColumns[col]?.header || "";
|
|
1276
|
+
setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
|
|
1277
|
+
}
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
[
|
|
1282
|
+
data,
|
|
1283
|
+
visibleColumns,
|
|
1284
|
+
focusedCell,
|
|
1285
|
+
selectable,
|
|
1286
|
+
expandedRowConfig,
|
|
1287
|
+
onEdit,
|
|
1288
|
+
onRowDoubleClick,
|
|
1289
|
+
onRowClick,
|
|
1290
|
+
getRowKey,
|
|
1291
|
+
handleExpansionWithMode,
|
|
1292
|
+
handleRowSelect,
|
|
1293
|
+
selectedRowsSet,
|
|
1294
|
+
],
|
|
1295
|
+
);
|
|
1169
1296
|
|
|
1170
1297
|
// Focus the appropriate cell when focusedCell changes
|
|
1171
1298
|
useEffect(() => {
|
|
1172
1299
|
if (focusedCell && tableBodyRef.current) {
|
|
1173
1300
|
const { row, col } = focusedCell;
|
|
1174
|
-
const rows = tableBodyRef.current.querySelectorAll(
|
|
1301
|
+
const rows = tableBodyRef.current.querySelectorAll("tr[data-row-index]");
|
|
1175
1302
|
const targetRow = rows[row] as HTMLTableRowElement | undefined;
|
|
1176
1303
|
if (targetRow) {
|
|
1177
1304
|
// Calculate actual column index including extra columns
|
|
1178
1305
|
const hasSelectionCol = selectable;
|
|
1179
|
-
const hasExpandCol =
|
|
1306
|
+
const hasExpandCol =
|
|
1307
|
+
(expandable || expandedRowConfig) && showExpandChevron;
|
|
1180
1308
|
const hasActionsCol = hasAnyActions;
|
|
1181
|
-
const extraColsBefore =
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1309
|
+
const extraColsBefore =
|
|
1310
|
+
(hasSelectionCol ? 1 : 0) +
|
|
1311
|
+
(hasExpandCol ? 1 : 0) +
|
|
1312
|
+
(hasActionsCol ? 1 : 0);
|
|
1313
|
+
|
|
1314
|
+
const cells = targetRow.querySelectorAll("td");
|
|
1315
|
+
const targetCell = cells[col + extraColsBefore] as
|
|
1316
|
+
| HTMLTableCellElement
|
|
1317
|
+
| undefined;
|
|
1185
1318
|
if (targetCell) {
|
|
1186
1319
|
targetCell.focus();
|
|
1187
1320
|
// Scroll into view if needed
|
|
1188
|
-
targetCell.scrollIntoView({ block:
|
|
1321
|
+
targetCell.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1189
1322
|
}
|
|
1190
1323
|
}
|
|
1191
1324
|
}
|
|
1192
|
-
}, [
|
|
1325
|
+
}, [
|
|
1326
|
+
focusedCell,
|
|
1327
|
+
selectable,
|
|
1328
|
+
expandable,
|
|
1329
|
+
expandedRowConfig,
|
|
1330
|
+
showExpandChevron,
|
|
1331
|
+
hasAnyActions,
|
|
1332
|
+
]);
|
|
1193
1333
|
|
|
1194
1334
|
// Handle column header click for sorting
|
|
1195
1335
|
const handleSort = (column: DataTableColumn<T>) => {
|
|
1196
1336
|
if (!column.sortable || !onSortChange) return;
|
|
1197
1337
|
|
|
1198
1338
|
const columnKey = String(column.key);
|
|
1199
|
-
|
|
1339
|
+
|
|
1200
1340
|
// If clicking the same column, toggle direction
|
|
1201
1341
|
if (currentSort?.key === columnKey) {
|
|
1202
|
-
if (currentSort.direction ===
|
|
1203
|
-
onSortChange({
|
|
1342
|
+
if (currentSort.direction === "asc") {
|
|
1343
|
+
onSortChange({
|
|
1344
|
+
key: columnKey,
|
|
1345
|
+
direction: "desc",
|
|
1346
|
+
label: column.header,
|
|
1347
|
+
});
|
|
1204
1348
|
} else {
|
|
1205
1349
|
// Remove sort on third click
|
|
1206
1350
|
onSortChange(null);
|
|
1207
1351
|
}
|
|
1208
1352
|
} else {
|
|
1209
1353
|
// New column - start with ascending
|
|
1210
|
-
onSortChange({ key: columnKey, direction:
|
|
1354
|
+
onSortChange({ key: columnKey, direction: "asc", label: column.header });
|
|
1211
1355
|
}
|
|
1212
1356
|
};
|
|
1213
1357
|
|
|
@@ -1217,13 +1361,23 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1217
1361
|
|
|
1218
1362
|
const columnKey = String(column.key);
|
|
1219
1363
|
const isActive = currentSort?.key === columnKey;
|
|
1220
|
-
const isAscending = currentSort?.direction ===
|
|
1364
|
+
const isAscending = currentSort?.direction === "asc";
|
|
1221
1365
|
|
|
1222
1366
|
// Inactive state - show neutral up/down arrows
|
|
1223
1367
|
if (!isActive) {
|
|
1224
1368
|
return (
|
|
1225
|
-
<svg
|
|
1226
|
-
|
|
1369
|
+
<svg
|
|
1370
|
+
className="ml-2 w-4 h-4 text-ink-400 group-hover:text-ink-700"
|
|
1371
|
+
fill="none"
|
|
1372
|
+
stroke="currentColor"
|
|
1373
|
+
viewBox="0 0 24 24"
|
|
1374
|
+
>
|
|
1375
|
+
<path
|
|
1376
|
+
strokeLinecap="round"
|
|
1377
|
+
strokeLinejoin="round"
|
|
1378
|
+
strokeWidth={2}
|
|
1379
|
+
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
|
1380
|
+
/>
|
|
1227
1381
|
</svg>
|
|
1228
1382
|
);
|
|
1229
1383
|
}
|
|
@@ -1231,16 +1385,36 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1231
1385
|
// Active ascending state - show up arrow highlighted
|
|
1232
1386
|
if (isAscending) {
|
|
1233
1387
|
return (
|
|
1234
|
-
<svg
|
|
1235
|
-
|
|
1388
|
+
<svg
|
|
1389
|
+
className="ml-2 w-4 h-4 text-accent-600"
|
|
1390
|
+
fill="none"
|
|
1391
|
+
stroke="currentColor"
|
|
1392
|
+
viewBox="0 0 24 24"
|
|
1393
|
+
>
|
|
1394
|
+
<path
|
|
1395
|
+
strokeLinecap="round"
|
|
1396
|
+
strokeLinejoin="round"
|
|
1397
|
+
strokeWidth={2}
|
|
1398
|
+
d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
|
|
1399
|
+
/>
|
|
1236
1400
|
</svg>
|
|
1237
1401
|
);
|
|
1238
1402
|
}
|
|
1239
1403
|
|
|
1240
1404
|
// Active descending state - show down arrow highlighted
|
|
1241
1405
|
return (
|
|
1242
|
-
<svg
|
|
1243
|
-
|
|
1406
|
+
<svg
|
|
1407
|
+
className="ml-2 w-4 h-4 text-accent-600"
|
|
1408
|
+
fill="none"
|
|
1409
|
+
stroke="currentColor"
|
|
1410
|
+
viewBox="0 0 24 24"
|
|
1411
|
+
>
|
|
1412
|
+
<path
|
|
1413
|
+
strokeLinecap="round"
|
|
1414
|
+
strokeLinejoin="round"
|
|
1415
|
+
strokeWidth={2}
|
|
1416
|
+
d="M16 17l-4 4m0 0l-4-4m4 4V3"
|
|
1417
|
+
/>
|
|
1244
1418
|
</svg>
|
|
1245
1419
|
);
|
|
1246
1420
|
};
|
|
@@ -1249,19 +1423,28 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1249
1423
|
const renderLoadingSkeleton = () => (
|
|
1250
1424
|
<>
|
|
1251
1425
|
{Array.from({ length: loadingRows }, (_, i) => (
|
|
1252
|
-
<tr
|
|
1426
|
+
<tr
|
|
1427
|
+
key={`loading-${i}`}
|
|
1428
|
+
className={`animate-pulse table-row-stable ${bordered ? `border-b ${borderColor}` : ""}`}
|
|
1429
|
+
>
|
|
1253
1430
|
{selectable && (
|
|
1254
|
-
<td
|
|
1431
|
+
<td
|
|
1432
|
+
className={`sticky left-0 bg-white ${currentDensity.cell} border-b ${borderColor} z-10 align-middle`}
|
|
1433
|
+
>
|
|
1255
1434
|
<div className="h-4 w-4 bg-paper-200 rounded"></div>
|
|
1256
1435
|
</td>
|
|
1257
1436
|
)}
|
|
1258
1437
|
{expandable && (
|
|
1259
|
-
<td
|
|
1438
|
+
<td
|
|
1439
|
+
className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}
|
|
1440
|
+
>
|
|
1260
1441
|
<div className="h-4 w-4 bg-paper-200 rounded"></div>
|
|
1261
1442
|
</td>
|
|
1262
1443
|
)}
|
|
1263
1444
|
{allActions.length > 0 && (
|
|
1264
|
-
<td
|
|
1445
|
+
<td
|
|
1446
|
+
className={`sticky left-0 bg-white px-2 ${currentDensity.cell} border-b ${borderColor} z-10`}
|
|
1447
|
+
>
|
|
1265
1448
|
<div className="h-8 w-8 bg-paper-200 rounded"></div>
|
|
1266
1449
|
</td>
|
|
1267
1450
|
)}
|
|
@@ -1271,7 +1454,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1271
1454
|
return (
|
|
1272
1455
|
<td
|
|
1273
1456
|
key={`loading-${i}-${colIndex}`}
|
|
1274
|
-
className={`${currentDensity.cell} whitespace-nowrap table-row-stable ${bordered ? `border ${borderColor}` :
|
|
1457
|
+
className={`${currentDensity.cell} whitespace-nowrap table-row-stable ${bordered ? `border ${borderColor}` : ""}`}
|
|
1275
1458
|
style={getColumnStyle(column, dynamicWidth)}
|
|
1276
1459
|
>
|
|
1277
1460
|
<div className="h-4 bg-paper-200 rounded mb-1"></div>
|
|
@@ -1289,7 +1472,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1289
1472
|
if (customRenderEmptyState) {
|
|
1290
1473
|
return (
|
|
1291
1474
|
<tr>
|
|
1292
|
-
<td
|
|
1475
|
+
<td
|
|
1476
|
+
colSpan={
|
|
1477
|
+
visibleColumns.length +
|
|
1478
|
+
(allActions.length > 0 ? 1 : 0) +
|
|
1479
|
+
(selectable ? 1 : 0) +
|
|
1480
|
+
(expandable ? 1 : 0)
|
|
1481
|
+
}
|
|
1482
|
+
>
|
|
1293
1483
|
{customRenderEmptyState()}
|
|
1294
1484
|
</td>
|
|
1295
1485
|
</tr>
|
|
@@ -1297,7 +1487,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1297
1487
|
}
|
|
1298
1488
|
return (
|
|
1299
1489
|
<tr>
|
|
1300
|
-
<td
|
|
1490
|
+
<td
|
|
1491
|
+
colSpan={
|
|
1492
|
+
visibleColumns.length +
|
|
1493
|
+
(allActions.length > 0 ? 1 : 0) +
|
|
1494
|
+
(selectable ? 1 : 0) +
|
|
1495
|
+
(expandable ? 1 : 0)
|
|
1496
|
+
}
|
|
1497
|
+
className={`${currentDensity.cell} py-8 text-center text-ink-500`}
|
|
1498
|
+
>
|
|
1301
1499
|
{error || emptyMessage}
|
|
1302
1500
|
</td>
|
|
1303
1501
|
</tr>
|
|
@@ -1307,12 +1505,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1307
1505
|
// Virtual scrolling calculations
|
|
1308
1506
|
const getVisibleRange = () => {
|
|
1309
1507
|
if (!virtualized) return { start: 0, end: data.length };
|
|
1310
|
-
|
|
1508
|
+
|
|
1311
1509
|
const overscan = 5;
|
|
1312
|
-
const start = Math.max(
|
|
1510
|
+
const start = Math.max(
|
|
1511
|
+
0,
|
|
1512
|
+
Math.floor(scrollTop / virtualRowHeight) - overscan,
|
|
1513
|
+
);
|
|
1313
1514
|
const visibleCount = Math.ceil(parseInt(virtualHeight) / virtualRowHeight);
|
|
1314
1515
|
const end = Math.min(data.length, start + visibleCount + overscan * 2);
|
|
1315
|
-
|
|
1516
|
+
|
|
1316
1517
|
return { start, end };
|
|
1317
1518
|
};
|
|
1318
1519
|
|
|
@@ -1327,7 +1528,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1327
1528
|
|
|
1328
1529
|
// Render data rows
|
|
1329
1530
|
const renderDataRows = () => {
|
|
1330
|
-
const rowsToRender = virtualized
|
|
1531
|
+
const rowsToRender = virtualized
|
|
1331
1532
|
? data.slice(visibleStart, visibleEnd)
|
|
1332
1533
|
: data;
|
|
1333
1534
|
|
|
@@ -1337,327 +1538,443 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1337
1538
|
const isSelected = selectedRowsSet.has(rowKey);
|
|
1338
1539
|
const isExpanded = expandedRowsSet.has(rowKey);
|
|
1339
1540
|
const rowBgClass = getRowBackgroundClass(item, index);
|
|
1340
|
-
const borderClass = bordered
|
|
1341
|
-
|
|
1541
|
+
const borderClass = bordered
|
|
1542
|
+
? `border-b ${borderColor}`
|
|
1543
|
+
: !visibleColumns.some((col) => !!col.renderSecondary)
|
|
1544
|
+
? `border-b ${borderColor}`
|
|
1545
|
+
: "";
|
|
1546
|
+
const hasSecondaryRow = visibleColumns.some(
|
|
1547
|
+
(col) => !!col.renderSecondary,
|
|
1548
|
+
);
|
|
1342
1549
|
|
|
1343
1550
|
// Hover state for row pair (primary + secondary)
|
|
1344
1551
|
const isHovered = hoveredRowKey === rowKey;
|
|
1345
|
-
const hoverClass = disableHover ?
|
|
1552
|
+
const hoverClass = disableHover ? "" : isHovered ? "bg-paper-100" : "";
|
|
1346
1553
|
|
|
1347
1554
|
// Check if this row is keyboard-focused
|
|
1348
1555
|
const isKeyboardFocused = focusedCell?.row === index;
|
|
1349
1556
|
|
|
1350
1557
|
return (
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}}
|
|
1373
|
-
onDoubleClick={() => {
|
|
1374
|
-
// Priority 1: If there's an onEdit handler (legacy), trigger it
|
|
1375
|
-
if (onEdit) {
|
|
1376
|
-
onEdit(item);
|
|
1377
|
-
}
|
|
1378
|
-
// Priority 2: If there's an expandable edit mode, trigger it
|
|
1379
|
-
else if (expandedRowConfig?.edit) {
|
|
1380
|
-
handleExpansionWithMode(rowKey, 'edit');
|
|
1381
|
-
}
|
|
1382
|
-
// Priority 3: If there's an expandable details mode, trigger it
|
|
1383
|
-
else if (expandedRowConfig?.details) {
|
|
1384
|
-
handleExpansionWithMode(rowKey, 'details');
|
|
1385
|
-
}
|
|
1386
|
-
// Priority 4: If there's any addRelated mode, trigger the first one
|
|
1387
|
-
else if (expandedRowConfig?.addRelated && expandedRowConfig.addRelated.length > 0) {
|
|
1388
|
-
handleExpansionWithMode(rowKey, `addRelated-${expandedRowConfig.addRelated[0].key}`);
|
|
1389
|
-
}
|
|
1390
|
-
// Priority 5: If there's any manageRelated mode, trigger the first one
|
|
1391
|
-
else if (expandedRowConfig?.manageRelated && expandedRowConfig.manageRelated.length > 0) {
|
|
1392
|
-
handleExpansionWithMode(rowKey, `manageRelated-${expandedRowConfig.manageRelated[0].key}`);
|
|
1393
|
-
}
|
|
1394
|
-
// Priority 6: Legacy onRowDoubleClick handler
|
|
1395
|
-
else {
|
|
1396
|
-
onRowDoubleClick?.(item);
|
|
1397
|
-
}
|
|
1398
|
-
}}
|
|
1399
|
-
title={
|
|
1400
|
-
onEdit ? 'Double-click to edit' :
|
|
1401
|
-
expandedRowConfig?.edit ? 'Double-click to edit inline' :
|
|
1402
|
-
expandedRowConfig?.details ? 'Double-click to view details' :
|
|
1403
|
-
expandedRowConfig?.addRelated && expandedRowConfig.addRelated.length > 0 ? `Double-click to ${expandedRowConfig.addRelated[0].label}` :
|
|
1404
|
-
expandedRowConfig?.manageRelated && expandedRowConfig.manageRelated.length > 0 ? `Double-click to ${expandedRowConfig.manageRelated[0].label}` :
|
|
1405
|
-
onRowDoubleClick ? 'Double-click for details' :
|
|
1406
|
-
onRowClick ? 'Click to select' :
|
|
1407
|
-
undefined
|
|
1408
|
-
}
|
|
1409
|
-
>
|
|
1410
|
-
{selectable && (
|
|
1411
|
-
<td
|
|
1412
|
-
className={`sticky left-0 z-10 ${bordered ? `border ${borderColor}` : ''}`}
|
|
1413
|
-
style={{
|
|
1414
|
-
backgroundColor: 'inherit',
|
|
1415
|
-
verticalAlign: 'middle',
|
|
1416
|
-
padding: '0.375rem 0.75rem',
|
|
1417
|
-
textAlign: 'center'
|
|
1558
|
+
<React.Fragment key={rowKey}>
|
|
1559
|
+
<tr
|
|
1560
|
+
data-row-index={index}
|
|
1561
|
+
className={`table-row-stable ${onRowDoubleClick || onRowClick || onEdit || expandedRowConfig?.edit || expandedRowConfig?.details || expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length ? "cursor-pointer" : ""} ${isSelected ? "bg-accent-50 border-l-2 border-accent-500" : hoverClass || rowBgClass} ${borderClass} ${isKeyboardFocused ? "ring-2 ring-inset ring-accent-400" : ""}`}
|
|
1562
|
+
onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
|
|
1563
|
+
onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
|
|
1564
|
+
onClick={() => onRowClick?.(item)}
|
|
1565
|
+
onContextMenu={(e) => {
|
|
1566
|
+
if (enableContextMenu && allActions.length > 0) {
|
|
1567
|
+
e.preventDefault();
|
|
1568
|
+
e.stopPropagation();
|
|
1569
|
+
|
|
1570
|
+
const x = e.clientX;
|
|
1571
|
+
const y = e.clientY;
|
|
1572
|
+
|
|
1573
|
+
setContextMenuState({
|
|
1574
|
+
isOpen: true,
|
|
1575
|
+
position: { x, y },
|
|
1576
|
+
item,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1418
1579
|
}}
|
|
1419
|
-
|
|
1580
|
+
onDoubleClick={() => {
|
|
1581
|
+
// Priority 1: If there's an onEdit handler (legacy), trigger it
|
|
1582
|
+
if (onEdit) {
|
|
1583
|
+
onEdit(item);
|
|
1584
|
+
}
|
|
1585
|
+
// Priority 2: If there's an expandable edit mode, trigger it
|
|
1586
|
+
else if (expandedRowConfig?.edit) {
|
|
1587
|
+
handleExpansionWithMode(rowKey, "edit");
|
|
1588
|
+
}
|
|
1589
|
+
// Priority 3: If there's an expandable details mode, trigger it
|
|
1590
|
+
else if (expandedRowConfig?.details) {
|
|
1591
|
+
handleExpansionWithMode(rowKey, "details");
|
|
1592
|
+
}
|
|
1593
|
+
// Priority 4: If there's any addRelated mode, trigger the first one
|
|
1594
|
+
else if (
|
|
1595
|
+
expandedRowConfig?.addRelated &&
|
|
1596
|
+
expandedRowConfig.addRelated.length > 0
|
|
1597
|
+
) {
|
|
1598
|
+
handleExpansionWithMode(
|
|
1599
|
+
rowKey,
|
|
1600
|
+
`addRelated-${expandedRowConfig.addRelated[0].key}`,
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
// Priority 5: If there's any manageRelated mode, trigger the first one
|
|
1604
|
+
else if (
|
|
1605
|
+
expandedRowConfig?.manageRelated &&
|
|
1606
|
+
expandedRowConfig.manageRelated.length > 0
|
|
1607
|
+
) {
|
|
1608
|
+
handleExpansionWithMode(
|
|
1609
|
+
rowKey,
|
|
1610
|
+
`manageRelated-${expandedRowConfig.manageRelated[0].key}`,
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
// Priority 6: Legacy onRowDoubleClick handler
|
|
1614
|
+
else {
|
|
1615
|
+
onRowDoubleClick?.(item);
|
|
1616
|
+
}
|
|
1617
|
+
}}
|
|
1618
|
+
title={
|
|
1619
|
+
onEdit
|
|
1620
|
+
? "Double-click to edit"
|
|
1621
|
+
: expandedRowConfig?.edit
|
|
1622
|
+
? "Double-click to edit inline"
|
|
1623
|
+
: expandedRowConfig?.details
|
|
1624
|
+
? "Double-click to view details"
|
|
1625
|
+
: expandedRowConfig?.addRelated &&
|
|
1626
|
+
expandedRowConfig.addRelated.length > 0
|
|
1627
|
+
? `Double-click to ${expandedRowConfig.addRelated[0].label}`
|
|
1628
|
+
: expandedRowConfig?.manageRelated &&
|
|
1629
|
+
expandedRowConfig.manageRelated.length > 0
|
|
1630
|
+
? `Double-click to ${expandedRowConfig.manageRelated[0].label}`
|
|
1631
|
+
: onRowDoubleClick
|
|
1632
|
+
? "Double-click for details"
|
|
1633
|
+
: onRowClick
|
|
1634
|
+
? "Click to select"
|
|
1635
|
+
: undefined
|
|
1636
|
+
}
|
|
1420
1637
|
>
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1638
|
+
{selectable && (
|
|
1639
|
+
<td
|
|
1640
|
+
className={`sticky left-0 z-10 ${bordered ? `border ${borderColor}` : ""}`}
|
|
1641
|
+
style={{
|
|
1642
|
+
backgroundColor: "inherit",
|
|
1643
|
+
verticalAlign: "middle",
|
|
1644
|
+
padding: "0.375rem 0.75rem",
|
|
1645
|
+
textAlign: "center",
|
|
1646
|
+
}}
|
|
1647
|
+
rowSpan={hasSecondaryRow ? 2 : 1}
|
|
1648
|
+
>
|
|
1649
|
+
<input
|
|
1650
|
+
type="checkbox"
|
|
1651
|
+
checked={isSelected}
|
|
1652
|
+
onChange={() => handleRowSelect(rowKey)}
|
|
1653
|
+
className="w-4 h-4 text-accent-600 border-paper-300 rounded focus:ring-accent-400"
|
|
1654
|
+
aria-label={`Select row ${rowKey}`}
|
|
1655
|
+
/>
|
|
1656
|
+
</td>
|
|
1657
|
+
)}
|
|
1658
|
+
{(expandable || expandedRowConfig) && showExpandChevron && (
|
|
1659
|
+
<td
|
|
1660
|
+
className={`sticky left-0 px-2 ${currentDensity.cell} z-10 ${bordered ? `border ${borderColor}` : ""}`}
|
|
1661
|
+
style={{ backgroundColor: "inherit", verticalAlign: "middle" }}
|
|
1662
|
+
rowSpan={hasSecondaryRow ? 2 : 1}
|
|
1663
|
+
>
|
|
1664
|
+
<button
|
|
1665
|
+
onClick={() => {
|
|
1666
|
+
// NEW: Enhanced logic for expandedRowConfig
|
|
1667
|
+
if (
|
|
1668
|
+
expandedRowConfig?.details &&
|
|
1669
|
+
expandedRowConfig.details.triggerOnExpand !== false
|
|
1670
|
+
) {
|
|
1671
|
+
// Trigger details mode if configured
|
|
1672
|
+
handleExpansionWithMode(rowKey, "details");
|
|
1673
|
+
} else if (
|
|
1674
|
+
expandedRowConfig?.edit &&
|
|
1675
|
+
expandedRowConfig.edit.triggerOnDoubleClick !== false
|
|
1676
|
+
) {
|
|
1677
|
+
// Fallback to edit mode if no details but edit is available
|
|
1678
|
+
handleExpansionWithMode(rowKey, "edit");
|
|
1679
|
+
} else {
|
|
1680
|
+
// Legacy: use handleRowExpand
|
|
1681
|
+
handleRowExpand(rowKey);
|
|
1682
|
+
}
|
|
1683
|
+
}}
|
|
1684
|
+
className="text-ink-500 hover:text-ink-900 transition-colors"
|
|
1685
|
+
aria-label={
|
|
1686
|
+
isExpanded || expansionState?.rowKey === rowKey
|
|
1687
|
+
? "Collapse row"
|
|
1688
|
+
: "Expand row"
|
|
1448
1689
|
}
|
|
1690
|
+
>
|
|
1691
|
+
{isExpanded || expansionState?.rowKey === rowKey ? (
|
|
1692
|
+
<ChevronDown className="h-4 w-4" />
|
|
1693
|
+
) : (
|
|
1694
|
+
<ChevronRight className="h-4 w-4" />
|
|
1695
|
+
)}
|
|
1696
|
+
</button>
|
|
1697
|
+
</td>
|
|
1698
|
+
)}
|
|
1699
|
+
{allActions.length > 0 && (
|
|
1700
|
+
<td
|
|
1701
|
+
className="sticky left-0 whitespace-nowrap shadow-[4px_0_6px_-2px_rgba(0,0,0,0.1)] z-10"
|
|
1702
|
+
style={{
|
|
1703
|
+
width: "28px",
|
|
1704
|
+
padding: "0",
|
|
1705
|
+
backgroundColor: "inherit",
|
|
1706
|
+
verticalAlign: "middle",
|
|
1449
1707
|
}}
|
|
1450
|
-
|
|
1451
|
-
|
|
1708
|
+
onClick={(e) => e.stopPropagation()}
|
|
1709
|
+
rowSpan={hasSecondaryRow ? 2 : 1}
|
|
1452
1710
|
>
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1711
|
+
<div
|
|
1712
|
+
style={{
|
|
1713
|
+
display: "inline-flex",
|
|
1714
|
+
alignItems: "center",
|
|
1715
|
+
justifyContent: "center",
|
|
1716
|
+
width: "28px",
|
|
1717
|
+
}}
|
|
1718
|
+
>
|
|
1719
|
+
<ActionMenu actions={allActions} item={item} />
|
|
1720
|
+
</div>
|
|
1721
|
+
</td>
|
|
1722
|
+
)}
|
|
1723
|
+
{visibleColumns.map((column, colIdx) => {
|
|
1724
|
+
const columnKey = String(column.key);
|
|
1725
|
+
const dynamicWidth = columnWidths[columnKey];
|
|
1726
|
+
const value =
|
|
1727
|
+
typeof column.key === "string"
|
|
1728
|
+
? item[column.key as keyof T]
|
|
1729
|
+
: item[column.key];
|
|
1730
|
+
|
|
1731
|
+
const primaryContent = column.render
|
|
1732
|
+
? column.render(item, value)
|
|
1733
|
+
: String(value || "");
|
|
1734
|
+
|
|
1735
|
+
// Tooltip: caller-provided > raw value stringified. Empty
|
|
1736
|
+
// strings get dropped so we don't render an empty title
|
|
1737
|
+
// attribute (which the browser would still show as a
|
|
1738
|
+
// 0-width tooltip box on hover).
|
|
1739
|
+
const primaryTooltipText = column.tooltip
|
|
1740
|
+
? column.tooltip(item, value)
|
|
1741
|
+
: value !== null && value !== undefined && value !== ""
|
|
1742
|
+
? String(value)
|
|
1743
|
+
: undefined;
|
|
1744
|
+
|
|
1745
|
+
// Reduce left padding on first column when there are action buttons
|
|
1746
|
+
const isFirstColumn = colIdx === 0;
|
|
1747
|
+
const paddingClass =
|
|
1748
|
+
isFirstColumn && allActions.length > 0 ? "pl-3" : "";
|
|
1486
1749
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1750
|
+
// Check if this cell is keyboard-focused
|
|
1751
|
+
const isCellFocused =
|
|
1752
|
+
focusedCell?.row === index && focusedCell?.col === colIdx;
|
|
1490
1753
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1754
|
+
return (
|
|
1755
|
+
<td
|
|
1756
|
+
key={`${item.id}-${columnKey}`}
|
|
1757
|
+
className={`${currentDensity.cell} ${paddingClass} ${column.className || ""} ${bordered ? `border ${borderColor}` : ""} ${isCellFocused ? "outline outline-2 outline-accent-500 outline-offset-[-2px]" : ""}`}
|
|
1758
|
+
style={getColumnStyle(column, dynamicWidth)}
|
|
1759
|
+
tabIndex={isCellFocused ? 0 : -1}
|
|
1760
|
+
role="gridcell"
|
|
1761
|
+
aria-colindex={colIdx + 1}
|
|
1762
|
+
>
|
|
1763
|
+
<div
|
|
1764
|
+
className={`${currentDensity.text} leading-tight`}
|
|
1765
|
+
title={primaryTooltipText}
|
|
1766
|
+
>
|
|
1767
|
+
{primaryContent}
|
|
1768
|
+
</div>
|
|
1769
|
+
</td>
|
|
1770
|
+
);
|
|
1771
|
+
})}
|
|
1772
|
+
</tr>
|
|
1493
1773
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
className={
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
role="gridcell"
|
|
1501
|
-
aria-colindex={colIdx + 1}
|
|
1774
|
+
{/* Secondary row - only render if any column has renderSecondary */}
|
|
1775
|
+
{hasSecondaryRow && (
|
|
1776
|
+
<tr
|
|
1777
|
+
className={`secondary-row ${isSelected ? "bg-accent-50 border-l-2 border-accent-500" : hoverClass || rowBgClass} border-b ${borderColor}`}
|
|
1778
|
+
onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
|
|
1779
|
+
onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
|
|
1502
1780
|
>
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
<tr
|
|
1512
|
-
className={`secondary-row ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} border-b ${borderColor}`}
|
|
1513
|
-
onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
|
|
1514
|
-
onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
|
|
1515
|
-
>
|
|
1516
|
-
{/* Selectable checkbox uses rowspan from primary row, no cell needed here */}
|
|
1517
|
-
{/* Expand chevron uses rowspan from primary row, no cell needed here */}
|
|
1518
|
-
{/* Actions column uses rowspan from primary row, no cell needed here */}
|
|
1519
|
-
{visibleColumns.map((column, colIdx) => {
|
|
1520
|
-
const columnKey = String(column.key);
|
|
1521
|
-
const dynamicWidth = columnWidths[columnKey];
|
|
1522
|
-
const value = typeof column.key === 'string'
|
|
1781
|
+
{/* Selectable checkbox uses rowspan from primary row, no cell needed here */}
|
|
1782
|
+
{/* Expand chevron uses rowspan from primary row, no cell needed here */}
|
|
1783
|
+
{/* Actions column uses rowspan from primary row, no cell needed here */}
|
|
1784
|
+
{visibleColumns.map((column, colIdx) => {
|
|
1785
|
+
const columnKey = String(column.key);
|
|
1786
|
+
const dynamicWidth = columnWidths[columnKey];
|
|
1787
|
+
const value =
|
|
1788
|
+
typeof column.key === "string"
|
|
1523
1789
|
? item[column.key as keyof T]
|
|
1524
1790
|
: item[column.key];
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1791
|
+
const secondaryContent = column.renderSecondary
|
|
1792
|
+
? column.renderSecondary(item, value)
|
|
1793
|
+
: null;
|
|
1794
|
+
|
|
1795
|
+
// Tooltip on the secondary row prefixes the field label when
|
|
1796
|
+
// available so the otherwise-unlabeled second row stays
|
|
1797
|
+
// self-describing on hover. Caller can override entirely
|
|
1798
|
+
// via `secondaryTooltip`.
|
|
1799
|
+
const hasSecondaryValue =
|
|
1800
|
+
value !== null && value !== undefined && value !== "";
|
|
1801
|
+
let secondaryTooltipText: string | undefined;
|
|
1802
|
+
if (column.secondaryTooltip) {
|
|
1803
|
+
secondaryTooltipText = column.secondaryTooltip(item, value);
|
|
1804
|
+
} else if (hasSecondaryValue) {
|
|
1805
|
+
secondaryTooltipText = column.secondaryHeader
|
|
1806
|
+
? `${column.secondaryHeader}: ${value}`
|
|
1807
|
+
: String(value);
|
|
1808
|
+
} else if (column.secondaryHeader) {
|
|
1809
|
+
secondaryTooltipText = column.secondaryHeader;
|
|
1810
|
+
}
|
|
1530
1811
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1812
|
+
// Reduce left padding on first column when there are action buttons
|
|
1813
|
+
const isFirstColumn = colIdx === 0;
|
|
1814
|
+
const paddingClass =
|
|
1815
|
+
isFirstColumn && allActions.length > 0 ? "pl-3" : "";
|
|
1816
|
+
|
|
1817
|
+
return (
|
|
1818
|
+
<td
|
|
1819
|
+
key={`${item.id}-${columnKey}-secondary`}
|
|
1820
|
+
className={`${currentDensity.cell} py-0.5 ${paddingClass} ${column.className || ""} ${bordered ? `border ${borderColor}` : ""}`}
|
|
1821
|
+
style={getColumnStyle(column, dynamicWidth)}
|
|
1822
|
+
>
|
|
1823
|
+
<div
|
|
1824
|
+
className="text-xs text-ink-500 leading-tight"
|
|
1825
|
+
title={secondaryTooltipText}
|
|
1536
1826
|
>
|
|
1537
|
-
<
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
<td
|
|
1549
|
-
colSpan={
|
|
1550
|
-
visibleColumns.length +
|
|
1551
|
-
(selectable ? 1 : 0) +
|
|
1552
|
-
(((expandable || expandedRowConfig) && showExpandChevron) ? 1 : 0) +
|
|
1553
|
-
(allActions.length > 0 ? 1 : 0)
|
|
1554
|
-
}
|
|
1555
|
-
className={`${currentDensity.cell} py-4 bg-paper-50`}
|
|
1556
|
-
>
|
|
1557
|
-
{renderExpandedRow(item)}
|
|
1558
|
-
</td>
|
|
1559
|
-
</tr>
|
|
1560
|
-
)}
|
|
1561
|
-
|
|
1562
|
-
{/* Expanded row content - NEW: Multiple expansion modes */}
|
|
1563
|
-
{expansionState && expansionState.rowKey === rowKey && expandedRowConfig && (() => {
|
|
1564
|
-
const mode = expansionState.mode;
|
|
1565
|
-
let content: React.ReactNode = null;
|
|
1566
|
-
let bgColorClass = 'bg-paper-50'; // Default
|
|
1567
|
-
|
|
1568
|
-
// Edit mode
|
|
1569
|
-
if (mode === 'edit' && expandedRowConfig.edit) {
|
|
1570
|
-
bgColorClass = 'bg-paper-100/80 border-t border-b border-paper-300/80';
|
|
1571
|
-
content = expandedRowConfig.edit.render(
|
|
1572
|
-
item,
|
|
1573
|
-
async (_updated: T) => {
|
|
1574
|
-
// Handle save
|
|
1575
|
-
handleCollapseExpansion();
|
|
1576
|
-
},
|
|
1577
|
-
() => {
|
|
1578
|
-
// Handle cancel
|
|
1579
|
-
handleCollapseExpansion();
|
|
1580
|
-
}
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// Details mode
|
|
1585
|
-
else if (mode === 'details' && expandedRowConfig.details) {
|
|
1586
|
-
bgColorClass = 'bg-primary-50/80 border-t border-b border-primary-200/80';
|
|
1587
|
-
content = expandedRowConfig.details.render(item);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// Add related modes
|
|
1591
|
-
else if (mode.startsWith('addRelated-') && expandedRowConfig.addRelated) {
|
|
1592
|
-
const key = mode.replace('addRelated-', '');
|
|
1593
|
-
const config = expandedRowConfig.addRelated.find(c => c.key === key);
|
|
1594
|
-
if (config) {
|
|
1595
|
-
bgColorClass = 'bg-success-50/80 border-t border-b border-success-200/80';
|
|
1596
|
-
content = config.render(
|
|
1597
|
-
item,
|
|
1598
|
-
async (_newItem: any) => {
|
|
1599
|
-
// Handle save
|
|
1600
|
-
handleCollapseExpansion();
|
|
1601
|
-
},
|
|
1602
|
-
() => {
|
|
1603
|
-
// Handle cancel
|
|
1604
|
-
handleCollapseExpansion();
|
|
1605
|
-
}
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
// Manage related modes
|
|
1611
|
-
else if (mode.startsWith('manageRelated-') && expandedRowConfig.manageRelated) {
|
|
1612
|
-
const key = mode.replace('manageRelated-', '');
|
|
1613
|
-
const config = expandedRowConfig.manageRelated.find(c => c.key === key);
|
|
1614
|
-
if (config) {
|
|
1615
|
-
bgColorClass = 'bg-slate-50/80 border-t border-b border-slate-200/80';
|
|
1616
|
-
const handleClose = () => setExpansionState(null);
|
|
1617
|
-
content = config.render(item, handleClose);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
if (!content) return null;
|
|
1622
|
-
|
|
1623
|
-
return (
|
|
1624
|
-
<tr key={`expanded-${rowKey}`}>
|
|
1827
|
+
{secondaryContent || <span className="invisible">—</span>}
|
|
1828
|
+
</div>
|
|
1829
|
+
</td>
|
|
1830
|
+
);
|
|
1831
|
+
})}
|
|
1832
|
+
</tr>
|
|
1833
|
+
)}
|
|
1834
|
+
|
|
1835
|
+
{/* Expanded row content - Legacy mode */}
|
|
1836
|
+
{expandable && isExpanded && renderExpandedRow && (
|
|
1837
|
+
<tr>
|
|
1625
1838
|
<td
|
|
1626
1839
|
colSpan={
|
|
1627
1840
|
visibleColumns.length +
|
|
1628
1841
|
(selectable ? 1 : 0) +
|
|
1629
|
-
((
|
|
1842
|
+
((expandable || expandedRowConfig) && showExpandChevron
|
|
1843
|
+
? 1
|
|
1844
|
+
: 0) +
|
|
1630
1845
|
(allActions.length > 0 ? 1 : 0)
|
|
1631
1846
|
}
|
|
1632
|
-
className={`${currentDensity.cell} py-4
|
|
1847
|
+
className={`${currentDensity.cell} py-4 bg-paper-50`}
|
|
1633
1848
|
>
|
|
1634
|
-
{
|
|
1849
|
+
{renderExpandedRow(item)}
|
|
1635
1850
|
</td>
|
|
1636
1851
|
</tr>
|
|
1637
|
-
)
|
|
1638
|
-
|
|
1639
|
-
|
|
1852
|
+
)}
|
|
1853
|
+
|
|
1854
|
+
{/* Expanded row content - NEW: Multiple expansion modes */}
|
|
1855
|
+
{expansionState &&
|
|
1856
|
+
expansionState.rowKey === rowKey &&
|
|
1857
|
+
expandedRowConfig &&
|
|
1858
|
+
(() => {
|
|
1859
|
+
const mode = expansionState.mode;
|
|
1860
|
+
let content: React.ReactNode = null;
|
|
1861
|
+
let bgColorClass = "bg-paper-50"; // Default
|
|
1862
|
+
|
|
1863
|
+
// Edit mode
|
|
1864
|
+
if (mode === "edit" && expandedRowConfig.edit) {
|
|
1865
|
+
bgColorClass =
|
|
1866
|
+
"bg-paper-100/80 border-t border-b border-paper-300/80";
|
|
1867
|
+
content = expandedRowConfig.edit.render(
|
|
1868
|
+
item,
|
|
1869
|
+
async (_updated: T) => {
|
|
1870
|
+
// Handle save
|
|
1871
|
+
handleCollapseExpansion();
|
|
1872
|
+
},
|
|
1873
|
+
() => {
|
|
1874
|
+
// Handle cancel
|
|
1875
|
+
handleCollapseExpansion();
|
|
1876
|
+
},
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Details mode
|
|
1881
|
+
else if (mode === "details" && expandedRowConfig.details) {
|
|
1882
|
+
bgColorClass =
|
|
1883
|
+
"bg-primary-50/80 border-t border-b border-primary-200/80";
|
|
1884
|
+
content = expandedRowConfig.details.render(item);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Add related modes
|
|
1888
|
+
else if (
|
|
1889
|
+
mode.startsWith("addRelated-") &&
|
|
1890
|
+
expandedRowConfig.addRelated
|
|
1891
|
+
) {
|
|
1892
|
+
const key = mode.replace("addRelated-", "");
|
|
1893
|
+
const config = expandedRowConfig.addRelated.find(
|
|
1894
|
+
(c) => c.key === key,
|
|
1895
|
+
);
|
|
1896
|
+
if (config) {
|
|
1897
|
+
bgColorClass =
|
|
1898
|
+
"bg-success-50/80 border-t border-b border-success-200/80";
|
|
1899
|
+
content = config.render(
|
|
1900
|
+
item,
|
|
1901
|
+
async (_newItem: any) => {
|
|
1902
|
+
// Handle save
|
|
1903
|
+
handleCollapseExpansion();
|
|
1904
|
+
},
|
|
1905
|
+
() => {
|
|
1906
|
+
// Handle cancel
|
|
1907
|
+
handleCollapseExpansion();
|
|
1908
|
+
},
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Manage related modes
|
|
1914
|
+
else if (
|
|
1915
|
+
mode.startsWith("manageRelated-") &&
|
|
1916
|
+
expandedRowConfig.manageRelated
|
|
1917
|
+
) {
|
|
1918
|
+
const key = mode.replace("manageRelated-", "");
|
|
1919
|
+
const config = expandedRowConfig.manageRelated.find(
|
|
1920
|
+
(c) => c.key === key,
|
|
1921
|
+
);
|
|
1922
|
+
if (config) {
|
|
1923
|
+
bgColorClass =
|
|
1924
|
+
"bg-slate-50/80 border-t border-b border-slate-200/80";
|
|
1925
|
+
const handleClose = () => setExpansionState(null);
|
|
1926
|
+
content = config.render(item, handleClose);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (!content) return null;
|
|
1931
|
+
|
|
1932
|
+
return (
|
|
1933
|
+
<tr key={`expanded-${rowKey}`}>
|
|
1934
|
+
<td
|
|
1935
|
+
colSpan={
|
|
1936
|
+
visibleColumns.length +
|
|
1937
|
+
(selectable ? 1 : 0) +
|
|
1938
|
+
((expandable || expandedRowConfig) && showExpandChevron
|
|
1939
|
+
? 1
|
|
1940
|
+
: 0) +
|
|
1941
|
+
(allActions.length > 0 ? 1 : 0)
|
|
1942
|
+
}
|
|
1943
|
+
className={`${currentDensity.cell} py-4 ${bgColorClass} animate-expand`}
|
|
1944
|
+
>
|
|
1945
|
+
{content}
|
|
1946
|
+
</td>
|
|
1947
|
+
</tr>
|
|
1948
|
+
);
|
|
1949
|
+
})()}
|
|
1950
|
+
</React.Fragment>
|
|
1640
1951
|
);
|
|
1641
1952
|
});
|
|
1642
1953
|
};
|
|
1643
1954
|
|
|
1644
1955
|
const tableContent = (
|
|
1645
|
-
<div
|
|
1956
|
+
<div
|
|
1957
|
+
className={`bg-white rounded-lg shadow border-2 ${borderColor} ${virtualized ? "overflow-hidden" : "overflow-x-auto overflow-y-visible"} ${className}`}
|
|
1958
|
+
style={{ position: "relative" }}
|
|
1959
|
+
>
|
|
1646
1960
|
{/* Loading overlay for when data is being refreshed */}
|
|
1647
1961
|
{loading && data.length > 0 && (
|
|
1648
1962
|
<div
|
|
1649
1963
|
className="absolute inset-0 bg-white/75 flex items-center justify-center z-20"
|
|
1650
|
-
style={{ backdropFilter:
|
|
1964
|
+
style={{ backdropFilter: "blur(2px)" }}
|
|
1651
1965
|
>
|
|
1652
1966
|
<div className="flex flex-col items-center gap-3">
|
|
1653
|
-
<div
|
|
1967
|
+
<div
|
|
1968
|
+
className="loading-spinner"
|
|
1969
|
+
style={{ width: "32px", height: "32px", borderWidth: "3px" }}
|
|
1970
|
+
></div>
|
|
1654
1971
|
<span className="text-sm font-medium text-ink-600">Loading...</span>
|
|
1655
1972
|
</div>
|
|
1656
1973
|
</div>
|
|
1657
1974
|
)}
|
|
1658
1975
|
|
|
1659
1976
|
<table
|
|
1660
|
-
className={`table-stable w-full ${bordered ?
|
|
1977
|
+
className={`table-stable w-full ${bordered ? "border-collapse" : ""}`}
|
|
1661
1978
|
role="grid"
|
|
1662
1979
|
aria-label="Data table"
|
|
1663
1980
|
aria-rowcount={data.length}
|
|
@@ -1665,8 +1982,10 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1665
1982
|
>
|
|
1666
1983
|
<colgroup>
|
|
1667
1984
|
{selectable && <col className="w-12" />}
|
|
1668
|
-
{(
|
|
1669
|
-
|
|
1985
|
+
{(expandable || expandedRowConfig) && showExpandChevron && (
|
|
1986
|
+
<col className="w-10" />
|
|
1987
|
+
)}
|
|
1988
|
+
{allActions.length > 0 && <col style={{ width: "28px" }} />}
|
|
1670
1989
|
{visibleColumns.map((column, index) => {
|
|
1671
1990
|
const columnKey = String(column.key);
|
|
1672
1991
|
const dynamicWidth = columnWidths[columnKey];
|
|
@@ -1678,23 +1997,32 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1678
1997
|
<thead className={`bg-paper-100 sticky top-0 z-10 ${headerClassName}`}>
|
|
1679
1998
|
<tr className="table-header-row">
|
|
1680
1999
|
{selectable && (
|
|
1681
|
-
<th
|
|
2000
|
+
<th
|
|
2001
|
+
className={`sticky left-0 bg-paper-100 ${currentDensity.header} border-b ${borderColor} z-20 w-12 ${bordered ? `border ${borderColor}` : ""}`}
|
|
2002
|
+
>
|
|
1682
2003
|
<input
|
|
1683
2004
|
type="checkbox"
|
|
1684
|
-
checked={
|
|
2005
|
+
checked={
|
|
2006
|
+
selectedRowsSet.size === data.length && data.length > 0
|
|
2007
|
+
}
|
|
1685
2008
|
onChange={handleSelectAll}
|
|
1686
2009
|
className="w-4 h-4 text-accent-600 border-paper-300 rounded focus:ring-accent-400"
|
|
1687
2010
|
aria-label="Select all rows"
|
|
1688
2011
|
/>
|
|
1689
2012
|
</th>
|
|
1690
2013
|
)}
|
|
1691
|
-
{(
|
|
1692
|
-
<th
|
|
2014
|
+
{(expandable || expandedRowConfig) && showExpandChevron && (
|
|
2015
|
+
<th
|
|
2016
|
+
className={`sticky left-0 bg-paper-100 px-2 ${currentDensity.header} border-b ${borderColor} z-19 w-10 ${bordered ? `border ${borderColor}` : ""}`}
|
|
2017
|
+
>
|
|
1693
2018
|
{/* Empty header for expand column */}
|
|
1694
2019
|
</th>
|
|
1695
2020
|
)}
|
|
1696
2021
|
{allActions.length > 0 && (
|
|
1697
|
-
<th
|
|
2022
|
+
<th
|
|
2023
|
+
className={`sticky left-0 bg-paper-100 text-center text-xs font-medium text-ink-700 uppercase tracking-wider border-b ${borderColor} z-20 ${bordered ? `border ${borderColor}` : ""}`}
|
|
2024
|
+
style={{ width: "28px", padding: "0" }}
|
|
2025
|
+
>
|
|
1698
2026
|
{/* Actions column header */}
|
|
1699
2027
|
</th>
|
|
1700
2028
|
)}
|
|
@@ -1707,22 +2035,27 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1707
2035
|
|
|
1708
2036
|
// Reduce left padding on first column when there are action buttons (match body cells)
|
|
1709
2037
|
const isFirstColumn = colIdx === 0;
|
|
1710
|
-
const headerPaddingClass =
|
|
2038
|
+
const headerPaddingClass =
|
|
2039
|
+
isFirstColumn && allActions.length > 0 ? "pl-3" : "";
|
|
1711
2040
|
|
|
1712
2041
|
return (
|
|
1713
2042
|
<th
|
|
1714
2043
|
key={columnKey}
|
|
1715
2044
|
ref={thRef}
|
|
1716
2045
|
draggable={reorderable}
|
|
1717
|
-
onDragStart={(e) =>
|
|
1718
|
-
|
|
2046
|
+
onDragStart={(e) =>
|
|
2047
|
+
reorderable && handleDragStart(e, columnKey)
|
|
2048
|
+
}
|
|
2049
|
+
onDragOver={(e) =>
|
|
2050
|
+
reorderable && handleDragOver(e, columnKey)
|
|
2051
|
+
}
|
|
1719
2052
|
onDragEnd={handleDragEnd}
|
|
1720
2053
|
onDrop={(e) => reorderable && handleDrop(e, columnKey)}
|
|
1721
2054
|
className={`
|
|
1722
|
-
${currentDensity.header} ${headerPaddingClass} text-left border-b ${borderColor} ${bordered ? `border ${borderColor}` :
|
|
1723
|
-
${reorderable ?
|
|
1724
|
-
${isDragging ?
|
|
1725
|
-
${isDragOver ?
|
|
2055
|
+
${currentDensity.header} ${headerPaddingClass} text-left border-b ${borderColor} ${bordered ? `border ${borderColor}` : ""} relative
|
|
2056
|
+
${reorderable ? "cursor-move" : ""}
|
|
2057
|
+
${isDragging ? "opacity-50" : ""}
|
|
2058
|
+
${isDragOver ? "bg-accent-100" : ""}
|
|
1726
2059
|
`}
|
|
1727
2060
|
style={getColumnStyle(column, dynamicWidth)}
|
|
1728
2061
|
>
|
|
@@ -1763,13 +2096,11 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1763
2096
|
role="rowgroup"
|
|
1764
2097
|
aria-label="Table data"
|
|
1765
2098
|
>
|
|
1766
|
-
{loading && data.length === 0
|
|
1767
|
-
renderLoadingSkeleton()
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
renderDataRows()
|
|
1772
|
-
)}
|
|
2099
|
+
{loading && data.length === 0
|
|
2100
|
+
? renderLoadingSkeleton()
|
|
2101
|
+
: data.length === 0
|
|
2102
|
+
? renderEmptyStateContent()
|
|
2103
|
+
: renderDataRows()}
|
|
1773
2104
|
</tbody>
|
|
1774
2105
|
</table>
|
|
1775
2106
|
</div>
|
|
@@ -1780,19 +2111,21 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1780
2111
|
<div
|
|
1781
2112
|
ref={tableContainerRef}
|
|
1782
2113
|
onScroll={handleScroll}
|
|
1783
|
-
style={{ height: virtualHeight, overflow:
|
|
2114
|
+
style={{ height: virtualHeight, overflow: "auto" }}
|
|
1784
2115
|
className="rounded-lg"
|
|
1785
2116
|
>
|
|
1786
2117
|
{tableContent}
|
|
1787
2118
|
</div>
|
|
1788
|
-
) :
|
|
2119
|
+
) : (
|
|
2120
|
+
tableContent
|
|
2121
|
+
);
|
|
1789
2122
|
|
|
1790
2123
|
// Calculate pagination values
|
|
1791
2124
|
const effectiveTotalItems = totalItems ?? data.length;
|
|
1792
2125
|
const totalPages = Math.ceil(effectiveTotalItems / pageSize);
|
|
1793
2126
|
|
|
1794
2127
|
// Page size selector options
|
|
1795
|
-
const pageSizeSelectOptions = pageSizeOptions.map(size => ({
|
|
2128
|
+
const pageSizeSelectOptions = pageSizeOptions.map((size) => ({
|
|
1796
2129
|
value: String(size),
|
|
1797
2130
|
label: `${size} per page`,
|
|
1798
2131
|
}));
|
|
@@ -1817,10 +2150,12 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1817
2150
|
<span className="text-sm text-ink-600">
|
|
1818
2151
|
{effectiveTotalItems > 0 ? (
|
|
1819
2152
|
<>
|
|
1820
|
-
Showing {(
|
|
2153
|
+
Showing {(currentPage - 1) * pageSize + 1} -{" "}
|
|
2154
|
+
{Math.min(currentPage * pageSize, effectiveTotalItems)} of{" "}
|
|
2155
|
+
{effectiveTotalItems}
|
|
1821
2156
|
</>
|
|
1822
2157
|
) : (
|
|
1823
|
-
|
|
2158
|
+
"No items"
|
|
1824
2159
|
)}
|
|
1825
2160
|
</span>
|
|
1826
2161
|
</div>
|
|
@@ -1836,9 +2171,8 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1836
2171
|
};
|
|
1837
2172
|
|
|
1838
2173
|
// Determine if we should show card view
|
|
1839
|
-
const shouldShowCardView =
|
|
1840
|
-
mobileView ===
|
|
1841
|
-
(mobileView === 'auto' && isMobileViewport);
|
|
2174
|
+
const shouldShowCardView =
|
|
2175
|
+
mobileView === "card" || (mobileView === "auto" && isMobileViewport);
|
|
1842
2176
|
|
|
1843
2177
|
// Card view content
|
|
1844
2178
|
const cardViewContent = shouldShowCardView ? (
|
|
@@ -1853,8 +2187,14 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1853
2187
|
onCardLongPress={(item, event) => {
|
|
1854
2188
|
if (enableContextMenu && allActions.length > 0) {
|
|
1855
2189
|
event.preventDefault();
|
|
1856
|
-
const clientX =
|
|
1857
|
-
|
|
2190
|
+
const clientX =
|
|
2191
|
+
"touches" in event
|
|
2192
|
+
? event.touches[0].clientX
|
|
2193
|
+
: (event as React.MouseEvent).clientX;
|
|
2194
|
+
const clientY =
|
|
2195
|
+
"touches" in event
|
|
2196
|
+
? event.touches[0].clientY
|
|
2197
|
+
: (event as React.MouseEvent).clientY;
|
|
1858
2198
|
setContextMenuState({
|
|
1859
2199
|
isOpen: true,
|
|
1860
2200
|
position: { x: clientX, y: clientY },
|
|
@@ -1893,7 +2233,13 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1893
2233
|
<Menu
|
|
1894
2234
|
items={convertActionsToMenuItems(contextMenuState.item)}
|
|
1895
2235
|
position={contextMenuState.position}
|
|
1896
|
-
onClose={() =>
|
|
2236
|
+
onClose={() =>
|
|
2237
|
+
setContextMenuState({
|
|
2238
|
+
isOpen: false,
|
|
2239
|
+
position: { x: 0, y: 0 },
|
|
2240
|
+
item: null,
|
|
2241
|
+
})
|
|
2242
|
+
}
|
|
1897
2243
|
/>
|
|
1898
2244
|
)}
|
|
1899
2245
|
</>
|