Skip to content

Commit 91cd9e0

Browse files
authored
feat: add column sorting to DataTable component (#1282)
* feat: add column sorting to DataTable component - Import getSortedRowModel and SortingState from @tanstack/react-table - Import ArrowUp, ArrowDown, ArrowUpDown icons from lucide-react - Add sorting state with optional defaultSort prop - Wire getSortedRowModel and onSortingChange into useReactTable config - Export SortableHeader component for use in column definitions * fix: add aria-sort and aria-label to SortableHeader for accessibility Screen reader users can now identify sort state and available actions via proper ARIA attributes on the sortable column header button. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 1bcc534 commit 91cd9e0

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

frontend/src/components/shared/data-table.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import * as React from 'react'
22
import {
33
flexRender,
44
getCoreRowModel,
5+
getSortedRowModel,
56
useReactTable,
67
type ColumnDef,
8+
type SortingState,
79
} from '@tanstack/react-table'
810
import { useQuery, type QueryKey } from '@tanstack/react-query'
911
import {
@@ -17,6 +19,7 @@ import {
1719
import { Button } from '@/components/ui/button'
1820
import { Input } from '@/components/ui/input'
1921
import { cn } from '@/lib/utils'
22+
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'
2023

2124
export interface FilterOption {
2225
label: string
@@ -50,8 +53,43 @@ export interface DataTableProps<T> {
5053
filters?: FilterConfig[]
5154
onRowClick?: (row: T) => void
5255
className?: string
56+
defaultSort?: SortingState
5357
}
5458

59+
interface SortableHeaderProps {
60+
label: string
61+
isSorted: false | 'asc' | 'desc'
62+
onToggle: () => void
63+
}
64+
65+
function SortableHeader({ label, isSorted, onToggle }: SortableHeaderProps) {
66+
const ariaSort = isSorted === 'asc' ? 'ascending' : isSorted === 'desc' ? 'descending' : 'none'
67+
const ariaLabel = isSorted
68+
? `${label}, sorted ${isSorted === 'asc' ? 'ascending' : 'descending'}, click to change`
69+
: `${label}, click to sort`
70+
71+
return (
72+
<button
73+
type="button"
74+
onClick={onToggle}
75+
aria-sort={ariaSort}
76+
aria-label={ariaLabel}
77+
className="flex items-center gap-1 font-medium hover:text-foreground"
78+
>
79+
{label}
80+
{isSorted === 'asc' ? (
81+
<ArrowUp className="h-4 w-4" />
82+
) : isSorted === 'desc' ? (
83+
<ArrowDown className="h-4 w-4" />
84+
) : (
85+
<ArrowUpDown className="h-4 w-4 opacity-50" />
86+
)}
87+
</button>
88+
)
89+
}
90+
91+
export { SortableHeader }
92+
5593
function SkeletonRow({ colCount }: { colCount: number }) {
5694
return (
5795
<TableRow data-testid="skeleton-row">
@@ -184,10 +222,12 @@ export function DataTable<T>({
184222
filters,
185223
onRowClick,
186224
className,
225+
defaultSort = [],
187226
}: DataTableProps<T>) {
188227
const [activeFilters, setActiveFilters] = React.useState<Record<string, string>>({})
189228
const [pageToken, setPageToken] = React.useState<string | undefined>(undefined)
190229
const [hasPrev, setHasPrev] = React.useState(false)
230+
const [sorting, setSorting] = React.useState<SortingState>(defaultSort)
191231

192232
const { data, isLoading, isError, refetch } = useQuery({
193233
queryKey: [...(queryKey as unknown[]), { pageToken, ...activeFilters }],
@@ -198,6 +238,9 @@ export function DataTable<T>({
198238
data: data?.items ?? [],
199239
columns,
200240
getCoreRowModel: getCoreRowModel(),
241+
getSortedRowModel: getSortedRowModel(),
242+
onSortingChange: setSorting,
243+
state: { sorting },
201244
manualPagination: true,
202245
})
203246

0 commit comments

Comments
 (0)