@ceed/ads 1.20.2 → 1.21.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 +37 -37
- 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
|
@@ -3281,6 +3281,7 @@ function useDataTableRenderer({
|
|
|
3281
3281
|
const onSelectionModelChangeRef = (0, import_react25.useRef)(onSelectionModelChange);
|
|
3282
3282
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3283
3283
|
const [focusedRowId, setFocusedRowId] = (0, import_react25.useState)(null);
|
|
3284
|
+
const [selectionAnchor, setSelectionAnchor] = (0, import_react25.useState)(null);
|
|
3284
3285
|
const [sortModel, setSortModel] = useControlledState(
|
|
3285
3286
|
controlledSortModel,
|
|
3286
3287
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3358,6 +3359,29 @@ function useDataTableRenderer({
|
|
|
3358
3359
|
}),
|
|
3359
3360
|
[dataInPage, isRowSelectable, getId]
|
|
3360
3361
|
);
|
|
3362
|
+
const handleRangeSelection = (0, import_react25.useCallback)(
|
|
3363
|
+
(anchor, targetIndex) => {
|
|
3364
|
+
const startIndex = Math.min(anchor.rowIndex, targetIndex);
|
|
3365
|
+
const endIndex = Math.max(anchor.rowIndex, targetIndex);
|
|
3366
|
+
const rowIdsInRange = [];
|
|
3367
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
3368
|
+
const row = dataInPage[i];
|
|
3369
|
+
if (!row) continue;
|
|
3370
|
+
const rowId = getId(row, i);
|
|
3371
|
+
if (isRowSelectable && !isRowSelectable({ row, id: rowId })) continue;
|
|
3372
|
+
rowIdsInRange.push(rowId);
|
|
3373
|
+
}
|
|
3374
|
+
if (anchor.wasSelected) {
|
|
3375
|
+
const currentSet = new Set(selectionModel || []);
|
|
3376
|
+
rowIdsInRange.forEach((id) => currentSet.add(id));
|
|
3377
|
+
onSelectionModelChange?.(Array.from(currentSet));
|
|
3378
|
+
} else {
|
|
3379
|
+
const removeSet = new Set(rowIdsInRange);
|
|
3380
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => !removeSet.has(id)));
|
|
3381
|
+
}
|
|
3382
|
+
},
|
|
3383
|
+
[dataInPage, getId, isRowSelectable, selectionModel, onSelectionModelChange]
|
|
3384
|
+
);
|
|
3361
3385
|
const isAllSelected = (0, import_react25.useMemo)(
|
|
3362
3386
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3363
3387
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3457,7 +3481,15 @@ function useDataTableRenderer({
|
|
|
3457
3481
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3458
3482
|
(0, import_react25.useEffect)(() => {
|
|
3459
3483
|
onSelectionModelChangeRef.current?.([]);
|
|
3484
|
+
setSelectionAnchor(null);
|
|
3460
3485
|
}, [page]);
|
|
3486
|
+
const prevRowsRef = (0, import_react25.useRef)(_rows);
|
|
3487
|
+
(0, import_react25.useEffect)(() => {
|
|
3488
|
+
if (prevRowsRef.current !== _rows) {
|
|
3489
|
+
setSelectionAnchor(null);
|
|
3490
|
+
prevRowsRef.current = _rows;
|
|
3491
|
+
}
|
|
3492
|
+
}, [_rows]);
|
|
3461
3493
|
return {
|
|
3462
3494
|
rowCount,
|
|
3463
3495
|
selectableRowCount,
|
|
@@ -3488,15 +3520,34 @@ function useDataTableRenderer({
|
|
|
3488
3520
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3489
3521
|
onCheckboxChange: (0, import_react25.useCallback)(
|
|
3490
3522
|
(event, selectedModel) => {
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3523
|
+
const isShiftClick = "shiftKey" in event && event.shiftKey;
|
|
3524
|
+
const rowIndex = dataInPage.findIndex((row, i) => getId(row, i) === selectedModel);
|
|
3525
|
+
if (isShiftClick && selectionAnchor !== null) {
|
|
3526
|
+
handleRangeSelection(selectionAnchor, rowIndex);
|
|
3494
3527
|
} else {
|
|
3495
|
-
const
|
|
3496
|
-
|
|
3528
|
+
const isCurrentlySelected = selectedModelSet.has(selectedModel);
|
|
3529
|
+
const newIsSelected = !isCurrentlySelected;
|
|
3530
|
+
if (isCurrentlySelected) {
|
|
3531
|
+
onSelectionModelChange?.((selectionModel || []).filter((m) => m !== selectedModel));
|
|
3532
|
+
} else {
|
|
3533
|
+
onSelectionModelChange?.([...selectionModel || [], selectedModel]);
|
|
3534
|
+
}
|
|
3535
|
+
setSelectionAnchor({
|
|
3536
|
+
rowId: selectedModel,
|
|
3537
|
+
rowIndex,
|
|
3538
|
+
wasSelected: newIsSelected
|
|
3539
|
+
});
|
|
3497
3540
|
}
|
|
3498
3541
|
},
|
|
3499
|
-
[
|
|
3542
|
+
[
|
|
3543
|
+
selectionModel,
|
|
3544
|
+
onSelectionModelChange,
|
|
3545
|
+
selectedModelSet,
|
|
3546
|
+
dataInPage,
|
|
3547
|
+
getId,
|
|
3548
|
+
selectionAnchor,
|
|
3549
|
+
handleRangeSelection
|
|
3550
|
+
]
|
|
3500
3551
|
),
|
|
3501
3552
|
columns,
|
|
3502
3553
|
processedColumnGroups,
|
|
@@ -3506,7 +3557,10 @@ function useDataTableRenderer({
|
|
|
3506
3557
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3507
3558
|
});
|
|
3508
3559
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3509
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3560
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3561
|
+
// Selection anchor for keyboard navigation
|
|
3562
|
+
selectionAnchor,
|
|
3563
|
+
setSelectionAnchor
|
|
3510
3564
|
};
|
|
3511
3565
|
}
|
|
3512
3566
|
|
|
@@ -3873,7 +3927,10 @@ function Component(props, apiRef) {
|
|
|
3873
3927
|
onRowFocus,
|
|
3874
3928
|
onTotalSelect,
|
|
3875
3929
|
HeadCell: HeadCell2,
|
|
3876
|
-
BodyRow: BodyRow2
|
|
3930
|
+
BodyRow: BodyRow2,
|
|
3931
|
+
// For keyboard selection
|
|
3932
|
+
selectionAnchor,
|
|
3933
|
+
setSelectionAnchor
|
|
3877
3934
|
} = useDataTableRenderer(props);
|
|
3878
3935
|
const virtualizer = (0, import_react_virtual2.useVirtualizer)({
|
|
3879
3936
|
count: dataInPage.length,
|
|
@@ -3899,18 +3956,104 @@ function Component(props, apiRef) {
|
|
|
3899
3956
|
[onRowFocus]
|
|
3900
3957
|
);
|
|
3901
3958
|
const getCheckboxClickHandler = (0, import_react28.useCallback)(
|
|
3902
|
-
() => (e) => {
|
|
3903
|
-
e.stopPropagation();
|
|
3904
|
-
},
|
|
3905
|
-
[]
|
|
3906
|
-
);
|
|
3907
|
-
const getCheckboxChangeHandler = (0, import_react28.useCallback)(
|
|
3908
3959
|
(rowId, row) => (e) => {
|
|
3960
|
+
e.stopPropagation();
|
|
3909
3961
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3910
3962
|
onCheckboxChange(e, rowId);
|
|
3911
3963
|
}
|
|
3964
|
+
onRowFocus(rowId);
|
|
3965
|
+
},
|
|
3966
|
+
[onCheckboxChange, isRowSelectableCheck, onRowFocus]
|
|
3967
|
+
);
|
|
3968
|
+
const handleTableKeyDown = (0, import_react28.useCallback)(
|
|
3969
|
+
(e) => {
|
|
3970
|
+
const supportedKeys = ["ArrowUp", "ArrowDown", " ", "Home", "End", "PageUp", "PageDown"];
|
|
3971
|
+
if (!supportedKeys.includes(e.key)) return;
|
|
3972
|
+
const activeElement = document.activeElement;
|
|
3973
|
+
const currentRowId = activeElement?.closest("[data-row-id]")?.getAttribute("data-row-id");
|
|
3974
|
+
if (!currentRowId) return;
|
|
3975
|
+
const currentIndex = dataInPage.findIndex((row, i) => String(getId(row, i)) === currentRowId);
|
|
3976
|
+
if (currentIndex === -1) return;
|
|
3977
|
+
if (e.key === " ") {
|
|
3978
|
+
e.preventDefault();
|
|
3979
|
+
const row = dataInPage[currentIndex];
|
|
3980
|
+
const rowId = getId(row, currentIndex);
|
|
3981
|
+
if (checkboxSelection && isRowSelectableCheck(rowId, row)) {
|
|
3982
|
+
onCheckboxChange(e, rowId);
|
|
3983
|
+
}
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
let nextIndex;
|
|
3987
|
+
switch (e.key) {
|
|
3988
|
+
case "ArrowUp":
|
|
3989
|
+
nextIndex = currentIndex - 1;
|
|
3990
|
+
break;
|
|
3991
|
+
case "ArrowDown":
|
|
3992
|
+
nextIndex = currentIndex + 1;
|
|
3993
|
+
break;
|
|
3994
|
+
case "Home":
|
|
3995
|
+
nextIndex = 0;
|
|
3996
|
+
break;
|
|
3997
|
+
case "End":
|
|
3998
|
+
nextIndex = dataInPage.length - 1;
|
|
3999
|
+
break;
|
|
4000
|
+
case "PageUp":
|
|
4001
|
+
nextIndex = Math.max(0, currentIndex - 10);
|
|
4002
|
+
break;
|
|
4003
|
+
case "PageDown":
|
|
4004
|
+
nextIndex = Math.min(dataInPage.length - 1, currentIndex + 10);
|
|
4005
|
+
break;
|
|
4006
|
+
default:
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
if (nextIndex < 0 || nextIndex >= dataInPage.length) return;
|
|
4010
|
+
if (nextIndex === currentIndex) return;
|
|
4011
|
+
e.preventDefault();
|
|
4012
|
+
const nextRow = dataInPage[nextIndex];
|
|
4013
|
+
const nextRowId = getId(nextRow, nextIndex);
|
|
4014
|
+
if (e.shiftKey && checkboxSelection && isRowSelectableCheck(nextRowId, nextRow)) {
|
|
4015
|
+
let anchor = selectionAnchor;
|
|
4016
|
+
if (anchor === null) {
|
|
4017
|
+
const currentRow = dataInPage[currentIndex];
|
|
4018
|
+
const currentRowId2 = getId(currentRow, currentIndex);
|
|
4019
|
+
const currentIsSelected = isSelectedRow(currentRowId2);
|
|
4020
|
+
anchor = {
|
|
4021
|
+
rowId: currentRowId2,
|
|
4022
|
+
rowIndex: currentIndex,
|
|
4023
|
+
wasSelected: currentIsSelected
|
|
4024
|
+
};
|
|
4025
|
+
setSelectionAnchor(anchor);
|
|
4026
|
+
}
|
|
4027
|
+
const targetShouldBeSelected = anchor.wasSelected;
|
|
4028
|
+
const isCurrentlySelected = isSelectedRow(nextRowId);
|
|
4029
|
+
if (targetShouldBeSelected !== isCurrentlySelected) {
|
|
4030
|
+
if (targetShouldBeSelected) {
|
|
4031
|
+
onSelectionModelChange?.([...selectionModel || [], nextRowId]);
|
|
4032
|
+
} else {
|
|
4033
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => id !== nextRowId));
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
onRowFocus(nextRowId);
|
|
4038
|
+
virtualizer.scrollToIndex(nextIndex, { align: "auto" });
|
|
4039
|
+
requestAnimationFrame(() => {
|
|
4040
|
+
tableBodyRef.current?.querySelector(`[data-row-id='${nextRowId}']`)?.focus();
|
|
4041
|
+
});
|
|
3912
4042
|
},
|
|
3913
|
-
[
|
|
4043
|
+
[
|
|
4044
|
+
dataInPage,
|
|
4045
|
+
getId,
|
|
4046
|
+
checkboxSelection,
|
|
4047
|
+
isRowSelectableCheck,
|
|
4048
|
+
isSelectedRow,
|
|
4049
|
+
onCheckboxChange,
|
|
4050
|
+
selectionAnchor,
|
|
4051
|
+
setSelectionAnchor,
|
|
4052
|
+
selectionModel,
|
|
4053
|
+
onSelectionModelChange,
|
|
4054
|
+
onRowFocus,
|
|
4055
|
+
virtualizer
|
|
4056
|
+
]
|
|
3914
4057
|
);
|
|
3915
4058
|
(0, import_react28.useImperativeHandle)(apiRef, () => ({
|
|
3916
4059
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4061,6 +4204,7 @@ function Component(props, apiRef) {
|
|
|
4061
4204
|
VirtualizedTableBody,
|
|
4062
4205
|
{
|
|
4063
4206
|
ref: tableBodyRef,
|
|
4207
|
+
onKeyDown: handleTableKeyDown,
|
|
4064
4208
|
style: {
|
|
4065
4209
|
height: `${totalSize}px`
|
|
4066
4210
|
},
|
|
@@ -4123,10 +4267,10 @@ function Component(props, apiRef) {
|
|
|
4123
4267
|
/* @__PURE__ */ import_react28.default.createElement(
|
|
4124
4268
|
RenderCheckbox,
|
|
4125
4269
|
{
|
|
4126
|
-
onClick: getCheckboxClickHandler(),
|
|
4127
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4270
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4128
4271
|
checked: isSelectedRow(rowId),
|
|
4129
4272
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4273
|
+
tabIndex: -1,
|
|
4130
4274
|
...checkboxProps
|
|
4131
4275
|
}
|
|
4132
4276
|
)
|
package/dist/index.js
CHANGED
|
@@ -3212,6 +3212,7 @@ function useDataTableRenderer({
|
|
|
3212
3212
|
const onSelectionModelChangeRef = useRef5(onSelectionModelChange);
|
|
3213
3213
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3214
3214
|
const [focusedRowId, setFocusedRowId] = useState8(null);
|
|
3215
|
+
const [selectionAnchor, setSelectionAnchor] = useState8(null);
|
|
3215
3216
|
const [sortModel, setSortModel] = useControlledState(
|
|
3216
3217
|
controlledSortModel,
|
|
3217
3218
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3289,6 +3290,29 @@ function useDataTableRenderer({
|
|
|
3289
3290
|
}),
|
|
3290
3291
|
[dataInPage, isRowSelectable, getId]
|
|
3291
3292
|
);
|
|
3293
|
+
const handleRangeSelection = useCallback9(
|
|
3294
|
+
(anchor, targetIndex) => {
|
|
3295
|
+
const startIndex = Math.min(anchor.rowIndex, targetIndex);
|
|
3296
|
+
const endIndex = Math.max(anchor.rowIndex, targetIndex);
|
|
3297
|
+
const rowIdsInRange = [];
|
|
3298
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
3299
|
+
const row = dataInPage[i];
|
|
3300
|
+
if (!row) continue;
|
|
3301
|
+
const rowId = getId(row, i);
|
|
3302
|
+
if (isRowSelectable && !isRowSelectable({ row, id: rowId })) continue;
|
|
3303
|
+
rowIdsInRange.push(rowId);
|
|
3304
|
+
}
|
|
3305
|
+
if (anchor.wasSelected) {
|
|
3306
|
+
const currentSet = new Set(selectionModel || []);
|
|
3307
|
+
rowIdsInRange.forEach((id) => currentSet.add(id));
|
|
3308
|
+
onSelectionModelChange?.(Array.from(currentSet));
|
|
3309
|
+
} else {
|
|
3310
|
+
const removeSet = new Set(rowIdsInRange);
|
|
3311
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => !removeSet.has(id)));
|
|
3312
|
+
}
|
|
3313
|
+
},
|
|
3314
|
+
[dataInPage, getId, isRowSelectable, selectionModel, onSelectionModelChange]
|
|
3315
|
+
);
|
|
3292
3316
|
const isAllSelected = useMemo9(
|
|
3293
3317
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3294
3318
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3388,7 +3412,15 @@ function useDataTableRenderer({
|
|
|
3388
3412
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3389
3413
|
useEffect5(() => {
|
|
3390
3414
|
onSelectionModelChangeRef.current?.([]);
|
|
3415
|
+
setSelectionAnchor(null);
|
|
3391
3416
|
}, [page]);
|
|
3417
|
+
const prevRowsRef = useRef5(_rows);
|
|
3418
|
+
useEffect5(() => {
|
|
3419
|
+
if (prevRowsRef.current !== _rows) {
|
|
3420
|
+
setSelectionAnchor(null);
|
|
3421
|
+
prevRowsRef.current = _rows;
|
|
3422
|
+
}
|
|
3423
|
+
}, [_rows]);
|
|
3392
3424
|
return {
|
|
3393
3425
|
rowCount,
|
|
3394
3426
|
selectableRowCount,
|
|
@@ -3419,15 +3451,34 @@ function useDataTableRenderer({
|
|
|
3419
3451
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3420
3452
|
onCheckboxChange: useCallback9(
|
|
3421
3453
|
(event, selectedModel) => {
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3454
|
+
const isShiftClick = "shiftKey" in event && event.shiftKey;
|
|
3455
|
+
const rowIndex = dataInPage.findIndex((row, i) => getId(row, i) === selectedModel);
|
|
3456
|
+
if (isShiftClick && selectionAnchor !== null) {
|
|
3457
|
+
handleRangeSelection(selectionAnchor, rowIndex);
|
|
3425
3458
|
} else {
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3459
|
+
const isCurrentlySelected = selectedModelSet.has(selectedModel);
|
|
3460
|
+
const newIsSelected = !isCurrentlySelected;
|
|
3461
|
+
if (isCurrentlySelected) {
|
|
3462
|
+
onSelectionModelChange?.((selectionModel || []).filter((m) => m !== selectedModel));
|
|
3463
|
+
} else {
|
|
3464
|
+
onSelectionModelChange?.([...selectionModel || [], selectedModel]);
|
|
3465
|
+
}
|
|
3466
|
+
setSelectionAnchor({
|
|
3467
|
+
rowId: selectedModel,
|
|
3468
|
+
rowIndex,
|
|
3469
|
+
wasSelected: newIsSelected
|
|
3470
|
+
});
|
|
3428
3471
|
}
|
|
3429
3472
|
},
|
|
3430
|
-
[
|
|
3473
|
+
[
|
|
3474
|
+
selectionModel,
|
|
3475
|
+
onSelectionModelChange,
|
|
3476
|
+
selectedModelSet,
|
|
3477
|
+
dataInPage,
|
|
3478
|
+
getId,
|
|
3479
|
+
selectionAnchor,
|
|
3480
|
+
handleRangeSelection
|
|
3481
|
+
]
|
|
3431
3482
|
),
|
|
3432
3483
|
columns,
|
|
3433
3484
|
processedColumnGroups,
|
|
@@ -3437,7 +3488,10 @@ function useDataTableRenderer({
|
|
|
3437
3488
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3438
3489
|
});
|
|
3439
3490
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3440
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3491
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3492
|
+
// Selection anchor for keyboard navigation
|
|
3493
|
+
selectionAnchor,
|
|
3494
|
+
setSelectionAnchor
|
|
3441
3495
|
};
|
|
3442
3496
|
}
|
|
3443
3497
|
|
|
@@ -3804,7 +3858,10 @@ function Component(props, apiRef) {
|
|
|
3804
3858
|
onRowFocus,
|
|
3805
3859
|
onTotalSelect,
|
|
3806
3860
|
HeadCell: HeadCell2,
|
|
3807
|
-
BodyRow: BodyRow2
|
|
3861
|
+
BodyRow: BodyRow2,
|
|
3862
|
+
// For keyboard selection
|
|
3863
|
+
selectionAnchor,
|
|
3864
|
+
setSelectionAnchor
|
|
3808
3865
|
} = useDataTableRenderer(props);
|
|
3809
3866
|
const virtualizer = useVirtualizer2({
|
|
3810
3867
|
count: dataInPage.length,
|
|
@@ -3830,18 +3887,104 @@ function Component(props, apiRef) {
|
|
|
3830
3887
|
[onRowFocus]
|
|
3831
3888
|
);
|
|
3832
3889
|
const getCheckboxClickHandler = useCallback11(
|
|
3833
|
-
() => (e) => {
|
|
3834
|
-
e.stopPropagation();
|
|
3835
|
-
},
|
|
3836
|
-
[]
|
|
3837
|
-
);
|
|
3838
|
-
const getCheckboxChangeHandler = useCallback11(
|
|
3839
3890
|
(rowId, row) => (e) => {
|
|
3891
|
+
e.stopPropagation();
|
|
3840
3892
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3841
3893
|
onCheckboxChange(e, rowId);
|
|
3842
3894
|
}
|
|
3895
|
+
onRowFocus(rowId);
|
|
3896
|
+
},
|
|
3897
|
+
[onCheckboxChange, isRowSelectableCheck, onRowFocus]
|
|
3898
|
+
);
|
|
3899
|
+
const handleTableKeyDown = useCallback11(
|
|
3900
|
+
(e) => {
|
|
3901
|
+
const supportedKeys = ["ArrowUp", "ArrowDown", " ", "Home", "End", "PageUp", "PageDown"];
|
|
3902
|
+
if (!supportedKeys.includes(e.key)) return;
|
|
3903
|
+
const activeElement = document.activeElement;
|
|
3904
|
+
const currentRowId = activeElement?.closest("[data-row-id]")?.getAttribute("data-row-id");
|
|
3905
|
+
if (!currentRowId) return;
|
|
3906
|
+
const currentIndex = dataInPage.findIndex((row, i) => String(getId(row, i)) === currentRowId);
|
|
3907
|
+
if (currentIndex === -1) return;
|
|
3908
|
+
if (e.key === " ") {
|
|
3909
|
+
e.preventDefault();
|
|
3910
|
+
const row = dataInPage[currentIndex];
|
|
3911
|
+
const rowId = getId(row, currentIndex);
|
|
3912
|
+
if (checkboxSelection && isRowSelectableCheck(rowId, row)) {
|
|
3913
|
+
onCheckboxChange(e, rowId);
|
|
3914
|
+
}
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
let nextIndex;
|
|
3918
|
+
switch (e.key) {
|
|
3919
|
+
case "ArrowUp":
|
|
3920
|
+
nextIndex = currentIndex - 1;
|
|
3921
|
+
break;
|
|
3922
|
+
case "ArrowDown":
|
|
3923
|
+
nextIndex = currentIndex + 1;
|
|
3924
|
+
break;
|
|
3925
|
+
case "Home":
|
|
3926
|
+
nextIndex = 0;
|
|
3927
|
+
break;
|
|
3928
|
+
case "End":
|
|
3929
|
+
nextIndex = dataInPage.length - 1;
|
|
3930
|
+
break;
|
|
3931
|
+
case "PageUp":
|
|
3932
|
+
nextIndex = Math.max(0, currentIndex - 10);
|
|
3933
|
+
break;
|
|
3934
|
+
case "PageDown":
|
|
3935
|
+
nextIndex = Math.min(dataInPage.length - 1, currentIndex + 10);
|
|
3936
|
+
break;
|
|
3937
|
+
default:
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3940
|
+
if (nextIndex < 0 || nextIndex >= dataInPage.length) return;
|
|
3941
|
+
if (nextIndex === currentIndex) return;
|
|
3942
|
+
e.preventDefault();
|
|
3943
|
+
const nextRow = dataInPage[nextIndex];
|
|
3944
|
+
const nextRowId = getId(nextRow, nextIndex);
|
|
3945
|
+
if (e.shiftKey && checkboxSelection && isRowSelectableCheck(nextRowId, nextRow)) {
|
|
3946
|
+
let anchor = selectionAnchor;
|
|
3947
|
+
if (anchor === null) {
|
|
3948
|
+
const currentRow = dataInPage[currentIndex];
|
|
3949
|
+
const currentRowId2 = getId(currentRow, currentIndex);
|
|
3950
|
+
const currentIsSelected = isSelectedRow(currentRowId2);
|
|
3951
|
+
anchor = {
|
|
3952
|
+
rowId: currentRowId2,
|
|
3953
|
+
rowIndex: currentIndex,
|
|
3954
|
+
wasSelected: currentIsSelected
|
|
3955
|
+
};
|
|
3956
|
+
setSelectionAnchor(anchor);
|
|
3957
|
+
}
|
|
3958
|
+
const targetShouldBeSelected = anchor.wasSelected;
|
|
3959
|
+
const isCurrentlySelected = isSelectedRow(nextRowId);
|
|
3960
|
+
if (targetShouldBeSelected !== isCurrentlySelected) {
|
|
3961
|
+
if (targetShouldBeSelected) {
|
|
3962
|
+
onSelectionModelChange?.([...selectionModel || [], nextRowId]);
|
|
3963
|
+
} else {
|
|
3964
|
+
onSelectionModelChange?.((selectionModel || []).filter((id) => id !== nextRowId));
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
onRowFocus(nextRowId);
|
|
3969
|
+
virtualizer.scrollToIndex(nextIndex, { align: "auto" });
|
|
3970
|
+
requestAnimationFrame(() => {
|
|
3971
|
+
tableBodyRef.current?.querySelector(`[data-row-id='${nextRowId}']`)?.focus();
|
|
3972
|
+
});
|
|
3843
3973
|
},
|
|
3844
|
-
[
|
|
3974
|
+
[
|
|
3975
|
+
dataInPage,
|
|
3976
|
+
getId,
|
|
3977
|
+
checkboxSelection,
|
|
3978
|
+
isRowSelectableCheck,
|
|
3979
|
+
isSelectedRow,
|
|
3980
|
+
onCheckboxChange,
|
|
3981
|
+
selectionAnchor,
|
|
3982
|
+
setSelectionAnchor,
|
|
3983
|
+
selectionModel,
|
|
3984
|
+
onSelectionModelChange,
|
|
3985
|
+
onRowFocus,
|
|
3986
|
+
virtualizer
|
|
3987
|
+
]
|
|
3845
3988
|
);
|
|
3846
3989
|
useImperativeHandle2(apiRef, () => ({
|
|
3847
3990
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -3992,6 +4135,7 @@ function Component(props, apiRef) {
|
|
|
3992
4135
|
VirtualizedTableBody,
|
|
3993
4136
|
{
|
|
3994
4137
|
ref: tableBodyRef,
|
|
4138
|
+
onKeyDown: handleTableKeyDown,
|
|
3995
4139
|
style: {
|
|
3996
4140
|
height: `${totalSize}px`
|
|
3997
4141
|
},
|
|
@@ -4054,10 +4198,10 @@ function Component(props, apiRef) {
|
|
|
4054
4198
|
/* @__PURE__ */ React25.createElement(
|
|
4055
4199
|
RenderCheckbox,
|
|
4056
4200
|
{
|
|
4057
|
-
onClick: getCheckboxClickHandler(),
|
|
4058
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4201
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4059
4202
|
checked: isSelectedRow(rowId),
|
|
4060
4203
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4204
|
+
tabIndex: -1,
|
|
4061
4205
|
...checkboxProps
|
|
4062
4206
|
}
|
|
4063
4207
|
)
|