Files
supabase/packages/ui-patterns/src/Table/TanStackTableHeadSort.test.tsx
T
Danny White cca4e52dd0 refactor(ui-patterns): Standardise TanStack sort headers (#44212)
## What kind of change does this PR introduce?

Component update.

## What is the current behaviour?

TanStack tables in the repo are split between the shared `TableHeadSort`
primitive and the older Studio-local `DataTableColumnHeader` helper,
which makes the sorting UI and integration path inconsistent.

If you were to just use `DataTableColumnHeader` in `ui-patterns/Table`,
you’d get a very different visual result to the `TableHeadSort` UI you
see in most other tables.

## What is the new behaviour?

Adds a shared `TanStackTableHeadSort` adapter in `ui-patterns/Table`,
backed by the existing `TableHeadSort` primitive, and switches the
webhook table plus the design-system TanStack demo to that canonical
path. `DataTableColumnHeader` stays as a deprecated wrapper for now,
Studio gets a lint guard to block new imports of it, and the table docs
now point TanStack tables at the shared adapter explicitly.

## To test

Check out column sorting on the Platform Webhook endpoint deliveries
table.
2026-03-30 21:48:52 +11:00

125 lines
3.6 KiB
TypeScript

import {
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type SortingState,
} from '@tanstack/react-table'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { describe, expect, it } from 'vitest'
import { TanStackTableHeadSort } from './TanStackTableHeadSort'
type Row = {
name: string
amount: number
}
const data: Row[] = [
{ name: 'Bravo', amount: 200 },
{ name: 'Alpha', amount: 100 },
]
const columns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: ({ column }) => <TanStackTableHeadSort column={column}>Name</TanStackTableHeadSort>,
cell: ({ row }) => row.getValue('name'),
},
{
accessorKey: 'amount',
enableSorting: false,
header: ({ column }) => <TanStackTableHeadSort column={column}>Amount</TanStackTableHeadSort>,
cell: ({ row }) => row.getValue('amount'),
},
]
const classNameColumns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: ({ column }) => (
<TanStackTableHeadSort column={column} className="justify-end">
Name
</TanStackTableHeadSort>
),
cell: ({ row }) => row.getValue('name'),
},
]
const TestTable = ({ tableColumns = columns }: { tableColumns?: ColumnDef<Row>[] }) => {
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data,
columns: tableColumns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}
describe('TanStackTableHeadSort', () => {
it('cycles unsorted, ascending, descending, and cleared', async () => {
const user = userEvent.setup()
render(<TestTable />)
expect(screen.getAllByRole('row')[1]).toHaveTextContent('Bravo')
await user.click(screen.getByRole('button', { name: 'Name' }))
expect(screen.getAllByRole('row')[1]).toHaveTextContent('Alpha')
await user.click(screen.getByRole('button', { name: 'Name' }))
expect(screen.getAllByRole('row')[1]).toHaveTextContent('Bravo')
await user.click(screen.getByRole('button', { name: 'Name' }))
expect(screen.getAllByRole('row')[1]).toHaveTextContent('Bravo')
})
it('renders non-sortable columns as plain content', () => {
render(<TestTable />)
expect(screen.getByText('Amount')).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Amount' })).not.toBeInTheDocument()
})
it('passes className through to the rendered sort control', () => {
render(<TestTable tableColumns={classNameColumns} />)
expect(screen.getByRole('button', { name: 'Name' })).toHaveClass('justify-end')
})
})