import type { ReactNode } from "react"; import type { SortDirection } from "../SortableTableHeader/SortableTableHeader.tsx"; export type DataTableColumn = { /** * Lands on this column's `` **and** on every `` under it, * which is what makes it the place a width goes: `w-32` on one * column is the only sizing this component has, and it is * deliberately the consumer's to write. */ className?: string; /** * The column's name. Rendered twice on purpose — once in the * header row, and once inside each cell as the label the stacked * layout needs — so it has to be a node that can be rendered more * than once. The in-cell copy is `aria-hidden`, because the * `columnheader` association already says it to assistive * technology. */ header: ReactNode; /** * Whether this column offers a sort control. A column without one * renders a plain `` with **no** `aria-sort` at all, which is * the attribute's way of saying "this column cannot be sorted" — * `"none"` would claim the opposite. */ isSortable?: boolean; /** Stable id. Also the `columnKey` a sort request comes back with. */ key: string; renderCell: (row: Row) => ReactNode; }; export type DataTableSort = { columnKey: string; direction: SortDirection; }; export type DataTableSelection = { /** * The accessible name of a row's checkbox. A tick box called * "Select row" seven times over is seven identical controls in a * screen reader's forms list, so this is required rather than * defaulted — it is normally the same value the first column * renders. */ getRowLabel: (row: Row) => string; /** The whole next set, never a delta. */ onSelectionChange: (selectedRowKeys: string[]) => void; selectAllLabel?: string; selectedRowKeys: readonly string[]; }; export type DataTableReflowAt = "md" | "lg" | "xl"; export type DataTableProps = { /** Auto uses the container width; stacked keeps the labelled list at every width. */ layout?: "auto" | "stacked"; /** Dense tables can reflow earlier: md=32rem, lg=48rem, xl=64rem. */ reflowAt?: DataTableReflowAt; className?: string; columns: readonly DataTableColumn[]; /** * What fills the table when `rows` is empty — an `EmptyState`, * normally. Rendered in a `colSpan` cell **inside** the table, so * the header row and the sort the reader just performed are still * there. Swapping the whole table out for a message is how a * filter that matches nothing takes the controls away with it. */ emptyState?: ReactNode; getRowKey: (row: Row) => string; /** * Shows the ``. Off by default: most tables sit under a * heading the page already renders, and printing the name twice is * how a table ends up with two titles. */ isLabelVisible?: boolean; /** * The table's accessible name, as a real ``. Required, * because a screen reader listing "table with 6 columns, 40 rows" * and no name is a table nobody can choose to enter. */ label: string; onSortChange?: (sort: DataTableSort) => void; rows: readonly Row[]; selection?: DataTableSelection; /** * Which column is sorted and which way — **the fact, not the * doing.** The rows arrive already ordered; this only says which * header lights up and what `aria-sort` announces. */ sort?: DataTableSort; }; /** * **A table that reflows instead of scrolling, sized by its * container.** * * ### It composes `SortableTableHeader`; it does not replace it * * The header cell shipped first, on its own, and stays on its own: * it is already in a published 1.0.0 with a consumer of its own * (mux-magic's `FileExplorerModal`), and subsuming it would be a * breaking change bought for nothing. Every sortable column here * renders that component, so there is exactly one place in the fleet * that knows `aria-sort` belongs on the `` while the click * target is a `