Files
TeleWave/frontend/src/shared/ui/sortable.tsx
T

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
})
}