@cerebruminc/cerebellum 20.5.0 → 20.6.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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/components/UserCard/UserCard.module.css +2 -2
- package/src/helpers/deprecation.test.ts +74 -1
- package/src/helpers/deprecation.ts +29 -2
- package/src/index.ts +3 -0
- package/src/mantine/components/DataTable/DataTable.module.css +42 -0
- package/src/mantine/components/DataTable/DataTable.tsx +107 -16
- package/src/mantine/components/DataTable/DataTableSelection.test.tsx +692 -0
- package/src/mantine/components/DataTable/DataTableSelection.tsx +209 -0
- package/src/mantine/components/DataTable/index.ts +2 -0
- package/src/mantine/components/Radio.module.css +48 -0
- package/src/mantine/components/RadioIcon.tsx +35 -0
- package/src/mantine/components/Tooltip.module.css +18 -0
- package/src/mantine/mantineTheme.test.tsx +166 -3
- package/src/mantine/mantineTheme.ts +62 -2
- package/src/mantine/stories/DataTable.stories.tsx +68 -13
- package/src/mantine/stories/DataTableStory.test.tsx +124 -0
- package/src/mantine/stories/Radio.stories.tsx +52 -0
- package/src/mantine/stories/dataTableTemplates.tsx +60 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [20.6.0](https://github.com/cerebruminc/cerebellum/compare/v20.5.0...v20.6.0) (2026-09-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **data-table:** add warning if getRowId is missing when checkboxes are used ([60788a1](https://github.com/cerebruminc/cerebellum/commit/60788a15ef7b2e0fc868acc8d0744dc60234055e))
|
|
9
|
+
* **datatable:** add a checkbox and radio row-selection column ([e79c816](https://github.com/cerebruminc/cerebellum/commit/e79c8164f5e424ffbcf02b37756b2da884c450be))
|
|
10
|
+
* **datatable:** add row-selection stories for checkboxes and radios ([2d0d12d](https://github.com/cerebruminc/cerebellum/commit/2d0d12dd0a2865df06096fe6cf3b61527db2943a))
|
|
11
|
+
* **mantine-theme:** support Radio in the mantineTheme ([72e8573](https://github.com/cerebruminc/cerebellum/commit/72e857346fe2d6d86aebcacd3044dc3fea250497))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **tooltip:** update the Tooltip styles ([155f939](https://github.com/cerebruminc/cerebellum/commit/155f939a1e5bc22c9a270de98775a681297a5884))
|
|
17
|
+
* **userCard:** fix UserCard spacing ([0bcd605](https://github.com/cerebruminc/cerebellum/commit/0bcd605b0a6a94ee7e0994e20794e06328ef37b9))
|
|
18
|
+
|
|
3
19
|
## [20.5.0](https://github.com/cerebruminc/cerebellum/compare/v20.4.0...v20.5.0) (2026-09-09)
|
|
4
20
|
|
|
5
21
|
|
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
/* Layout groups */
|
|
71
71
|
.optionsBox {
|
|
72
72
|
margin-top: 27px;
|
|
73
|
-
padding-bottom:
|
|
73
|
+
padding-bottom: 27px;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
.nonScrollingGroup {
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
.orgDivider.orgDivider {
|
|
317
|
-
margin:
|
|
317
|
+
margin: 0 -7px 16px;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
.signOutDivider.signOutDivider {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/helpers/deprecation.test.ts
|
|
3
|
-
* @summary Pins the "once
|
|
3
|
+
* @summary Pins the "once" contract of both notices this package emits.
|
|
4
4
|
* @remarks
|
|
5
5
|
* The value of this notice depends entirely on it being quiet: one line, once. A regression
|
|
6
6
|
* that made it fire per render would put it in front of developers hundreds of times a page
|
|
@@ -103,3 +103,76 @@ describe("warnStyledComponentsDeprecated", () => {
|
|
|
103
103
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* `warnOnceFor` counts per scope rather than per session, which is the whole point:
|
|
109
|
+
* one misconfigured table should not swallow the warning owed to a second one, while
|
|
110
|
+
* that table's own re-renders and StrictMode double-mount stay a single line.
|
|
111
|
+
*/
|
|
112
|
+
describe("warnOnceFor", () => {
|
|
113
|
+
let warn: jest.SpyInstance;
|
|
114
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
warn.mockRestore();
|
|
122
|
+
process.env.NODE_ENV = originalNodeEnv;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("warns on the first call for a scope", () => {
|
|
126
|
+
freshModule().warnOnceFor({}, "first");
|
|
127
|
+
|
|
128
|
+
expect(warn).toHaveBeenCalledWith("first");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("stays silent on every later call for the same scope", () => {
|
|
132
|
+
const { warnOnceFor } = freshModule();
|
|
133
|
+
const scope = {};
|
|
134
|
+
|
|
135
|
+
warnOnceFor(scope, "same");
|
|
136
|
+
warnOnceFor(scope, "same");
|
|
137
|
+
warnOnceFor(scope, "same");
|
|
138
|
+
|
|
139
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("warns once for each scope, so a second offender is still heard", () => {
|
|
143
|
+
const { warnOnceFor } = freshModule();
|
|
144
|
+
|
|
145
|
+
warnOnceFor({}, "same");
|
|
146
|
+
warnOnceFor({}, "same");
|
|
147
|
+
|
|
148
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("counts each distinct message separately within one scope", () => {
|
|
152
|
+
const { warnOnceFor } = freshModule();
|
|
153
|
+
const scope = {};
|
|
154
|
+
|
|
155
|
+
warnOnceFor(scope, "one");
|
|
156
|
+
warnOnceFor(scope, "two");
|
|
157
|
+
|
|
158
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("stays silent in production, where the notice reaches no developer", () => {
|
|
162
|
+
process.env.NODE_ENV = "production";
|
|
163
|
+
|
|
164
|
+
freshModule().warnOnceFor({}, "hidden");
|
|
165
|
+
|
|
166
|
+
expect(warn).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/* The deprecation opt-out is for a long migration; it should not hide a live bug. */
|
|
170
|
+
it("is not silenced by setDeprecationWarningsEnabled(false)", () => {
|
|
171
|
+
const { setDeprecationWarningsEnabled, warnOnceFor } = freshModule();
|
|
172
|
+
|
|
173
|
+
setDeprecationWarningsEnabled(false);
|
|
174
|
+
warnOnceFor({}, "still shown");
|
|
175
|
+
|
|
176
|
+
expect(warn).toHaveBeenCalledWith("still shown");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/helpers/deprecation.ts
|
|
3
|
-
* @exports setDeprecationWarningsEnabled, warnStyledComponentsDeprecated
|
|
4
|
-
* @summary The single place this package
|
|
3
|
+
* @exports setDeprecationWarningsEnabled, warnOnceFor, warnStyledComponentsDeprecated
|
|
4
|
+
* @summary The single place this package warns a developer about anything.
|
|
5
5
|
* @remarks
|
|
6
|
+
* Two notices live here, for one reason: each depends on module state to stay quiet, and a
|
|
7
|
+
* stray `console.warn` elsewhere would not respect it. The deprecation notice is the
|
|
8
|
+
* original; `warnOnceFor` is the general form, for a misconfiguration a consumer can fix.
|
|
6
9
|
* The original styled-components components are deprecated, but not removed, in
|
|
7
10
|
* favor of the Mantine layer under `src/mantine`. The notice is emitted once per app
|
|
8
11
|
* session from `ThemeProvider`'s mount effect rather than from each component — an effect
|
|
@@ -51,3 +54,27 @@ export const warnStyledComponentsDeprecated = () => {
|
|
|
51
54
|
hasWarned = true;
|
|
52
55
|
console.warn(MESSAGE);
|
|
53
56
|
};
|
|
57
|
+
|
|
58
|
+
const warnedByScope = new WeakMap<object, Set<string>>();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Warns once per `scope` — a long-lived object the notice belongs to, such as a
|
|
62
|
+
* `useReactTable` instance, whose identity `useReactTable` keeps stable for the life of
|
|
63
|
+
* the component. Two misconfigured tables therefore each get heard, while re-renders and
|
|
64
|
+
* StrictMode's double-mount of one table are counted once. A `WeakMap` so a scope that
|
|
65
|
+
* unmounts is collectable rather than pinned for the session.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately not gated on `setDeprecationWarningsEnabled`: that switch is for a team
|
|
68
|
+
* living with the deprecation through a long migration, and silencing it should not also
|
|
69
|
+
* hide a bug they can fix today.
|
|
70
|
+
*/
|
|
71
|
+
export const warnOnceFor = (scope: object, message: string) => {
|
|
72
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
73
|
+
|
|
74
|
+
const seen = warnedByScope.get(scope) ?? new Set<string>();
|
|
75
|
+
if (seen.has(message)) return;
|
|
76
|
+
|
|
77
|
+
seen.add(message);
|
|
78
|
+
warnedByScope.set(scope, seen);
|
|
79
|
+
console.warn(message);
|
|
80
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -681,6 +681,7 @@ export {
|
|
|
681
681
|
DataTableRoot,
|
|
682
682
|
DataTableRowActions,
|
|
683
683
|
dataTableRowActionsColumn,
|
|
684
|
+
dataTableSelectionColumn,
|
|
684
685
|
DataTableSidebar,
|
|
685
686
|
DataTableTotalRecords,
|
|
686
687
|
} from "./mantine/components/DataTable";
|
|
@@ -703,6 +704,8 @@ export type {
|
|
|
703
704
|
DataTableRowOptionGroup,
|
|
704
705
|
DataTableRowOptionIcon,
|
|
705
706
|
DataTableSectionProps,
|
|
707
|
+
DataTableSelectionColumnOptions,
|
|
708
|
+
DataTableSelectionMode,
|
|
706
709
|
DataTableSidebarProps,
|
|
707
710
|
DataTableTotalRecordsProps,
|
|
708
711
|
} from "./mantine/components/DataTable";
|
|
@@ -358,6 +358,48 @@
|
|
|
358
358
|
justify-content: center;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/* ----- Row selection ----- */
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* CheckboxCell's geometry: the control sits in the 40px track its `meta.fixedWidth`
|
|
365
|
+
* already cleared of side padding, with the legacy 17px lead. Single-class, like
|
|
366
|
+
* .rowActionsCell beside it -- this is our own div, not a Mantine slot, so there is
|
|
367
|
+
* nothing to outrank.
|
|
368
|
+
*/
|
|
369
|
+
.selectionCell {
|
|
370
|
+
align-items: center;
|
|
371
|
+
display: flex;
|
|
372
|
+
height: 100%;
|
|
373
|
+
padding-left: 17px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/*
|
|
377
|
+
* Composed onto .selectionCell in the header, for `CheckboxHeaderCell`'s extra pixel
|
|
378
|
+
* of lead and to sit the control on the header labels rather than above them.
|
|
379
|
+
*
|
|
380
|
+
* `flex-end` rather than the centring above, and no `padding-bottom` of the legacy
|
|
381
|
+
* cell's own: the labels are bottom-aligned in the header, so the control belongs at
|
|
382
|
+
* the bottom of the cell's content box too. Centring put it 11px high, because
|
|
383
|
+
* `CheckboxHeaderCell` *was* the grid cell and owned that padding, whereas this
|
|
384
|
+
* wrapper sits inside a `.th` that already pads 11px at the bottom -- so restating it
|
|
385
|
+
* here counted it twice, once as the cell's inset and once as the control's.
|
|
386
|
+
*/
|
|
387
|
+
.selectionHeaderCell {
|
|
388
|
+
align-items: flex-end;
|
|
389
|
+
padding-left: 17px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/*
|
|
393
|
+
* Mantine draws the checkbox glyph at 60% of the control, so the 20px control this
|
|
394
|
+
* column needs would render a 12px glyph -- smaller than the legacy Checkbox's 15px
|
|
395
|
+
* and than every other checkbox in the Mantine layer, which are both 25px x 60%.
|
|
396
|
+
* 75% puts it back at 15px. The radio needs no equivalent: its icon is pinned to
|
|
397
|
+
* the control's size, so it fills the 20px the legacy radio also measured.
|
|
398
|
+
*/
|
|
399
|
+
.selectionCheckboxIcon.selectionCheckboxIcon {
|
|
400
|
+
width: 75%;
|
|
401
|
+
}
|
|
402
|
+
|
|
361
403
|
/*
|
|
362
404
|
* Keyed on plain `.tr.tr:hover` rather than the elevated-hover selector above, so
|
|
363
405
|
* `disableHighlight` drops the row treatment without also disabling the button.
|
|
@@ -108,15 +108,17 @@ import Form, { FormProps as RJSFormProps } from "@rjsf/core";
|
|
|
108
108
|
import { GenericObjectType, RJSFSchema } from "@rjsf/utils";
|
|
109
109
|
import { Row, RowData, flexRender, useReactTable } from "@tanstack/react-table";
|
|
110
110
|
import { clsx } from "clsx";
|
|
111
|
-
import React, { createContext, FC, Fragment, PropsWithChildren, useCallback, useContext, useRef } from "react";
|
|
111
|
+
import React, { createContext, FC, Fragment, PropsWithChildren, useCallback, useContext, useEffect, useRef } from "react";
|
|
112
112
|
import { Sort, SortDown, SortUp } from "../../../components/Icons";
|
|
113
113
|
|
|
114
114
|
import { EmptySearchIcon } from "../../../assets/EmptySearchIcon";
|
|
115
|
+
import { warnOnceFor } from "../../../helpers/deprecation";
|
|
115
116
|
import { MantineRJSForm } from "../../rjsf/createMantineRJSForm";
|
|
116
117
|
import { mantineValidator } from "../../rjsf/mantineValidator";
|
|
117
118
|
import { showBar } from "../../services/notifications";
|
|
118
119
|
|
|
119
120
|
import classes from "./DataTable.module.css";
|
|
121
|
+
import type { DataTableSelectionMode } from "./DataTableSelection";
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
124
|
* The per-column styling a column def asks for, through `columnDef.meta`.
|
|
@@ -176,6 +178,14 @@ export type DataTableColumnMeta = {
|
|
|
176
178
|
* for how it interacts with density and `fixedWidth`.
|
|
177
179
|
*/
|
|
178
180
|
paddingRight?: number | string;
|
|
181
|
+
/**
|
|
182
|
+
* Set by `dataTableSelectionColumn`, and the only field here that is not styling.
|
|
183
|
+
* It is what tells `DataTableBody` the table has a selection column and which
|
|
184
|
+
* control it renders, so a row click can select without a prop saying so. Not for
|
|
185
|
+
* hand-authoring: it does not render a control, it only describes the one the
|
|
186
|
+
* factory rendered.
|
|
187
|
+
*/
|
|
188
|
+
selectionMode?: DataTableSelectionMode;
|
|
179
189
|
/**
|
|
180
190
|
* The old ColumnType's `textStyle`, and the same four values: the legacy BodyS*
|
|
181
191
|
* styles were all 15px and differed only in weight and colour.
|
|
@@ -832,6 +842,12 @@ const cellProps = (meta: DataTableColumnMeta | undefined) => {
|
|
|
832
842
|
*/
|
|
833
843
|
const bodyCellProps = (meta: DataTableColumnMeta | undefined) => ({ ...cellProps(meta), "data-text-style": meta?.textStyle });
|
|
834
844
|
|
|
845
|
+
const MISSING_GET_ROW_ID_WARNING = [
|
|
846
|
+
"[@cerebruminc/cerebellum] DataTable has a row-selection column but its table has no `getRowId`.",
|
|
847
|
+
"TanStack then keys row selection by each record's index in `data`, so a selection silently moves to whatever record takes that index whenever `data` changes -- filtered, reordered, or fetched a page at a time.",
|
|
848
|
+
"Pass `getRowId: (record) => record.id` -- or whatever identifies your record -- to useReactTable.",
|
|
849
|
+
].join(" ");
|
|
850
|
+
|
|
835
851
|
export const DataTableBody = <TRecord,>({
|
|
836
852
|
density,
|
|
837
853
|
disableHighlight,
|
|
@@ -850,6 +866,68 @@ export const DataTableBody = <TRecord,>({
|
|
|
850
866
|
const rows = dataTable.rt.getRowModel().rows;
|
|
851
867
|
const hasNoRecords = !dataTable.isLoading && rows.length === 0;
|
|
852
868
|
|
|
869
|
+
/*
|
|
870
|
+
* Read off `dataTableSelectionColumn`'s own meta rather than taken as a prop. Its
|
|
871
|
+
* presence is what says the table has selection at all -- which matters because
|
|
872
|
+
* TanStack's `enableRowSelection` defaults to on, so `row.getCanSelect()` is true
|
|
873
|
+
* for every table that never asked for selection and would make all of them
|
|
874
|
+
* click-to-select. Its value is the control the column renders, which is the one
|
|
875
|
+
* thing a row handler cannot otherwise know.
|
|
876
|
+
*/
|
|
877
|
+
const selectionMode = dataTable.rt.getAllLeafColumns().find((column) => column.columnDef.meta?.selectionMode)?.columnDef
|
|
878
|
+
.meta?.selectionMode;
|
|
879
|
+
|
|
880
|
+
/*
|
|
881
|
+
* A selection column with no `getRowId` is always a bug, never a choice: TanStack keys
|
|
882
|
+
* `rowSelection` by `row.id`, which is the record's *index in `data`* until the caller
|
|
883
|
+
* configures one. Client-side paging and sorting are safe, because neither changes
|
|
884
|
+
* `data` -- it is replacing that array, as a filtered or server-paged table does, that
|
|
885
|
+
* silently moves the selection to whatever record now sits at the index. Nothing here
|
|
886
|
+
* can fix it -- `getRowId` is an option on the caller's own `useReactTable` call,
|
|
887
|
+
* resolved before DataTable is handed the result -- so saying so is the most this
|
|
888
|
+
* layer can do.
|
|
889
|
+
*
|
|
890
|
+
* In an effect, so a discarded or server-side render cannot spend the one warning.
|
|
891
|
+
*/
|
|
892
|
+
useEffect(() => {
|
|
893
|
+
if (selectionMode && !dataTable.rt.options.getRowId) {
|
|
894
|
+
warnOnceFor(dataTable.rt, MISSING_GET_ROW_ID_WARNING);
|
|
895
|
+
}
|
|
896
|
+
}, [dataTable.rt, selectionMode]);
|
|
897
|
+
|
|
898
|
+
/*
|
|
899
|
+
* `onRowClick` wins, so a table can have both a row action and a selection column
|
|
900
|
+
* without the two fighting over the same click. Otherwise a selection column makes
|
|
901
|
+
* the whole row a selection target, so selection is never a 40px one. Undefined
|
|
902
|
+
* when neither applies, which leaves the row unclickable rather than clickable and
|
|
903
|
+
* inert.
|
|
904
|
+
*
|
|
905
|
+
* The row does what a click on its own control would: a checkbox toggles, and a
|
|
906
|
+
* radio only ever selects, because a native radio does not clear itself when
|
|
907
|
+
* clicked again.
|
|
908
|
+
*/
|
|
909
|
+
const activateRow = onRowClick
|
|
910
|
+
? onRowClick
|
|
911
|
+
: selectionMode
|
|
912
|
+
? (row: Row<TRecord>) => row.toggleSelected(selectionMode === "radio" ? true : undefined)
|
|
913
|
+
: undefined;
|
|
914
|
+
|
|
915
|
+
/*
|
|
916
|
+
* Per row rather than per table: a row that cannot be selected must not carry the
|
|
917
|
+
* pointer cursor or a click that would do nothing.
|
|
918
|
+
*/
|
|
919
|
+
const canActivateRow = (row: Row<TRecord>) => Boolean(activateRow) && (Boolean(onRowClick) || row.getCanSelect());
|
|
920
|
+
|
|
921
|
+
/*
|
|
922
|
+
* Only a row that carries a handler of its own is a tab stop. Under a selection
|
|
923
|
+
* column the control inside the row is already focusable and already does exactly
|
|
924
|
+
* what the row does, so a tab stop on the row as well would be two stops per row
|
|
925
|
+
* for one action -- and the keyboard user would meet the row's focus ring before
|
|
926
|
+
* the control that explains it. The mouse path is unaffected: the row keeps its
|
|
927
|
+
* pointer cursor and its click either way.
|
|
928
|
+
*/
|
|
929
|
+
const rowIsFocusable = Boolean(onRowClick);
|
|
930
|
+
|
|
853
931
|
return (
|
|
854
932
|
/*
|
|
855
933
|
* ScrollArea rather than a plain overflow container, so the scrollbar is the
|
|
@@ -967,19 +1045,25 @@ export const DataTableBody = <TRecord,>({
|
|
|
967
1045
|
const selected = isRowSelected?.(row);
|
|
968
1046
|
const isSelected = Boolean(selected);
|
|
969
1047
|
const isBusy = Boolean(isRowLoading?.(row));
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1048
|
+
/*
|
|
1049
|
+
* A busy row is inert: neither the mouse nor Enter/Space reaches the
|
|
1050
|
+
* caller, and the pointer cursor goes with them. `canActivateRow`
|
|
1051
|
+
* rather than `onRowClick`, so that now covers a click that would
|
|
1052
|
+
* have selected the row as well as one the caller handles.
|
|
1053
|
+
*/
|
|
1054
|
+
const isActivatable = canActivateRow(row) && !isBusy;
|
|
973
1055
|
return (
|
|
974
1056
|
<TableTr
|
|
975
1057
|
aria-busy={isBusy || undefined}
|
|
976
1058
|
/*
|
|
977
|
-
* A
|
|
978
|
-
* or Space. No role="button" — that would strip the
|
|
979
|
-
* own role and break the table for screen readers — so
|
|
980
|
-
* keys are handled explicitly instead of coming free.
|
|
1059
|
+
* A row with `onRowClick` is reachable by Tab and activated
|
|
1060
|
+
* by Enter or Space. No role="button" — that would strip the
|
|
1061
|
+
* row's own role and break the table for screen readers — so
|
|
1062
|
+
* the keys are handled explicitly instead of coming free.
|
|
1063
|
+
* A row that is clickable only because the table has a
|
|
1064
|
+
* selection column is not a tab stop; its control is.
|
|
981
1065
|
*/
|
|
982
|
-
data-clickable={
|
|
1066
|
+
data-clickable={isActivatable || undefined}
|
|
983
1067
|
// The same attribute the whole-table skeleton rows carry, so
|
|
984
1068
|
// .tr:not([data-loading]) already excludes a busy row from hover.
|
|
985
1069
|
data-loading={isBusy || undefined}
|
|
@@ -988,15 +1072,19 @@ export const DataTableBody = <TRecord,>({
|
|
|
988
1072
|
data-selected={isSelected || undefined}
|
|
989
1073
|
data-testid={getRowTestId?.(row, isSelected)}
|
|
990
1074
|
key={row.id}
|
|
991
|
-
onClick={
|
|
1075
|
+
onClick={isActivatable ? (e) => activateRow?.(row, e) : undefined}
|
|
992
1076
|
/*
|
|
993
1077
|
* Still bound while busy, unlike onClick: the row keeps the focus
|
|
994
1078
|
* it had when it went busy, so Space has to keep being swallowed
|
|
995
1079
|
* or it would scroll the table under the user. Only the call to
|
|
996
1080
|
* the caller drops out.
|
|
1081
|
+
*
|
|
1082
|
+
* Bound on `rowIsFocusable` rather than on `isActivatable`, which
|
|
1083
|
+
* folds in `isBusy` — gating on that would unbind the handler for
|
|
1084
|
+
* exactly the row that still needs it.
|
|
997
1085
|
*/
|
|
998
1086
|
onKeyDown={
|
|
999
|
-
|
|
1087
|
+
rowIsFocusable && activateRow
|
|
1000
1088
|
? (event) => {
|
|
1001
1089
|
// Only the row itself: a button or link inside a cell
|
|
1002
1090
|
// handles its own keys, and its event bubbles here.
|
|
@@ -1008,14 +1096,17 @@ export const DataTableBody = <TRecord,>({
|
|
|
1008
1096
|
if (isBusy) {
|
|
1009
1097
|
return;
|
|
1010
1098
|
}
|
|
1011
|
-
|
|
1099
|
+
activateRow(row, event);
|
|
1012
1100
|
}
|
|
1013
1101
|
: undefined
|
|
1014
1102
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1103
|
+
/*
|
|
1104
|
+
* -1 rather than no attribute while busy: out of the tab order
|
|
1105
|
+
* either way, but still focusable, so a row focused when it went
|
|
1106
|
+
* busy keeps focus instead of dropping it on <body>. A row that was
|
|
1107
|
+
* never a tab stop has no focus to keep, so it stays absent.
|
|
1108
|
+
*/
|
|
1109
|
+
tabIndex={rowIsFocusable ? (isBusy ? -1 : 0) : undefined}
|
|
1019
1110
|
>
|
|
1020
1111
|
{row.getVisibleCells().map((cell) => {
|
|
1021
1112
|
const content = flexRender(cell.column.columnDef.cell, cell.getContext());
|