import { ColumnDef, ColumnFiltersState, ColumnOrderState, ExpandedState, PaginationState, Row, RowSelectionState, SortingState, Table as ReactTable, VisibilityState } from '@tanstack/react-table';
import { CSSProperties, ReactNode } from 'react';
import { CollapseButtonProps } from '../../buttons/collapse-button/collapse-button';
import { CheckboxProps } from '../../form/checkbox/checkbox';
import { RadioProps } from '../../form/radio/radio';
import { TextFieldProps } from '../../form/textfield/textfield';
import { PaginationProps } from '../../navigation/pagination';
/**
* Optional shape that columns can put in `columnDef.meta` to:
*
* - drive the column-filter aria-label when the header is non-textual (`label`),
* - align the column's `
` / ` | ` content horizontally (`align`) or vertically
* (`vAlign`) without wrapping every cell render in a styled span.
*
* Wrapper spans still work for the "I want this *one* cell to be different" case;
* `meta` covers the common "every cell in the column lines up the same way".
*/
export interface TableColumnMeta {
/** Accessible label used when the column header isn't a plain string. */
label?: string;
/**
* Props forwarded to the built-in per-column filter input (the `TextField`
* rendered for this column when `enableColumnFilters` is set). Use it to
* constrain or tweak the filter field without replacing it — e.g. cap the
* length with `{ input: { maxLength: 40 } }`, set a custom `placeholder`, or
* attach `helper` text. The Table owns the field's identity and state
* (`id`, `name`, `label`, `hideLabel`, `value`, `onChange`), so those are not
* overridable.
*/
filterProps?: Omit;
/**
* Horizontal alignment applied to every header / body / footer cell in the column.
* Maps directly to `text-align`. Defaults to `left` (the table's CSS default).
*/
align?: 'left' | 'center' | 'right';
/**
* Vertical alignment applied to every header / body / footer cell in the column.
* Maps directly to `vertical-align`. Defaults to `middle` for body cells via the
* table's stylesheet.
*/
vAlign?: 'top' | 'middle' | 'bottom';
/**
* Per-cell `rowSpan` resolver. Lets a column collapse consecutive rows that
* share a value into a single vertically-spanning cell ("grouped rows").
*
* Return:
* - `n >= 2` — render the cell with `rowSpan={n}`,
* - `1` (or undefined) — render the cell normally,
* - `0` — skip the `` entirely; a previous row's spanning cell covers it.
*
* Pair with the `groupRowSpan` helper for the common "consecutive rows sharing
* a key" case. Only the body cells honor this hook — header and footer cells
* are unaffected.
*/
rowSpan?: (row: Row) => number;
}
/**
* Internal type alias used in `TableColumnMeta.rowSpan` so the resolver can
* accept any row shape without forcing the meta interface to be generic
* (TanStack's `columnDef.meta` is intentionally loosely-typed).
*/
type UnknownTData = unknown;
/**
* Builds a `meta.rowSpan` resolver that collapses **consecutive** rows sharing
* the same key into a single spanning cell — the React equivalent of
* Angular's `groupRowSpan`.
*
* Pass the same array you hand to `` plus a key extractor:
*
* ```tsx
* const data = [
* { id: '1', date: '2026-05-20', doctor: 'Dr Tamm' },
* { id: '2', date: '2026-05-20', doctor: 'Dr Tamm' },
* { id: '3', date: '2026-05-21', doctor: 'Dr Kask' },
* ];
* const columns: ColumnDef[] = [
* { accessorKey: 'date', header: 'Date', meta: { rowSpan: groupRowSpan(data, (d) => d.date) } },
* { accessorKey: 'doctor', header: 'Doctor' },
* ];
* ```
*
* Resolves against the original `data` array order — sorting / filtering /
* pagination will not regroup rows, so the data should already be ordered
* for the grouping you want before passing it in.
*/
export declare const groupRowSpan: (data: TData[], getKey: (item: TData) => unknown) => ((row: Row) => number);
/**
* Persistable state slices owned by Table. Each slice can be controlled via
* `state`/`onStateChange`, defaulted via `defaultState`, or persisted via
* `persist`.
*/
export interface TableState {
columnVisibility?: VisibilityState;
columnOrder?: ColumnOrderState;
rowSelection?: RowSelectionState;
expanded?: ExpandedState;
columnFilters?: ColumnFiltersState;
sorting?: SortingState;
pagination?: PaginationState;
}
export type TableSize = 'medium' | 'small';
export type TableSelectionCheckboxProps = Omit;
export type TableSelectionRadioProps = Omit;
/**
* Row-selection indicator type. `'multiple'` (default) uses checkboxes with a
* select-all header; `'single'` uses radios, hides the header, and limits
* `rowSelection` to a single row at a time.
*/
export type TableSelectionMode = 'multiple' | 'single';
export type TableExpandCollapseProps = Omit;
export interface TablePersistOptions {
/**
* Storage key used to read/write persisted state. Must be stable per table.
*/
key: string;
/**
* Storage backend. Defaults to `window.localStorage` when available.
*/
storage?: Storage;
/**
* Subset of state slices to persist. Defaults to user-preference slices
* only: `columnVisibility`, `columnOrder`.
* Task-scoped slices (`rowSelection`, `expanded`, `columnFilters`, `sorting`,
* `pagination`) are deliberately excluded by default — pass them explicitly
* via `include` if you want them to survive across sessions.
*/
include?: (keyof TableState)[];
}
export interface TablePaginationOptions {
/**
* Rows per page.
* @default 10
*/
pageSize?: number;
/**
* Options rendered in the built-in page-size selector. Pass `false` to hide
* the selector entirely.
* @default [10, 25, 50]
*/
pageSizeOptions?: number[] | false;
/**
* Props forwarded to the built-in `Pagination` to customise its appearance
* and behaviour — e.g. `background`, `borders`, `hideResults`,
* `showPrevNextButtons`, `arrowVariant`, `labels`. The data / state props the
* Table owns (`pageCount`, `page`, `totalItems`, `pageSize`, `pageSizeOptions`
* and the change handlers) are managed internally and cannot be overridden.
*/
paginationProps?: Omit;
}
export interface TableProps {
/**
* Unique identifier for the table. Used for accessibility and as the default
* persistence key namespace.
*/
id?: string;
/**
* Row data. Render order mirrors the array order; reorder the array yourself
* (e.g. in `onRowDrop`) to change it.
*/
data: TData[];
/**
* Returns a stable id for each row. Without it, TanStack falls back to the
* row index — so identifiers shift when rows sort / paginate / reorder, which
* breaks record stability for `rowSelection`, `activeRowId`, and the
* `onRowDrop` `fromId`/`toId`. Set this whenever your data has a stable key.
*/
getRowId?: (originalRow: TData, index: number, parent?: Row) => string;
/**
* Column definitions. Must include a stable `id` on every column when
* column visibility / reorder / persistence are used.
*/
columns: ColumnDef[];
/**
* Visual size of the table. Matches Figma: `medium` = 49px rows, `small` = 41px rows.
* @default medium
*/
size?: TableSize;
/**
* Caption rendered above the table. Announced to assistive technology.
*/
caption?: ReactNode;
/**
* Alternating row backgrounds.
* @default false
*/
striped?: boolean;
/**
* Renders vertical separators between columns.
* @default false
*/
verticalBorders?: boolean;
/**
* Removes the outer border + radius around the table, keeping only internal
* row dividers.
* @default false
*/
borderless?: boolean;
/**
* Freezes the first column during horizontal scroll.
* @default false
*/
stickyFirstColumn?: boolean;
/**
* Pins the `` row(s) to the top during vertical scroll. Requires
* `maxHeight` so the table's internal scroll container becomes the sticky
* anchor — wrapping the Table in an external scrollable div will NOT work,
* because the `` sticks to its nearest scrolling ancestor (which is
* always the internal `.tedi-table__scroll` div). Combines safely with
* `stickyFirstColumn`.
* @default false
*/
stickyHeader?: boolean;
/**
* Constrains the height of the table's internal scroll container. Accepts
* any CSS length value (`number` is treated as pixels). Pair with
* `stickyHeader` for a fixed-height table whose header stays pinned during
* vertical scroll.
*/
maxHeight?: number | string;
/**
* Fires when a data row is clicked. Adds `role="button"`, a pointer cursor
* and Enter/Space keyboard activation to every row.
*/
onRowClick?: (row: Row) => void;
/**
* Highlights the row whose `id` matches as the currently-active row.
*
* Useful in master-detail layouts where the click handler navigates or opens a side
* panel and the table should stay visibly anchored to that row. Distinct from row
* selection (which is checkbox-driven via `enableRowSelection`) and from the
* transient `:hover` state. Renders with `aria-current="true"` for screen readers.
*/
activeRowId?: string;
/**
* Paint a hover background on data rows when the cursor is over them. Off by default —
* a hover highlight implies the row is interactive, which is misleading when there's
* nothing to click. Pass `true` for read-only tables that still want the affordance,
* or omit and the table will turn hover on automatically whenever `onRowClick` is set.
*/
rowHover?: boolean;
/**
* Enables row selection. When true, Table prepends a selection column with
* a checkbox (or radio, when `selectionMode === 'single'`) bound to
* `rowSelection` state.
*/
enableRowSelection?: boolean | ((row: Row) => boolean);
/**
* How rows are selected when `enableRowSelection` is on:
* - `'multiple'` (default) — checkbox per row + select-all in the header.
* `rowSelection` can hold any number of row ids.
* - `'single'` — radio per row, no select-all. Selecting a row clears any
* previous selection, so `rowSelection` holds at most one row id.
*
* In both modes the controlled `state.rowSelection` / `onStateChange` API
* is unchanged — only the indicator and TanStack's underlying multi-select
* behavior change.
* @default multiple
*/
selectionMode?: TableSelectionMode;
/**
* Paints the `--table-active` background tint on every selected row. Turn
* off when checkboxes are used purely for "pick some rows for a bulk
* action" and the row-level highlight would be visually noisy. The
* checkbox itself still reflects the selected state.
* @default true
*/
highlightSelectedRows?: boolean;
/**
* Enables per-column filter inputs rendered below the header row.
* Only columns whose `columnDef.enableColumnFilter !== false` render an input.
*/
enableColumnFilters?: boolean;
/**
* Render function for the expanded content of a row. When provided, Table
* prepends an expand/collapse toggle column and renders this node in a full-
* width row below every expanded parent row.
*/
renderSubComponent?: (row: Row) => ReactNode;
/**
* Predicate controlling which rows can be expanded. Defaults to "all rows"
* when `renderSubComponent` is provided, otherwise "none".
*/
getRowCanExpand?: (row: Row) => boolean;
/**
* Returns the sub-rows of a data row. Enables nested hierarchical data.
*/
getSubRows?: (row: TData) => TData[] | undefined;
/**
* How an expandable row is toggled:
* - `'button'` (default) — only the chevron button toggles expansion, and it
* renders in the bordered `secondary` arrow style.
* - `'row'` — a click anywhere on an expandable row toggles it (Enter/Space
* too), and the chevron renders in the neutral `default` arrow style as a
* plain indicator. Only rows that can expand become clickable.
*
* Has no effect unless the table is expandable (`getSubRows` or
* `renderSubComponent` is set).
* @default button
*/
expandTrigger?: 'button' | 'row';
/**
* Whether expanded sub-rows count toward pagination (TanStack's
* `paginateExpandedRows`). Defaults to `false` so expanding a parent keeps its
* children on the same page and only top-level rows fill `pageSize` — opening
* a row never pushes siblings to the next page or splits a parent's children
* across pages. Set to `true` for TanStack's default, where sub-rows occupy
* page slots like any other row.
*
* Has no effect unless the table is both expandable and paginated.
* @default false
*/
paginateExpandedRows?: boolean;
/**
* Enables client-side pagination and renders a built-in page-switcher footer.
* Pass `true` for default settings or an options object to customise.
* Page state lives on `TableState.pagination` so it is fully controllable and
* persistable.
*/
pagination?: boolean | TablePaginationOptions;
/**
* Whether the current page resets to the first page whenever `data` changes
* (TanStack's `autoResetPageIndex`). Defaults to `true`, which keeps the user
* on a valid page after filtering or sorting. Set to `false` for tables that
* mutate `data` in place — e.g. inline row editing — so saving a row doesn't
* yank the user back to page 1.
* @default true
*/
autoResetPageIndex?: boolean;
/**
* Switches pagination to server-side mode. When `true`, Table stops slicing
* `data` locally — `data` is treated as the rows for the current page only.
* Pair with `pageCount` (or `rowCount`) and a controlled `state.pagination`
* + `onStateChange` to fetch the right page from the server on each change.
* @default false
*/
manualPagination?: boolean;
/**
* Switches sorting to server-side mode. When `true`, Table no longer sorts
* `data` locally — sort state still updates and fires `onStateChange` so
* the parent can refetch in the new order, but the rows are rendered in
* the order they arrive in `data`.
* @default false
*/
manualSorting?: boolean;
/**
* Switches filtering to server-side mode. When `true`, Table stops applying
* `columnFilters` locally; the parent is expected to translate filter state
* (visible via `onStateChange`) into a server query.
* @default false
*/
manualFiltering?: boolean;
/**
* Total number of pages on the server. Required when `manualPagination` is
* `true` so the pagination footer can render the right page count — local
* row-count math is otherwise wrong, since `data` only holds the current
* page's rows.
*/
pageCount?: number;
/**
* Total number of rows on the server (across all pages). Used as the
* "X tulemust" / "X results" counter in the pagination footer when
* `manualPagination` is on. Falls back to the locally filtered row count
* when omitted.
*/
rowCount?: number;
/**
* Controlled state. Pair with `onStateChange`. Any key left undefined falls
* back to the corresponding default or internal state.
*/
state?: Partial;
/**
* Initial state for uncontrolled usage. Ignored when `state` is provided.
*/
defaultState?: Partial;
/**
* Callback fired whenever any state slice changes.
*/
onStateChange?: (state: TableState) => void;
/**
* When set, Table wires a localStorage adapter for the named key. Acts as a
* default state provider and persists subsequent changes. Consumers can still
* supply `state`/`onStateChange` to layer extra behavior on top.
*/
persist?: TablePersistOptions;
/**
* Rendered inside `` when `data` is empty — typically an `EmptyState`
* node, or any string / node.
* @default "No data"
*/
emptyState?: ReactNode;
/**
* ARIA live-region role wrapping the empty state. Use `'status'` for polite
* announcements (recommended for "no results" feedback when the user changes
* a filter) or `'alert'` for assertive announcements that interrupt the
* current SR utterance. Omit when the empty state should not announce changes
* — e.g. when the table is empty on first render and the content never changes.
*/
emptyStateRole?: 'alert' | 'status';
/**
* Additional class name on the root element.
*/
className?: string;
/**
* Props forwarded to the row-selection checkboxes (the header select-all
* checkbox and every per-row cell checkbox). Use this to e.g. switch the
* checkboxes to `size: 'large'`. Wiring props (id, name, label, value,
* checked, indeterminate, disabled, onChange, hideLabel) are owned by
* Table and cannot be overridden. Ignored when `selectionMode === 'single'`.
*/
checkboxProps?: TableSelectionCheckboxProps;
/**
* Props forwarded to the row-selection radios when `selectionMode === 'single'`.
* Use this to e.g. switch the radios to `size: 'large'`. Wiring props
* (id, name, label, value, checked, disabled, onChange, hideLabel) are owned
* by Table and cannot be overridden.
*/
radioProps?: TableSelectionRadioProps;
/**
* Props forwarded to the row-expand `CollapseButton` toggle. Use this to
* e.g. switch to `arrowType: 'default'` or change the `size`. Wiring props
* (id, open, onOpenChange, openText, closeText, aria-controls, hideText) are
* owned by Table and cannot be overridden.
*/
collapseProps?: TableExpandCollapseProps;
/**
* Per-row className / inline style hook. Called once for every data row and
* the returned `className` is merged with Table's own row classes; the
* returned `style` is applied to the ``. Useful for drag-and-drop drop
* indicators, conditional row tinting, etc. Row event handlers are NOT
* accepted here — Table owns row click / keyboard behavior.
*/
rowProps?: (row: Row) => {
className?: string;
style?: CSSProperties;
} | undefined;
/**
* Per-column className / inline style hook. Called for every header cell
* and every body cell; whatever is returned is applied to *both* the `| `
* (including the column-filter row) and every ` | ` belonging to the
* column. Useful for drag-and-drop drop indicators that should span the
* full column height, or for column-level theming. Identify the column via
* the `columnId` argument.
*/
columnProps?: (columnId: string) => {
className?: string;
style?: CSSProperties;
} | undefined;
/**
* Toolbar + Table subcomponents such as ``.
*/
children?: ReactNode;
/**
* Enables row reordering by **mouse and keyboard**. The Table auto-injects a
* leading drag-handle column. Mouse: drag a row by its handle. Keyboard: focus
* a handle, `Space`/`Enter` to pick the row up, `↑`/`↓` to move it (clamped to
* the rendered rows), `Space`/`Enter` to drop, `Escape` to cancel — every move
* (and the drop/cancel) is announced via a live region. Both paths emit
* `onRowDrop`; the consumer applies the new order to the source `data` array.
*
* Each `row.id` returned by TanStack is used as the drag identifier, so the
* data must have a stable `id` (or supply `getRowId`).
*
* @default false
*/
reorderableRows?: boolean;
/**
* Enables column reordering by **mouse and keyboard**. Each (non-built-in,
* non-grouped) leaf header gets a grip button next to its label. Mouse: drag
* the header by its grip. Keyboard: focus the grip, `Space`/`Enter` to pick
* the column up, `←`/`→` to move it, `Space`/`Enter` to drop, `Escape` to
* cancel (announced via a live region). Reordering pushes the new order into
* TanStack's `columnOrder` state — flows through `onStateChange` and the
* `persist` adapter automatically; no `onRowDrop`-style wiring needed.
*
* Grouped header parents are never draggable; only flat / leaf columns are.
* Built-in slots (`__drag__`, `__select__`, `__expand__`) are skipped.
*
* @default false
*/
reorderableColumns?: boolean;
/**
* Fires when a draggable row is dropped to a new position. Receives the
* before/after `row.id`s and the convenience indexes. Apply the move to your
* data with `arrayMove(data, fromIndex, toIndex)` and pass the result back
* via `data`.
*
* Only invoked when `reorderableRows` is enabled and the drop actually changes
* the row order.
*/
onRowDrop?: (event: TableRowDropEvent) => void;
}
export interface TableContextValue {
table: ReactTable;
size: TableSize;
id?: string;
state: Partial;
}
export interface TableRowDropEvent {
fromId: string;
toId: string;
fromIndex: number;
toIndex: number;
}
declare function TableBase(props: TableProps): JSX.Element;
declare namespace TableBase {
var displayName: string;
}
export declare const Table: typeof TableBase & {
Toolbar: {
({ children, className }: import('.').TableToolbarProps): import("react/jsx-runtime").JSX.Element;
displayName: string;
};
ColumnsMenu: {
({ triggerLabel, className }: import('.').TableColumnsMenuProps): import("react/jsx-runtime").JSX.Element;
displayName: string;
};
HeaderButton: import('react').ForwardRefExoticComponent>;
};
export default Table;
| | |