78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
import { useState } from 'react'
|
|
import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
|
|
import { cn } from '@/shared/lib/cn'
|
|
|
|
type SortState = { key: string; desc: boolean }
|
|
|
|
/**
|
|
* Состояние сортировки таблицы. Клик по новому столбцу — по возрастанию; повторный клик по тому же —
|
|
* переключает направление. Для серверных списков `sort`/`desc` передаются в API (и в queryKey), для
|
|
* клиентских — в {@link sortRows}.
|
|
*/
|
|
export function useTableSort(defaultKey: string, defaultDesc = false) {
|
|
const [sort, setSort] = useState<SortState>({ key: defaultKey, desc: defaultDesc })
|
|
const toggle = (key: string) =>
|
|
setSort((s) => (s.key === key ? { key, desc: !s.desc } : { key, desc: false }))
|
|
return { sort, toggle }
|
|
}
|
|
|
|
/** Заголовок-кнопка столбца со стрелкой сортировки. */
|
|
export function SortHeader({
|
|
label,
|
|
sortKey,
|
|
sort,
|
|
onToggle,
|
|
className,
|
|
}: {
|
|
label: string
|
|
sortKey: string
|
|
sort: SortState
|
|
onToggle: (key: string) => void
|
|
className?: string
|
|
}) {
|
|
const active = sort.key === sortKey
|
|
const Icon = !active ? ChevronsUpDown : sort.desc ? ArrowDown : ArrowUp
|
|
return (
|
|
// aria-sort — атрибут заголовка столбца, а не кнопки внутри него: у роли button его нет,
|
|
// и скринридер там его просто не прочтёт.
|
|
<th
|
|
className={cn('px-4 py-2 font-medium', className)}
|
|
aria-sort={active ? (sort.desc ? 'descending' : 'ascending') : 'none'}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => onToggle(sortKey)}
|
|
className={cn(
|
|
'inline-flex items-center gap-1 hover:text-foreground',
|
|
active && 'text-foreground',
|
|
)}
|
|
>
|
|
{label}
|
|
<Icon className={cn('h-3.5 w-3.5', !active && 'opacity-40')} />
|
|
</button>
|
|
</th>
|
|
)
|
|
}
|
|
|
|
type Comparable = string | number | boolean | null | undefined
|
|
|
|
/** Клиентская сортировка строк по выбранному ключу (для непагинированных списков). nulls — в конец. */
|
|
export function sortRows<T>(
|
|
rows: T[],
|
|
sort: SortState,
|
|
accessors: Record<string, (row: T) => Comparable>,
|
|
): T[] {
|
|
const accessor = accessors[sort.key]
|
|
if (!accessor) return rows
|
|
const dir = sort.desc ? -1 : 1
|
|
return [...rows].sort((a, b) => {
|
|
const av = accessor(a)
|
|
const bv = accessor(b)
|
|
if (av == null && bv == null) return 0
|
|
if (av == null) return 1
|
|
if (bv == null) return -1
|
|
if (typeof av === 'string' && typeof bv === 'string') return av.localeCompare(bv) * dir
|
|
return (av < bv ? -1 : av > bv ? 1 : 0) * dir
|
|
})
|
|
}
|