@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.
@@ -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: any, selectedModel: InferredIdType<T, GetId>) => void;
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&apos;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
- if (selectedModelSet.has(selectedModel)) {
3492
- const newSelectionModel = (selectionModel || []).filter((model) => model !== selectedModel);
3493
- onSelectionModelChange?.(newSelectionModel);
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 newSelectionModel = [...selectionModel || [], selectedModel];
3496
- onSelectionModelChange?.(newSelectionModel);
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
- [selectionModel, onSelectionModelChange, selectedModelSet]
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
- [onCheckboxChange, isRowSelectableCheck]
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
- if (selectedModelSet.has(selectedModel)) {
3423
- const newSelectionModel = (selectionModel || []).filter((model) => model !== selectedModel);
3424
- onSelectionModelChange?.(newSelectionModel);
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 newSelectionModel = [...selectionModel || [], selectedModel];
3427
- onSelectionModelChange?.(newSelectionModel);
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
- [selectionModel, onSelectionModelChange, selectedModelSet]
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
- [onCheckboxChange, isRowSelectableCheck]
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
  )