Skip to content

DataTable: add per-column filtering via filterBy#7855

Open
ianwinsemius wants to merge 1 commit into
primer:mainfrom
ianwinsemius:feat/datatable-column-filtering
Open

DataTable: add per-column filtering via filterBy#7855
ianwinsemius wants to merge 1 commit into
primer:mainfrom
ianwinsemius:feat/datatable-column-filtering

Conversation

@ianwinsemius
Copy link
Copy Markdown

Closes #

Adds first-class per-column filtering to the experimental DataTable. Columns opt in with a new filterBy prop that mirrors the shape of sortBy, and the table renders an inline filter row beneath the column headers when at least one column is filterable.

<DataTable
  data={repos}
  columns={[
    {header: 'Repository', field: 'name', filterBy: true, sortBy: 'alphanumeric'},
    {header: 'Type', field: 'type', filterBy: 'startsWith'},
    {header: 'Updated', field: 'updatedAt'},
  ]}
  filterable
/>

Goals:

  • Match sortBy ergonomics (true | named strategy | custom function) so the column API feels native.
  • Cover both controlled (filters / onFilterChange) and uncontrolled (defaultFilters) modes.
  • Provide an externalFiltering escape hatch that parallels the existing externalSorting prop.
  • Expose Table.FilterRow / Table.FilterCellInput primitives for consumers composing their own header.

Changelog

New

  • Column.filterBy?: boolean | 'substring' | 'startsWith' | CustomFilterStrategy<Data> — opts a column into filtering.
  • DataTableProps.filterable — renders the per-column filter row.
  • DataTableProps.filters / defaultFilters / onFilterChange — controlled and uncontrolled filter state.
  • DataTableProps.externalFiltering — defer filtering to the server.
  • DataTableProps.filterPlaceholder — customize the input placeholder.
  • Table.FilterRow / Table.FilterCellInput — public primitives.
  • FilterStrategy / CustomFilterStrategy type exports.
  • Stories: WithFiltering, WithControlledFilters, WithCustomFilter.

Changed

  • useTable now derives filtered rows on top of the sorted row order; no impact on existing consumers.
  • DataTable.docs.json updated with new prop docs and story IDs.

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

Testing & Reviewing

  • 23 new unit tests in packages/react/src/DataTable/__tests__/filtering.test.tsx cover strategies (substring, startsWith, custom), rendering (filter row only when at least one column has filterBy), behaviour (controlled, uncontrolled, defaultFilters, onFilterChange, externalFiltering, sort+filter composition), and a11y (aria-labels, decorative non-filterable cells).
  • Existing 117 DataTable tests pass unchanged — the refactor is fully backward compatible.
  • npm run build, npm run type-check, npm run lint, npm run lint:css, and npm test -- --run packages/react/src/DataTable/ all pass locally.
  • Stories available under Experimental / Components / DataTable / Features / With Filtering (and the two adjacent ones).

Merge checklist

Mirrors the shape of `sortBy`: columns opt in with
`filterBy: true | 'substring' | 'startsWith' | CustomFilterStrategy<Data>`
and the DataTable renders an inline filter row beneath the column
headers when at least one column is filterable.

- New: `Column.filterBy` opt-in per column.
- New: `DataTableProps.filterable` toggles the filter row.
- New: `filters` / `defaultFilters` / `onFilterChange` for controlled
  and uncontrolled modes.
- New: `externalFiltering` to defer filtering to the server (matches
  the existing `externalSorting` escape hatch).
- New: `Table.FilterRow` / `Table.FilterCellInput` primitives for
  consumers composing their own header.
- New: 'substring' and 'startsWith' built-in filter strategies in
  `./filtering.ts`.
- Stories: WithFiltering, WithControlledFilters, WithCustomFilter.
- Tests: 23 new cases covering strategies, controlled/uncontrolled
  state, externalFiltering, sort+filter composition, and a11y.
- Docs: DataTable.docs.json updated with new story ids and prop docs.
- Changeset: minor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ianwinsemius ianwinsemius requested a review from a team as a code owner May 19, 2026 23:33
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

🦋 Changeset detected

