Skip to content

Commit 07426f6

Browse files
feat(table): add onCheckedChange prop to CheckCell and CheckHead (#437)
1 parent 919151f commit 07426f6

5 files changed

Lines changed: 169 additions & 14 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@cloudflare/kumo": minor
3+
---
4+
5+
feat(table): add `onCheckedChange` prop to `Table.CheckCell` and `Table.CheckHead`, aligning with the `Checkbox` component's signature.
6+
7+
The new prop exposes an optional second argument with event details, matching Base UI's idiom:
8+
9+
```tsx
10+
<Table.CheckCell
11+
checked={selected.has(row.id)}
12+
onCheckedChange={(checked, eventDetails) => {
13+
toggle(row.id);
14+
eventDetails?.event.stopPropagation();
15+
}}
16+
/>
17+
```
18+
19+
The existing `onValueChange` prop still works but is now deprecated and flagged by the `no-deprecated-props` lint rule. It will be removed in a future major version. Migrate by renaming the prop — the single-argument callback shape is preserved.
20+
21+
This change is additive and does not require consumer code changes at this time.

packages/kumo-docs-astro/src/components/demos/TableDemo.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export function TableWithCheckboxDemo() {
108108
indeterminate={
109109
selectedIds.size > 0 && selectedIds.size < rows.length
110110
}
111-
onValueChange={toggleAll}
111+
onCheckedChange={toggleAll}
112112
aria-label="Select all rows"
113113
/>
114114
<Table.Head>Subject</Table.Head>
@@ -121,7 +121,7 @@ export function TableWithCheckboxDemo() {
121121
<Table.Row key={row.id}>
122122
<Table.CheckCell
123123
checked={selectedIds.has(row.id)}
124-
onValueChange={() => toggleRow(row.id)}
124+
onCheckedChange={() => toggleRow(row.id)}
125125
aria-label={`Select ${row.subject}`}
126126
/>
127127
<Table.Cell>{row.subject}</Table.Cell>
@@ -194,7 +194,7 @@ export function TableSelectedRowDemo() {
194194
indeterminate={
195195
selectedIds.size > 0 && selectedIds.size < rows.length
196196
}
197-
onValueChange={toggleAll}
197+
onCheckedChange={toggleAll}
198198
aria-label="Select all rows"
199199
/>
200200
<Table.Head>Subject</Table.Head>
@@ -210,7 +210,7 @@ export function TableSelectedRowDemo() {
210210
>
211211
<Table.CheckCell
212212
checked={selectedIds.has(row.id)}
213-
onValueChange={() => toggleRow(row.id)}
213+
onCheckedChange={() => toggleRow(row.id)}
214214
aria-label={`Select ${row.subject}`}
215215
/>
216216
<Table.Cell>{row.subject}</Table.Cell>
@@ -450,7 +450,7 @@ export function TableFullDemo() {
450450
indeterminate={
451451
selectedIds.size > 0 && selectedIds.size < emailData.length
452452
}
453-
onValueChange={toggleAll}
453+
onCheckedChange={toggleAll}
454454
aria-label="Select all rows"
455455
/>
456456
<Table.Head>Subject</Table.Head>
@@ -467,7 +467,7 @@ export function TableFullDemo() {
467467
>
468468
<Table.CheckCell
469469
checked={selectedIds.has(row.id)}
470-
onValueChange={() => toggleRow(row.id)}
470+
onCheckedChange={() => toggleRow(row.id)}
471471
aria-label={`Select ${row.subject}`}
472472
/>
473473
<Table.Cell>

packages/kumo-docs-astro/src/pages/components/table.mdx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,33 @@ export default function Example() {
9090

9191
### With Checkboxes
9292

93-
<p>Add row selection with `Table.CheckHead` and `Table.CheckCell`.</p>
93+
<p>
94+
Add row selection with `Table.CheckHead` and `Table.CheckCell`. Both accept
95+
`onCheckedChange`, which matches the underlying `Checkbox` component's
96+
signature and exposes optional event details as a second argument.
97+
</p>
98+
<p>
99+
The older `onValueChange` prop still works but is deprecated — the lint
100+
rule will flag it and it will be removed in a future major version. Migrate
101+
by renaming the prop:
102+
</p>
103+
104+
```tsx
105+
// Before (deprecated)
106+
<Table.CheckCell onValueChange={(checked) => toggleRow(id)} />
107+
108+
// After
109+
<Table.CheckCell onCheckedChange={(checked) => toggleRow(id)} />
110+
111+
// With event details
112+
<Table.CheckCell
113+
onCheckedChange={(checked, eventDetails) => {
114+
toggleRow(id);
115+
eventDetails?.event.stopPropagation();
116+
}}
117+
/>
118+
```
119+
94120
<ComponentExample demo="TableWithCheckboxDemo">
95121
<TableWithCheckboxDemo client:visible />
96122
</ComponentExample>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import { Table } from "./table";
4+
5+
describe("Table.CheckCell / Table.CheckHead", () => {
6+
it("calls onCheckedChange with the new checked state", async () => {
7+
const onCheckedChange = vi.fn();
8+
render(
9+
<table>
10+
<tbody>
11+
<tr>
12+
<Table.CheckCell
13+
checked={false}
14+
onCheckedChange={onCheckedChange}
15+
/>
16+
</tr>
17+
</tbody>
18+
</table>,
19+
);
20+
21+
const checkbox = screen.getByRole("checkbox");
22+
checkbox.click();
23+
24+
expect(onCheckedChange).toHaveBeenCalledTimes(1);
25+
expect(onCheckedChange.mock.calls[0][0]).toBe(true);
26+
// second arg is optional event details object
27+
expect(onCheckedChange.mock.calls[0][1]).toBeDefined();
28+
});
29+
30+
it("still calls the deprecated onValueChange for backward compatibility", () => {
31+
const onValueChange = vi.fn();
32+
render(
33+
<table>
34+
<tbody>
35+
<tr>
36+
<Table.CheckCell checked={false} onValueChange={onValueChange} />
37+
</tr>
38+
</tbody>
39+
</table>,
40+
);
41+
42+
screen.getByRole("checkbox").click();
43+
44+
expect(onValueChange).toHaveBeenCalledTimes(1);
45+
expect(onValueChange).toHaveBeenCalledWith(true);
46+
});
47+
48+
it("calls both onCheckedChange and onValueChange when both are provided", () => {
49+
const onCheckedChange = vi.fn();
50+
const onValueChange = vi.fn();
51+
render(
52+
<table>
53+
<thead>
54+
<tr>
55+
<Table.CheckHead
56+
checked={false}
57+
onCheckedChange={onCheckedChange}
58+
onValueChange={onValueChange}
59+
/>
60+
</tr>
61+
</thead>
62+
</table>,
63+
);
64+
65+
screen.getByRole("checkbox").click();
66+
67+
expect(onCheckedChange).toHaveBeenCalledTimes(1);
68+
expect(onValueChange).toHaveBeenCalledTimes(1);
69+
});
70+
});

packages/kumo/src/components/table/table.tsx

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { forwardRef } from "react";
22
import { cn } from "../../utils";
3-
import { Checkbox } from "../checkbox";
3+
import { Checkbox, type CheckboxChangeEventDetails } from "../checkbox";
44

55
/** Table layout and row variant definitions mapping names to their Tailwind classes. */
66
export const KUMO_TABLE_VARIANTS = {
@@ -269,13 +269,31 @@ const TableCheckCell = forwardRef<
269269
React.TdHTMLAttributes<HTMLTableCellElement> & {
270270
checked?: boolean;
271271
indeterminate?: boolean;
272+
/**
273+
* Called when the checkbox's checked state changes. The optional second
274+
* argument exposes event details from the underlying Checkbox, matching
275+
* the Checkbox component's signature.
276+
*/
277+
onCheckedChange?: (
278+
checked: boolean,
279+
eventDetails?: CheckboxChangeEventDetails,
280+
) => void;
281+
/** @deprecated Use `onCheckedChange` instead. Will be removed in a future major version. */
272282
onValueChange?: (checked: boolean) => void;
273283
label?: string;
274284
disabled?: boolean;
275285
}
276286
>(
277287
(
278-
{ checked, indeterminate, onValueChange, label, disabled, ...props },
288+
{
289+
checked,
290+
indeterminate,
291+
onCheckedChange,
292+
onValueChange,
293+
label,
294+
disabled,
295+
...props
296+
},
279297
ref,
280298
) => {
281299
return (
@@ -287,7 +305,8 @@ const TableCheckCell = forwardRef<
287305
<Checkbox
288306
checked={checked}
289307
indeterminate={indeterminate}
290-
onCheckedChange={(newChecked) => {
308+
onCheckedChange={(newChecked, eventDetails) => {
309+
onCheckedChange?.(newChecked, eventDetails);
291310
onValueChange?.(newChecked);
292311
}}
293312
aria-label={label ?? "Select row"}
@@ -304,13 +323,31 @@ const TableCheckHead = forwardRef<
304323
React.ThHTMLAttributes<HTMLTableCellElement> & {
305324
checked?: boolean;
306325
indeterminate?: boolean;
326+
/**
327+
* Called when the checkbox's checked state changes. The optional second
328+
* argument exposes event details from the underlying Checkbox, matching
329+
* the Checkbox component's signature.
330+
*/
331+
onCheckedChange?: (
332+
checked: boolean,
333+
eventDetails?: CheckboxChangeEventDetails,
334+
) => void;
335+
/** @deprecated Use `onCheckedChange` instead. Will be removed in a future major version. */
307336
onValueChange?: (checked: boolean) => void;
308337
label?: string;
309338
disabled?: boolean;
310339
}
311340
>(
312341
(
313-
{ checked, indeterminate, onValueChange, label, disabled, ...props },
342+
{
343+
checked,
344+
indeterminate,
345+
onCheckedChange,
346+
onValueChange,
347+
label,
348+
disabled,
349+
...props
350+
},
314351
ref,
315352
) => {
316353
return (
@@ -322,7 +359,8 @@ const TableCheckHead = forwardRef<
322359
<Checkbox
323360
checked={checked}
324361
indeterminate={indeterminate}
325-
onCheckedChange={(newChecked) => {
362+
onCheckedChange={(newChecked, eventDetails) => {
363+
onCheckedChange?.(newChecked, eventDetails);
326364
onValueChange?.(newChecked);
327365
}}
328366
aria-label={label ?? "Select all rows"}
@@ -356,14 +394,14 @@ TableCheckHead.displayName = "Table.CheckHead";
356394
* <Table>
357395
* <Table.Header>
358396
* <Table.Row>
359-
* <Table.CheckHead checked={allSelected} onValueChange={toggleAll} />
397+
* <Table.CheckHead checked={allSelected} onCheckedChange={toggleAll} />
360398
* <Table.Head>Name</Table.Head>
361399
* </Table.Row>
362400
* </Table.Header>
363401
* <Table.Body>
364402
* {rows.map((row) => (
365403
* <Table.Row key={row.id} variant={selected.has(row.id) ? "selected" : "default"}>
366-
* <Table.CheckCell checked={selected.has(row.id)} onValueChange={() => toggle(row.id)} />
404+
* <Table.CheckCell checked={selected.has(row.id)} onCheckedChange={() => toggle(row.id)} />
367405
* <Table.Cell>{row.name}</Table.Cell>
368406
* </Table.Row>
369407
* ))}

0 commit comments

Comments
 (0)