Skip to content

Commit 25d0617

Browse files
authored
Add isRowSelectionDisabled prop (adazzle#3577)
This prop can be used to disable checkboxes
1 parent 902ce6c commit 25d0617

15 files changed

+288
-129
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ A number defining the height of summary rows.
168168

169169
###### `selectedRows?: Maybe<ReadonlySet<K>>`
170170

171+
###### `isRowSelectionDisabled?: Maybe<(row: NoInfer<R>) => boolean>`
172+
171173
###### `onSelectedRowsChange?: Maybe<(selectedRows: Set<K>) => void>`
172174

173175
###### `sortColumns?: Maybe<readonly SortColumn[]>`

src/Columns.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,51 @@
1-
import { useRowSelection } from './hooks/useRowSelection';
1+
import { useHeaderRowSelection, useRowSelection } from './hooks/useRowSelection';
22
import type { Column, RenderCellProps, RenderGroupCellProps, RenderHeaderCellProps } from './types';
33
import { SelectCellFormatter } from './cellRenderers';
44

5-
export const SELECT_COLUMN_KEY = 'select-row';
5+
export const SELECT_COLUMN_KEY = 'rdg-select-column';
66

77
function HeaderRenderer(props: RenderHeaderCellProps<unknown>) {
8-
const [isRowSelected, onRowSelectionChange] = useRowSelection();
8+
const { isIndeterminate, isRowSelected, onRowSelectionChange } = useHeaderRowSelection();
99

1010
return (
1111
<SelectCellFormatter
1212
aria-label="Select All"
1313
tabIndex={props.tabIndex}
14+
indeterminate={isIndeterminate}
1415
value={isRowSelected}
1516
onChange={(checked) => {
16-
onRowSelectionChange({ type: 'HEADER', checked });
17+
onRowSelectionChange({ checked: isIndeterminate ? false : checked });
1718
}}
1819
/>
1920
);
2021
}
2122

2223
function SelectFormatter(props: RenderCellProps<unknown>) {
23-
const [isRowSelected, onRowSelectionChange] = useRowSelection();
24+
const { isRowSelectionDisabled, isRowSelected, onRowSelectionChange } = useRowSelection();
2425

2526
return (
2627
<SelectCellFormatter
2728
aria-label="Select"
2829
tabIndex={props.tabIndex}
30+
disabled={isRowSelectionDisabled}
2931
value={isRowSelected}
3032
onChange={(checked, isShiftClick) => {
31-
onRowSelectionChange({ type: 'ROW', row: props.row, checked, isShiftClick });
33+
onRowSelectionChange({ row: props.row, checked, isShiftClick });
3234
}}
3335
/>
3436
);
3537
}
3638

3739
function SelectGroupFormatter(props: RenderGroupCellProps<unknown>) {
38-
const [isRowSelected, onRowSelectionChange] = useRowSelection();
40+
const { isRowSelected, onRowSelectionChange } = useRowSelection();
3941

4042
return (
4143
<SelectCellFormatter
4244
aria-label="Select Group"
4345
tabIndex={props.tabIndex}
4446
value={isRowSelected}
4547
onChange={(checked) => {
46-
onRowSelectionChange({ type: 'ROW', row: props.row, checked, isShiftClick: false });
48+
onRowSelectionChange({ row: props.row, checked, isShiftClick: false });
4749
}}
4850
/>
4951
);

src/DataGrid.tsx

+113-87
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import { flushSync } from 'react-dom';
44
import clsx from 'clsx';
55

66
import {
7+
HeaderRowSelectionChangeProvider,
8+
HeaderRowSelectionProvider,
79
RowSelectionChangeProvider,
8-
RowSelectionProvider,
910
useCalculatedColumns,
1011
useColumnWidths,
1112
useGridDimensions,
1213
useLatestFunc,
1314
useLayoutEffect,
1415
useViewportColumns,
15-
useViewportRows
16+
useViewportRows,
17+
type HeaderRowSelectionContextValue
1618
} from './hooks';
1719
import {
1820
abs,
@@ -46,6 +48,7 @@ import type {
4648
Position,
4749
Renderers,
4850
RowsChangeData,
51+
SelectHeaderRowEvent,
4952
SelectRowEvent,
5053
SortColumn
5154
} from './types';
@@ -147,6 +150,8 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
147150
*/
148151
/** Set of selected row keys */
149152
selectedRows?: Maybe<ReadonlySet<K>>;
153+
/** Determines if row selection is disabled, per row */
154+
isRowSelectionDisabled?: Maybe<(row: NoInfer<R>) => boolean>;
150155
/** Function called whenever row selection is changed */
151156
onSelectedRowsChange?: Maybe<(selectedRows: Set<NoInfer<K>>) => void>;
152157
/** Used for multi column sorting */
@@ -225,6 +230,7 @@ function DataGrid<R, SR, K extends Key>(
225230
summaryRowHeight: rawSummaryRowHeight,
226231
// Feature props
227232
selectedRows,
233+
isRowSelectionDisabled,
228234
onSelectedRowsChange,
229235
sortColumns,
230236
onSortColumnsChange,
@@ -363,16 +369,27 @@ function DataGrid<R, SR, K extends Key>(
363369
[renderCheckbox, renderSortStatus]
364370
);
365371

366-
const allRowsSelected = useMemo((): boolean => {
372+
const headerSelectionValue = useMemo((): HeaderRowSelectionContextValue => {
367373
// no rows to select = explicitely unchecked
368-
const { length } = rows;
369-
return (
370-
length !== 0 &&
371-
selectedRows != null &&
372-
rowKeyGetter != null &&
373-
selectedRows.size >= length &&
374-
rows.every((row) => selectedRows.has(rowKeyGetter(row)))
375-
);
374+
let hasSelectedRow = false;
375+
let hasUnselectedRow = false;
376+
377+
if (rowKeyGetter != null && selectedRows != null && selectedRows.size > 0) {
378+
for (const row of rows) {
379+
if (selectedRows.has(rowKeyGetter(row))) {
380+
hasSelectedRow = true;
381+
} else {
382+
hasUnselectedRow = true;
383+
}
384+
385+
if (hasSelectedRow && hasUnselectedRow) break;
386+
}
387+
}
388+
389+
return {
390+
isRowSelected: hasSelectedRow && !hasUnselectedRow,
391+
isIndeterminate: hasSelectedRow && hasUnselectedRow
392+
};
376393
}, [rows, selectedRows, rowKeyGetter]);
377394

378395
const {
@@ -433,6 +450,7 @@ function DataGrid<R, SR, K extends Key>(
433450
const onCellClickLatest = useLatestFunc(onCellClick);
434451
const onCellDoubleClickLatest = useLatestFunc(onCellDoubleClick);
435452
const onCellContextMenuLatest = useLatestFunc(onCellContextMenu);
453+
const selectHeaderRowLatest = useLatestFunc(selectHeaderRow);
436454
const selectRowLatest = useLatestFunc(selectRow);
437455
const handleFormatterRowChangeLatest = useLatestFunc(updateRow);
438456
const selectCellLatest = useLatestFunc(selectCell);
@@ -492,26 +510,30 @@ function DataGrid<R, SR, K extends Key>(
492510
/**
493511
* event handlers
494512
*/
495-
function selectRow(args: SelectRowEvent<R>) {
513+
function selectHeaderRow(args: SelectHeaderRowEvent) {
496514
if (!onSelectedRowsChange) return;
497515

498516
assertIsValidKeyGetter<R, K>(rowKeyGetter);
499517

500-
if (args.type === 'HEADER') {
501-
const newSelectedRows = new Set(selectedRows);
502-
for (const row of rows) {
503-
const rowKey = rowKeyGetter(row);
504-
if (args.checked) {
505-
newSelectedRows.add(rowKey);
506-
} else {
507-
newSelectedRows.delete(rowKey);
508-
}
518+
const newSelectedRows = new Set(selectedRows);
519+
for (const row of rows) {
520+
if (isRowSelectionDisabled?.(row) === true) continue;
521+
const rowKey = rowKeyGetter(row);
522+
if (args.checked) {
523+
newSelectedRows.add(rowKey);
524+
} else {
525+
newSelectedRows.delete(rowKey);
509526
}
510-
onSelectedRowsChange(newSelectedRows);
511-
return;
512527
}
528+
onSelectedRowsChange(newSelectedRows);
529+
}
530+
531+
function selectRow(args: SelectRowEvent<R>) {
532+
if (!onSelectedRowsChange) return;
513533

534+
assertIsValidKeyGetter<R, K>(rowKeyGetter);
514535
const { row, checked, isShiftClick } = args;
536+
if (isRowSelectionDisabled?.(row) === true) return;
515537
const newSelectedRows = new Set(selectedRows);
516538
const rowKey = rowKeyGetter(row);
517539
const previousRowIdx = lastSelectedRowIdx.current;
@@ -533,6 +555,7 @@ function DataGrid<R, SR, K extends Key>(
533555
const step = sign(rowIdx - previousRowIdx);
534556
for (let i = previousRowIdx + step; i !== rowIdx; i += step) {
535557
const row = rows[i];
558+
if (isRowSelectionDisabled?.(row) === true) continue;
536559
if (checked) {
537560
newSelectedRows.add(rowKeyGetter(row));
538561
} else {
@@ -674,7 +697,7 @@ function DataGrid<R, SR, K extends Key>(
674697
if (isSelectable && shiftKey && key === ' ') {
675698
assertIsValidKeyGetter<R, K>(rowKeyGetter);
676699
const rowKey = rowKeyGetter(row);
677-
selectRow({ type: 'ROW', row, checked: !selectedRows.has(rowKey), isShiftClick: false });
700+
selectRow({ row, checked: !selectedRows.has(rowKey), isShiftClick: false });
678701
// do not scroll
679702
event.preventDefault();
680703
return;
@@ -1008,6 +1031,7 @@ function DataGrid<R, SR, K extends Key>(
10081031
rowIdx,
10091032
row,
10101033
viewportColumns: rowColumns,
1034+
isRowSelectionDisabled: isRowSelectionDisabled?.(row) ?? false,
10111035
isRowSelected,
10121036
onCellClick: onCellClickLatest,
10131037
onCellDoubleClick: onCellDoubleClickLatest,
@@ -1098,8 +1122,8 @@ function DataGrid<R, SR, K extends Key>(
10981122
data-testid={testId}
10991123
>
11001124
<DataGridDefaultRenderersProvider value={defaultGridComponents}>
1101-
<RowSelectionChangeProvider value={selectRowLatest}>
1102-
<RowSelectionProvider value={allRowsSelected}>
1125+
<HeaderRowSelectionChangeProvider value={selectHeaderRowLatest}>
1126+
<HeaderRowSelectionProvider value={headerSelectionValue}>
11031127
{Array.from({ length: groupedColumnHeaderRowsCount }, (_, index) => (
11041128
<GroupedColumnHeaderRow
11051129
key={index}
@@ -1127,68 +1151,70 @@ function DataGrid<R, SR, K extends Key>(
11271151
shouldFocusGrid={!selectedCellIsWithinSelectionBounds}
11281152
direction={direction}
11291153
/>
1130-
</RowSelectionProvider>
1131-
{rows.length === 0 && noRowsFallback ? (
1132-
noRowsFallback
1133-
) : (
1134-
<>
1135-
{topSummaryRows?.map((row, rowIdx) => {
1136-
const gridRowStart = headerRowsCount + 1 + rowIdx;
1137-
const summaryRowIdx = mainHeaderRowIdx + 1 + rowIdx;
1138-
const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx;
1139-
const top = headerRowsHeight + summaryRowHeight * rowIdx;
1140-
1141-
return (
1142-
<SummaryRow
1143-
key={rowIdx}
1144-
aria-rowindex={gridRowStart}
1145-
rowIdx={summaryRowIdx}
1146-
gridRowStart={gridRowStart}
1147-
row={row}
1148-
top={top}
1149-
bottom={undefined}
1150-
viewportColumns={getRowViewportColumns(summaryRowIdx)}
1151-
lastFrozenColumnIndex={lastFrozenColumnIndex}
1152-
selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined}
1153-
isTop
1154-
selectCell={selectCellLatest}
1155-
/>
1156-
);
1157-
})}
1154+
</HeaderRowSelectionProvider>
1155+
</HeaderRowSelectionChangeProvider>
1156+
{rows.length === 0 && noRowsFallback ? (
1157+
noRowsFallback
1158+
) : (
1159+
<>
1160+
{topSummaryRows?.map((row, rowIdx) => {
1161+
const gridRowStart = headerRowsCount + 1 + rowIdx;
1162+
const summaryRowIdx = mainHeaderRowIdx + 1 + rowIdx;
1163+
const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx;
1164+
const top = headerRowsHeight + summaryRowHeight * rowIdx;
1165+
1166+
return (
1167+
<SummaryRow
1168+
key={rowIdx}
1169+
aria-rowindex={gridRowStart}
1170+
rowIdx={summaryRowIdx}
1171+
gridRowStart={gridRowStart}
1172+
row={row}
1173+
top={top}
1174+
bottom={undefined}
1175+
viewportColumns={getRowViewportColumns(summaryRowIdx)}
1176+
lastFrozenColumnIndex={lastFrozenColumnIndex}
1177+
selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined}
1178+
isTop
1179+
selectCell={selectCellLatest}
1180+
/>
1181+
);
1182+
})}
1183+
<RowSelectionChangeProvider value={selectRowLatest}>
11581184
{getViewportRows()}
1159-
{bottomSummaryRows?.map((row, rowIdx) => {
1160-
const gridRowStart = headerAndTopSummaryRowsCount + rows.length + rowIdx + 1;
1161-
const summaryRowIdx = rows.length + rowIdx;
1162-
const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx;
1163-
const top =
1164-
clientHeight > totalRowHeight
1165-
? gridHeight - summaryRowHeight * (bottomSummaryRows.length - rowIdx)
1166-
: undefined;
1167-
const bottom =
1168-
top === undefined
1169-
? summaryRowHeight * (bottomSummaryRows.length - 1 - rowIdx)
1170-
: undefined;
1171-
1172-
return (
1173-
<SummaryRow
1174-
aria-rowindex={ariaRowCount - bottomSummaryRowsCount + rowIdx + 1}
1175-
key={rowIdx}
1176-
rowIdx={summaryRowIdx}
1177-
gridRowStart={gridRowStart}
1178-
row={row}
1179-
top={top}
1180-
bottom={bottom}
1181-
viewportColumns={getRowViewportColumns(summaryRowIdx)}
1182-
lastFrozenColumnIndex={lastFrozenColumnIndex}
1183-
selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined}
1184-
isTop={false}
1185-
selectCell={selectCellLatest}
1186-
/>
1187-
);
1188-
})}
1189-
</>
1190-
)}
1191-
</RowSelectionChangeProvider>
1185+
</RowSelectionChangeProvider>
1186+
{bottomSummaryRows?.map((row, rowIdx) => {
1187+
const gridRowStart = headerAndTopSummaryRowsCount + rows.length + rowIdx + 1;
1188+
const summaryRowIdx = rows.length + rowIdx;
1189+
const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx;
1190+
const top =
1191+
clientHeight > totalRowHeight
1192+
? gridHeight - summaryRowHeight * (bottomSummaryRows.length - rowIdx)
1193+
: undefined;
1194+
const bottom =
1195+
top === undefined
1196+
? summaryRowHeight * (bottomSummaryRows.length - 1 - rowIdx)
1197+
: undefined;
1198+
1199+
return (
1200+
<SummaryRow
1201+
aria-rowindex={ariaRowCount - bottomSummaryRowsCount + rowIdx + 1}
1202+
key={rowIdx}
1203+
rowIdx={summaryRowIdx}
1204+
gridRowStart={gridRowStart}
1205+
row={row}
1206+
top={top}
1207+
bottom={bottom}
1208+
viewportColumns={getRowViewportColumns(summaryRowIdx)}
1209+
lastFrozenColumnIndex={lastFrozenColumnIndex}
1210+
selectedCellIdx={isSummaryRowSelected ? selectedPosition.idx : undefined}
1211+
isTop={false}
1212+
selectCell={selectCellLatest}
1213+
/>
1214+
);
1215+
})}
1216+
</>
1217+
)}
11921218
</DataGridDefaultRenderersProvider>
11931219

11941220
{renderDragHandle()}

src/GroupRow.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { memo } from 'react';
1+
import { memo, useMemo } from 'react';
22
import { css } from '@linaria/core';
33
import clsx from 'clsx';
44

5-
import { RowSelectionProvider } from './hooks';
5+
import { RowSelectionProvider, type RowSelectionContextValue } from './hooks';
66
import { getRowStyle } from './utils';
77
import type { BaseRenderRowProps, GroupRow } from './types';
88
import { SELECT_COLUMN_KEY } from './Columns';
@@ -51,8 +51,13 @@ function GroupedRow<R, SR>({
5151
selectCell({ rowIdx, idx: -1 });
5252
}
5353

54+
const selectionValue = useMemo(
55+
(): RowSelectionContextValue => ({ isRowSelectionDisabled: false, isRowSelected }),
56+
[isRowSelected]
57+
);
58+
5459
return (
55-
<RowSelectionProvider value={isRowSelected}>
60+
<RowSelectionProvider value={selectionValue}>
5661
<div
5762
role="row"
5863
aria-level={row.level + 1} // aria-level is 1-based

0 commit comments

Comments
 (0)