From c8544bd6b07bad266851e7d3faafdcf5bf1830bb Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 21:10:03 -0400 Subject: [PATCH 1/8] feat: add onScroll prop to table API and update changelog for paginationPosition feature (#1319) --- apps/docs/.astro/content-assets.mjs | 1 - apps/docs/.astro/content-modules.mjs | 1 - apps/docs/.astro/content.d.ts | 199 ------------------ apps/docs/.astro/data-store.json | 1 - apps/docs/.astro/settings.json | 5 - apps/docs/.astro/types.d.ts | 2 - .../src/components/demos/OnScrollDemo.tsx | 67 ++++++ apps/docs/src/pages/docs/api.md | 2 +- apps/docs/src/pages/docs/changelog.astro | 5 + apps/docs/src/pages/docs/fixed-header.astro | 30 +++ src/DataTable.css | 2 +- src/themes/base.ts | 3 +- src/types.ts | 2 +- 13 files changed, 106 insertions(+), 214 deletions(-) delete mode 100644 apps/docs/.astro/content-assets.mjs delete mode 100644 apps/docs/.astro/content-modules.mjs delete mode 100644 apps/docs/.astro/content.d.ts delete mode 100644 apps/docs/.astro/data-store.json delete mode 100644 apps/docs/.astro/settings.json delete mode 100644 apps/docs/.astro/types.d.ts create mode 100644 apps/docs/src/components/demos/OnScrollDemo.tsx diff --git a/apps/docs/.astro/content-assets.mjs b/apps/docs/.astro/content-assets.mjs deleted file mode 100644 index 2b8b8234..00000000 --- a/apps/docs/.astro/content-assets.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/apps/docs/.astro/content-modules.mjs b/apps/docs/.astro/content-modules.mjs deleted file mode 100644 index 2b8b8234..00000000 --- a/apps/docs/.astro/content-modules.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/apps/docs/.astro/content.d.ts b/apps/docs/.astro/content.d.ts deleted file mode 100644 index c0082cc8..00000000 --- a/apps/docs/.astro/content.d.ts +++ /dev/null @@ -1,199 +0,0 @@ -declare module 'astro:content' { - export interface RenderResult { - Content: import('astro/runtime/server/index.js').AstroComponentFactory; - headings: import('astro').MarkdownHeading[]; - remarkPluginFrontmatter: Record; - } - interface Render { - '.md': Promise; - } - - export interface RenderedContent { - html: string; - metadata?: { - imagePaths: Array; - [key: string]: unknown; - }; - } -} - -declare module 'astro:content' { - type Flatten = T extends { [K: string]: infer U } ? U : never; - - export type CollectionKey = keyof AnyEntryMap; - export type CollectionEntry = Flatten; - - export type ContentCollectionKey = keyof ContentEntryMap; - export type DataCollectionKey = keyof DataEntryMap; - - type AllValuesOf = T extends any ? T[keyof T] : never; - type ValidContentEntrySlug = AllValuesOf< - ContentEntryMap[C] - >['slug']; - - export type ReferenceDataEntry< - C extends CollectionKey, - E extends keyof DataEntryMap[C] = string, - > = { - collection: C; - id: E; - }; - export type ReferenceContentEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}) = string, - > = { - collection: C; - slug: E; - }; - export type ReferenceLiveEntry = { - collection: C; - id: string; - }; - - /** @deprecated Use `getEntry` instead. */ - export function getEntryBySlug< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - // Note that this has to accept a regular string too, for SSR - entrySlug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - - /** @deprecated Use `getEntry` instead. */ - export function getDataEntryById( - collection: C, - entryId: E, - ): Promise>; - - export function getCollection>( - collection: C, - filter?: (entry: CollectionEntry) => entry is E, - ): Promise; - export function getCollection( - collection: C, - filter?: (entry: CollectionEntry) => unknown, - ): Promise[]>; - - export function getLiveCollection( - collection: C, - filter?: LiveLoaderCollectionFilterType, - ): Promise< - import('astro').LiveDataCollectionResult, LiveLoaderErrorType> - >; - - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - entry: ReferenceContentEntry, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - entry: ReferenceDataEntry, - ): E extends keyof DataEntryMap[C] - ? Promise - : Promise | undefined>; - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - slug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - collection: C, - id: E, - ): E extends keyof DataEntryMap[C] - ? string extends keyof DataEntryMap[C] - ? Promise | undefined - : Promise - : Promise | undefined>; - export function getLiveEntry( - collection: C, - filter: string | LiveLoaderEntryFilterType, - ): Promise, LiveLoaderErrorType>>; - - /** Resolve an array of entry references from the same collection */ - export function getEntries( - entries: ReferenceContentEntry>[], - ): Promise[]>; - export function getEntries( - entries: ReferenceDataEntry[], - ): Promise[]>; - - export function render( - entry: AnyEntryMap[C][string], - ): Promise; - - export function reference( - collection: C, - ): import('astro/zod').ZodEffects< - import('astro/zod').ZodString, - C extends keyof ContentEntryMap - ? ReferenceContentEntry> - : ReferenceDataEntry - >; - // Allow generic `string` to avoid excessive type errors in the config - // if `dev` is not running to update as you edit. - // Invalid collection names will be caught at build time. - export function reference( - collection: C, - ): import('astro/zod').ZodEffects; - - type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; - type InferEntrySchema = import('astro/zod').infer< - ReturnTypeOrOriginal['schema']> - >; - - type ContentEntryMap = { - - }; - - type DataEntryMap = { - - }; - - type AnyEntryMap = ContentEntryMap & DataEntryMap; - - type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< - infer TData, - infer TEntryFilter, - infer TCollectionFilter, - infer TError - > - ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } - : { data: never; entryFilter: never; collectionFilter: never; error: never }; - type ExtractDataType = ExtractLoaderTypes['data']; - type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; - type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; - type ExtractErrorType = ExtractLoaderTypes['error']; - - type LiveLoaderDataType = - LiveContentConfig['collections'][C]['schema'] extends undefined - ? ExtractDataType - : import('astro/zod').infer< - Exclude - >; - type LiveLoaderEntryFilterType = - ExtractEntryFilterType; - type LiveLoaderCollectionFilterType = - ExtractCollectionFilterType; - type LiveLoaderErrorType = ExtractErrorType< - LiveContentConfig['collections'][C]['loader'] - >; - - export type ContentConfig = typeof import("../src/content.config.mjs"); - export type LiveContentConfig = never; -} diff --git a/apps/docs/.astro/data-store.json b/apps/docs/.astro/data-store.json deleted file mode 100644 index 0caf6209..00000000 --- a/apps/docs/.astro/data-store.json +++ /dev/null @@ -1 +0,0 @@ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.18.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] \ No newline at end of file diff --git a/apps/docs/.astro/settings.json b/apps/docs/.astro/settings.json deleted file mode 100644 index 89086c3d..00000000 --- a/apps/docs/.astro/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "_variables": { - "lastUpdateCheck": 1778034170906 - } -} \ No newline at end of file diff --git a/apps/docs/.astro/types.d.ts b/apps/docs/.astro/types.d.ts deleted file mode 100644 index 03d7cc43..00000000 --- a/apps/docs/.astro/types.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// \ No newline at end of file diff --git a/apps/docs/src/components/demos/OnScrollDemo.tsx b/apps/docs/src/components/demos/OnScrollDemo.tsx new file mode 100644 index 00000000..56c3b182 --- /dev/null +++ b/apps/docs/src/components/demos/OnScrollDemo.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import DataTable from '../ThemedDataTable'; +import { type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + salary: number; + status: string; +} + +const data: Employee[] = Array.from({ length: 40 }, (_, i) => ({ + id: i + 1, + name: + [ + 'Aria Chen', + 'Marcus Webb', + 'Priya Kapoor', + 'Jordan Ellis', + 'Sam Rivera', + 'Taylor Brooks', + 'Casey Morgan', + 'Alex Kim', + 'Morgan Lee', + 'Drew Park', + ][i % 10] + (i >= 10 ? ` ${Math.floor(i / 10) + 1}` : ''), + department: ['Engineering', 'Product', 'Design', 'Analytics', 'Sales', 'HR'][i % 6], + salary: 80000 + ((i * 1237) % 70000), + status: ['Active', 'Remote', 'On Leave', 'Contractor'][i % 4], +})); + +const columns: TableColumn[] = [ + { name: '#', selector: r => r.id, width: '60px' }, + { name: 'Name', selector: r => r.name, sortable: true }, + { name: 'Department', selector: r => r.department, sortable: true }, + { + name: 'Salary', + selector: r => r.salary, + format: r => `$${r.salary.toLocaleString()}`, + right: true, + sortable: true, + }, + { name: 'Status', selector: r => r.status }, +]; + +export default function OnScrollDemo() { + const [scrollTop, setScrollTop] = useState(0); + + return ( +
+
+ scrollTop: {scrollTop}px +
+ + setScrollTop(Math.round((e.target as HTMLDivElement).scrollTop))} + /> +
+ ); +} diff --git a/apps/docs/src/pages/docs/api.md b/apps/docs/src/pages/docs/api.md index 798f5028..577d54ba 100644 --- a/apps/docs/src/pages/docs/api.md +++ b/apps/docs/src/pages/docs/api.md @@ -47,6 +47,7 @@ Complete reference for every prop, type, and export in `react-data-table-compone | `responsive` | `boolean` | `true` | Wrap the table in a horizontally scrollable container. | | `fixedHeader` | `boolean` | `false` | Stick the column header at the top when scrolling. | | `fixedHeaderScrollHeight` | `string` | `"100vh"` | Max height of the scrollable body when `fixedHeader` is on. | +| `onScroll` | `(event) => void` | - | Called when the user scrolls the table body. Works with both `fixedHeader` enabled and disabled. | | `direction` | `Direction` | `"ltr"` | Text direction (`ltr`, `rtl`, `auto`). | | `className` | `string` | - | Extra CSS class on the root element. | | `style` | `CSSProperties` | - | Inline styles on the root element. | @@ -150,7 +151,6 @@ Column-level footers live on each [`TableColumn`](#tablecolumnt) as the `foot | `onRowMiddleClicked` | `(row, event) => void` | Called when a row is middle-clicked (scroll-click). Use with `onRowClicked` to implement open-in-new-tab behaviour. | | `onRowMouseEnter` | `(row, event) => void` | Called when the pointer enters a row. | | `onRowMouseLeave` | `(row, event) => void` | Called when the pointer leaves a row. | -| `onScroll` | `(event) => void` | Called when the user scrolls the table body. Works with both `fixedHeader` enabled and disabled. | ### Column features diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro index a7dd5109..f3746e87 100644 --- a/apps/docs/src/pages/docs/changelog.astro +++ b/apps/docs/src/pages/docs/changelog.astro @@ -15,6 +15,11 @@ import CodeBlock from '../../components/CodeBlock.astro';

