diff --git a/.changeset/selectable-data-table-rows.md b/.changeset/selectable-data-table-rows.md
new file mode 100644
index 00000000000..fc43c5f62b8
--- /dev/null
+++ b/.changeset/selectable-data-table-rows.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+DataTable: Add controlled and uncontrolled row selection.
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-colorblind-linux.png
new file mode 100644
index 00000000000..fa53c87081e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-dimmed-linux.png
new file mode 100644
index 00000000000..766564e9bb4
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..c09697d8c39
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-linux.png
new file mode 100644
index 00000000000..fa53c87081e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..fa53c87081e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-colorblind-linux.png
new file mode 100644
index 00000000000..0d8c5546de4
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-high-contrast-linux.png
new file mode 100644
index 00000000000..a3ccaf4e9db
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-linux.png
new file mode 100644
index 00000000000..0d8c5546de4
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-tritanopia-linux.png
new file mode 100644
index 00000000000..0d8c5546de4
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-all-selected-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-colorblind-linux.png
new file mode 100644
index 00000000000..f1394c1cb5a
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-dimmed-linux.png
new file mode 100644
index 00000000000..3cd3ace58d2
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..6057ac4f8b0
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-linux.png
new file mode 100644
index 00000000000..f1394c1cb5a
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..f1394c1cb5a
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-colorblind-linux.png
new file mode 100644
index 00000000000..2670e43077e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-high-contrast-linux.png
new file mode 100644
index 00000000000..601706d8ca3
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-linux.png
new file mode 100644
index 00000000000..2670e43077e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-tritanopia-linux.png
new file mode 100644
index 00000000000..2670e43077e
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-colorblind-linux.png
new file mode 100644
index 00000000000..586fc2552ea
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-dimmed-linux.png
new file mode 100644
index 00000000000..f1a85db89e0
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..ed2b20a3032
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-linux.png
new file mode 100644
index 00000000000..586fc2552ea
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..586fc2552ea
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-colorblind-linux.png
new file mode 100644
index 00000000000..c5bb1f13a76
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-high-contrast-linux.png
new file mode 100644
index 00000000000..c2fab2ba272
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-linux.png
new file mode 100644
index 00000000000..c5bb1f13a76
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-tritanopia-linux.png
new file mode 100644
index 00000000000..c5bb1f13a76
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Grouped-Row-Selection-mixed-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-colorblind-linux.png
new file mode 100644
index 00000000000..7bd4a4d2eec
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-dimmed-linux.png
new file mode 100644
index 00000000000..da0e211d3af
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..d0c4a399f3c
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-linux.png
new file mode 100644
index 00000000000..7bd4a4d2eec
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..7bd4a4d2eec
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-colorblind-linux.png
new file mode 100644
index 00000000000..348e6003da8
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-high-contrast-linux.png
new file mode 100644
index 00000000000..09c4e8f6115
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-linux.png
new file mode 100644
index 00000000000..348e6003da8
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-tritanopia-linux.png
new file mode 100644
index 00000000000..348e6003da8
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Mixed-Row-Selection-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-colorblind-linux.png
new file mode 100644
index 00000000000..c3164ce37f6
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-dimmed-linux.png
new file mode 100644
index 00000000000..ec43a8c4273
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..3644b94448d
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-linux.png
new file mode 100644
index 00000000000..c3164ce37f6
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..c3164ce37f6
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-colorblind-linux.png
new file mode 100644
index 00000000000..0461a9f89e9
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-high-contrast-linux.png
new file mode 100644
index 00000000000..1267922a69f
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-linux.png
new file mode 100644
index 00000000000..0461a9f89e9
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-tritanopia-linux.png
new file mode 100644
index 00000000000..0461a9f89e9
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Paginated-Row-Selection-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-colorblind-linux.png
new file mode 100644
index 00000000000..3699be53d90
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-dimmed-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-dimmed-linux.png
new file mode 100644
index 00000000000..6ddfe20334b
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..50357fe30ea
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-linux.png
new file mode 100644
index 00000000000..6f38dea8571
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..3699be53d90
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-colorblind-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-colorblind-linux.png
new file mode 100644
index 00000000000..d787114e58a
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-high-contrast-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-high-contrast-linux.png
new file mode 100644
index 00000000000..cbd8c4561ef
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-linux.png
new file mode 100644
index 00000000000..09c654effa0
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-linux.png differ
diff --git a/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-tritanopia-linux.png b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-tritanopia-linux.png
new file mode 100644
index 00000000000..09c654effa0
Binary files /dev/null and b/.playwright/snapshots/components/DataTable.test.ts-snapshots/DataTable-With-Row-Selection-light-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-colorblind-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-colorblind-linux.png
new file mode 100644
index 00000000000..093733043a5
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-dimmed-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-dimmed-linux.png
new file mode 100644
index 00000000000..1d2e231de9e
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-high-contrast-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..821c56f12f9
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-linux.png
new file mode 100644
index 00000000000..093733043a5
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-tritanopia-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..093733043a5
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-colorblind-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-colorblind-linux.png
new file mode 100644
index 00000000000..c0020fb1c0d
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-high-contrast-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-high-contrast-linux.png
new file mode 100644
index 00000000000..0bfc6107f40
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-linux.png
new file mode 100644
index 00000000000..7d1d1ad05a8
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-linux.png differ
diff --git a/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-tritanopia-linux.png b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-tritanopia-linux.png
new file mode 100644
index 00000000000..7d1d1ad05a8
Binary files /dev/null and b/.playwright/snapshots/components/Table.test.ts-snapshots/Table-With-Row-Selection-light-tritanopia-linux.png differ
diff --git a/e2e/components/DataTable.test.ts b/e2e/components/DataTable.test.ts
index 690d6a2a0e0..86519ce1e9e 100644
--- a/e2e/components/DataTable.test.ts
+++ b/e2e/components/DataTable.test.ts
@@ -5,7 +5,7 @@ import {themes} from '../test-helpers/themes'
const stories: ReadonlyArray<{
title: string
id: string
- aat?: boolean
+ snapshotTitle?: string
}> = [
{
title: 'Default',
@@ -54,12 +54,27 @@ const stories: ReadonlyArray<{
{
title: 'With Groups',
id: 'experimental-components-datatable-features--with-groups',
- aat: true,
+ },
+ {
+ title: 'With Row Selection',
+ id: 'experimental-components-datatable-features--with-row-selection',
+ },
+ {
+ title: 'With Grouped Row Selection',
+ id: 'experimental-components-datatable-features--with-grouped-row-selection',
},
{
title: 'With Sortable Groups',
id: 'experimental-components-datatable-features--with-sortable-groups',
- aat: true,
+ },
+ {
+ title: 'With Standalone And Grouped Row Selection',
+ id: 'experimental-components-datatable-features--with-standalone-and-grouped-row-selection',
+ snapshotTitle: 'With Mixed Row Selection',
+ },
+ {
+ title: 'With Paginated Row Selection',
+ id: 'experimental-components-datatable-features--with-paginated-row-selection',
},
]
@@ -79,27 +94,49 @@ test.describe('DataTable', () => {
// Default state
expect(
await page.screenshot({
+ animations: 'disabled',
mask: await page
.locator('td', {
has: page.locator('relative-time'),
})
.all(),
}),
- ).toMatchSnapshot(`DataTable.${story.title}.${theme}.png`)
+ ).toMatchSnapshot(`DataTable.${story.snapshotTitle ?? story.title}.${theme}.png`)
})
- if (story.aat) {
- test('axe @aat', async ({page}) => {
- await visit(page, {
- id: story.id,
- globals: {
- colorScheme: theme,
- },
+ if (story.id === 'experimental-components-datatable-features--with-grouped-row-selection') {
+ for (const state of ['mixed', 'all selected'] as const) {
+ test(`${state} @vrt`, async ({page}) => {
+ await visit(page, {id: story.id, globals: {colorScheme: theme}})
+ const selectAll = page.getByRole('checkbox', {name: 'Select rows', exact: true})
+ if (state === 'mixed') {
+ await page.getByRole('checkbox', {name: 'Select strapi', exact: true}).click()
+ await expect(selectAll).toBeChecked({indeterminate: true})
+ } else {
+ await selectAll.click()
+ await expect(selectAll).toBeChecked()
+ }
+ await expect(page.getByRole('checkbox', {name: 'Select codeql-dca-worker'})).toBeDisabled()
+ expect(
+ await page.screenshot({
+ animations: 'disabled',
+ mask: await page.locator('td', {has: page.locator('relative-time')}).all(),
+ }),
+ ).toMatchSnapshot(`DataTable.${story.title}.${state}.${theme}.png`)
})
+ }
+ }
- await expect(page).toHaveNoViolations()
+ test('axe @aat', async ({page}) => {
+ await visit(page, {
+ id: story.id,
+ globals: {
+ colorScheme: theme,
+ },
})
- }
+
+ await expect(page).toHaveNoViolations()
+ })
if (story.id === 'experimental-components-datatable-features--with-groups') {
test('continued group on next page @vrt', async ({page}) => {
@@ -111,70 +148,108 @@ test.describe('DataTable', () => {
}
})
}
-
- if (story.id === 'experimental-components-datatable-features--with-groups') {
- test('pagination across a group boundary @aat', async ({page}) => {
- await visit(page, {id: story.id})
- const table = page.getByRole('table', {name: 'Paginated repositories by visibility'})
- await expect(table.getByRole('rowheader')).toHaveCount(10)
- await expect(table.getByRole('columnheader', {name: /^Public\s*, 9 rows$/})).toBeVisible()
- await expect(table.getByRole('columnheader', {name: /^Internal\s*, 3 rows$/})).toHaveCount(0)
- await expect(table.getByRole('rowheader').first()).toHaveText('standalone/before')
-
- await page.getByRole('button', {name: 'Next page', exact: true}).click()
- await expect(table.getByRole('rowheader')).toHaveCount(7)
- await expect(table.getByRole('columnheader', {name: /^Public\s*, 3 rows$/})).toBeVisible()
- await expect(table.getByRole('columnheader', {name: /^Internal\s*, 3 rows$/})).toBeVisible()
- await expect(table.getByRole('rowheader', {name: 'public/repository-10', exact: true})).toBeVisible()
- await expect(table.getByRole('rowheader', {name: 'public/repository-1', exact: true})).toHaveCount(0)
- await expect(table.getByRole('rowheader').last()).toHaveText('standalone/after')
- const standalone = table.getByRole('rowheader', {name: 'standalone/after', exact: true})
- const rowHeaderId = await standalone.getAttribute('id')
- const columnId = await table.getByRole('columnheader', {name: 'Visibility', exact: true}).getAttribute('id')
- await expect(table.getByRole('row', {name: 'standalone/after Unassigned'}).getByRole('cell')).toHaveAttribute(
- 'headers',
- `${rowHeaderId} ${columnId}`,
- )
-
- const unresolvedHeaders = await table
- .locator('[headers]')
- .evaluateAll(cells =>
- cells.flatMap(cell =>
- (cell.getAttribute('headers') ?? '')
- .split(' ')
- .filter(id => !cell.closest('table')?.querySelector(`[id="${id}"]`)),
- ),
- )
- expect(unresolvedHeaders).toEqual([])
- await expect(page).toHaveNoViolations()
-
- const sortButton = table.getByRole('button', {name: 'Name', exact: true})
- await sortButton.click()
- await sortButton.click()
- await expect(table.getByRole('rowheader')).toHaveText([
- 'public/repository-12',
- 'public/repository-11',
- 'public/repository-10',
- 'internal/repository-3',
- 'internal/repository-2',
- 'internal/repository-1',
- 'standalone/after',
- ])
- await expect(table.getByRole('columnheader', {name: 'Name', exact: true})).toHaveAttribute(
- 'aria-sort',
- 'descending',
- )
- await expect(table.getByRole('row', {name: 'standalone/after Unassigned'}).getByRole('cell')).toHaveAttribute(
- 'headers',
- `${rowHeaderId} ${columnId}`,
- )
-
- await page.getByRole('button', {name: 'Previous page', exact: true}).click()
- await expect(table.getByRole('rowheader')).toHaveCount(10)
- await expect(table.getByRole('columnheader', {name: /^Public\s*, 9 rows$/})).toBeVisible()
- await expect(table.getByRole('rowheader', {name: 'public/repository-1', exact: true})).toBeVisible()
- })
- }
})
}
+
+ test('standalone and grouped selection stays wired through keyboard input and sorting @aat', async ({page}) => {
+ await visit(page, {id: 'experimental-components-datatable-features--with-standalone-and-grouped-row-selection'})
+ const standalone = page.getByRole('checkbox', {name: 'Select codeql-dca-worker', exact: true})
+ const grouped = page.getByRole('checkbox', {name: 'Select strapi', exact: true})
+ const selectAll = page.getByRole('checkbox', {name: 'Select rows', exact: true})
+
+ await expect(standalone).toBeEnabled()
+ await standalone.focus()
+ await page.keyboard.press('Space')
+ await expect(standalone).toBeChecked()
+ await expect(standalone).toBeFocused()
+ await grouped.click()
+ await page.getByRole('button', {name: 'Name', exact: true}).click()
+ await expect(standalone).toBeChecked()
+ await expect(grouped).toBeChecked()
+ await expect(selectAll).toBeChecked({indeterminate: true})
+ await selectAll.focus()
+ await page.keyboard.press('Space')
+ await expect(selectAll).toBeChecked()
+ await expect(selectAll).toBeFocused()
+ await expect(page).toHaveNoViolations()
+ })
+
+ test('paginated selection example selects only the current page @aat', async ({page}) => {
+ await visit(page, {id: 'experimental-components-datatable-features--with-paginated-row-selection'})
+ const selectAll = page.getByRole('checkbox', {name: 'Select rows', exact: true})
+ const checkedRows = page.locator('tbody input[type="checkbox"]:checked')
+
+ await selectAll.click()
+ await expect(checkedRows).toHaveCount(10)
+ await page.getByRole('button', {name: 'Next page', exact: true}).click()
+ await expect(checkedRows).toHaveCount(0)
+ await expect(selectAll).toHaveAccessibleDescription('Select all 7 rows')
+ await selectAll.click()
+ await expect(checkedRows).toHaveCount(7)
+ await page.getByRole('button', {name: 'Previous page', exact: true}).click()
+ await expect(checkedRows).toHaveCount(0)
+ await expect(page).toHaveNoViolations()
+ })
+
+ test('pagination across a group boundary @aat', async ({page}) => {
+ await visit(page, {id: 'experimental-components-datatable-features--with-groups'})
+ const table = page.getByRole('table', {name: 'Paginated repositories by visibility'})
+ await expect(table.getByRole('rowheader')).toHaveCount(10)
+ await expect(table.getByRole('columnheader', {name: /^Public\s*, 9 rows$/})).toBeVisible()
+ await expect(table.getByRole('columnheader', {name: /^Internal\s*, 3 rows$/})).toHaveCount(0)
+ await expect(table.getByRole('rowheader').first()).toHaveText('standalone/before')
+
+ await page.getByRole('button', {name: 'Next page', exact: true}).click()
+ await expect(table.getByRole('rowheader')).toHaveCount(7)
+ await expect(table.getByRole('columnheader', {name: /^Public\s*, 3 rows$/})).toBeVisible()
+ await expect(table.getByRole('columnheader', {name: /^Internal\s*, 3 rows$/})).toBeVisible()
+ await expect(table.getByRole('rowheader', {name: 'public/repository-10', exact: true})).toBeVisible()
+ await expect(table.getByRole('rowheader', {name: 'public/repository-1', exact: true})).toHaveCount(0)
+ await expect(table.getByRole('rowheader').last()).toHaveText('standalone/after')
+ const standalone = table.getByRole('rowheader', {name: 'standalone/after', exact: true})
+ const rowHeaderId = await standalone.getAttribute('id')
+ const columnId = await table.getByRole('columnheader', {name: 'Visibility', exact: true}).getAttribute('id')
+ await expect(table.getByRole('row', {name: 'standalone/after Unassigned'}).getByRole('cell')).toHaveAttribute(
+ 'headers',
+ `${rowHeaderId} ${columnId}`,
+ )
+
+ const unresolvedHeaders = await table
+ .locator('[headers]')
+ .evaluateAll(cells =>
+ cells.flatMap(cell =>
+ (cell.getAttribute('headers') ?? '')
+ .split(' ')
+ .filter(id => !cell.closest('table')?.querySelector(`[id="${id}"]`)),
+ ),
+ )
+ expect(unresolvedHeaders).toEqual([])
+ await expect(page).toHaveNoViolations()
+
+ const sortButton = table.getByRole('button', {name: 'Name', exact: true})
+ await sortButton.click()
+ await sortButton.click()
+ await expect(table.getByRole('rowheader')).toHaveText([
+ 'public/repository-12',
+ 'public/repository-11',
+ 'public/repository-10',
+ 'internal/repository-3',
+ 'internal/repository-2',
+ 'internal/repository-1',
+ 'standalone/after',
+ ])
+ await expect(table.getByRole('columnheader', {name: 'Name', exact: true})).toHaveAttribute(
+ 'aria-sort',
+ 'descending',
+ )
+ await expect(table.getByRole('row', {name: 'standalone/after Unassigned'}).getByRole('cell')).toHaveAttribute(
+ 'headers',
+ `${rowHeaderId} ${columnId}`,
+ )
+
+ await page.getByRole('button', {name: 'Previous page', exact: true}).click()
+ await expect(table.getByRole('rowheader')).toHaveCount(10)
+ await expect(table.getByRole('columnheader', {name: /^Public\s*, 9 rows$/})).toBeVisible()
+ await expect(table.getByRole('rowheader', {name: 'public/repository-1', exact: true})).toBeVisible()
+ })
})
diff --git a/e2e/components/Table.test.ts b/e2e/components/Table.test.ts
index 7c5c58127e1..1619066b29e 100644
--- a/e2e/components/Table.test.ts
+++ b/e2e/components/Table.test.ts
@@ -2,6 +2,75 @@ import {test, expect} from '@playwright/test'
import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'
+test.describe('Table With Row Selection', () => {
+ const storyId = 'experimental-components-table-features--with-row-selection'
+
+ for (const theme of themes) {
+ test.describe(theme, () => {
+ test('default @vrt', async ({page}) => {
+ await visit(page, {id: storyId, globals: {colorScheme: theme}})
+ expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`Table.With Row Selection.${theme}.png`)
+ })
+
+ test('axe @aat', async ({page}) => {
+ await visit(page, {id: storyId, globals: {colorScheme: theme}})
+ await expect(page).toHaveNoViolations()
+ })
+ })
+ }
+
+ test('composed selection keyboard wiring and header associations @aat', async ({page}) => {
+ await visit(page, {id: storyId})
+ const table = page.getByRole('table', {name: 'Selectable repositories using Table'})
+ const selectionColumn = table.getByRole('columnheader', {name: 'Select rows', exact: true})
+ const selectionColumnId = await selectionColumn.getAttribute('id')
+ const selectAll = selectionColumn.getByRole('checkbox', {name: 'Select rows', exact: true})
+ const react = table.getByRole('checkbox', {name: 'Select primer/react', exact: true})
+ const css = table.getByRole('checkbox', {name: 'Select primer/css', exact: true})
+ const disabled = table.getByRole('checkbox', {name: 'Select github/github', exact: true})
+
+ await expect(selectAll).toHaveAccessibleDescription('Select all 2 rows')
+ await expect(selectAll).toBeChecked({indeterminate: true})
+ await expect(react).toBeChecked()
+ await expect(css).not.toBeChecked()
+ await expect(disabled).toBeDisabled()
+ await expect(disabled).not.toBeChecked()
+
+ for (const group of [
+ {name: /^Internal\s*, 1 row$/, rows: ['github/github']},
+ {name: /^Public\s*, 2 rows$/, rows: ['primer/react', 'primer/css']},
+ ]) {
+ const groupHeader = table.getByRole('columnheader', {name: group.name})
+ await expect(groupHeader).toHaveAttribute('colspan', '3')
+ const groupHeaderId = await groupHeader.getAttribute('id')
+
+ for (const name of group.rows) {
+ const rowHeaderId = await table.getByRole('rowheader', {name, exact: true}).getAttribute('id')
+ const checkbox = table.getByRole('checkbox', {name: `Select ${name}`, exact: true})
+ const cell = table.getByRole('cell', {name: `Select ${name}`, exact: true})
+ await expect(cell).toHaveAttribute('headers', `${groupHeaderId} ${selectionColumnId}`)
+ const labelledBy = await checkbox.getAttribute('aria-labelledby')
+ expect(labelledBy?.split(' ')).toContain(rowHeaderId)
+ }
+ }
+
+ await selectAll.focus()
+ await page.keyboard.press('Space')
+ await expect(selectAll).toBeChecked()
+ await expect(selectAll).not.toBeChecked({indeterminate: true})
+ await expect(selectAll).toBeFocused()
+ await expect(css).toBeChecked()
+ await expect(disabled).not.toBeChecked()
+ await react.focus()
+ await page.keyboard.press('Space')
+ await expect(react).not.toBeChecked()
+ await expect(react).toBeFocused()
+ await expect(selectAll).toBeChecked({indeterminate: true})
+ await expect(selectionColumn).toHaveAccessibleName('Select rows')
+ await expect(page).toHaveNoViolations()
+ })
+})
+
test.describe('Table With Groups', () => {
for (const theme of themes) {
test.describe(theme, () => {
diff --git a/packages/react/src/DataTable/DataTable.accessibility.md b/packages/react/src/DataTable/DataTable.accessibility.md
index 718bfc1be2c..76a5caac7f4 100644
--- a/packages/react/src/DataTable/DataTable.accessibility.md
+++ b/packages/react/src/DataTable/DataTable.accessibility.md
@@ -14,10 +14,10 @@ assigning different responsibilities to consumers.
| Localized group name | Consumer may provide `aria-label` | Consumer may provide `aria-label` in the group data |
| Column span | Consumer provides `colSpan` | Derived from the column count |
| Group header ID | Generated by `Table.Group` | Inherited from `Table.Group` |
-| Column header IDs | Consumer provides stable IDs | Generated for grouped tables |
+| Column header IDs | Consumer provides stable IDs | Generated for grouped tables and the selection column |
| Row header | Consumer renders `Table.Cell scope="row"` | Consumer marks a column with `rowHeader` |
| Cell header associations | Consumer references column and row headers; `Table.Group` prepends the group header | Automatically references group, row, and column headers |
-| Ungrouped tables | Native `scope` associations | Preserves native `scope` associations without explicit ID references |
+| Ungrouped tables | Native `scope` associations | Native `scope` for data cells; ID references for selection controls |
| Sorting | Consumer manages row order | Sorts each group and contiguous standalone-row run independently |
| Empty groups | Always renders the member `
` | Preserves the empty member `` |
| Nested tables | Clears inherited group context | Inherited from the presentational API |
@@ -42,8 +42,9 @@ An optional subtitle may be referenced by `aria-describedby`. These references
must point to rendered elements. Use the native table structure rather than
turning the table into an interactive grid.
-Ungrouped tables continue to use native column and row `scope` associations.
-Grouping must not add explicit `headers` attributes to the ungrouped path.
+Without row selection, ungrouped tables use native column and row `scope`
+associations without explicit `headers` attributes. Selection adds ID references
+for the selection column and checkbox labels.
```html
Repositories
@@ -112,7 +113,8 @@ prepends it to member cells' references.
In mixed tables, each contiguous run of standalone rows renders in its own
``, as a sibling of the group sections. Standalone cells reference only
their row and column headers, never an adjacent group's header. Entirely
-ungrouped tables retain native `scope` associations without generated IDs.
+ungrouped tables without row selection retain native `scope` associations
+without generated IDs.
With direct composition, consumers supply column IDs, row-header IDs, and the
corresponding `headers` references. `Table.Group` generates its own header ID
@@ -207,6 +209,54 @@ Generated IDs must not collide between table instances, and every `headers`
reference must resolve before and after hydration. Direct-composition consumers
must likewise provide instance-scoped, SSR-safe IDs.
+## Row selection
+
+`DataTable` manages selection by stable row ID (`DataTableRowId`, a string or number).
+With `Table.SelectionHeader` and `Table.RowSelection`, consumers own state, counts,
+and header references. The header uses `selection` (`all`, `some`, or `none`); each row
+uses `selected`. Both parts request changes through `onToggleSelect`.
+
+### Names and associations
+
+Use `Select rows` as the header name and put the count in its description,
+per the [follow-up audit](https://github.com/github/accessibility/issues/10807).
+Row checkboxes reference the visible row header, not a duplicated hidden name.
+The fragments below show these associations; `sr-only` denotes hidden text.
+
+```html
+
+
+
+
+
+ Select
+
+
+
primer/react
+```
+
+Without a row header, `DataTable` uses `Select row {id}` from `getRowId`.
+Place selection first and include it in group `colSpan`. `Table.Group` prepends
+its header ID to selection cells' `headers`; group headings have no checkbox.
+
+### State, scope, and focus
+
+- Select-all includes every selectable supplied row across groups and outside
+ the scroll viewport. Disabled rows do not affect selection totals.
+- `Checkbox` sets the native `indeterminate` property for mixed state.
+ Activating a mixed header selects the remaining rows; a checked header clears them.
+- With no selectable rows, keep disabled, unchecked, non-mixed checkboxes and
+ omit the header's count description.
+- Page/filter owners clear controlled `selectedRows` when scope changes.
+ Sorting and replacement data preserve selection and do not emit `onSelectionChange`.
+- Missing or non-selectable IDs stay stored and become selected again if eligible.
+ Consumers reconcile permanent removals; uncontrolled selection is for static pages.
+- Preserve native Tab/Space behavior and keep the focused checkbox mounted after
+ selection changes. Do not add row-level `aria-selected` or arrow-key navigation.
+
+Examples: `WithRowSelection` in [Table features](./Table.features.stories.tsx)
+and `WithPaginatedRowSelection` in [DataTable features](./DataTable.features.stories.tsx).
+
## Verification
- [Grouping tests](./__tests__/DataTableGrouping.test.tsx) cover mixed body
@@ -214,6 +264,8 @@ must likewise provide instance-scoped, SSR-safe IDs.
groups, and the server-rendered/hydrated ID-reference graph across multiple tables.
- [Presentational group tests](./__tests__/TableGroup.test.tsx) cover native
spanning headers, accessible names, and nested-table isolation.
+- [Selection tests](./__tests__/DataTableSelection.test.tsx) cover controlled
+ updates, disabled rows, focus, and hydration.
- [DataTable browser tests](../../../../e2e/components/DataTable.test.ts) cover
themed axe checks, visual snapshots, and a group split across pages.
- [Table browser tests](../../../../e2e/components/Table.test.ts) cover explicit
@@ -224,12 +276,16 @@ testing. For changes to these semantics, verify cell navigation through group
boundaries with NVDA and JAWS, and check VoiceOver while accounting for its
documented spanning-header limitation. Verify that sorting controls announce
their name, state, and next action without polluting cell header names.
+For selection, also verify mixed-state announcements and focus after select-all.
## Links and resources
- [Tetralogical audit: verbose interactive header names](https://github.com/github/accessibility/issues/10272)
- [Multi-level headers and native colspan investigation](https://github.com/github/accessibility/issues/10885)
- [Follow-up accessibility audit tracking](https://github.com/github/accessibility/issues/10807)
+- [Selection focus audit](https://github.com/github/accessibility/issues/10268)
+- [Select-all naming audit](https://github.com/github/accessibility/issues/10271)
+- [Agreed row-selection scope and ownership](https://github.com/github/primer/issues/6718#issuecomment-5588699022)
- [Grouping requirements and implementation discussion](https://github.com/github/primer/issues/6720)
- [Presentational grouping implementation](https://github.com/primer/react/pull/8372)
- [Sortable-header name and description implementation](https://github.com/primer/react/pull/8371)
diff --git a/packages/react/src/DataTable/DataTable.docs.json b/packages/react/src/DataTable/DataTable.docs.json
index 6ae01581e49..8688354566e 100644
--- a/packages/react/src/DataTable/DataTable.docs.json
+++ b/packages/react/src/DataTable/DataTable.docs.json
@@ -46,11 +46,26 @@
{
"id": "experimental-components-datatable-features--with-groups"
},
+ {
+ "id": "experimental-components-datatable-features--with-row-selection"
+ },
+ {
+ "id": "experimental-components-datatable-features--with-grouped-row-selection"
+ },
+ {
+ "id": "experimental-components-datatable-features--with-standalone-and-grouped-row-selection"
+ },
+ {
+ "id": "experimental-components-datatable-features--with-paginated-row-selection"
+ },
{
"id": "experimental-components-datatable-features--with-sortable-groups"
},
{
"id": "experimental-components-table-features--with-groups"
+ },
+ {
+ "id": "experimental-components-table-features--with-row-selection"
}
],
"importPath": "@primer/react/experimental",
@@ -75,6 +90,36 @@
"type": "Array>",
"description": "Provide the columns for the table and the fields in `data` to which they correspond"
},
+ {
+ "name": "getRowId",
+ "type": "(row: Data) => DataTableRowId",
+ "description": "Provide a stable row ID. Defaults to row.id. DataTableRowId is an exported string | number type."
+ },
+ {
+ "name": "rowSelection",
+ "type": "boolean",
+ "description": "Render row checkboxes and a select-all checkbox for the current page."
+ },
+ {
+ "name": "selectedRows",
+ "type": "ReadonlySet",
+ "description": "Control selection using stable IDs from `getRowId`. Missing or non-selectable IDs stay stored but do not count as selected."
+ },
+ {
+ "name": "defaultSelectedRows",
+ "type": "ReadonlySet",
+ "description": "Set the initial selection for an uncontrolled, static, single-page table."
+ },
+ {
+ "name": "onSelectionChange",
+ "type": "({ selectedRows }: { selectedRows: Set }) => void",
+ "description": "Receive a new selection set when a checkbox is toggled. Data changes alone do not trigger this callback."
+ },
+ {
+ "name": "isRowSelectable",
+ "type": "(row: Data) => boolean",
+ "description": "Determine which rows can be selected and counted by select-all."
+ },
{
"name": "cellPadding",
"type": "'condensed' | 'normal' | 'spacious'",
@@ -235,6 +280,80 @@
}
]
},
+ {
+ "name": "Table.SelectionHeader",
+ "props": [
+ {
+ "name": "selection",
+ "type": "'all' | 'some' | 'none'",
+ "required": true,
+ "description": "Whether all, some, or none of the selectable rows are selected."
+ },
+ {
+ "name": "disabled",
+ "type": "boolean",
+ "description": "Whether row selection is unavailable."
+ },
+ {
+ "name": "onToggleSelect",
+ "type": "() => void",
+ "description": "Request toggling selection for all selectable rows."
+ },
+ {
+ "name": "checkboxRef",
+ "type": "React.Ref",
+ "description": "Forward a ref to the native selection checkbox."
+ },
+ {
+ "name": "aria-label",
+ "type": "string",
+ "defaultValue": "'Select rows'",
+ "description": "Provide the selection checkbox's accessible name."
+ },
+ {
+ "name": "aria-description",
+ "type": "string",
+ "description": "Describe the select-all scope, for example \"Select all 10 rows\"."
+ }
+ ]
+ },
+ {
+ "name": "Table.RowSelection",
+ "props": [
+ {
+ "name": "selected",
+ "type": "boolean",
+ "required": true,
+ "description": "Whether the row is selected."
+ },
+ {
+ "name": "disabled",
+ "type": "boolean",
+ "description": "Whether the row cannot be selected."
+ },
+ {
+ "name": "onToggleSelect",
+ "type": "() => void",
+ "description": "Request toggling selection for the row."
+ },
+ {
+ "name": "checkboxRef",
+ "type": "React.Ref",
+ "description": "Forward a ref to the native row checkbox."
+ },
+ {
+ "name": "aria-label",
+ "type": "string",
+ "defaultValue": "'Select row'",
+ "description": "Name the row checkbox when `aria-labelledby` is not provided."
+ },
+ {
+ "name": "aria-labelledby",
+ "type": "string",
+ "description": "Reference the row header IDs. The checkbox name adds \"Select\" before their text."
+ }
+ ]
+ },
{
"name": "Table.Cell",
"props": [
diff --git a/packages/react/src/DataTable/DataTable.features.stories.tsx b/packages/react/src/DataTable/DataTable.features.stories.tsx
index df4242405ed..117c8668b77 100644
--- a/packages/react/src/DataTable/DataTable.features.stories.tsx
+++ b/packages/react/src/DataTable/DataTable.features.stories.tsx
@@ -14,7 +14,7 @@ import {ActionList} from '../ActionList'
import {ActionMenu} from '../ActionMenu'
import {Blankslate} from '../Blankslate'
import {Button, IconButton} from '../Button'
-import {DataTable, type DataTableProps, Table} from '../DataTable'
+import {DataTable, type DataTableProps, type DataTableRowId, Table} from '../DataTable'
import Heading from '../Heading'
import Label from '../Label'
import LabelGroup from '../LabelGroup'
@@ -1750,6 +1750,59 @@ const groupedColumns = [
}),
]
+export const WithRowSelection = () => {
+ const [selectedRows, setSelectedRows] = React.useState>(() => new Set([2]))
+
+ return (
+
+
+ Selectable repositories
+
+ {
+ setSelectedRows(nextSelectedRows)
+ }}
+ />
+
+ )
+}
+
+export const WithGroupedRowSelection = () => {
+ const [selectedRows, setSelectedRows] = React.useState>(() => new Set())
+
+ return (
+
+
+ Selectable repositories by visibility
+
+ repo.id !== 1}
+ onSelectionChange={({selectedRows: nextSelectedRows}) => {
+ setSelectedRows(nextSelectedRows)
+ }}
+ />
+
+ )
+}
+
+WithGroupedRowSelection.parameters = {
+ docs: {
+ description: {
+ story: '`codeql-dca-worker` is not selectable and is excluded from select-all.',
+ },
+ },
+}
+
const sortableGroupedColumns = [
columnHelper.column({
header: 'Name',
@@ -1761,6 +1814,28 @@ const sortableGroupedColumns = [
groupedColumns[1],
]
+export const WithStandaloneAndGroupedRowSelection = () => {
+ const [selectedRows, setSelectedRows] = React.useState>(() => new Set())
+
+ return (
+
+
+ Selectable standalone and grouped repositories
+
+ repo.type === 'internal'), repoGroups[1]]}
+ columns={sortableGroupedColumns}
+ rowSelection
+ selectedRows={selectedRows}
+ onSelectionChange={({selectedRows: nextSelectedRows}) => {
+ setSelectedRows(nextSelectedRows)
+ }}
+ />
+
+ )
+}
+
export const WithSortableGroups = () => (
@@ -1803,20 +1878,24 @@ const paginationData: DataTableData = [
{id: 101, name: 'standalone/after', visibility: 'Unassigned'},
]
-export const WithGroups = () => {
- const titleId = React.useId()
- const [pageIndex, setPageIndex] = React.useState(0)
- const pageSize = 10
- const allRows = paginationData.flatMap(item => ('rows' in item ? item.rows : [item]))
- const pageRows = new Set(allRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize).map(row => row.id))
+function getPageData(source: DataTableData, pageRows: ReadonlySet) {
// Count data rows, not headings, and preserve each item's position and group membership.
- const data = paginationData.flatMap((item): DataTableData => {
+ return source.flatMap((item): DataTableData => {
if ('rows' in item) {
const rows = item.rows.filter(row => pageRows.has(row.id))
return rows.length > 0 ? [{...item, rows}] : []
}
return pageRows.has(item.id) ? [item] : []
})
+}
+
+export const WithGroups = () => {
+ const titleId = React.useId()
+ const [pageIndex, setPageIndex] = React.useState(0)
+ const pageSize = 10
+ const allRows = paginationData.flatMap(item => ('rows' in item ? item.rows : [item]))
+ const pageRows = new Set(allRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize).map(row => row.id))
+ const data = getPageData(paginationData, pageRows)
return (
@@ -1840,3 +1919,42 @@ export const WithGroups = () => {
)
}
+
+export const WithPaginatedRowSelection = () => {
+ const titleId = React.useId()
+ const [pageIndex, setPageIndex] = React.useState(0)
+ const [selectedRows, setSelectedRows] = React.useState>(() => new Set())
+ const pageSize = 10
+ const allRows = paginationData.flatMap(item => ('rows' in item ? item.rows : [item]))
+ const pageRows = new Set(allRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize).map(row => row.id))
+ const data = getPageData(paginationData, pageRows)
+
+ return (
+
+
+ Selectable paginated repositories
+
+ setSelectedRows(selectedRows)}
+ />
+ {
+ setPageIndex(pageIndex)
+ // Selection applies only to the current page.
+ setSelectedRows(new Set())
+ }}
+ />
+
+ )
+}
diff --git a/packages/react/src/DataTable/DataTable.tsx b/packages/react/src/DataTable/DataTable.tsx
index c2a25488d55..36aa8daf354 100644
--- a/packages/react/src/DataTable/DataTable.tsx
+++ b/packages/react/src/DataTable/DataTable.tsx
@@ -2,9 +2,19 @@ import type {ReactElement, ReactNode} from 'react'
import type {Column} from './column'
import {useTable} from './useTable'
import type {SortDirection} from './sorting'
-import type {DataTableData, DataTableRowGroup, UniqueRow} from './row'
+import type {DataTableData, DataTableRowGroup, DataTableRowId, UniqueRow} from './row'
import type {IsAny, ObjectPaths} from './utils'
-import {Table, TableHead, TableBody, TableRow, TableHeader, TableSortHeader, TableCell} from './Table'
+import {
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableHeader,
+ TableSortHeader,
+ TableSelectionHeader,
+ TableCell,
+ TableRowSelection,
+} from './Table'
import {TableGroup} from './TableGroup'
// ----------------------------------------------------------------------------
@@ -61,7 +71,7 @@ type DataTableBaseProps = {
* @param rowData The row data object for which the ID is being retrieved.
* @returns The unique identifier for the row, which can be a string or number.
*/
- getRowId?: (rowData: Data) => string | number
+ getRowId?: (rowData: Data) => DataTableRowId
/**
* Fires every time the user clicks a sortable column header. It reports
@@ -69,6 +79,32 @@ type DataTableBaseProps = {
* (never `"NONE"`).
*/
onToggleSort?: (columnId: ObjectPaths | string | number, direction: Exclude) => void
+
+ /**
+ * Whether to render controls for selecting rows.
+ */
+ rowSelection?: boolean
+
+ /**
+ * Controls the selected row IDs.
+ */
+ selectedRows?: ReadonlySet
+
+ /**
+ * Provides the initially selected row IDs for an uncontrolled table.
+ * Uncontrolled selection is intended for static, single-page data.
+ */
+ defaultSelectedRows?: ReadonlySet
+
+ /**
+ * Handles changes to the selected row IDs.
+ */
+ onSelectionChange?: ({selectedRows}: {selectedRows: Set}) => void
+
+ /**
+ * Determines whether a row can be selected.
+ */
+ isRowSelectable?: (row: Data) => boolean
}
export type DataTableProps = DataTableBaseProps & {
@@ -107,6 +143,11 @@ function DataTableImplementation({
externalSorting,
getRowId = defaultGetRowId,
onToggleSort,
+ rowSelection = false,
+ selectedRows,
+ defaultSelectedRows,
+ onSelectionChange,
+ isRowSelectable,
}: DataTableProps) {
const table = useTable({
data,
@@ -115,11 +156,28 @@ function DataTableImplementation({
initialSortDirection,
getRowId,
externalSorting,
+ rowSelection,
+ selectedRows,
+ defaultSelectedRows,
+ onSelectionChange,
+ isRowSelectable,
})
- const {headers, bodies, actions, gridTemplateColumns} = table
+ const {headers, bodies, actions, selection, gridTemplateColumns} = table
const renderRow = (row: (typeof bodies)[number]['rows'][number]) => (
-
+
+ {rowSelection ? (
+ {
+ actions.toggleRowSelection(row)
+ }}
+ />
+ ) : null}
{row.getCells().map(cell => (
({
>
+ {rowSelection ? (
+ 0
+ ? `Select all ${selection.selectableCount} ${selection.selectableCount === 1 ? 'row' : 'rows'}`
+ : undefined
+ }
+ onToggleSelect={() => {
+ actions.toggleAllRows()
+ }}
+ />
+ ) : null}
{headers.map(header => {
if (header.isSortable()) {
return (
@@ -177,7 +251,7 @@ function DataTableImplementation({
id={body.id}
label={body.label}
rowCount={body.rows.length}
- colSpan={headers.length}
+ colSpan={headers.length + (rowSelection ? 1 : 0)}
aria-label={body['aria-label']}
>
{body.rows.map(renderRow)}
diff --git a/packages/react/src/DataTable/DataTable.types.test.tsx b/packages/react/src/DataTable/DataTable.types.test.tsx
index 45562b96042..099e5c20f65 100644
--- a/packages/react/src/DataTable/DataTable.types.test.tsx
+++ b/packages/react/src/DataTable/DataTable.types.test.tsx
@@ -3,6 +3,7 @@ import type {Column} from './column'
import {DataTable, type DataTableProps} from './DataTable'
import type {DataTableRowGroup} from './row'
import {useTable} from './useTable'
+import {Table, type DataTableRowId} from '../experimental'
interface Repository {
id: number
@@ -193,3 +194,197 @@ export function shouldPreserveBusinessTypeInference() {
/>
)
}
+
+export function shouldAcceptRowSelectionProps() {
+ const selectedRows: ReadonlySet = new Set([1, 'repository'])
+
+ return (
+ row.name !== ''}
+ onSelectionChange={({selectedRows: nextSelectedRows}) => {
+ nextSelectedRows.add('repository')
+ }}
+ />
+ )
+}
+
+export function shouldInferMixedSelectionCallbacks() {
+ return (
+ {
+ const id: number = row.id
+ return id
+ }}
+ isRowSelectable={row => {
+ const name: string = row.name
+ // @ts-expect-error Selection predicates receive member rows, not group metadata.
+ row.groupId
+ return name !== ''
+ }}
+ onSelectionChange={({selectedRows}) => {
+ const ids: Set = selectedRows
+ return ids
+ }}
+ />
+ )
+}
+
+export function shouldInferLazyFlatSelectionCallbacks() {
+ return (
+ row.name.toUpperCase()}]}
+ rowSelection
+ selectedRows={new Set([1, 'repository'])}
+ getRowId={row => {
+ const id: number = row.id
+ return id
+ }}
+ isRowSelectable={row => {
+ // @ts-expect-error Selection predicates must retain the inferred row type.
+ row.missing
+ return row.name !== ''
+ }}
+ onSelectionChange={({selectedRows}) => {
+ const ids: Set = selectedRows
+ // @ts-expect-error Selected IDs cannot contain objects.
+ selectedRows.add({id: 1})
+ return ids
+ }}
+ />
+ )
+}
+
+export function shouldInferLazyGroupedSelectionCallbacks() {
+ return (
+ row.name.toUpperCase()}]}
+ rowSelection
+ defaultSelectedRows={new Set([1])}
+ getRowId={row => {
+ const id: number = row.id
+ return id
+ }}
+ isRowSelectable={row => {
+ // @ts-expect-error Selection predicates receive rows, not group metadata.
+ row.groupId
+ return row.name !== ''
+ }}
+ onSelectionChange={({selectedRows}) => {
+ const ids: Set = selectedRows
+ // @ts-expect-error Selected IDs cannot contain booleans.
+ selectedRows.add(true)
+ return ids
+ }}
+ />
+ )
+}
+
+export function shouldInferLazyMixedSelectionCallbacks() {
+ return (
+ row.name.toUpperCase()}]}
+ rowSelection
+ selectedRows={new Set([1])}
+ getRowId={row => {
+ const id: number = row.id
+ return id
+ }}
+ isRowSelectable={row => {
+ // @ts-expect-error Selection predicates receive rows, not group metadata.
+ row.rows
+ return row.name !== ''
+ }}
+ onSelectionChange={({selectedRows}) => {
+ const ids: Set = selectedRows
+ // @ts-expect-error The callback provides a Set, not an array.
+ selectedRows.push(1)
+ return ids
+ }}
+ />
+ )
+}
+
+export function shouldAcceptLazySelectionProps() {
+ const selectedRows: ReadonlySet = new Set([1])
+ const props: DataTableProps = {
+ data: groups,
+ columns,
+ rowSelection: true,
+ selectedRows,
+ isRowSelectable: row => row.name !== '',
+ onSelectionChange: ({selectedRows: next}) => {
+ const ids: Set = next
+ return ids
+ },
+ }
+ const explicit = {...props} />
+ const spread =
+ return {explicit, spread}
+}
+
+export function shouldRejectInvalidLazySelectionProps() {
+ // @ts-expect-error Selection values must be sets of string or number IDs.
+ const invalidSelectedRows = data={groups} columns={columns} selectedRows={[1]} />
+ const invalidDefault = (
+ // @ts-expect-error Default selection cannot contain boolean IDs.
+ data={groups} columns={columns} defaultSelectedRows={new Set([true])} />
+ )
+ const invalidCallback = (
+ // @ts-expect-error The callback receives an object containing a Set, not an array.
+ data={groups} columns={columns} onSelectionChange={(_rows: number[]) => {}} />
+ )
+ const invalidPredicate = (
+
+ data={groups}
+ columns={columns}
+ // @ts-expect-error Selection predicates receive member rows, not groups.
+ isRowSelectable={(_group: DataTableRowGroup) => true}
+ />
+ )
+ return {invalidSelectedRows, invalidDefault, invalidCallback, invalidPredicate}
+}
+
+export function shouldAcceptSemanticSelectionParts() {
+ const ids: ReadonlySet = new Set([1, 'repository'])
+ const dataTable =
+ const header = {}} />
+ const row = {}} />
+ return {dataTable, header, row}
+}
+
+export function shouldRejectCheckboxSpecificSelectionProps() {
+ // @ts-expect-error Headers describe selection with one semantic state.
+ const checkedHeader =
+ // @ts-expect-error Header states are limited to all, some, and none.
+ const invalidHeader =
+ // @ts-expect-error Individual rows have no partial selection state.
+ const mixedRow =
+ // @ts-expect-error Row selection uses selected, not the underlying checkbox prop.
+ const checkedRow =
+ // @ts-expect-error Selection parts do not expose native input change events.
+ const inputHandler = {}} />
+ const eventHandler = (
+ // @ts-expect-error Semantic toggles do not receive an input event.
+ ) => {}} />
+ )
+ // @ts-expect-error Row IDs are strings or numbers, not objects.
+ const invalidId: DataTableRowId = {id: 1}
+ return {checkedHeader, invalidHeader, mixedRow, checkedRow, inputHandler, eventHandler, invalidId}
+}
diff --git a/packages/react/src/DataTable/Table.features.stories.tsx b/packages/react/src/DataTable/Table.features.stories.tsx
index 453458ae209..925067a05c1 100644
--- a/packages/react/src/DataTable/Table.features.stories.tsx
+++ b/packages/react/src/DataTable/Table.features.stories.tsx
@@ -1,6 +1,6 @@
import type {Meta} from '@storybook/react-vite'
-import {useId} from 'react'
-import {Table} from '../DataTable'
+import {useId, useState} from 'react'
+import {Table, type DataTableRowId} from '../DataTable'
export default {
title: 'Experimental/Components/Table/Features',
@@ -61,3 +61,79 @@ export const WithGroups = () => {
)
}
+
+export const WithRowSelection = () => {
+ const id = useId()
+ const titleId = `${id}-title`
+ const selectionColumnId = `${id}-selection`
+ const nameColumnId = `${id}-name`
+ const visibilityColumnId = `${id}-visibility`
+ const [selectedRows, setSelectedRows] = useState>(() => new Set([2]))
+ const selectableRows = groups.flatMap(group => group.rows).filter(row => row.id !== 1)
+ const selectedCount = selectableRows.filter(row => selectedRows.has(row.id)).length
+ const allSelected = selectableRows.length > 0 && selectedCount === selectableRows.length
+
+ return (
+
+
+ Selectable repositories using Table
+
+