Skip to content

DataTable: Add current-page row selection - #8415

Merged
primer[bot] merged 9 commits into
mainfrom
agents/implement-row-selection-datatable
Oct 14, 2026
Merged

primer[bot] merged 9 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: bacc392

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
@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
Comment thread e2e/components/DataTable.test.ts Outdated
Comment thread e2e/components/DataTable.test.ts Outdated
Comment thread e2e/components/DataTable.test.ts Outdated
Comment thread packages/react/src/DataTable/DataTable.docs.json
Comment thread packages/react/src/DataTable/DataTable.docs.json
Comment thread packages/react/src/DataTable/DataTable.docs.json Outdated
Comment thread packages/react/src/DataTable/DataTable.docs.json
Comment thread packages/react/src/DataTable/DataTable.features.stories.tsx Outdated
Comment thread packages/react/src/DataTable/Table.tsx Outdated
@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>
@github-actions github-actions Bot removed the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Sep 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rickyzhangca rickyzhangca added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Sep 29, 2026
@github-actions
github-actions Bot temporarily deployed to storybook-preview-8415 September 29, 2026 19:40 Inactive
@github-actions github-actions Bot removed the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Sep 29, 2026
@github-actions
github-actions Bot temporarily deployed to storybook-preview-8415 September 29, 2026 19:51 Inactive
@github-actions
github-actions Bot temporarily deployed to storybook-preview-8415 September 29, 2026 20:00 Inactive
@rickyzhangca

Copy link
Copy Markdown
Contributor Author

@joshblack a very gentle bump!!

@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! Just had some questions / comments

Comment on lines +88 to +93
aria-label="Select rows"
aria-description={
selectableRows.length > 0
? `Select all ${selectableRows.length} ${selectableRows.length === 1 ? 'row' : 'rows'}`
: undefined
}

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.

@rickyzhangca do you happen to know if these are always required a11y-wise?

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 we got a11y tickets previously on checkboxes https://github.com/github/accessibility/issues/10271

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.

aria-label probably can be removed though since aria-description already does the job, removed!

Comment thread packages/react/src/DataTable/Table.tsx Outdated
return (
<TableHeader
{...rest}
className={clsx('TableSelectionHeader', className, classes.TableSelectionHeader)}

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.

For the new components, feel free to leave off the global classes (e.g. TableSelectionHeader in this case). These are just in the file currently for backwards-compat but we'll remove them in the future.

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 +66 to +74
const id = useId()
const titleId = `${id}-title`
const selectionColumnId = `${id}-selection`
const nameColumnId = `${id}-name`
const visibilityColumnId = `${id}-visibility`
const [selectedRows, setSelectedRows] = useState<ReadonlySet<DataTableRowId>>(() => 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

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.

For these Table-specific stories, how would you feel about using useTable with them? I wasn't sure but could help reduce duplication stuff. Also would get if it feels like re-creating data table though lol.

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.

haha yea it'll be kinda like another small data table

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.

these ones are there just because I wanted to demo "BYOD", composing a table with table components only, also useTable isn't public for others to use anyway, so I'd lean towards keeping these examples small and minimal

Comment on lines +285 to +288
selectionId: rowId,
selectionLabelledBy: rowHeaderIds.length > 0 ? rowHeaderIds.join(' ') : undefined,
selected: selectable && selectedRows.has(rowId),
selectable,

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.

Did you notice any difference between calculating these and making them lazy either behind a getter or explicit get* function? I always wondered about this with tanstack's table so was curious if you noticed anything 👀

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.

i viewed it as a precaution, lemme test

@rickyzhangca rickyzhangca Oct 9, 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.

2 rounds each, without DOM

# of rows data current getter get*()
10 Flat 0.00170 / 0.00170 0.00325 / 0.00325 0.00150 / 0.00145
10 Grouped 0.00170 / 0.00170 0.00325 / 0.00325 0.00150 / 0.00150
10 Mixed 0.00170 / 0.00170 0.00330 / 0.00330 0.00150 / 0.00150
100 Flat 0.0160 / 0.0160 0.0325 / 0.0325 0.0140 / 0.0140
100 Grouped 0.0165 / 0.0170 0.0330 / 0.0340 0.0140 / 0.0140
100 Mixed 0.0160 / 0.0165 0.0330 / 0.0335 0.0145 / 0.0140
1,000 Flat 0.160 / 0.160 0.335 / 0.335 0.140 / 0.140
1,000 Grouped 0.160 / 0.165 0.335 / 0.340 0.140 / 0.145
1,000 Mixed 0.165 / 0.165 0.340 / 0.340 0.145 / 0.145

with DOM

# of rows data current getter get*()
10 Flat 0.07 / 0.08 0.07 / 0.08 0.07 / 0.07
10 Grouped 0.07 / 0.08 0.08 / 0.08 0.08 / 0.07
10 Mixed 0.07 / 0.07 0.08 / 0.08 0.07 / 0.07
100 Flat 0.48 / 0.48 0.50 / 0.51 0.50 / 0.50
100 Grouped 0.54 / 0.54 0.56 / 0.57 0.55 / 0.55
100 Mixed 0.53 / 0.54 0.57 / 0.57 0.54 / 0.54
1,000 Flat 5.50 / 5.50 5.85 / 5.85 5.60 / 5.55
1,000 Grouped 6.30 / 6.30 6.40 / 6.35 6.40 / 6.35
1,000 Mixed 6.15 / 6.30 6.50 / 6.50 6.60 / 6.65

don't think it's significant enough difference, we could keep it as is?

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

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Passed  Changes   Passed
Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@github-actions github-actions Bot added integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh and removed integration-tests: failing Changes in this PR cause breaking changes in gh/gh labels Oct 9, 2026
@primer
primer Bot disabled auto-merge October 14, 2026 15:59
@primer
primer Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 66a9f31 Oct 14, 2026
55 checks passed
@primer
primer Bot deleted the agents/implement-row-selection-datatable branch October 14, 2026 16:07
@primer primer Bot mentioned this pull request Oct 14, 2026

This branch was successfully deployed

1 active and 1 inactive deployments
storybook-preview-8415 — bacc3928 Deployed Oct 9, 2026 by github-actions[bot]
github-pages — bacc3928 Deployed Oct 9, 2026 by rickyzhangca via deploy-preview #33617
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants