Skip to content
Merged
9 changes: 9 additions & 0 deletions .changeset/fix-table-checkbox-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/kumo": patch
---

fix(Table): Add indeterminate prop and fix checkbox click handling

- Added `indeterminate` prop to `Table.CheckHead` and `Table.CheckCell` components to support indeterminate checkbox state (shows minus icon when some but not all rows are selected)
- Fixed checkbox click handling - clicking directly on the checkbox now works correctly (previously only clicking the cell area next to the checkbox worked)
- Updated Table demos (`TableSelectedRowDemo` and `TableFullDemo`) with proper React state management for interactive row selection
195 changes: 158 additions & 37 deletions packages/kumo-docs-astro/src/components/demos/TableDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { Badge, Button, LayerCard, Table } from "@cloudflare/kumo";
import { DotsThree, EnvelopeSimple } from "@phosphor-icons/react";
import { useState } from "react";
import {
Badge,
Button,
DropdownMenu,
LayerCard,
Table,
} from "@cloudflare/kumo";
import {
DotsThree,
EnvelopeSimple,
Eye,
PencilSimple,
Trash,
} from "@phosphor-icons/react";

// Sample data for demos
const emailData = [
Expand Down Expand Up @@ -64,22 +77,56 @@ export function TableBasicDemo() {
}

export function TableWithCheckboxDemo() {
const rows = emailData.slice(0, 3);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

const toggleRow = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};

const toggleAll = () => {
if (selectedIds.size === rows.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(rows.map((r) => r.id)));
}
};

return (
<LayerCard>
<LayerCard.Primary className="p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.CheckHead aria-label="Select all rows" />
<Table.CheckHead
checked={selectedIds.size === rows.length}
indeterminate={
selectedIds.size > 0 && selectedIds.size < rows.length
}
onValueChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{emailData.slice(0, 3).map((row) => (
{rows.map((row) => (
<Table.Row key={row.id}>
<Table.CheckCell aria-label={`Select ${row.subject}`} />
<Table.CheckCell
checked={selectedIds.has(row.id)}
onValueChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
Expand All @@ -93,37 +140,64 @@ export function TableWithCheckboxDemo() {
}

export function TableSelectedRowDemo() {
const rows = emailData.slice(0, 3);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(["2"]));

const toggleRow = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};

const toggleAll = () => {
if (selectedIds.size === rows.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(rows.map((r) => r.id)));
}
};

return (
<LayerCard>
<LayerCard.Primary className="p-0">
<Table>
<Table.Header>
<Table.Row>
<Table.CheckHead aria-label="Select all rows" />
<Table.CheckHead
checked={selectedIds.size === rows.length}
indeterminate={
selectedIds.size > 0 && selectedIds.size < rows.length
}
onValueChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.CheckCell aria-label="Select row 1" />
<Table.Cell>Kumo v1.0.0 released</Table.Cell>
<Table.Cell>Visal In</Table.Cell>
<Table.Cell>5 seconds ago</Table.Cell>
</Table.Row>
<Table.Row variant="selected">
<Table.CheckCell checked aria-label="Select row 2" />
<Table.Cell>New Job Offer</Table.Cell>
<Table.Cell>Cloudflare</Table.Cell>
<Table.Cell>10 minutes ago</Table.Cell>
</Table.Row>
<Table.Row>
<Table.CheckCell aria-label="Select row 3" />
<Table.Cell>Daily Email Digest</Table.Cell>
<Table.Cell>Cloudflare</Table.Cell>
<Table.Cell>1 hour ago</Table.Cell>
</Table.Row>
{rows.map((row) => (
<Table.Row
key={row.id}
variant={selectedIds.has(row.id) ? "selected" : "default"}
>
<Table.CheckCell
checked={selectedIds.has(row.id)}
onValueChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>{row.subject}</Table.Cell>
<Table.Cell>{row.from}</Table.Cell>
<Table.Cell>{row.date}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</LayerCard.Primary>
Expand Down Expand Up @@ -164,34 +238,65 @@ export function TableFixedLayoutDemo() {
}

export function TableFullDemo() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(["2"]));

const toggleRow = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};

const toggleAll = () => {
if (selectedIds.size === emailData.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(emailData.map((r) => r.id)));
}
};

return (
<LayerCard>
<LayerCard.Primary className="w-full overflow-x-auto p-0">
<Table layout="fixed">
<colgroup>
<col style={{ width: "40px" }} />
<col />{" "}
{/* Checkbox column - width handled by Table.CheckHead/CheckCell */}
<col />
<col style={{ width: "150px" }} />
<col style={{ width: "120px" }} />
<col style={{ width: "50px" }} />
</colgroup>
<Table.Header>
<Table.Row>
<Table.CheckHead aria-label="Select all rows" />
<Table.CheckHead
checked={selectedIds.size === emailData.length}
indeterminate={
selectedIds.size > 0 && selectedIds.size < emailData.length
}
onValueChange={toggleAll}
aria-label="Select all rows"
/>
<Table.Head>Subject</Table.Head>
<Table.Head>From</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{emailData.map((row, index) => (
{emailData.map((row) => (
<Table.Row
key={row.id}
variant={index === 1 ? "selected" : "default"}
variant={selectedIds.has(row.id) ? "selected" : "default"}
>
<Table.CheckCell
checked={index === 1}
checked={selectedIds.has(row.id)}
onValueChange={() => toggleRow(row.id)}
aria-label={`Select ${row.subject}`}
/>
<Table.Cell>
Expand All @@ -214,14 +319,30 @@ export function TableFullDemo() {
<span className="truncate">{row.date}</span>
</Table.Cell>
<Table.Cell className="text-right">
<Button
variant="ghost"
size="sm"
shape="square"
aria-label="More options"
>
<DotsThree weight="bold" size={16} />
</Button>
<DropdownMenu>
<DropdownMenu.Trigger
render={
<Button
variant="ghost"
size="sm"
shape="square"
aria-label="More options"
>
<DotsThree weight="bold" size={16} />
</Button>
}
/>
<DropdownMenu.Content>
<DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
<DropdownMenu.Item icon={PencilSimple}>
Edit
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={Trash} variant="danger">
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</Table.Cell>
</Table.Row>
))}
Expand Down
Loading
Loading