Latest commit: 346ea82

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class per-column filtering to the experimental DataTable by introducing a Column.filterBy API (mirroring sortBy) and rendering an optional inline filter row beneath the header. The implementation supports controlled/uncontrolled filter state, an externalFiltering escape hatch, and exports Table.FilterRow / Table.FilterCellInput primitives for custom compositions.

Changes:

  • Add filtering strategies + matching helpers (substring, startsWith, custom) and integrate filtering into useTable row derivation.
  • Render a filter row in DataTable when filterable is enabled and at least one column opts into filterBy.
  • Update docs, stories, and add a dedicated unit test suite for filtering behavior and a11y.
Show a summary per file
File Description
packages/react/src/DataTable/useTable.ts Adds controlled/uncontrolled filter state + derives filtered rows (and counts) on top of current row order.
packages/react/src/DataTable/Table.tsx Introduces TableFilterRow and TableFilterCellInput primitives backed by TextInput.
packages/react/src/DataTable/Table.module.css Adds styling hooks for filter cells/inputs.
packages/react/src/DataTable/index.ts Exposes Table.FilterRow / Table.FilterCellInput and filter strategy types.
packages/react/src/DataTable/filtering.ts New filtering strategy implementations and matches() dispatcher.
packages/react/src/DataTable/DataTable.tsx Wires filterable UI + filter props into useTable and renders the filter row.
packages/react/src/DataTable/DataTable.features.stories.tsx Adds stories demonstrating filtering, controlled filters, and custom strategy usage.
packages/react/src/DataTable/DataTable.docs.json Documents new props and adds story IDs.
packages/react/src/DataTable/column.ts Adds Column.filterBy type + docs.
packages/react/src/DataTable/tests/filtering.test.tsx New test coverage for strategies, rendering rules, controlled/uncontrolled, external filtering, composition with sorting, and a11y.
.changeset/datatable-add-column-filtering.md Minor changeset for the new filtering API surface.

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 5

Comment on lines +320 to +335
export type TableFilterCellInputProps = {
/** Unique column identifier (matches `Column.id` or `Column.field`) */
columnId: string

/** Current filter value for this column */
value: string

/** Called when the filter value changes */
onChange: (value: string) => void

/** Accessible label for the input (defaults to "Filter {header}") */
'aria-label': string

/** Placeholder text (defaults to "Filter") */
placeholder?: string
}
function TableFilterRow<Data extends UniqueRow>({headers, filters, onChange, placeholder}: TableFilterRowProps<Data>) {
return (
<tr
className={clsx('TableRow', 'TableFilterRow', classes.TableRow, classes.TableFilterRow)}
Comment on lines +394 to +400
// Purely decorative cell that keeps the grid layout aligned for
// non-filterable columns. Left without children so screen readers
// surface nothing meaningful for it.
return (
<td
key={`${header.id}-filter`}
className={clsx('TableFilterCell', classes.TableFilterCell)}
Comment on lines +255 to +266
const activeFilters = Object.entries(filters).filter(([, value]) => value && value.trim() !== '')
const filteredRowOrder =
externalFiltering || activeFilters.length === 0
? rowOrder
: rowOrder.filter(row =>
activeFilters.every(([columnId, query]) => {
const column = columns.find(column => (column.id ?? column.field) === columnId)
if (!column || column.filterBy === undefined || column.filterBy === false) return true
const value = column.field !== undefined ? get(row, column.field) : row
return filterMatches(column.filterBy as true | Parameters<typeof filterMatches<Data>>[0], value, query, row)
}),
)
Comment on lines +293 to +306
/* TableFilterRow ----------------------------------------------------------- */
.TableFilterCell {
/* Visually distinguish the filter row from the header row above and the
* data rows below; intentionally mirrors the existing TableHeader treatment
* but with a subtler background so the inputs stand out. */
background-color: var(--bgColor-inset);
font-weight: var(--base-text-weight-normal);
/* stylelint-disable-next-line primer/spacing */
padding-block: 0.25rem;
}

.TableHead .TableFilterRow .TableFilterCell {
border-block-start: 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants