@ceed/cds 1.19.2 → 1.20.0
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/hooks.d.ts +11 -1
- package/dist/components/data-display/DataTable.md +71 -1
- package/dist/index.cjs +161 -17
- package/dist/index.js +161 -17
- package/framer/index.js +36 -36
- package/package.json +2 -1
|
@@ -45,7 +45,7 @@ export declare function useDataTableRenderer<T extends Record<PropertyKey, unkno
|
|
|
45
45
|
focusedRowId: InferredIdType<T, GetId> | null;
|
|
46
46
|
onRowFocus: (rowId: InferredIdType<T, GetId>) => void;
|
|
47
47
|
onAllCheckboxChange: () => void;
|
|
48
|
-
onCheckboxChange: (event:
|
|
48
|
+
onCheckboxChange: (event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, selectedModel: InferredIdType<T, GetId>) => void;
|
|
49
49
|
columns: ColumnDef<T, InferredIdType<T, GetId>>[];
|
|
50
50
|
processedColumnGroups: {
|
|
51
51
|
groups: import("./types").ProcessedColumnGroup[][];
|
|
@@ -53,4 +53,14 @@ export declare function useDataTableRenderer<T extends Record<PropertyKey, unkno
|
|
|
53
53
|
fieldsInGroupingModel: Set<keyof T>;
|
|
54
54
|
} | null;
|
|
55
55
|
onTotalSelect: () => void;
|
|
56
|
+
selectionAnchor: {
|
|
57
|
+
rowId: InferredIdType<T, GetId>;
|
|
58
|
+
rowIndex: number;
|
|
59
|
+
wasSelected: boolean;
|
|
60
|
+
} | null;
|
|
61
|
+
setSelectionAnchor: import("react").Dispatch<import("react").SetStateAction<{
|
|
62
|
+
rowId: InferredIdType<T, GetId>;
|
|
63
|
+
rowIndex: number;
|
|
64
|
+
wasSelected: boolean;
|
|
65
|
+
} | null>>;
|
|
56
66
|
};
|
|
@@ -377,12 +377,82 @@ const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
|
|
377
377
|
/>;
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
## Keyboard Accessibility
|
|
381
|
+
|
|
382
|
+
DataTable supports keyboard interactions for improved accessibility.
|
|
383
|
+
|
|
384
|
+
### Shift+Click Range Selection
|
|
385
|
+
|
|
386
|
+
Hold the Shift key while clicking checkboxes to select or deselect a range of rows at once.
|
|
387
|
+
The selection state is applied based on the last clicked checkbox's resulting state.
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
<Stack spacing={2}>
|
|
391
|
+
<Typography level="body-sm">
|
|
392
|
+
Hold Shift and click checkboxes to select a range of rows. The selection state is applied based on the first
|
|
393
|
+
clicked checkbox's resulting state.
|
|
394
|
+
</Typography>
|
|
395
|
+
<DataTable {...args} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
|
|
396
|
+
<Typography level="body-xs">Selected IDs: {selectionModel.join(', ') || 'None'}</Typography>
|
|
397
|
+
</Stack>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**How it works:**
|
|
401
|
+
|
|
402
|
+
1. Click a checkbox to select/deselect a row (this becomes the "anchor")
|
|
403
|
+
2. Hold Shift and click another checkbox
|
|
404
|
+
3. All rows between the anchor and the clicked row will be set to the same state
|
|
405
|
+
|
|
406
|
+
**Edge cases handled:**
|
|
407
|
+
|
|
408
|
+
- Works seamlessly with virtual scrolling (rows don't need to be visible)
|
|
409
|
+
- Selection anchor resets when data changes or page changes
|
|
410
|
+
- Respects `isRowSelectable` - non-selectable rows are skipped in range
|
|
411
|
+
|
|
412
|
+
### Keyboard Navigation
|
|
413
|
+
|
|
414
|
+
When a row has focus, use keyboard shortcuts to navigate and interact with rows.
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
<Stack spacing={2}>
|
|
418
|
+
<Typography level="body-sm">
|
|
419
|
+
Click a row to focus, then use keyboard to navigate. Arrow Up/Down to move, Space to toggle selection,
|
|
420
|
+
Home/End to jump to first/last row, PageUp/PageDown to move by 10 rows. Hold Shift with any navigation key to
|
|
421
|
+
extend selection.
|
|
422
|
+
</Typography>
|
|
423
|
+
<DataTable {...args} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
|
|
424
|
+
<Typography level="body-xs">Selected IDs: {selectionModel.join(', ') || 'None'}</Typography>
|
|
425
|
+
</Stack>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Supported keys:**
|
|
429
|
+
|
|
430
|
+
| Key | Action |
|
|
431
|
+
| ---------------------- | ------------------------------------------- |
|
|
432
|
+
| `↑` Arrow Up | Move focus to previous row |
|
|
433
|
+
| `↓` Arrow Down | Move focus to next row |
|
|
434
|
+
| `Space` | Toggle checkbox selection |
|
|
435
|
+
| `Home` | Move focus to first row |
|
|
436
|
+
| `End` | Move focus to last row |
|
|
437
|
+
| `Page Up` | Move focus up by 10 rows |
|
|
438
|
+
| `Page Down` | Move focus down by 10 rows |
|
|
439
|
+
| `Shift + ↑/↓` | Move focus and extend selection |
|
|
440
|
+
| `Shift + Home/End` | Jump to first/last row and extend selection |
|
|
441
|
+
| `Shift + Page Up/Down` | Move by 10 rows and extend selection |
|
|
442
|
+
|
|
443
|
+
**Notes:**
|
|
444
|
+
|
|
445
|
+
- Click any row first to establish focus
|
|
446
|
+
- Tab key skips checkboxes and moves directly between rows
|
|
447
|
+
- Works with virtual scrolling - target row is automatically scrolled into view
|
|
448
|
+
- Follows WAI-ARIA Grid pattern for accessibility
|
|
449
|
+
|
|
380
450
|
## Styles and Layout
|
|
381
451
|
|
|
382
452
|
### Back Office Style
|
|
383
453
|
|
|
384
454
|
```tsx
|
|
385
|
-
<DataTable rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} hoverRow={args.hoverRow} noWrap={args.noWrap} stripe={args.stripe} stickyHeader={args.stickyHeader} slots={args.slots} slotProps={args.slotProps} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
|
|
455
|
+
<DataTable rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} hoverRow={args.hoverRow} noWrap={args.noWrap} stripe={args.stripe} stickyHeader={args.stickyHeader} slots={args.slots} slotProps={args.slotProps} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} pagination={args.pagination} />
|
|
386
456
|
```
|
|
387
457
|
|
|
388
458
|
#### Commonly Used Table Options
|
package/dist/index.cjs
CHANGED
|
@@ -3282,6 +3282,7 @@ function useDataTableRenderer({
|
|
|
3282
3282
|
const onSelectionModelChangeRef = (0, import_react25.useRef)(onSelectionModelChange);
|
|
3283
3283
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3284
3284
|
const [focusedRowId, setFocusedRowId] = (0, import_react25.useState)(null);
|
|
3285
|
+
const [selectionAnchor, setSelectionAnchor] = (0, import_react25.useState)(null);
|
|
3285
3286
|
const [sortModel, setSortModel] = useControlledState(
|
|
3286
3287
|
controlledSortModel,
|
|
3287
3288
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3359,6 +3360,29 @@ function useDataTableRenderer({
|
|
|
3359
3360
|
}),
|
|
3360
3361
|
[dataInPage, isRowSelectable, getId]
|
|
3361
3362
|
);
|
|
3363
|
+
const handleRangeSelection = (0, import_react25.useCallback)(
|
|
3364
|
+
(anchor, targetIndex) => {
|
|
3365
|
+
const startIndex = Math.min(anchor.rowIndex, targetIndex);
|
|
3366
|
+
const endIndex = Math.max(anchor.rowIndex, targetIndex);
|
|
3367
|
+
const rowIdsInRange = [];
|
|
3368
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
3369
|
+
const row = dataInPage[i];
|
|
3370
|
+
if (!row) continue;
|
|
3371
|
+
const rowId = getId(row, i);
|
|
3372
|
+
if (isRowSelectable && !isRowSelectable({ row, id: rowId })) continue;
|
|
3373
|
+
rowIdsInRange.push(rowId);
|
|
3374
|
+
}
|
|
3375
|
+
if (anchor.wasSelected) {
|
|
3376
|
+
const currentSet = new Set(selectionModel || []);
|
|
3377
|
+
rowIdsInRange.forEach((id) => currentSet.add(id));
|
|
3378
|
+
onSelectionModelChange?.(Array.from(currentSet));
|
|
3379
|
+
} else {
|
|
3380
|
+
const removeSet = new Set(rowIdsInRange);
|
|
3381
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => !removeSet.has(id)));
|
|
3382
|
+
}
|
|
3383
|
+
},
|
|
3384
|
+
[dataInPage, getId, isRowSelectable, selectionModel, onSelectionModelChange]
|
|
3385
|
+
);
|
|
3362
3386
|
const isAllSelected = (0, import_react25.useMemo)(
|
|
3363
3387
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3364
3388
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3458,7 +3482,15 @@ function useDataTableRenderer({
|
|
|
3458
3482
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3459
3483
|
(0, import_react25.useEffect)(() => {
|
|
3460
3484
|
onSelectionModelChangeRef.current?.([]);
|
|
3485
|
+
setSelectionAnchor(null);
|
|
3461
3486
|
}, [page]);
|
|
3487
|
+
const prevRowsRef = (0, import_react25.useRef)(_rows);
|
|
3488
|
+
(0, import_react25.useEffect)(() => {
|
|
3489
|
+
if (prevRowsRef.current !== _rows) {
|
|
3490
|
+
setSelectionAnchor(null);
|
|
3491
|
+
prevRowsRef.current = _rows;
|
|
3492
|
+
}
|
|
3493
|
+
}, [_rows]);
|
|
3462
3494
|
return {
|
|
3463
3495
|
rowCount,
|
|
3464
3496
|
selectableRowCount,
|
|
@@ -3489,15 +3521,34 @@ function useDataTableRenderer({
|
|
|
3489
3521
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3490
3522
|
onCheckboxChange: (0, import_react25.useCallback)(
|
|
3491
3523
|
(event, selectedModel) => {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3524
|
+
const isShiftClick = "shiftKey" in event && event.shiftKey;
|
|
3525
|
+
const rowIndex = dataInPage.findIndex((row, i) => getId(row, i) === selectedModel);
|
|
3526
|
+
if (isShiftClick && selectionAnchor !== null) {
|
|
3527
|
+
handleRangeSelection(selectionAnchor, rowIndex);
|
|
3495
3528
|
} else {
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3529
|
+
const isCurrentlySelected = selectedModelSet.has(selectedModel);
|
|
3530
|
+
const newIsSelected = !isCurrentlySelected;
|
|
3531
|
+
if (isCurrentlySelected) {
|
|
3532
|
+
onSelectionModelChange?.((selectionModel || []).filter((m) => m !== selectedModel));
|
|
3533
|
+
} else {
|
|
3534
|
+
onSelectionModelChange?.([...selectionModel || [], selectedModel]);
|
|
3535
|
+
}
|
|
3536
|
+
setSelectionAnchor({
|
|
3537
|
+
rowId: selectedModel,
|
|
3538
|
+
rowIndex,
|
|
3539
|
+
wasSelected: newIsSelected
|
|
3540
|
+
});
|
|
3498
3541
|
}
|
|
3499
3542
|
},
|
|
3500
|
-
[
|
|
3543
|
+
[
|
|
3544
|
+
selectionModel,
|
|
3545
|
+
onSelectionModelChange,
|
|
3546
|
+
selectedModelSet,
|
|
3547
|
+
dataInPage,
|
|
3548
|
+
getId,
|
|
3549
|
+
selectionAnchor,
|
|
3550
|
+
handleRangeSelection
|
|
3551
|
+
]
|
|
3501
3552
|
),
|
|
3502
3553
|
columns,
|
|
3503
3554
|
processedColumnGroups,
|
|
@@ -3507,7 +3558,10 @@ function useDataTableRenderer({
|
|
|
3507
3558
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3508
3559
|
});
|
|
3509
3560
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3510
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3561
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3562
|
+
// Selection anchor for keyboard navigation
|
|
3563
|
+
selectionAnchor,
|
|
3564
|
+
setSelectionAnchor
|
|
3511
3565
|
};
|
|
3512
3566
|
}
|
|
3513
3567
|
|
|
@@ -3874,7 +3928,10 @@ function Component(props, apiRef) {
|
|
|
3874
3928
|
onRowFocus,
|
|
3875
3929
|
onTotalSelect,
|
|
3876
3930
|
HeadCell: HeadCell2,
|
|
3877
|
-
BodyRow: BodyRow2
|
|
3931
|
+
BodyRow: BodyRow2,
|
|
3932
|
+
// For keyboard selection
|
|
3933
|
+
selectionAnchor,
|
|
3934
|
+
setSelectionAnchor
|
|
3878
3935
|
} = useDataTableRenderer(props);
|
|
3879
3936
|
const virtualizer = (0, import_react_virtual2.useVirtualizer)({
|
|
3880
3937
|
count: dataInPage.length,
|
|
@@ -3900,18 +3957,104 @@ function Component(props, apiRef) {
|
|
|
3900
3957
|
[onRowFocus]
|
|
3901
3958
|
);
|
|
3902
3959
|
const getCheckboxClickHandler = (0, import_react28.useCallback)(
|
|
3903
|
-
() => (e) => {
|
|
3904
|
-
e.stopPropagation();
|
|
3905
|
-
},
|
|
3906
|
-
[]
|
|
3907
|
-
);
|
|
3908
|
-
const getCheckboxChangeHandler = (0, import_react28.useCallback)(
|
|
3909
3960
|
(rowId, row) => (e) => {
|
|
3961
|
+
e.stopPropagation();
|
|
3910
3962
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3911
3963
|
onCheckboxChange(e, rowId);
|
|
3912
3964
|
}
|
|
3965
|
+
onRowFocus(rowId);
|
|
3966
|
+
},
|
|
3967
|
+
[onCheckboxChange, isRowSelectableCheck, onRowFocus]
|
|
3968
|
+
);
|
|
3969
|
+
const handleTableKeyDown = (0, import_react28.useCallback)(
|
|
3970
|
+
(e) => {
|
|
3971
|
+
const supportedKeys = ["ArrowUp", "ArrowDown", " ", "Home", "End", "PageUp", "PageDown"];
|
|
3972
|
+
if (!supportedKeys.includes(e.key)) return;
|
|
3973
|
+
const activeElement = document.activeElement;
|
|
3974
|
+
const currentRowId = activeElement?.closest("[data-row-id]")?.getAttribute("data-row-id");
|
|
3975
|
+
if (!currentRowId) return;
|
|
3976
|
+
const currentIndex = dataInPage.findIndex((row, i) => String(getId(row, i)) === currentRowId);
|
|
3977
|
+
if (currentIndex === -1) return;
|
|
3978
|
+
if (e.key === " ") {
|
|
3979
|
+
e.preventDefault();
|
|
3980
|
+
const row = dataInPage[currentIndex];
|
|
3981
|
+
const rowId = getId(row, currentIndex);
|
|
3982
|
+
if (checkboxSelection && isRowSelectableCheck(rowId, row)) {
|
|
3983
|
+
onCheckboxChange(e, rowId);
|
|
3984
|
+
}
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
let nextIndex;
|
|
3988
|
+
switch (e.key) {
|
|
3989
|
+
case "ArrowUp":
|
|
3990
|
+
nextIndex = currentIndex - 1;
|
|
3991
|
+
break;
|
|
3992
|
+
case "ArrowDown":
|
|
3993
|
+
nextIndex = currentIndex + 1;
|
|
3994
|
+
break;
|
|
3995
|
+
case "Home":
|
|
3996
|
+
nextIndex = 0;
|
|
3997
|
+
break;
|
|
3998
|
+
case "End":
|
|
3999
|
+
nextIndex = dataInPage.length - 1;
|
|
4000
|
+
break;
|
|
4001
|
+
case "PageUp":
|
|
4002
|
+
nextIndex = Math.max(0, currentIndex - 10);
|
|
4003
|
+
break;
|
|
4004
|
+
case "PageDown":
|
|
4005
|
+
nextIndex = Math.min(dataInPage.length - 1, currentIndex + 10);
|
|
4006
|
+
break;
|
|
4007
|
+
default:
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
if (nextIndex < 0 || nextIndex >= dataInPage.length) return;
|
|
4011
|
+
if (nextIndex === currentIndex) return;
|
|
4012
|
+
e.preventDefault();
|
|
4013
|
+
const nextRow = dataInPage[nextIndex];
|
|
4014
|
+
const nextRowId = getId(nextRow, nextIndex);
|
|
4015
|
+
if (e.shiftKey && checkboxSelection && isRowSelectableCheck(nextRowId, nextRow)) {
|
|
4016
|
+
let anchor = selectionAnchor;
|
|
4017
|
+
if (anchor === null) {
|
|
4018
|
+
const currentRow = dataInPage[currentIndex];
|
|
4019
|
+
const currentRowId2 = getId(currentRow, currentIndex);
|
|
4020
|
+
const currentIsSelected = isSelectedRow(currentRowId2);
|
|
4021
|
+
anchor = {
|
|
4022
|
+
rowId: currentRowId2,
|
|
4023
|
+
rowIndex: currentIndex,
|
|
4024
|
+
wasSelected: currentIsSelected
|
|
4025
|
+
};
|
|
4026
|
+
setSelectionAnchor(anchor);
|
|
4027
|
+
}
|
|
4028
|
+
const targetShouldBeSelected = anchor.wasSelected;
|
|
4029
|
+
const isCurrentlySelected = isSelectedRow(nextRowId);
|
|
4030
|
+
if (targetShouldBeSelected !== isCurrentlySelected) {
|
|
4031
|
+
if (targetShouldBeSelected) {
|
|
4032
|
+
onSelectionModelChange?.([...selectionModel || [], nextRowId]);
|
|
4033
|
+
} else {
|
|
4034
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => id !== nextRowId));
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
onRowFocus(nextRowId);
|
|
4039
|
+
virtualizer.scrollToIndex(nextIndex, { align: "auto" });
|
|
4040
|
+
requestAnimationFrame(() => {
|
|
4041
|
+
tableBodyRef.current?.querySelector(`[data-row-id='${nextRowId}']`)?.focus();
|
|
4042
|
+
});
|
|
3913
4043
|
},
|
|
3914
|
-
[
|
|
4044
|
+
[
|
|
4045
|
+
dataInPage,
|
|
4046
|
+
getId,
|
|
4047
|
+
checkboxSelection,
|
|
4048
|
+
isRowSelectableCheck,
|
|
4049
|
+
isSelectedRow,
|
|
4050
|
+
onCheckboxChange,
|
|
4051
|
+
selectionAnchor,
|
|
4052
|
+
setSelectionAnchor,
|
|
4053
|
+
selectionModel,
|
|
4054
|
+
onSelectionModelChange,
|
|
4055
|
+
onRowFocus,
|
|
4056
|
+
virtualizer
|
|
4057
|
+
]
|
|
3915
4058
|
);
|
|
3916
4059
|
(0, import_react28.useImperativeHandle)(apiRef, () => ({
|
|
3917
4060
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4062,6 +4205,7 @@ function Component(props, apiRef) {
|
|
|
4062
4205
|
VirtualizedTableBody,
|
|
4063
4206
|
{
|
|
4064
4207
|
ref: tableBodyRef,
|
|
4208
|
+
onKeyDown: handleTableKeyDown,
|
|
4065
4209
|
style: {
|
|
4066
4210
|
height: `${totalSize}px`
|
|
4067
4211
|
},
|
|
@@ -4124,10 +4268,10 @@ function Component(props, apiRef) {
|
|
|
4124
4268
|
/* @__PURE__ */ import_react28.default.createElement(
|
|
4125
4269
|
RenderCheckbox,
|
|
4126
4270
|
{
|
|
4127
|
-
onClick: getCheckboxClickHandler(),
|
|
4128
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4271
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4129
4272
|
checked: isSelectedRow(rowId),
|
|
4130
4273
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4274
|
+
tabIndex: -1,
|
|
4131
4275
|
...checkboxProps
|
|
4132
4276
|
}
|
|
4133
4277
|
)
|
package/dist/index.js
CHANGED
|
@@ -3222,6 +3222,7 @@ function useDataTableRenderer({
|
|
|
3222
3222
|
const onSelectionModelChangeRef = useRef5(onSelectionModelChange);
|
|
3223
3223
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3224
3224
|
const [focusedRowId, setFocusedRowId] = useState8(null);
|
|
3225
|
+
const [selectionAnchor, setSelectionAnchor] = useState8(null);
|
|
3225
3226
|
const [sortModel, setSortModel] = useControlledState(
|
|
3226
3227
|
controlledSortModel,
|
|
3227
3228
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3299,6 +3300,29 @@ function useDataTableRenderer({
|
|
|
3299
3300
|
}),
|
|
3300
3301
|
[dataInPage, isRowSelectable, getId]
|
|
3301
3302
|
);
|
|
3303
|
+
const handleRangeSelection = useCallback9(
|
|
3304
|
+
(anchor, targetIndex) => {
|
|
3305
|
+
const startIndex = Math.min(anchor.rowIndex, targetIndex);
|
|
3306
|
+
const endIndex = Math.max(anchor.rowIndex, targetIndex);
|
|
3307
|
+
const rowIdsInRange = [];
|
|
3308
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
3309
|
+
const row = dataInPage[i];
|
|
3310
|
+
if (!row) continue;
|
|
3311
|
+
const rowId = getId(row, i);
|
|
3312
|
+
if (isRowSelectable && !isRowSelectable({ row, id: rowId })) continue;
|
|
3313
|
+
rowIdsInRange.push(rowId);
|
|
3314
|
+
}
|
|
3315
|
+
if (anchor.wasSelected) {
|
|
3316
|
+
const currentSet = new Set(selectionModel || []);
|
|
3317
|
+
rowIdsInRange.forEach((id) => currentSet.add(id));
|
|
3318
|
+
onSelectionModelChange?.(Array.from(currentSet));
|
|
3319
|
+
} else {
|
|
3320
|
+
const removeSet = new Set(rowIdsInRange);
|
|
3321
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => !removeSet.has(id)));
|
|
3322
|
+
}
|
|
3323
|
+
},
|
|
3324
|
+
[dataInPage, getId, isRowSelectable, selectionModel, onSelectionModelChange]
|
|
3325
|
+
);
|
|
3302
3326
|
const isAllSelected = useMemo9(
|
|
3303
3327
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3304
3328
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3398,7 +3422,15 @@ function useDataTableRenderer({
|
|
|
3398
3422
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3399
3423
|
useEffect5(() => {
|
|
3400
3424
|
onSelectionModelChangeRef.current?.([]);
|
|
3425
|
+
setSelectionAnchor(null);
|
|
3401
3426
|
}, [page]);
|
|
3427
|
+
const prevRowsRef = useRef5(_rows);
|
|
3428
|
+
useEffect5(() => {
|
|
3429
|
+
if (prevRowsRef.current !== _rows) {
|
|
3430
|
+
setSelectionAnchor(null);
|
|
3431
|
+
prevRowsRef.current = _rows;
|
|
3432
|
+
}
|
|
3433
|
+
}, [_rows]);
|
|
3402
3434
|
return {
|
|
3403
3435
|
rowCount,
|
|
3404
3436
|
selectableRowCount,
|
|
@@ -3429,15 +3461,34 @@ function useDataTableRenderer({
|
|
|
3429
3461
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3430
3462
|
onCheckboxChange: useCallback9(
|
|
3431
3463
|
(event, selectedModel) => {
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3464
|
+
const isShiftClick = "shiftKey" in event && event.shiftKey;
|
|
3465
|
+
const rowIndex = dataInPage.findIndex((row, i) => getId(row, i) === selectedModel);
|
|
3466
|
+
if (isShiftClick && selectionAnchor !== null) {
|
|
3467
|
+
handleRangeSelection(selectionAnchor, rowIndex);
|
|
3435
3468
|
} else {
|
|
3436
|
-
const
|
|
3437
|
-
|
|
3469
|
+
const isCurrentlySelected = selectedModelSet.has(selectedModel);
|
|
3470
|
+
const newIsSelected = !isCurrentlySelected;
|
|
3471
|
+
if (isCurrentlySelected) {
|
|
3472
|
+
onSelectionModelChange?.((selectionModel || []).filter((m) => m !== selectedModel));
|
|
3473
|
+
} else {
|
|
3474
|
+
onSelectionModelChange?.([...selectionModel || [], selectedModel]);
|
|
3475
|
+
}
|
|
3476
|
+
setSelectionAnchor({
|
|
3477
|
+
rowId: selectedModel,
|
|
3478
|
+
rowIndex,
|
|
3479
|
+
wasSelected: newIsSelected
|
|
3480
|
+
});
|
|
3438
3481
|
}
|
|
3439
3482
|
},
|
|
3440
|
-
[
|
|
3483
|
+
[
|
|
3484
|
+
selectionModel,
|
|
3485
|
+
onSelectionModelChange,
|
|
3486
|
+
selectedModelSet,
|
|
3487
|
+
dataInPage,
|
|
3488
|
+
getId,
|
|
3489
|
+
selectionAnchor,
|
|
3490
|
+
handleRangeSelection
|
|
3491
|
+
]
|
|
3441
3492
|
),
|
|
3442
3493
|
columns,
|
|
3443
3494
|
processedColumnGroups,
|
|
@@ -3447,7 +3498,10 @@ function useDataTableRenderer({
|
|
|
3447
3498
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3448
3499
|
});
|
|
3449
3500
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3450
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3501
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3502
|
+
// Selection anchor for keyboard navigation
|
|
3503
|
+
selectionAnchor,
|
|
3504
|
+
setSelectionAnchor
|
|
3451
3505
|
};
|
|
3452
3506
|
}
|
|
3453
3507
|
|
|
@@ -3814,7 +3868,10 @@ function Component(props, apiRef) {
|
|
|
3814
3868
|
onRowFocus,
|
|
3815
3869
|
onTotalSelect,
|
|
3816
3870
|
HeadCell: HeadCell2,
|
|
3817
|
-
BodyRow: BodyRow2
|
|
3871
|
+
BodyRow: BodyRow2,
|
|
3872
|
+
// For keyboard selection
|
|
3873
|
+
selectionAnchor,
|
|
3874
|
+
setSelectionAnchor
|
|
3818
3875
|
} = useDataTableRenderer(props);
|
|
3819
3876
|
const virtualizer = useVirtualizer2({
|
|
3820
3877
|
count: dataInPage.length,
|
|
@@ -3840,18 +3897,104 @@ function Component(props, apiRef) {
|
|
|
3840
3897
|
[onRowFocus]
|
|
3841
3898
|
);
|
|
3842
3899
|
const getCheckboxClickHandler = useCallback11(
|
|
3843
|
-
() => (e) => {
|
|
3844
|
-
e.stopPropagation();
|
|
3845
|
-
},
|
|
3846
|
-
[]
|
|
3847
|
-
);
|
|
3848
|
-
const getCheckboxChangeHandler = useCallback11(
|
|
3849
3900
|
(rowId, row) => (e) => {
|
|
3901
|
+
e.stopPropagation();
|
|
3850
3902
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3851
3903
|
onCheckboxChange(e, rowId);
|
|
3852
3904
|
}
|
|
3905
|
+
onRowFocus(rowId);
|
|
3906
|
+
},
|
|
3907
|
+
[onCheckboxChange, isRowSelectableCheck, onRowFocus]
|
|
3908
|
+
);
|
|
3909
|
+
const handleTableKeyDown = useCallback11(
|
|
3910
|
+
(e) => {
|
|
3911
|
+
const supportedKeys = ["ArrowUp", "ArrowDown", " ", "Home", "End", "PageUp", "PageDown"];
|
|
3912
|
+
if (!supportedKeys.includes(e.key)) return;
|
|
3913
|
+
const activeElement = document.activeElement;
|
|
3914
|
+
const currentRowId = activeElement?.closest("[data-row-id]")?.getAttribute("data-row-id");
|
|
3915
|
+
if (!currentRowId) return;
|
|
3916
|
+
const currentIndex = dataInPage.findIndex((row, i) => String(getId(row, i)) === currentRowId);
|
|
3917
|
+
if (currentIndex === -1) return;
|
|
3918
|
+
if (e.key === " ") {
|
|
3919
|
+
e.preventDefault();
|
|
3920
|
+
const row = dataInPage[currentIndex];
|
|
3921
|
+
const rowId = getId(row, currentIndex);
|
|
3922
|
+
if (checkboxSelection && isRowSelectableCheck(rowId, row)) {
|
|
3923
|
+
onCheckboxChange(e, rowId);
|
|
3924
|
+
}
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
let nextIndex;
|
|
3928
|
+
switch (e.key) {
|
|
3929
|
+
case "ArrowUp":
|
|
3930
|
+
nextIndex = currentIndex - 1;
|
|
3931
|
+
break;
|
|
3932
|
+
case "ArrowDown":
|
|
3933
|
+
nextIndex = currentIndex + 1;
|
|
3934
|
+
break;
|
|
3935
|
+
case "Home":
|
|
3936
|
+
nextIndex = 0;
|
|
3937
|
+
break;
|
|
3938
|
+
case "End":
|
|
3939
|
+
nextIndex = dataInPage.length - 1;
|
|
3940
|
+
break;
|
|
3941
|
+
case "PageUp":
|
|
3942
|
+
nextIndex = Math.max(0, currentIndex - 10);
|
|
3943
|
+
break;
|
|
3944
|
+
case "PageDown":
|
|
3945
|
+
nextIndex = Math.min(dataInPage.length - 1, currentIndex + 10);
|
|
3946
|
+
break;
|
|
3947
|
+
default:
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (nextIndex < 0 || nextIndex >= dataInPage.length) return;
|
|
3951
|
+
if (nextIndex === currentIndex) return;
|
|
3952
|
+
e.preventDefault();
|
|
3953
|
+
const nextRow = dataInPage[nextIndex];
|
|
3954
|
+
const nextRowId = getId(nextRow, nextIndex);
|
|
3955
|
+
if (e.shiftKey && checkboxSelection && isRowSelectableCheck(nextRowId, nextRow)) {
|
|
3956
|
+
let anchor = selectionAnchor;
|
|
3957
|
+
if (anchor === null) {
|
|
3958
|
+
const currentRow = dataInPage[currentIndex];
|
|
3959
|
+
const currentRowId2 = getId(currentRow, currentIndex);
|
|
3960
|
+
const currentIsSelected = isSelectedRow(currentRowId2);
|
|
3961
|
+
anchor = {
|
|
3962
|
+
rowId: currentRowId2,
|
|
3963
|
+
rowIndex: currentIndex,
|
|
3964
|
+
wasSelected: currentIsSelected
|
|
3965
|
+
};
|
|
3966
|
+
setSelectionAnchor(anchor);
|
|
3967
|
+
}
|
|
3968
|
+
const targetShouldBeSelected = anchor.wasSelected;
|
|
3969
|
+
const isCurrentlySelected = isSelectedRow(nextRowId);
|
|
3970
|
+
if (targetShouldBeSelected !== isCurrentlySelected) {
|
|
3971
|
+
if (targetShouldBeSelected) {
|
|
3972
|
+
onSelectionModelChange?.([...selectionModel || [], nextRowId]);
|
|
3973
|
+
} else {
|
|
3974
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => id !== nextRowId));
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
onRowFocus(nextRowId);
|
|
3979
|
+
virtualizer.scrollToIndex(nextIndex, { align: "auto" });
|
|
3980
|
+
requestAnimationFrame(() => {
|
|
3981
|
+
tableBodyRef.current?.querySelector(`[data-row-id='${nextRowId}']`)?.focus();
|
|
3982
|
+
});
|
|
3853
3983
|
},
|
|
3854
|
-
[
|
|
3984
|
+
[
|
|
3985
|
+
dataInPage,
|
|
3986
|
+
getId,
|
|
3987
|
+
checkboxSelection,
|
|
3988
|
+
isRowSelectableCheck,
|
|
3989
|
+
isSelectedRow,
|
|
3990
|
+
onCheckboxChange,
|
|
3991
|
+
selectionAnchor,
|
|
3992
|
+
setSelectionAnchor,
|
|
3993
|
+
selectionModel,
|
|
3994
|
+
onSelectionModelChange,
|
|
3995
|
+
onRowFocus,
|
|
3996
|
+
virtualizer
|
|
3997
|
+
]
|
|
3855
3998
|
);
|
|
3856
3999
|
useImperativeHandle2(apiRef, () => ({
|
|
3857
4000
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4002,6 +4145,7 @@ function Component(props, apiRef) {
|
|
|
4002
4145
|
VirtualizedTableBody,
|
|
4003
4146
|
{
|
|
4004
4147
|
ref: tableBodyRef,
|
|
4148
|
+
onKeyDown: handleTableKeyDown,
|
|
4005
4149
|
style: {
|
|
4006
4150
|
height: `${totalSize}px`
|
|
4007
4151
|
},
|
|
@@ -4064,10 +4208,10 @@ function Component(props, apiRef) {
|
|
|
4064
4208
|
/* @__PURE__ */ React25.createElement(
|
|
4065
4209
|
RenderCheckbox,
|
|
4066
4210
|
{
|
|
4067
|
-
onClick: getCheckboxClickHandler(),
|
|
4068
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4211
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4069
4212
|
checked: isSelectedRow(rowId),
|
|
4070
4213
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4214
|
+
tabIndex: -1,
|
|
4071
4215
|
...checkboxProps
|
|
4072
4216
|
}
|
|
4073
4217
|
)
|