Skip to content

DataTable: Add current-page row selection - #8415

Open
rickyzhangca wants to merge 4 commits into
mainfrom
agents/implement-row-selection-datatable
Open

rickyzhangca wants to merge 4 commits into
mainfrom
agents/implement-row-selection-datatable

Conversation

@rickyzhangca

@rickyzhangca rickyzhangca commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Refs github/primer#6718

Implements the row-selection stage for DataTable, building on the grouping support merged in #8386. This PR targets main directly and does not close the tracking issue: bulk actions remain a follow-up.

CleanShot.2026-09-14.at.10.28.16.AM.mp4

Changelog

New

  • Opt-in selection for flat, grouped, and mixed standalone/grouped rows.
  • rowSelection, selectedRows, defaultSelectedRows, onSelectionChange, and isRowSelectable props, using the existing getRowId for stable identity.
  • Composable Table.SelectionHeader and Table.RowSelection parts using Primer Checkbox.
  • Current-page select-all, mixed state, disabled rows, documentation, and flat/grouped/mixed examples.

Changed

  • Selection integrates with the private table model and includes the leading selection column in group spans and accessible header associations.
  • Sorting preserves selection by stable ID. Select-all covers every selectable supplied row, including rows outside the scroll viewport.

Removed

  • None.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Includes a minor changeset. Selection is opt-in; existing consumers do not need to migrate.

Testing & Reviewing

State ownership and scope

  • selectedRows / defaultSelectedRows accept ReadonlySet<string | number>; onSelectionChange receives {selectedRows: Set<string | number>}.
  • Pagination/filter owners clear controlled selection on scope changes. A new data reference or changed membership alone does not reset selection or emit a callback.
  • Missing or currently non-selectable IDs remain in the owning set, but are excluded from effective selection and counts. Permanent removal is owner-reconciled; future bulk actions must use effective selection rather than blindly consume the stored set.
  • Mixed-state activation selects the remaining rows. With zero selectable rows, controls remain disabled and the header is unchecked/non-mixed.
  • Bulk toolbar, cross-page selection, group-level select-all, and collapsible groups are out of scope.

Automated validation on this HEAD

  • Build, full type-check, JS/TS lint, CSS lint, and formatting checks passed.
  • Full unit suite: 159 files passed; 2,704 tests passed, 2 skipped, 10 todo.
  • Focused DataTable suite: 183 tests passed across 8 files.
  • Playwright DataTable accessibility/interaction suite: 49 tests passed, including controlled selection round-trips, mixed-data sorting, themed Axe checks, and upstream grouping pagination.
  • Flat/grouped/mixed hydration tests verify selection ID references.

Reviewer focus / outstanding validation

  • Review the selection API and owner-reconciled reset/removal policy.
  • Check repeated row selection/deselection, accumulation of selected rows, mixed-to-all-to-clear, and focus preservation on select-all.
  • Stories: WithRowSelection, WithGroupedRowSelection, and WithMixedRowSelection under DataTable / Features.
  • Manual NVDA, JAWS, and VoiceOver validation remains outstanding, especially header announcements and navigation across group boundaries. Automated Axe and DOM checks do not establish screen-reader parity.
  • Selection stories have interaction/Axe coverage, not visual regression snapshots. The mixed story was also inspected in the browser.
  • Storybook MCP was unavailable; browser verification used local Playwright and the integrated browser instead.

Kept as draft pending review and manual accessibility validation.

Add controlled and uncontrolled selection for flat and grouped tables with accessible selection controls and regression coverage.\n\nRefs github/primer#6718

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 76eb548

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions
github-actions Bot temporarily deployed to storybook-preview-8415 September 14, 2026 16:34 Inactive
@rickyzhangca rickyzhangca added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Sep 14, 2026
@github-actions github-actions Bot removed the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Sep 14, 2026
@github-actions
github-actions Bot temporarily deployed to storybook-preview-8415 September 14, 2026 17:02 Inactive
@primer-integration

