@ceed/cds 1.19.1 → 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 +164 -18
- package/dist/index.js +164 -18
- 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
|
@@ -628,11 +628,13 @@ function Autocomplete(props) {
|
|
|
628
628
|
);
|
|
629
629
|
const slotProps = (0, import_react6.useMemo)(
|
|
630
630
|
() => ({
|
|
631
|
+
...props.slotProps,
|
|
631
632
|
listbox: {
|
|
633
|
+
...props.slotProps?.listbox,
|
|
632
634
|
hasSecondaryText: options.some((opt) => opt.secondaryText)
|
|
633
635
|
}
|
|
634
636
|
}),
|
|
635
|
-
[options]
|
|
637
|
+
[options, props.slotProps]
|
|
636
638
|
);
|
|
637
639
|
const handleChange = (0, import_react6.useCallback)(
|
|
638
640
|
(event, value2) => {
|
|
@@ -3280,6 +3282,7 @@ function useDataTableRenderer({
|
|
|
3280
3282
|
const onSelectionModelChangeRef = (0, import_react25.useRef)(onSelectionModelChange);
|
|
3281
3283
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3282
3284
|
const [focusedRowId, setFocusedRowId] = (0, import_react25.useState)(null);
|
|
3285
|
+
const [selectionAnchor, setSelectionAnchor] = (0, import_react25.useState)(null);
|
|
3283
3286
|
const [sortModel, setSortModel] = useControlledState(
|
|
3284
3287
|
controlledSortModel,
|
|
3285
3288
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3357,6 +3360,29 @@ function useDataTableRenderer({
|
|
|
3357
3360
|
}),
|
|
3358
3361
|
[dataInPage, isRowSelectable, getId]
|
|
3359
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
|
+
);
|
|
3360
3386
|
const isAllSelected = (0, import_react25.useMemo)(
|
|
3361
3387
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3362
3388
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3456,7 +3482,15 @@ function useDataTableRenderer({
|
|
|
3456
3482
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3457
3483
|
(0, import_react25.useEffect)(() => {
|
|
3458
3484
|
onSelectionModelChangeRef.current?.([]);
|
|
3485
|
+
setSelectionAnchor(null);
|
|
3459
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]);
|
|
3460
3494
|
return {
|
|
3461
3495
|
rowCount,
|
|
3462
3496
|
selectableRowCount,
|
|
@@ -3487,15 +3521,34 @@ function useDataTableRenderer({
|
|
|
3487
3521
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3488
3522
|
onCheckboxChange: (0, import_react25.useCallback)(
|
|
3489
3523
|
(event, selectedModel) => {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
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);
|
|
3493
3528
|
} else {
|
|
3494
|
-
const
|
|
3495
|
-
|
|
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
|
+
});
|
|
3496
3541
|
}
|
|
3497
3542
|
},
|
|
3498
|
-
[
|
|
3543
|
+
[
|
|
3544
|
+
selectionModel,
|
|
3545
|
+
onSelectionModelChange,
|
|
3546
|
+
selectedModelSet,
|
|
3547
|
+
dataInPage,
|
|
3548
|
+
getId,
|
|
3549
|
+
selectionAnchor,
|
|
3550
|
+
handleRangeSelection
|
|
3551
|
+
]
|
|
3499
3552
|
),
|
|
3500
3553
|
columns,
|
|
3501
3554
|
processedColumnGroups,
|
|
@@ -3505,7 +3558,10 @@ function useDataTableRenderer({
|
|
|
3505
3558
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3506
3559
|
});
|
|
3507
3560
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3508
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3561
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3562
|
+
// Selection anchor for keyboard navigation
|
|
3563
|
+
selectionAnchor,
|
|
3564
|
+
setSelectionAnchor
|
|
3509
3565
|
};
|
|
3510
3566
|
}
|
|
3511
3567
|
|
|
@@ -3872,7 +3928,10 @@ function Component(props, apiRef) {
|
|
|
3872
3928
|
onRowFocus,
|
|
3873
3929
|
onTotalSelect,
|
|
3874
3930
|
HeadCell: HeadCell2,
|
|
3875
|
-
BodyRow: BodyRow2
|
|
3931
|
+
BodyRow: BodyRow2,
|
|
3932
|
+
// For keyboard selection
|
|
3933
|
+
selectionAnchor,
|
|
3934
|
+
setSelectionAnchor
|
|
3876
3935
|
} = useDataTableRenderer(props);
|
|
3877
3936
|
const virtualizer = (0, import_react_virtual2.useVirtualizer)({
|
|
3878
3937
|
count: dataInPage.length,
|
|
@@ -3898,18 +3957,104 @@ function Component(props, apiRef) {
|
|
|
3898
3957
|
[onRowFocus]
|
|
3899
3958
|
);
|
|
3900
3959
|
const getCheckboxClickHandler = (0, import_react28.useCallback)(
|
|
3901
|
-
() => (e) => {
|
|
3902
|
-
e.stopPropagation();
|
|
3903
|
-
},
|
|
3904
|
-
[]
|
|
3905
|
-
);
|
|
3906
|
-
const getCheckboxChangeHandler = (0, import_react28.useCallback)(
|
|
3907
3960
|
(rowId, row) => (e) => {
|
|
3961
|
+
e.stopPropagation();
|
|
3908
3962
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3909
3963
|
onCheckboxChange(e, rowId);
|
|
3910
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
|
+
});
|
|
3911
4043
|
},
|
|
3912
|
-
[
|
|
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
|
+
]
|
|
3913
4058
|
);
|
|
3914
4059
|
(0, import_react28.useImperativeHandle)(apiRef, () => ({
|
|
3915
4060
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4060,6 +4205,7 @@ function Component(props, apiRef) {
|
|
|
4060
4205
|
VirtualizedTableBody,
|
|
4061
4206
|
{
|
|
4062
4207
|
ref: tableBodyRef,
|
|
4208
|
+
onKeyDown: handleTableKeyDown,
|
|
4063
4209
|
style: {
|
|
4064
4210
|
height: `${totalSize}px`
|
|
4065
4211
|
},
|
|
@@ -4122,10 +4268,10 @@ function Component(props, apiRef) {
|
|
|
4122
4268
|
/* @__PURE__ */ import_react28.default.createElement(
|
|
4123
4269
|
RenderCheckbox,
|
|
4124
4270
|
{
|
|
4125
|
-
onClick: getCheckboxClickHandler(),
|
|
4126
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4271
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4127
4272
|
checked: isSelectedRow(rowId),
|
|
4128
4273
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4274
|
+
tabIndex: -1,
|
|
4129
4275
|
...checkboxProps
|
|
4130
4276
|
}
|
|
4131
4277
|
)
|
package/dist/index.js
CHANGED
|
@@ -541,11 +541,13 @@ function Autocomplete(props) {
|
|
|
541
541
|
);
|
|
542
542
|
const slotProps = useMemo(
|
|
543
543
|
() => ({
|
|
544
|
+
...props.slotProps,
|
|
544
545
|
listbox: {
|
|
546
|
+
...props.slotProps?.listbox,
|
|
545
547
|
hasSecondaryText: options.some((opt) => opt.secondaryText)
|
|
546
548
|
}
|
|
547
549
|
}),
|
|
548
|
-
[options]
|
|
550
|
+
[options, props.slotProps]
|
|
549
551
|
);
|
|
550
552
|
const handleChange = useCallback2(
|
|
551
553
|
(event, value2) => {
|
|
@@ -3220,6 +3222,7 @@ function useDataTableRenderer({
|
|
|
3220
3222
|
const onSelectionModelChangeRef = useRef5(onSelectionModelChange);
|
|
3221
3223
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3222
3224
|
const [focusedRowId, setFocusedRowId] = useState8(null);
|
|
3225
|
+
const [selectionAnchor, setSelectionAnchor] = useState8(null);
|
|
3223
3226
|
const [sortModel, setSortModel] = useControlledState(
|
|
3224
3227
|
controlledSortModel,
|
|
3225
3228
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3297,6 +3300,29 @@ function useDataTableRenderer({
|
|
|
3297
3300
|
}),
|
|
3298
3301
|
[dataInPage, isRowSelectable, getId]
|
|
3299
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
|
+
);
|
|
3300
3326
|
const isAllSelected = useMemo9(
|
|
3301
3327
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3302
3328
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3396,7 +3422,15 @@ function useDataTableRenderer({
|
|
|
3396
3422
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3397
3423
|
useEffect5(() => {
|
|
3398
3424
|
onSelectionModelChangeRef.current?.([]);
|
|
3425
|
+
setSelectionAnchor(null);
|
|
3399
3426
|
}, [page]);
|
|
3427
|
+
const prevRowsRef = useRef5(_rows);
|
|
3428
|
+
useEffect5(() => {
|
|
3429
|
+
if (prevRowsRef.current !== _rows) {
|
|
3430
|
+
setSelectionAnchor(null);
|
|
3431
|
+
prevRowsRef.current = _rows;
|
|
3432
|
+
}
|
|
3433
|
+
}, [_rows]);
|
|
3400
3434
|
return {
|
|
3401
3435
|
rowCount,
|
|
3402
3436
|
selectableRowCount,
|
|
@@ -3427,15 +3461,34 @@ function useDataTableRenderer({
|
|
|
3427
3461
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3428
3462
|
onCheckboxChange: useCallback9(
|
|
3429
3463
|
(event, selectedModel) => {
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
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);
|
|
3433
3468
|
} else {
|
|
3434
|
-
const
|
|
3435
|
-
|
|
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
|
+
});
|
|
3436
3481
|
}
|
|
3437
3482
|
},
|
|
3438
|
-
[
|
|
3483
|
+
[
|
|
3484
|
+
selectionModel,
|
|
3485
|
+
onSelectionModelChange,
|
|
3486
|
+
selectedModelSet,
|
|
3487
|
+
dataInPage,
|
|
3488
|
+
getId,
|
|
3489
|
+
selectionAnchor,
|
|
3490
|
+
handleRangeSelection
|
|
3491
|
+
]
|
|
3439
3492
|
),
|
|
3440
3493
|
columns,
|
|
3441
3494
|
processedColumnGroups,
|
|
@@ -3445,7 +3498,10 @@ function useDataTableRenderer({
|
|
|
3445
3498
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3446
3499
|
});
|
|
3447
3500
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3448
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3501
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3502
|
+
// Selection anchor for keyboard navigation
|
|
3503
|
+
selectionAnchor,
|
|
3504
|
+
setSelectionAnchor
|
|
3449
3505
|
};
|
|
3450
3506
|
}
|
|
3451
3507
|
|
|
@@ -3812,7 +3868,10 @@ function Component(props, apiRef) {
|
|
|
3812
3868
|
onRowFocus,
|
|
3813
3869
|
onTotalSelect,
|
|
3814
3870
|
HeadCell: HeadCell2,
|
|
3815
|
-
BodyRow: BodyRow2
|
|
3871
|
+
BodyRow: BodyRow2,
|
|
3872
|
+
// For keyboard selection
|
|
3873
|
+
selectionAnchor,
|
|
3874
|
+
setSelectionAnchor
|
|
3816
3875
|
} = useDataTableRenderer(props);
|
|
3817
3876
|
const virtualizer = useVirtualizer2({
|
|
3818
3877
|
count: dataInPage.length,
|
|
@@ -3838,18 +3897,104 @@ function Component(props, apiRef) {
|
|
|
3838
3897
|
[onRowFocus]
|
|
3839
3898
|
);
|
|
3840
3899
|
const getCheckboxClickHandler = useCallback11(
|
|
3841
|
-
() => (e) => {
|
|
3842
|
-
e.stopPropagation();
|
|
3843
|
-
},
|
|
3844
|
-
[]
|
|
3845
|
-
);
|
|
3846
|
-
const getCheckboxChangeHandler = useCallback11(
|
|
3847
3900
|
(rowId, row) => (e) => {
|
|
3901
|
+
e.stopPropagation();
|
|
3848
3902
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3849
3903
|
onCheckboxChange(e, rowId);
|
|
3850
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
|
+
});
|
|
3851
3983
|
},
|
|
3852
|
-
[
|
|
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
|
+
]
|
|
3853
3998
|
);
|
|
3854
3999
|
useImperativeHandle2(apiRef, () => ({
|
|
3855
4000
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4000,6 +4145,7 @@ function Component(props, apiRef) {
|
|
|
4000
4145
|
VirtualizedTableBody,
|
|
4001
4146
|
{
|
|
4002
4147
|
ref: tableBodyRef,
|
|
4148
|
+
onKeyDown: handleTableKeyDown,
|
|
4003
4149
|
style: {
|
|
4004
4150
|
height: `${totalSize}px`
|
|
4005
4151
|
},
|
|
@@ -4062,10 +4208,10 @@ function Component(props, apiRef) {
|
|
|
4062
4208
|
/* @__PURE__ */ React25.createElement(
|
|
4063
4209
|
RenderCheckbox,
|
|
4064
4210
|
{
|
|
4065
|
-
onClick: getCheckboxClickHandler(),
|
|
4066
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4211
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4067
4212
|
checked: isSelectedRow(rowId),
|
|
4068
4213
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4214
|
+
tabIndex: -1,
|
|
4069
4215
|
...checkboxProps
|
|
4070
4216
|
}
|
|
4071
4217
|
)
|