From 2005380e58948eb0ccc4125316278f5983ed345e Mon Sep 17 00:00:00 2001 From: Rawi2115 Date: Thu, 27 Aug 2026 14:11:45 +0300 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat:=20add=20unsort=20state=20on=203r?= =?UTF-8?q?d=20column=20header=20click?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sorting previously only toggled between two states per column: 1st click -> asc, 2nd click -> desc, 3rd click -> back to asc, with no way to fully clear a column's sort via the header. Add a third click state that clears sorting instead of cycling back to ascending. Following the pattern already used in a community workaround for this request, unsorted is represented with the existing DataTableSortStatus shape by setting columnAccessor to an empty string, rather than changing the type to allow undefined/null. This keeps direction always defined and avoids touching every downstream consumer of sortStatus. The header cell's existing icon logic already falls back to the unsorted icon whenever sortStatus.columnAccessor doesn't match the column's own accessor, so no rendering changes were needed there - an empty-string sentinel naturally satisfies that check for every column. Also updates the sorting example to explicitly handle the empty columnAccessor case, and adds a note to the sorting docs page explaining the new third-click behavior. Fixes #812 --- app/examples/sorting/SortingExample.tsx | 5 +++++ app/examples/sorting/page.tsx | 5 +++++ package/DataTableHeaderCell.tsx | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/examples/sorting/SortingExample.tsx b/app/examples/sorting/SortingExample.tsx index a1442004..64e30768 100644 --- a/app/examples/sorting/SortingExample.tsx +++ b/app/examples/sorting/SortingExample.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'; import { type Company, companies } from '~/data'; export default function SortingExample() { + // third sorting click will cause the column to go back to unsorted state by making columnAccessor = "" const [sortStatus, setSortStatus] = useState>({ columnAccessor: 'name', direction: 'asc', @@ -13,6 +14,10 @@ export default function SortingExample() { const [records, setRecords] = useState(sortBy(companies, 'name')); useEffect(() => { + if (!sortStatus.columnAccessor) { + setRecords(companies); + return; + } const data = sortBy(companies, sortStatus.columnAccessor) as Company[]; setRecords(sortStatus.direction === 'desc' ? data.reverse() : data); }, [sortStatus]); diff --git a/app/examples/sorting/page.tsx b/app/examples/sorting/page.tsx index badafaf5..8f685028 100644 --- a/app/examples/sorting/page.tsx +++ b/app/examples/sorting/page.tsx @@ -40,6 +40,11 @@ export default async function SortingExamplePage() { sortable column header is clicked. + + Clicking a sorted column header a third time clears its sorting: onSortStatusChange is called with{' '} + columnAccessor set to an empty string, while direction resets to 'asc'. + Handle this case in your own sorting logic, as shown in the example below. + If you enable sorting, you might want to consider{' '} diff --git a/package/DataTableHeaderCell.tsx b/package/DataTableHeaderCell.tsx index 4a8cacf4..465c8b51 100644 --- a/package/DataTableHeaderCell.tsx +++ b/package/DataTableHeaderCell.tsx @@ -88,10 +88,10 @@ export function DataTableHeaderCell({ sortable && onSortStatusChange ? (e?: React.BaseSyntheticEvent) => { if (e?.defaultPrevented) return; - + const hasCycledBackToAsc = sortStatus?.direction === 'desc'; onSortStatusChange({ sortKey, - columnAccessor: accessor, + columnAccessor: sortStatus?.columnAccessor === accessor && hasCycledBackToAsc ? '' : accessor, direction: sortStatus?.columnAccessor === accessor ? sortStatus.direction === 'asc'