primer-integration Bot commented Sep 14, 2026

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Waiting  Changes   Waiting
Failed  CI   Failed
Passed  VRT   Passed
Passed  Projects   Passed

CI check runs linting, type checking, and unit tests. Check the workflow logs for specific failures.

Need help? If you believe this failure is unrelated to your changes, please reach out to the Primer team for assistance.

@github-actions github-actions Bot added integration-tests: failing Changes in this PR cause breaking changes in gh/gh and removed integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm labels Sep 14, 2026
@rickyzhangca
rickyzhangca requested a lite review from Copilot September 14, 2026 18:42
@rickyzhangca
rickyzhangca marked this pull request as ready for review September 14, 2026 18:42
@rickyzhangca
rickyzhangca requested a review from a team as a code owner September 14, 2026 18:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

A moderate accessibility issue remains unresolved in row checkbox fallback naming.

Review tier: Lite
Findings: None

What changed in this PR

Adds opt-in current-page row selection to DataTable, including controlled state, grouped rows, accessibility support, documentation, and tests.

Changes:

  • Added stable-ID selection APIs and select-all behavior.
  • Added composable selection components.
  • Added examples, accessibility documentation, browser tests, and a minor changeset.
File Summary
packages/​react/​src/​experimental/​index.ts Exports selection prop types.
packages/​react/​src/​DataTable/​useTable.ts Manages selection state and derived row status.
packages/​react/​src/​DataTable/​Table.tsx Adds composable selection parts.
packages/​react/​src/​DataTable/​Table.module.css Styles selection controls.
packages/​react/​src/​DataTable/​Table.features.stories.tsx Demonstrates composed selection.
packages/​react/​src/​DataTable/​index.ts Exports selection components and types.
packages/​react/​src/​DataTable/​DataTable.types.test.tsx Validates selection typings.
packages/​react/​src/​DataTable/​DataTable.tsx Integrates selection into DataTable.
packages/​react/​src/​DataTable/​DataTable.features.stories.tsx Adds selection examples.
packages/​react/​src/​DataTable/​DataTable.docs.json Documents selection APIs.
packages/​react/​src/​DataTable/​DataTable.accessibility.md Documents selection semantics.
packages/​react/​src/​DataTable/​__tests__/​DataTableSelection.test.tsx Tests selection behavior and hydration.
packages/​react/​src/​__tests__/​__snapshots__/​exports.test.ts.snap Updates export snapshots.
e2e/​components/​Table.test.ts Tests composed-selection interactions and accessibility.
e2e/​components/​DataTable.test.ts Tests DataTable selection scenarios.
.changeset/​selectable-data-table-rows.md Declares the minor release.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@joshblack joshblack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great so far! Thanks for putting this up. The config-based API makes a lot of sense to me 💯 Left a couple of comments/questions and then also had some broader things:

  • One though, do you think it'd be nice for data table to start to break down the sections more? We don't really do this for other components but I bet for data table it's worth it
  • For select all, are we including the "select all" bit or is that in the batch actions stuff you mentioned is coming later on? It'd be nice to include that with pagination because without it the example is kind of a bummer 😞
  • I was curious about some of the demo controls (like the buttons to refresh pages), what are they for? Would they be better as dev stories?
  • For the selectable standalone and grouped repo story I was a little confused since I couldn't select the standalone one, what is the intended behavior?

Thanks again for putting this up! Hope these all make sense, can't wait to hear what you think 👀

Comment thread e2e/components/DataTable.test.ts Outdated
{
title: 'With Row Selection',
id: 'experimental-components-datatable-features--with-row-selection',
aat: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I missed this in the last one but why did we need an aat flag for stories? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's storybook's automated a11y testing, somehow I thought we always want it, removed!

Comment thread e2e/components/DataTable.test.ts Outdated
]

