DataTable: add per-column filtering via filterBy#7855
Open
ianwinsemius wants to merge 1 commit into
Open
Conversation
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>
🦋 Changeset detectedLatest commit: 346ea82 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Contributor
There was a problem hiding this comment.
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 intouseTablerow derivation. - Render a filter row in
DataTablewhenfilterableis enabled and at least one column opts intofilterBy. - 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #
Adds first-class per-column filtering to the experimental
DataTable. Columns opt in with a newfilterByprop that mirrors the shape ofsortBy, and the table renders an inline filter row beneath the column headers when at least one column is filterable.Goals:
sortByergonomics (true| named strategy | custom function) so the column API feels native.filters/onFilterChange) and uncontrolled (defaultFilters) modes.externalFilteringescape hatch that parallels the existingexternalSortingprop.Table.FilterRow/Table.FilterCellInputprimitives 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/CustomFilterStrategytype exports.WithFiltering,WithControlledFilters,WithCustomFilter.Changed
useTablenow derives filtered rows on top of the sorted row order; no impact on existing consumers.DataTable.docs.jsonupdated with new prop docs and story IDs.Removed
(none)
Rollout strategy
Testing & Reviewing
packages/react/src/DataTable/__tests__/filtering.test.tsxcover strategies (substring,startsWith, custom), rendering (filter row only when at least one column hasfilterBy), behaviour (controlled, uncontrolled,defaultFilters,onFilterChange,externalFiltering, sort+filter composition), and a11y (aria-labels, decorative non-filterable cells).npm run build,npm run type-check,npm run lint,npm run lint:css, andnpm test -- --run packages/react/src/DataTable/all pass locally.Merge checklist