@ceed/ads 1.20.1 → 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 +164 -18
- package/dist/index.js +164 -18
- 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
|
@@ -627,11 +627,13 @@ function Autocomplete(props) {
|
|
|
627
627
|
);
|
|
628
628
|
const slotProps = (0, import_react6.useMemo)(
|
|
629
629
|
() => ({
|
|
630
|
+
...props.slotProps,
|
|
630
631
|
listbox: {
|
|
632
|
+
...props.slotProps?.listbox,
|
|
631
633
|
hasSecondaryText: options.some((opt) => opt.secondaryText)
|
|
632
634
|
}
|
|
633
635
|
}),
|
|
634
|
-
[options]
|
|
636
|
+
[options, props.slotProps]
|
|
635
637
|
);
|
|
636
638
|
const handleChange = (0, import_react6.useCallback)(
|
|
637
639
|
(event, value2) => {
|
|
@@ -3279,6 +3281,7 @@ function useDataTableRenderer({
|
|
|
3279
3281
|
const onSelectionModelChangeRef = (0, import_react25.useRef)(onSelectionModelChange);
|
|
3280
3282
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3281
3283
|
const [focusedRowId, setFocusedRowId] = (0, import_react25.useState)(null);
|
|
3284
|
+
const [selectionAnchor, setSelectionAnchor] = (0, import_react25.useState)(null);
|
|
3282
3285
|
const [sortModel, setSortModel] = useControlledState(
|
|
3283
3286
|
controlledSortModel,
|
|
3284
3287
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3356,6 +3359,29 @@ function useDataTableRenderer({
|
|
|
3356
3359
|
}),
|
|
3357
3360
|
[dataInPage, isRowSelectable, getId]
|
|
3358
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
|
+
);
|
|
3359
3385
|
const isAllSelected = (0, import_react25.useMemo)(
|
|
3360
3386
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3361
3387
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3455,7 +3481,15 @@ function useDataTableRenderer({
|
|
|
3455
3481
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3456
3482
|
(0, import_react25.useEffect)(() => {
|
|
3457
3483
|
onSelectionModelChangeRef.current?.([]);
|
|
3484
|
+
setSelectionAnchor(null);
|
|
3458
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]);
|
|
3459
3493
|
return {
|
|
3460
3494
|
rowCount,
|
|
3461
3495
|
selectableRowCount,
|
|
@@ -3486,15 +3520,34 @@ function useDataTableRenderer({
|
|
|
3486
3520
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3487
3521
|
onCheckboxChange: (0, import_react25.useCallback)(
|
|
3488
3522
|
(event, selectedModel) => {
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
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);
|
|
3492
3527
|
} else {
|
|
3493
|
-
const
|
|
3494
|
-
|
|
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
|
+
});
|
|
3495
3540
|
}
|
|
3496
3541
|
},
|
|
3497
|
-
[
|
|
3542
|
+
[
|
|
3543
|
+
selectionModel,
|
|
3544
|
+
onSelectionModelChange,
|
|
3545
|
+
selectedModelSet,
|
|
3546
|
+
dataInPage,
|
|
3547
|
+
getId,
|
|
3548
|
+
selectionAnchor,
|
|
3549
|
+
handleRangeSelection
|
|
3550
|
+
]
|
|
3498
3551
|
),
|
|
3499
3552
|
columns,
|
|
3500
3553
|
processedColumnGroups,
|
|
@@ -3504,7 +3557,10 @@ function useDataTableRenderer({
|
|
|
3504
3557
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3505
3558
|
});
|
|
3506
3559
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3507
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3560
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3561
|
+
// Selection anchor for keyboard navigation
|
|
3562
|
+
selectionAnchor,
|
|
3563
|
+
setSelectionAnchor
|
|
3508
3564
|
};
|
|
3509
3565
|
}
|
|
3510
3566
|
|
|
@@ -3871,7 +3927,10 @@ function Component(props, apiRef) {
|
|
|
3871
3927
|
onRowFocus,
|
|
3872
3928
|
onTotalSelect,
|
|
3873
3929
|
HeadCell: HeadCell2,
|
|
3874
|
-
BodyRow: BodyRow2
|
|
3930
|
+
BodyRow: BodyRow2,
|
|
3931
|
+
// For keyboard selection
|
|
3932
|
+
selectionAnchor,
|
|
3933
|
+
setSelectionAnchor
|
|
3875
3934
|
} = useDataTableRenderer(props);
|
|
3876
3935
|
const virtualizer = (0, import_react_virtual2.useVirtualizer)({
|
|
3877
3936
|
count: dataInPage.length,
|
|
@@ -3897,18 +3956,104 @@ function Component(props, apiRef) {
|
|
|
3897
3956
|
[onRowFocus]
|
|
3898
3957
|
);
|
|
3899
3958
|
const getCheckboxClickHandler = (0, import_react28.useCallback)(
|
|
3900
|
-
() => (e) => {
|
|
3901
|
-
e.stopPropagation();
|
|
3902
|
-
},
|
|
3903
|
-
[]
|
|
3904
|
-
);
|
|
3905
|
-
const getCheckboxChangeHandler = (0, import_react28.useCallback)(
|
|
3906
3959
|
(rowId, row) => (e) => {
|
|
3960
|
+
e.stopPropagation();
|
|
3907
3961
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3908
3962
|
onCheckboxChange(e, rowId);
|
|
3909
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
|
+
});
|
|
3910
4042
|
},
|
|
3911
|
-
[
|
|
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
|
+
]
|
|
3912
4057
|
);
|
|
3913
4058
|
(0, import_react28.useImperativeHandle)(apiRef, () => ({
|
|
3914
4059
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -4059,6 +4204,7 @@ function Component(props, apiRef) {
|
|
|
4059
4204
|
VirtualizedTableBody,
|
|
4060
4205
|
{
|
|
4061
4206
|
ref: tableBodyRef,
|
|
4207
|
+
onKeyDown: handleTableKeyDown,
|
|
4062
4208
|
style: {
|
|
4063
4209
|
height: `${totalSize}px`
|
|
4064
4210
|
},
|
|
@@ -4121,10 +4267,10 @@ function Component(props, apiRef) {
|
|
|
4121
4267
|
/* @__PURE__ */ import_react28.default.createElement(
|
|
4122
4268
|
RenderCheckbox,
|
|
4123
4269
|
{
|
|
4124
|
-
onClick: getCheckboxClickHandler(),
|
|
4125
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4270
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4126
4271
|
checked: isSelectedRow(rowId),
|
|
4127
4272
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4273
|
+
tabIndex: -1,
|
|
4128
4274
|
...checkboxProps
|
|
4129
4275
|
}
|
|
4130
4276
|
)
|
package/dist/index.js
CHANGED
|
@@ -531,11 +531,13 @@ function Autocomplete(props) {
|
|
|
531
531
|
);
|
|
532
532
|
const slotProps = useMemo(
|
|
533
533
|
() => ({
|
|
534
|
+
...props.slotProps,
|
|
534
535
|
listbox: {
|
|
536
|
+
...props.slotProps?.listbox,
|
|
535
537
|
hasSecondaryText: options.some((opt) => opt.secondaryText)
|
|
536
538
|
}
|
|
537
539
|
}),
|
|
538
|
-
[options]
|
|
540
|
+
[options, props.slotProps]
|
|
539
541
|
);
|
|
540
542
|
const handleChange = useCallback2(
|
|
541
543
|
(event, value2) => {
|
|
@@ -3210,6 +3212,7 @@ function useDataTableRenderer({
|
|
|
3210
3212
|
const onSelectionModelChangeRef = useRef5(onSelectionModelChange);
|
|
3211
3213
|
onSelectionModelChangeRef.current = onSelectionModelChange;
|
|
3212
3214
|
const [focusedRowId, setFocusedRowId] = useState8(null);
|
|
3215
|
+
const [selectionAnchor, setSelectionAnchor] = useState8(null);
|
|
3213
3216
|
const [sortModel, setSortModel] = useControlledState(
|
|
3214
3217
|
controlledSortModel,
|
|
3215
3218
|
initialState?.sorting?.sortModel ?? [],
|
|
@@ -3287,6 +3290,29 @@ function useDataTableRenderer({
|
|
|
3287
3290
|
}),
|
|
3288
3291
|
[dataInPage, isRowSelectable, getId]
|
|
3289
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
|
+
);
|
|
3290
3316
|
const isAllSelected = useMemo9(
|
|
3291
3317
|
() => selectableDataInPage.length > 0 && selectableDataInPage.every((row, i) => selectedModelSet.has(getId(row, i))),
|
|
3292
3318
|
[selectableDataInPage, selectedModelSet, getId]
|
|
@@ -3386,7 +3412,15 @@ function useDataTableRenderer({
|
|
|
3386
3412
|
}, [page, rowCount, pageSize, handlePageChange]);
|
|
3387
3413
|
useEffect5(() => {
|
|
3388
3414
|
onSelectionModelChangeRef.current?.([]);
|
|
3415
|
+
setSelectionAnchor(null);
|
|
3389
3416
|
}, [page]);
|
|
3417
|
+
const prevRowsRef = useRef5(_rows);
|
|
3418
|
+
useEffect5(() => {
|
|
3419
|
+
if (prevRowsRef.current !== _rows) {
|
|
3420
|
+
setSelectionAnchor(null);
|
|
3421
|
+
prevRowsRef.current = _rows;
|
|
3422
|
+
}
|
|
3423
|
+
}, [_rows]);
|
|
3390
3424
|
return {
|
|
3391
3425
|
rowCount,
|
|
3392
3426
|
selectableRowCount,
|
|
@@ -3417,15 +3451,34 @@ function useDataTableRenderer({
|
|
|
3417
3451
|
}, [isAllSelected, selectableDataInPage, onSelectionModelChange, getId]),
|
|
3418
3452
|
onCheckboxChange: useCallback9(
|
|
3419
3453
|
(event, selectedModel) => {
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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);
|
|
3423
3458
|
} else {
|
|
3424
|
-
const
|
|
3425
|
-
|
|
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
|
+
});
|
|
3426
3471
|
}
|
|
3427
3472
|
},
|
|
3428
|
-
[
|
|
3473
|
+
[
|
|
3474
|
+
selectionModel,
|
|
3475
|
+
onSelectionModelChange,
|
|
3476
|
+
selectedModelSet,
|
|
3477
|
+
dataInPage,
|
|
3478
|
+
getId,
|
|
3479
|
+
selectionAnchor,
|
|
3480
|
+
handleRangeSelection
|
|
3481
|
+
]
|
|
3429
3482
|
),
|
|
3430
3483
|
columns,
|
|
3431
3484
|
processedColumnGroups,
|
|
@@ -3435,7 +3488,10 @@ function useDataTableRenderer({
|
|
|
3435
3488
|
return isRowSelectable({ row, id: getId(row, i) });
|
|
3436
3489
|
});
|
|
3437
3490
|
onSelectionModelChange?.(isTotalSelected ? [] : selectableRows.map(getId), !isTotalSelected);
|
|
3438
|
-
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable])
|
|
3491
|
+
}, [isTotalSelected, rows, onSelectionModelChange, getId, isRowSelectable]),
|
|
3492
|
+
// Selection anchor for keyboard navigation
|
|
3493
|
+
selectionAnchor,
|
|
3494
|
+
setSelectionAnchor
|
|
3439
3495
|
};
|
|
3440
3496
|
}
|
|
3441
3497
|
|
|
@@ -3802,7 +3858,10 @@ function Component(props, apiRef) {
|
|
|
3802
3858
|
onRowFocus,
|
|
3803
3859
|
onTotalSelect,
|
|
3804
3860
|
HeadCell: HeadCell2,
|
|
3805
|
-
BodyRow: BodyRow2
|
|
3861
|
+
BodyRow: BodyRow2,
|
|
3862
|
+
// For keyboard selection
|
|
3863
|
+
selectionAnchor,
|
|
3864
|
+
setSelectionAnchor
|
|
3806
3865
|
} = useDataTableRenderer(props);
|
|
3807
3866
|
const virtualizer = useVirtualizer2({
|
|
3808
3867
|
count: dataInPage.length,
|
|
@@ -3828,18 +3887,104 @@ function Component(props, apiRef) {
|
|
|
3828
3887
|
[onRowFocus]
|
|
3829
3888
|
);
|
|
3830
3889
|
const getCheckboxClickHandler = useCallback11(
|
|
3831
|
-
() => (e) => {
|
|
3832
|
-
e.stopPropagation();
|
|
3833
|
-
},
|
|
3834
|
-
[]
|
|
3835
|
-
);
|
|
3836
|
-
const getCheckboxChangeHandler = useCallback11(
|
|
3837
3890
|
(rowId, row) => (e) => {
|
|
3891
|
+
e.stopPropagation();
|
|
3838
3892
|
if (isRowSelectableCheck(rowId, row)) {
|
|
3839
3893
|
onCheckboxChange(e, rowId);
|
|
3840
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
|
+
});
|
|
3841
3973
|
},
|
|
3842
|
-
[
|
|
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
|
+
]
|
|
3843
3988
|
);
|
|
3844
3989
|
useImperativeHandle2(apiRef, () => ({
|
|
3845
3990
|
getRowIndexRelativeToVisibleRows(rowId) {
|
|
@@ -3990,6 +4135,7 @@ function Component(props, apiRef) {
|
|
|
3990
4135
|
VirtualizedTableBody,
|
|
3991
4136
|
{
|
|
3992
4137
|
ref: tableBodyRef,
|
|
4138
|
+
onKeyDown: handleTableKeyDown,
|
|
3993
4139
|
style: {
|
|
3994
4140
|
height: `${totalSize}px`
|
|
3995
4141
|
},
|
|
@@ -4052,10 +4198,10 @@ function Component(props, apiRef) {
|
|
|
4052
4198
|
/* @__PURE__ */ React25.createElement(
|
|
4053
4199
|
RenderCheckbox,
|
|
4054
4200
|
{
|
|
4055
|
-
onClick: getCheckboxClickHandler(),
|
|
4056
|
-
onChange: getCheckboxChangeHandler(rowId, row),
|
|
4201
|
+
onClick: getCheckboxClickHandler(rowId, row),
|
|
4057
4202
|
checked: isSelectedRow(rowId),
|
|
4058
4203
|
disabled: !isRowSelectableCheck(rowId, row),
|
|
4204
|
+
tabIndex: -1,
|
|
4059
4205
|
...checkboxProps
|
|
4060
4206
|
}
|
|
4061
4207
|
)
|