New features

    +
  • + paginationPosition — controls where the pagination bar renders relative to the + table. Accepts 'bottom' (default), 'top', or 'both'. + → Pagination docs +
  • paginationPage — controlled active-page prop. Set it to navigate the table programmatically (e.g. reset to page 1 after a filter change). Use together with diff --git a/apps/docs/src/pages/docs/fixed-header.astro b/apps/docs/src/pages/docs/fixed-header.astro index 8df199aa..7ef652a2 100644 --- a/apps/docs/src/pages/docs/fixed-header.astro +++ b/apps/docs/src/pages/docs/fixed-header.astro @@ -3,6 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import Demo from '../../components/Demo.astro'; import CodeBlock from '../../components/CodeBlock.astro'; import FixedHeaderDemo from '../../components/demos/FixedHeaderDemo.tsx'; +import OnScrollDemo from '../../components/demos/OnScrollDemo.tsx'; --- @@ -82,6 +83,29 @@ const columns: TableColumn[] = [ selectableRows />`} /> +

    Tracking scroll position with onScroll

    +

    + The onScroll prop fires on every scroll event inside the table body. It receives a + standard React.UIEvent<HTMLDivElement> so you can read scrollTop, + scrollLeft, or any other scroll geometry from event.target. +

    + + setScrollTop(Math.round((e.target as HTMLDivElement).scrollTop))} +/>`} + > + + +

    Prop reference

    @@ -110,6 +134,12 @@ const columns: TableColumn[] = [ Only applies when fixedHeader is true. + + + + + +
    onScroll(event: React.UIEvent<HTMLDivElement>) => void-Called when the user scrolls the table body. Works with both fixedHeader enabled and disabled.
    diff --git a/src/DataTable.css b/src/DataTable.css index 9f93c883..1d6e006e 100644 --- a/src/DataTable.css +++ b/src/DataTable.css @@ -1095,7 +1095,7 @@ .rdt_footer { display: flex; flex-direction: column; - background-color: var(--rdt-color-footer-bg, var(--rdt-color-header-bg, var(--rdt-color-bg, #fff))); + background-color: var(--rdt-color-footer-bg, var(--rdt-color-bg, #fff)); } .rdt_footerRow { diff --git a/src/themes/base.ts b/src/themes/base.ts index c4a7f8b8..8dbca6e7 100644 --- a/src/themes/base.ts +++ b/src/themes/base.ts @@ -7,7 +7,6 @@ export const defaultTheme = { }, background: { default: '#FFFFFF', - footer: '#FAFAFA', }, context: { background: '#e3f2fd', @@ -39,7 +38,7 @@ export const defaultTheme = { export const defaultDarkMode = { primary: '#90CAF9', text: { primary: '#FFFFFF', secondary: 'rgba(255,255,255,0.7)', disabled: 'rgba(0,0,0,.12)' }, - background: { default: '#424242', footer: '#373737' }, + background: { default: '#424242' }, context: { background: '#E91E63', text: '#FFFFFF' }, divider: { default: 'rgba(81,81,81,1)' }, button: { diff --git a/src/types.ts b/src/types.ts index 18a9501d..4d150a95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -526,7 +526,7 @@ type ThemeBackground = { default: string; /** Optional separate background for column header rows. Falls back to `default`. */ header?: string; - /** Optional separate background for the footer row. Falls back to `header`, then `default`. */ + /** Optional separate background for the footer row. Falls back to `default`. */ footer?: string; }; From b27a90f5137e3684fec35651b8b6053c232750da Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:14:00 -0400 Subject: [PATCH 2/8] feat: implement clearSort functionality and add SortResetDemo example (#1320) --- .../src/components/demos/SortResetDemo.tsx | 62 +++++++++++++++++++ apps/docs/src/pages/docs/api.md | 8 ++- apps/docs/src/pages/docs/changelog.astro | 16 +++++ apps/docs/src/pages/docs/sorting.astro | 43 ++++++++++++- src/__tests__/DataTable.test.tsx | 22 ++++++- src/__tests__/tableReducer.test.ts | 29 +++++++++ src/components/DataTable.tsx | 6 +- src/hooks/useTableState.ts | 6 ++ src/tableReducer.ts | 10 +++ src/types.ts | 10 ++- 10 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 apps/docs/src/components/demos/SortResetDemo.tsx diff --git a/apps/docs/src/components/demos/SortResetDemo.tsx b/apps/docs/src/components/demos/SortResetDemo.tsx new file mode 100644 index 00000000..8d57f01d --- /dev/null +++ b/apps/docs/src/components/demos/SortResetDemo.tsx @@ -0,0 +1,62 @@ +import React, { useRef, useState } from 'react'; +import DataTable from '../ThemedDataTable'; +import { type TableColumn, type DataTableHandle } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + salary: number; + hired: string; +} + +const data: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000, hired: '2019-03-12' }, + { id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000, hired: '2020-07-01' }, + { id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000, hired: '2021-01-15' }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000, hired: '2018-11-30' }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000, hired: '2022-04-22' }, + { id: 6, name: 'Taylor Brooks', department: 'Sales', salary: 97000, hired: '2023-02-08' }, + { id: 7, name: 'Morgan Lee', department: 'Engineering', salary: 162000, hired: '2017-09-05' }, + { id: 8, name: 'Casey Park', department: 'Design', salary: 109000, hired: '2022-11-19' }, +]; + +const columns: TableColumn[] = [ + { id: 'name', name: 'Name', selector: r => r.name, sortable: true }, + { id: 'department', name: 'Department', selector: r => r.department, sortable: true }, + { id: 'salary', name: 'Salary', selector: r => r.salary, format: r => `$${r.salary.toLocaleString()}`, right: true, sortable: true }, + { id: 'hired', name: 'Hired', selector: r => r.hired, sortable: true }, +]; + +export default function SortResetDemo() { + const ref = useRef(null); + const [lastSort, setLastSort] = useState(null); + + return ( +
    +
    + + + {lastSort ?? 'Sort by any column, then reset'} + +
    + setLastSort(`sorted by "${col.id}" ${dir}`)} + highlightOnHover + /> +
    + ); +} diff --git a/apps/docs/src/pages/docs/api.md b/apps/docs/src/pages/docs/api.md index 577d54ba..b1c518eb 100644 --- a/apps/docs/src/pages/docs/api.md +++ b/apps/docs/src/pages/docs/api.md @@ -78,6 +78,7 @@ Complete reference for every prop, type, and export in `react-data-table-compone | `sortFunction` | `SortFunction \| null` | - | Global custom sort function applied to all sortable columns. | | `sortIcon` | `ReactNode` | built-in chevron | Custom sort direction indicator. | | `onSort` | `(column, direction, sortedRows) => void` | - | Called whenever the sort column or direction changes. | +| `ref.clearSort()` | `DataTableHandle` | - | Imperatively reset sort to the default state. See [DataTableHandle](#datatablehandle-ref). | ### Pagination @@ -125,6 +126,7 @@ Column-level footers live on each [`TableColumn`](#tablecolumnt) as the `foot | `selectableRowsComponentProps` | `object` | - | Extra props forwarded to the custom checkbox component. | | `onSelectedRowsChange` | `(state) => void` | - | Called whenever selection changes. Receives `{ allSelected, selectedCount, selectedRows }`. | | `clearSelectedRows` | `boolean` | - | **Deprecated.** Toggle to clear selection. Use `ref.current.clearSelectedRows()` instead. | +| `ref.clearSelectedRows()` | `DataTableHandle` | - | Imperatively deselect all selected rows. See [DataTableHandle](#datatablehandle-ref). | ### Expandable rows @@ -312,9 +314,8 @@ function App() { return ( <> - + + ); @@ -324,6 +325,7 @@ function App() { | Method | Description | |---|---| | `clearSelectedRows()` | Deselect all currently selected rows. | +| `clearSort()` | Reset sort to the default (`defaultSortFieldId` / `defaultSortAsc`), or unsorted if no defaults are set. | ## TableStyles diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro index f3746e87..33f76b94 100644 --- a/apps/docs/src/pages/docs/changelog.astro +++ b/apps/docs/src/pages/docs/changelog.astro @@ -39,6 +39,22 @@ import CodeBlock from '../../components/CodeBlock.astro'; and "Last Page" are now configurable via paginationComponentOptions, enabling proper i18n for screen readers.
  • +
  • + ref.clearSort() — new DataTableHandle method to programmatically + reset sort back to its default state (defaultSortFieldId / defaultSortAsc), + or unsorted if no defaults are set. + → Sorting docs +
  • +
  • + Sortable column indicator — sortable columns now show a faint sort icon at + reduced opacity so users can discover which columns are sortable before clicking. The inactive + opacity is themable via the --rdt-sort-icon-inactive-opacity CSS custom property + (default 0.3). +
  • +
  • + onScroll — new prop that fires whenever the table's scroll wrapper scrolls. + Receives the native React.UIEvent<HTMLDivElement>. +

Bug fixes

diff --git a/apps/docs/src/pages/docs/sorting.astro b/apps/docs/src/pages/docs/sorting.astro index 122a33cd..808cda5c 100644 --- a/apps/docs/src/pages/docs/sorting.astro +++ b/apps/docs/src/pages/docs/sorting.astro @@ -6,6 +6,7 @@ import SortingDemo from '../../components/demos/SortingDemo.tsx'; import PrioritySortDemo from '../../components/demos/PrioritySortDemo.tsx'; import ServerSideSortingDemo from '../../components/demos/ServerSideSortingDemo.tsx'; import ServerSideSortPaginationDemo from '../../components/demos/ServerSideSortPaginationDemo.tsx'; +import SortResetDemo from '../../components/demos/SortResetDemo.tsx'; --- @@ -323,12 +324,46 @@ export default function App() { +

Resetting sort programmatically

+

+ Attach a ref to DataTable and call ref.current.clearSort() to reset + the sort back to its default state (defaultSortFieldId / defaultSortAsc, + or unsorted if no defaults are set). Useful when the user switches a view mode that makes the + current sort invalid. +

+ + (null); + + return ( + <> + + + + ); +}`} + > + + +

Prop reference

- + @@ -389,6 +424,12 @@ export default function App() { + + + + + +
PropProp / method Type Default Description- Backend field name passed to onSort when it differs from column.id.
ref.clearSort()DataTableHandle-Imperatively reset sort to the default (defaultSortFieldId / defaultSortAsc), or unsorted if no defaults are set. See DataTableHandle.
diff --git a/src/__tests__/DataTable.test.tsx b/src/__tests__/DataTable.test.tsx index c3a132f4..40324a7e 100644 --- a/src/__tests__/DataTable.test.tsx +++ b/src/__tests__/DataTable.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, act } from '@testing-library/react'; import DataTable from '../components/DataTable'; import { Direction, STOP_PROP_TAG } from '../constants'; import { Alignment } from '../index'; -import { ConditionalStyles, SortOrder } from '../types'; +import { ConditionalStyles, SortOrder, DataTableHandle } from '../types'; interface Data { id: number; @@ -772,6 +772,24 @@ describe('DataTable::sorting', () => { const rows = container.querySelectorAll('.rdt_row'); expect(rows[0].id).toBe('row-1'); }); + + test('ref.clearSort resets row order back to defaultSortFieldId asc', () => { + const columns = [{ id: 'name', name: 'Test', selector: (row: Data) => row.some.name, sortable: true }]; + const ref = React.createRef(); + const { container } = render( + , + ); + + // column already active asc; one click flips to desc — Zuchinni (row-2) first + fireEvent.click(container.querySelector('div[data-sort-id="name"]') as HTMLElement); + expect(container.querySelectorAll('.rdt_row')[0].id).toBe('row-2'); + + // reset — back to defaultSortAsc=true so Apple (row-1) first + act(() => { + ref.current?.clearSort(); + }); + expect(container.querySelectorAll('.rdt_row')[0].id).toBe('row-1'); + }); }); describe('DataTable::expandableRows', () => { diff --git a/src/__tests__/tableReducer.test.ts b/src/__tests__/tableReducer.test.ts index 63b92c6d..70aed29c 100644 --- a/src/__tests__/tableReducer.test.ts +++ b/src/__tests__/tableReducer.test.ts @@ -284,6 +284,35 @@ describe('tableReducer:CLEAR_SELECTED_ROWS', () => { }); }); +describe('tableReducer:CLEAR_SORT', () => { + const defaultColumn: TableColumn = { id: 99, name: 'default', selector: r => r.name }; + + test('resets selectedColumn and sortDirection to the supplied defaults', () => { + const sortedState = baseState({ selectedColumn: column, sortDirection: SortOrder.DESC }); + const next = tableReducer(sortedState, { + type: 'CLEAR_SORT', + defaultSortColumn: defaultColumn, + defaultSortDirection: SortOrder.ASC, + }); + + expect(next.selectedColumn).toBe(defaultColumn); + expect(next.sortDirection).toBe(SortOrder.ASC); + }); + + test('preserves all other state fields', () => { + const state = baseState({ currentPage: 3, rowsPerPage: 25, selectedRows: [r1] }); + const next = tableReducer(state, { + type: 'CLEAR_SORT', + defaultSortColumn: column, + defaultSortDirection: SortOrder.ASC, + }); + + expect(next.currentPage).toBe(3); + expect(next.rowsPerPage).toBe(25); + expect(next.selectedRows).toEqual([r1]); + }); +}); + describe('tableReducer:SORT_CHANGE', () => { test('updates sort column / direction and resets to page 1', () => { const next = tableReducer(baseState({ currentPage: 4 }), { diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index 4ef8246f..01242f2b 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -234,6 +234,7 @@ function DataTableInner(props: TableProps, ref: React.ForwardedRef(props: TableProps, ref: React.ForwardedRef([]); const lastSelectedKeyRef = React.useRef(null); - React.useImperativeHandle(ref, () => ({ clearSelectedRows: handleClearSelectedRows }), [handleClearSelectedRows]); + React.useImperativeHandle(ref, () => ({ clearSelectedRows: handleClearSelectedRows, clearSort: handleClearSort }), [ + handleClearSelectedRows, + handleClearSort, + ]); // Snapshot row Y-positions synchronously before dispatching sort, so // DataTableBody can FLIP rows from their old positions to the new ones. diff --git a/src/hooks/useTableState.ts b/src/hooks/useTableState.ts index 18e26887..fab9d8ed 100644 --- a/src/hooks/useTableState.ts +++ b/src/hooks/useTableState.ts @@ -52,6 +52,7 @@ interface UseTableStateReturn { handleChangePage: (page: number) => void; handleChangeRowsPerPage: (newRowsPerPage: number, tableRowsLength: number) => void; handleClearSelectedRows: () => void; + handleClearSort: () => void; } /** @@ -101,6 +102,10 @@ export default function useTableState(props: UseTableStateProps): UseTable dispatch({ type: 'CLEAR_SELECTED_ROWS', selectedRowsFlag: false }); }, []); + const handleClearSort = React.useCallback(() => { + dispatch({ type: 'CLEAR_SORT', defaultSortColumn, defaultSortDirection }); + }, [defaultSortColumn, defaultSortDirection]); + const handleSort = React.useCallback((action: SortAction) => { dispatch(action); }, []); @@ -241,5 +246,6 @@ export default function useTableState(props: UseTableStateProps): UseTable handleChangePage, handleChangeRowsPerPage, handleClearSelectedRows, + handleClearSort, }; } diff --git a/src/tableReducer.ts b/src/tableReducer.ts index bc93b8bf..c3f085a9 100644 --- a/src/tableReducer.ts +++ b/src/tableReducer.ts @@ -139,6 +139,16 @@ export function tableReducer(state: TableState, action: Action): TableS }; } + case 'CLEAR_SORT': { + const { defaultSortColumn, defaultSortDirection } = action; + + return { + ...state, + selectedColumn: defaultSortColumn, + sortDirection: defaultSortDirection, + }; + } + case 'SORT_CHANGE': { const { sortDirection, selectedColumn, clearSelectedOnSort } = action; diff --git a/src/types.ts b/src/types.ts index 4d150a95..60b3aedb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,6 +86,7 @@ export type FooterComponent = React.ComponentType>; export type DataTableHandle = { clearSelectedRows: () => void; + clearSort: () => void; }; // ── Feature-group prop types ────────────────────────────────────────────────── @@ -708,6 +709,12 @@ export interface ClearSelectedRowsAction { selectedRowsFlag: boolean; } +export interface ClearSortAction { + type: 'CLEAR_SORT'; + defaultSortColumn: TableColumn; + defaultSortDirection: SortOrder; +} + export type Action = | AllRowsAction | SingleRowAction @@ -716,4 +723,5 @@ export type Action = | SortAction | PaginationPageAction | PaginationRowsPerPageAction - | ClearSelectedRowsAction; + | ClearSelectedRowsAction + | ClearSortAction; From 5c53323f6465b96dbd4984630f5926caa29b5e9f Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:28:15 -0400 Subject: [PATCH 3/8] update release flow --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 040ddbc7..7e72db73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: run: npm run build - name: Publish to npm - run: npm publish --provenance --access public + run: npm publish --provenance --access public --ignore-scripts --verbose - name: Commit and tag run: | From ee6ebc0177c25d4369bacd7fe28f53c20f3c2c7b Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:30:09 -0400 Subject: [PATCH 4/8] update release action --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e72db73..b3ada1e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,8 +26,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 - registry-url: https://registry.npmjs.org + node-version: 24 - name: Install dependencies run: npm ci From 9faa27536786b85d9e237c67af0e3d0bb3862218 Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:37:01 -0400 Subject: [PATCH 5/8] update release action --- .github/workflows/release.yml | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3ada1e5..df085109 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,33 @@ on: - major jobs: + lint-typecheck-test-build: + name: Lint, typecheck, test, build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + - name: Build + run: npm run build + release: + needs: lint-typecheck-test-build runs-on: ubuntu-latest permissions: contents: write @@ -31,9 +57,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Run tests - run: npm test - - name: Configure git run: | git config user.name "github-actions[bot]" @@ -50,12 +73,12 @@ jobs: run: npm run build - name: Publish to npm - run: npm publish --provenance --access public --ignore-scripts --verbose + run: npm publish --provenance --access public --ignore-scripts - name: Commit and tag run: | git add package.json - git commit -m "chore: release v${{ steps.version.outputs.value }}" + git commit -m "chore: release v${{ steps.version.outputs.value }} [skip ci]" git tag "v${{ steps.version.outputs.value }}" git push origin HEAD:master --follow-tags From 21c2c69fad301fb20c9fced10d1eceacd14207ef Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:38:07 -0400 Subject: [PATCH 6/8] bump master --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df0ebb95..ff3c042c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-data-table-component", - "version": "8.1.0", + "version": "8.2.0", "description": "A fast, feature-rich React data table. Working table in 10 lines.", "main": "dist/index.js", "module": "dist/index.mjs", From 7a73cc779eed454864524f9123b8041f2ce0c443 Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 22:42:50 -0400 Subject: [PATCH 7/8] remove unreleased --- apps/docs/src/pages/docs/changelog.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro index 33f76b94..61d80de5 100644 --- a/apps/docs/src/pages/docs/changelog.astro +++ b/apps/docs/src/pages/docs/changelog.astro @@ -11,7 +11,7 @@ import CodeBlock from '../../components/CodeBlock.astro'; repository on GitHub.

-

8.2.0 (unreleased)

+

8.2.0

New features

    From c65efc41cd90f9c46a7f8f64a8feba227cdc0e1d Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Tue, 19 May 2026 23:14:22 -0400 Subject: [PATCH 8/8] add mobile layout support and demo for responsive data table (#1321) --- apps/docs/src/components/demos/MobileDemo.tsx | 127 ++++++++++++++++++ apps/docs/src/layouts/DocsLayout.astro | 1 + apps/docs/src/pages/docs/mobile.astro | 107 +++++++++++++++ src/DataTable.css | 44 ++++++ 4 files changed, 279 insertions(+) create mode 100644 apps/docs/src/components/demos/MobileDemo.tsx create mode 100644 apps/docs/src/pages/docs/mobile.astro diff --git a/apps/docs/src/components/demos/MobileDemo.tsx b/apps/docs/src/components/demos/MobileDemo.tsx new file mode 100644 index 00000000..b464d322 --- /dev/null +++ b/apps/docs/src/components/demos/MobileDemo.tsx @@ -0,0 +1,127 @@ +import React, { useState } from 'react'; +import DataTable from '../ThemedDataTable'; +import { type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + salary: number; + status: 'Active' | 'Remote' | 'Contractor' | 'On Leave'; +} + +const data: Employee[] = Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + name: ['Aria Chen', 'Marcus Webb', 'Priya Kapoor', 'Jordan Ellis', 'Sam Rivera', 'Taylor Brooks', 'Casey Morgan', 'Alex Kim', 'Morgan Lee', 'Drew Park'][i % 10] + (i >= 10 ? ` ${Math.floor(i / 10) + 1}` : ''), + department: ['Engineering', 'Product', 'Design', 'Analytics', 'Sales', 'HR'][i % 6], + salary: 80000 + i * 4200, + status: (['Active', 'Remote', 'Contractor', 'On Leave'] as const)[i % 4], +})); + +const statusColors: Record = { + Active: 'bg-green-100 text-green-700', + Remote: 'bg-blue-100 text-blue-700', + Contractor: 'bg-yellow-100 text-yellow-700', + 'On Leave': 'bg-gray-100 text-gray-600', +}; + +export default function MobileDemo() { + const [width, setWidth] = useState<'375' | '430' | '100%'>('375'); + const [variant, setVariant] = useState<'basic' | 'selection' | 'pagination'>('basic'); + + const isNarrow = width === '375' || width === '430'; + + const baseColumns: TableColumn[] = [ + { + name: 'Name', + selector: r => r.name, + sortable: true, + grow: 1.5, + }, + { + name: 'Dept', + selector: r => r.department, + sortable: true, + omit: isNarrow, + }, + { + name: 'Salary', + selector: r => r.salary, + sortable: true, + right: true, + format: r => `$${r.salary.toLocaleString()}`, + omit: isNarrow, + }, + { + name: 'Status', + selector: r => r.status, + center: true, + cell: r => ( + + {r.status} + + ), + }, + ]; + + const btnBase = 'px-2.5 py-1 rounded-md text-xs font-medium border transition-colors cursor-pointer'; + const btnOn = 'bg-brand-600 text-white border-brand-600'; + const btnOff = 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'; + + return ( +
    +
    +
    + Viewport: + {(['375', '430', '100%'] as const).map(w => ( + + ))} +
    +
    + Mode: + {(['basic', 'selection', 'pagination'] as const).map(v => ( + + ))} +
    +
    + +
    + {width !== '100%' && ( +
    + {width}px viewport +
    +
    +
    +
    +
    +
    + )} + +
    + + {isNarrow && ( +

    + Department and Salary are hidden at this width — use omit or hide: "sm" to drop columns on small screens. +

    + )} +
    + ); +} diff --git a/apps/docs/src/layouts/DocsLayout.astro b/apps/docs/src/layouts/DocsLayout.astro index b7a0d3da..b06e1655 100644 --- a/apps/docs/src/layouts/DocsLayout.astro +++ b/apps/docs/src/layouts/DocsLayout.astro @@ -60,6 +60,7 @@ const nav = [ { label: 'Animations', href: '/docs/animations' }, { label: 'Accessibility', href: '/docs/accessibility' }, { label: 'RTL Support', href: '/docs/rtl' }, + { label: 'Mobile', href: '/docs/mobile' }, ], }, { diff --git a/apps/docs/src/pages/docs/mobile.astro b/apps/docs/src/pages/docs/mobile.astro new file mode 100644 index 00000000..a230376b --- /dev/null +++ b/apps/docs/src/pages/docs/mobile.astro @@ -0,0 +1,107 @@ +--- +import DocsLayout from '../../layouts/DocsLayout.astro'; +import Demo from '../../components/Demo.astro'; +import CodeBlock from '../../components/CodeBlock.astro'; +import MobileDemo from '../../components/demos/MobileDemo.tsx'; +--- + + +

    Mobile

    + +

    + A data table is fundamentally a desktop pattern — comparing values across rows requires seeing + multiple columns at once. On small screens the table stays a table, but several defaults are + tuned to make that experience as comfortable as possible. +

    + + [] = [ + { name: 'Name', selector: r => r.name, sortable: true }, + { name: 'Department', selector: r => r.department, hide: 'sm' }, + { name: 'Salary', selector: r => r.salary, right: true, hide: 'sm', + format: r => \`$\${r.salary.toLocaleString()}\` }, + { name: 'Status', selector: r => r.status, center: true, + cell: r => }, +]; + +`} + > + + + +

    How it works

    + +

    + The table wraps in a horizontally-scrollable container with momentum scrolling and + scroll-chaining prevention. On touch devices users can swipe left/right to reveal hidden columns + without disrupting the page's vertical scroll. +

    + +

    Hiding columns on small screens

    + +

    + Use the hide property on a column to remove it below a breakpoint. + The column header and all its cells disappear — the remaining columns fill the available width. +

    + + [] = [ + { name: 'Name', selector: r => r.name }, // always visible + { name: 'Email', selector: r => r.email, hide: 'sm' }, // hidden below 600px + { name: 'Salary', selector: r => r.salary, hide: 'md' }, // hidden below 960px + { name: 'Joined', selector: r => r.joined, hide: 'lg' }, // hidden below 1280px +];`} /> + +

    + You can also pass a numeric pixel value to hide at a custom breakpoint: +

    + + r.notes, hide: 480 }`} /> + +

    Pagination on small screens

    + +

    + Below 600px the rows-per-page label and the current-range text (1–5 of 30) are hidden + automatically, giving the pagination controls more breathing room. The rows-per-page select and + nav buttons remain. +

    + +

    Touch targets

    + +

    + Row height enforces a minimum of 48px on small screens so tap targets meet accessibility + guidelines. Pagination buttons expand to 44×44px. Cell padding tightens slightly + (--rdt-cell-padding-x-mobile, default 12px) to reclaim horizontal space. +

    + +

    CSS custom properties

    + +

    You can override the mobile padding via CSS variables on the table container:

    + + + +

    Recommendations for app developers

    + +

    + For apps where the primary use case on mobile is looking up a single record rather than + comparing rows, consider rendering a completely different component below your breakpoint — + a simple list or card feed — and only mounting DataTable on wider viewports: +

    + + ; + } + + return ; +}`} /> +
    diff --git a/src/DataTable.css b/src/DataTable.css index 1d6e006e..5dd5a6cf 100644 --- a/src/DataTable.css +++ b/src/DataTable.css @@ -40,6 +40,7 @@ min-height: 0; overscroll-behavior-x: contain; touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; } .rdt_responsiveWrapperFixed { @@ -398,6 +399,8 @@ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14); padding: 10px; min-width: 260px; + max-width: calc(100vw - 24px); + box-sizing: border-box; } /* A single condition row: [select] [input] ([and] [input]) [remove] */ @@ -975,6 +978,19 @@ min-height: 56px; padding-left: 16px; padding-right: 8px; + gap: 4px; +} + +@media (max-width: 599px) { + .rdt_header { + font-size: 18px; + padding-left: 12px; + padding-right: 6px; + } + + .rdt_headerActions { + flex: 0 0 auto; + } } .rdt_headerTitle { @@ -1193,6 +1209,17 @@ width: 100%; justify-content: space-around; } + + .rdt_paginationRange, + .rdt_paginationRowLabel { + display: none; + } + + .rdt_pagination { + justify-content: center; + padding-left: 4px; + padding-right: 4px; + } } .rdt_paginationSpan { @@ -1277,6 +1304,23 @@ background-color: var(--rdt-color-bg, #fff); } +/* ─── Mobile touch targets ───────────────────────────────────────────────────── */ +@media (max-width: 599px) { + .rdt_row { + min-height: max(var(--rdt-row-height, 48px), 48px); + } + + .rdt_cellBase { + padding-left: var(--rdt-cell-padding-x-mobile, 12px); + padding-right: var(--rdt-cell-padding-x-mobile, 12px); + } + + .rdt_paginationButton { + height: 44px; + width: 44px; + } +} + /* ─── Responsive column hiding ───────────────────────────────────────────────── */ @media (max-width: 599px) { .rdt_hideOnSm {