test.describe('DataTable', () => {
test('selection scope is owned by pagination and filtering @aat', async ({page}) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organization-wise this should probably live after the loop tests we have (just as the convention we have for other ones)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved!

Comment thread e2e/components/DataTable.test.ts Outdated
]

test.describe('DataTable', () => {
test('selection scope is owned by pagination and filtering @aat', async ({page}) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separately, what are we looking to test with this in Playwright? I wasn't quite sure so wanted to check. Is it that we want to run axe at the end?

@rickyzhangca rickyzhangca Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not just for axe, mainly for testing the interactions

this end to end one is too long to follow, they are covered by the dev story anyway, removed!

Comment thread e2e/components/DataTable.test.ts Outdated
'experimental-components-datatable-features--with-grouped-row-selection',
'experimental-components-datatable-features--with-mixed-row-selection',
]) {
test(`controlled selection round-trip ${id} @aat`, async ({page}) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a similar question with this one, just wanted to see what we wanted to capture with these tests in playwright 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking checkboxes are clickable in no grouping + all rows are in groups + some rows are standalone and some are in groups

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit hard to read, updated it to be testing all 3 cases in one some rows are standalone and some are in groups setting!

Comment on lines +281 to +291
{
"name": "checked",
"type": "boolean",
"required": true,
"description": "Whether every selectable row is selected."
},
{
"name": "indeterminate",
"type": "boolean",
"description": "Whether some, but not all, selectable rows are selected."
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help at all to collapse these into one prop? e.g. selection="all|some|none" just to make it more semantic and remove the ambiguity if checked and indeterminate are applied

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment on lines +297 to +301
{
"name": "onChange",
"type": "React.ChangeEventHandler<HTMLInputElement>",
"description": "Handle changes to the native selection checkbox."
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose the underlying checkbox even for this or could it be like an onToggleSelect kind of thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to onToggleSelect?: () => void!

Comment on lines +329 to +333
{
"name": "indeterminate",
"type": "boolean",
"description": "Whether the row is in a mixed selection state."
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure what this one would mean with mixed row selection, could you share more about when this would show up? I assume for groups, potentially? 👀 The mixed row selection story didn't make it clear to me what was going on

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea you right, individual rows shouldn't have intermediate state, removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“Mixed” referred to standalone rows alongside groups, not mixed row selection, fixed the name so it's clear, my bad

]
},
{
"name": "Table.RowSelection",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar prop-style questions to the header one above, curious what you think!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

]

export const WithRowSelection = () => {
const [selectedRows, setSelectedRows] = React.useState<ReadonlySet<string | number>>(() => new Set([2]))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a general question, should we make the row id type consumable instead of doing string | number everywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment thread packages/react/src/DataTable/Table.tsx Outdated
...rest
}: TableSelectionHeaderProps) {
return (
<th

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random, is there a reason we couldn't use the TableHeader component directly and then add on top of it? My thought was that maybe it didn't allow us to override something that we wanted but wanted to check-in to see 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

@rickyzhangca

Copy link
Copy Markdown
Contributor Author

One though, do you think it'd be nice for data table to start to break down the sections more? We don't really do this for other components but I bet for data table it's worth it

totally, i also found Features getting so long, will make a follow up PR just to regroup stories

For select all, are we including the "select all" bit or is that in the batch actions stuff you mentioned is coming later on? It'd be nice to include that with pagination because without it the example is kind of a bummer 😞

sorry I didn't follow this, do you mean the ability to select all and do batch actions? this pr will enable selecting all, later on we add bulk actions

I was curious about some of the demo controls (like the buttons to refresh pages), what are they for? Would they be better as dev stories?

they are for demoing certain behaviours, for example the refresh button demos the selection will persist between data refreshes

For the selectable standalone and grouped repo story I was a little confused since I couldn't select the standalone one, what is the intended behavior?

yea that can be confusing, remove that disabled row

Address review feedback with semantic selection props and a shared row ID type. Simplify examples and browser coverage without changing current-page selection ownership.

Refs github/primer#6718

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: failing Changes in this PR cause breaking changes in gh/gh

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants