84 lines
2.1 KiB
TypeScript
84 lines
2.1 KiB
TypeScript
// Table-Basis-Komponente — SHL-01. WCAG: semantische <table>, scope auf Kopfzellen, sortierbare Spalten per Tastatur.
|
|
|
|
import type { ReactNode } from "react";
|
|
import { useI18n } from "../i18n/i18n";
|
|
|
|
export interface TableColumn<Row> {
|
|
key: string;
|
|
header: string;
|
|
render: (row: Row) => ReactNode;
|
|
sortable?: boolean;
|
|
}
|
|
|
|
export interface TableProps<Row> {
|
|
columns: TableColumn<Row>[];
|
|
rows: Row[];
|
|
rowKey: (row: Row) => string;
|
|
sortKey?: string;
|
|
sortDirection?: "asc" | "desc";
|
|
onSort?: (key: string) => void;
|
|
caption?: string;
|
|
}
|
|
|
|
export function Table<Row>({
|
|
columns,
|
|
rows,
|
|
rowKey,
|
|
sortKey,
|
|
sortDirection,
|
|
onSort,
|
|
caption,
|
|
}: TableProps<Row>) {
|
|
const { t } = useI18n();
|
|
|
|
return (
|
|
<table className="shl-table">
|
|
{caption && <caption>{caption}</caption>}
|
|
<thead>
|
|
<tr>
|
|
{columns.map((column) => {
|
|
const isSorted = column.key === sortKey;
|
|
const ariaSort = column.sortable
|
|
? isSorted
|
|
? sortDirection === "asc"
|
|
? "ascending"
|
|
: "descending"
|
|
: "none"
|
|
: undefined;
|
|
return (
|
|
<th key={column.key} scope="col" aria-sort={ariaSort}>
|
|
{column.sortable ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onSort?.(column.key)}
|
|
className="shl-table-sort-button"
|
|
>
|
|
{column.header}
|
|
</button>
|
|
) : (
|
|
column.header
|
|
)}
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length}>{t("shl.table.noRows")}</td>
|
|
</tr>
|
|
) : (
|
|
rows.map((row) => (
|
|
<tr key={rowKey(row)}>
|
|
{columns.map((column) => (
|
|
<td key={column.key}>{column.render(row)}</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
);
|
|
}
|