Table
Columnar list with resizable, sortable and hideable columns, laid out by a CSS grid the header writes. Six components that only work together, and only one table to a page.
Use this when / not when
- Use for data the user reads across and sorts: files, members, sessions, anything with columns worth resizing and hiding.
- Not for a list of items with one line each —
Rowsis the portal's own list, and a flex column of your own is smaller than either. - Not for a short, fixed table. This one persists column widths in
localStorage, measures the container and rewrites a grid on every resize; a<table>element of your own does not. - Not twice on one page. The container's id, the header's id and the column ids are all literal.
Import
import {
TableBody,
TableCell,
TableContainer,
TableHeader,
TableRow,
} from "@onlyoffice/apps-ui-kit/components/table";
Also exported from the root barrel @onlyoffice/apps-ui-kit.
None of the props types are exported either — TTableColumn and TGroupMenuItem are, and they
are the two you write by hand.
Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme, and
TranslationProvider from @onlyoffice/apps-ui-kit/providers/translation for the group menu's
own labels.
Minimal example
The header needs the container's ref to write the grid on it, and a columnStorageName to save
the widths under.
import { useRef } from "react";
import {
TableBody,
TableCell,
TableContainer,
TableHeader,
type TTableColumn,
} from "@onlyoffice/apps-ui-kit/components/table";
const COLUMNS: TTableColumn[] = [
{ key: "name", title: "Name", enable: true, resizable: true },
{ key: "role", title: "Role", enable: true },
];
export function MemberTable({ members }: { members: string[] }) {
const container = useRef<HTMLDivElement>(null);
return (
<TableContainer forwardedRef={container} useReactWindow={false}>
<TableHeader
containerRef={container}
columns={COLUMNS}
columnStorageName="membersTableColumns"
columnInfoPanelStorageName="membersTableColumnsInfoPanel"
sectionWidth={960}
useReactWindow={false}
showSettings={false}
/>
<TableBody
columnStorageName="membersTableColumns"
columnInfoPanelStorageName="membersTableColumnsInfoPanel"
filesLength={members.length}
itemCount={members.length}
itemHeight={41}
hasMoreFiles={false}
useReactWindow={false}
fetchMoreFiles={() => Promise.resolve()}
>
{members.map((name) => (
<TableCell key={name}>{name}</TableCell>
))}
</TableBody>
</TableContainer>
);
}
Props
Each part has its own props. TTableColumn, the shape of an entry in columns, is where most
of the table's behaviour is configured.
TableContainer
TableHeader
TableBody
TableRow
TableCell
TableGroupMenu
TTableColumn
Recipes
Sorting
A column sorts by calling back: the table does not reorder anything, and the arrow follows
sortBy and sorted on the header.
import { useRef, useState } from "react";
import {
TableContainer,
TableHeader,
type TTableColumn,
} from "@onlyoffice/apps-ui-kit/components/table";
export function SortableHeader() {
const container = useRef<HTMLDivElement>(null);
const [sortBy, setSortBy] = useState("name");
const [descending, setDescending] = useState(false);
const columns: TTableColumn[] = [
{
key: "name",
title: "Name",
enable: true,
sortBy: "name",
resizable: true,
onClick: (field) => {
setDescending(field === sortBy ? !descending : false);
setSortBy(field);
},
},
{
key: "date",
title: "Modified",
enable: true,
sortBy: "date",
onClick: (field) => setSortBy(field),
},
];
return (
<TableContainer forwardedRef={container} useReactWindow={false}>
<TableHeader
containerRef={container}
columns={columns}
sortBy={sortBy}
sorted={descending}
columnStorageName="filesTableColumns"
columnInfoPanelStorageName="filesTableColumnsInfoPanel"
sectionWidth={960}
useReactWindow={false}
showSettings
/>
</TableContainer>
);
}
Selection, with the group menu
The group menu appears in place of the header while something is selected; the select-all checkbox is its own, and the per-row checkbox is yours to put in a cell.
import { useState } from "react";
import { Checkbox } from "@onlyoffice/apps-ui-kit/components/checkbox";
import {
TableCell,
TableGroupMenu,
TableRow,
} from "@onlyoffice/apps-ui-kit/components/table";
export function SelectableRows({ names }: { names: string[] }) {
const [selected, setSelected] = useState<string[]>([]);
const toggle = (name: string) =>
setSelected((current) =>
current.includes(name)
? current.filter((item) => item !== name)
: [...current, name],
);
return (
<>
{selected.length > 0 ? (
<TableGroupMenu
isChecked={selected.length === names.length}
isIndeterminate={
selected.length > 0 && selected.length < names.length
}
withoutInfoPanelToggler
withComboBox={false}
headerMenu={[
{
id: "delete",
label: "Delete",
title: "Delete",
iconUrl: "",
disabled: false,
onClick: () => setSelected([]),
},
]}
onChange={(checked) => setSelected(checked ? names : [])}
/>
) : null}
{names.map((name) => (
<TableRow key={name} checked={selected.includes(name)}>
<TableCell>
<Checkbox
isChecked={selected.includes(name)}
onChange={() => toggle(name)}
/>
</TableCell>
<TableCell>{name}</TableCell>
</TableRow>
))}
</>
);
}
Behaviour the types don't state
- The header lays out the whole table. It computes a
grid-template-columnsstring and writes it onto the container element and onto itself. The plain body and everyTableRowaredisplay: contents, so each cell is an item of the container's grid and lines up under its header cell. Cells have no widths of their own, and a row rendered outside that container is not laid out at all. - With
useReactWindowthe grid moves onto the rows. The container and the body become full-height blocks, the virtual list wraps each row in a.table-list-itemelement whose grid is read from the stored widths, and the header rewrites every.table-list-item(and.table-row) inside the container as it resizes. - The first layout. A column with
defaultSizegets that width, anisShortone itsminWidth, and thedefaultcolumn 40% of what is left — all of it when no other column is enabled — while the other enabled columns share the remaining 60% equally, never below 110px. WithwithoutWideColumnevery column gets an equal share. - Running out of room. When the enabled columns no longer fit at their minimum widths, the
header collapses every column to
0pxexcept thedefaultone, which takes the free width, and theisShortanddefaultSizeones; it greys the settings cog out and reports the change throughsetHideColumns. TableBodyrenders an empty element unless both storage names are set. The guard isif (!columnStorageName || !columnInfoPanelStorageName) return <div />, so a table missing the info-panel key shows nothing — no error, no rows. Pass both, always.- Column widths live in
localStorageundercolumnStorageName, orcolumnInfoPanelStorageNamewhileinfoPanelVisibleis set. Two tables sharing a key overwrite each other's layout, and a stored string whose column count no longer matches makes the header reset the widths. - The ids are literal. The container is
#table-container, the header#table-container_caption-header, and each header cell#column_<index>; the virtual list measures the first of those by id. One table to a page. - Only columns with an
onChangeappear in the settings menu, andisDisabledkeeps a column out of it, so a column that must always show cannot be unticked. Each entry is a checkbox labelled with the column'stitleand ticked whileenableis set; ticking it callsonChangewith the column's key and changes nothing else — flippingenableand storing the choice is the caller's job. The list stays open across ticks and closes on a click anywhere else or on the cog again. A column whoseenableis false keeps its slot in the grid but renders no title. - The sort arrow belongs to a column with an
onClick. It shows while the pointer is over the header cell or while the column is the one sorted by (oractive), and it is turned over whilesortedis off. A click on the title callsonClickwith the column'ssortBy; a click on the arrow callsonIconClickinstead when the column has one. WithsortingVisibleoff the arrow is gone and clicks do nothing. isShortnarrows the space a header cell keeps for the resize handle from 22px to 12px, for a narrow column such as a row number.resizableon a column describes its neighbour. The handle is drawn on a cell when the next column is resizable, and dragging it moves the boundary between the two. The first column cannot go below 210px, the rest below 110px or their ownminWidth.TableRowrenders the context button from the presence ofcontextOptions, likeRow, and the button is aContextMenuButtonintogglemode, so the row's own right-click handler is what opens the menu. The context menu showscontextOptions, or the itemsgetContextModelbuilds at the moment it opens.checkedandisActivepaint nothing on the row.checkedadds acheckedclass, and on a device with hover, children markedcreate-share-linkstay hidden until the row is hovered, checked or active; the highlight itself is the caller's styles. Whiledraggingis set, children markeddroppable-hoverare filled with the drop colour, unless the row is active or the rows are being reordered.isIndexEditingModeis a reorder mode. The header stops columns being resized and greys the cog out, and each row drops its context-menu cell altogether.TableCellis a 48px box. It has a bottom border, centres its content vertically and clips whatever does not fit. WithhasAccess, a child markedtable-container_elementis swapped for a child markedtable-container_row-checkboxwhile the pointer is over the cell; withcheckedthe checkbox stays in place either way.TableCellandTableRoware memoised with a deep comparison of their props, so updating one row does not redraw the others.- Virtualisation. With
useReactWindow, the body mounts only the rows near the visible part of the section's scroller (#sectionScroll, or the window when there is none) and three beyond it, eachitemHeighttall. WhilehasMoreFilesis set it adds two placeholder rows after the loaded ones and callsfetchMoreFileswith the range to load as they come into view. Without it, every row is rendered at once, which suits a short list only. noSelecton the container stops text selection anywhere in the table, for example while rows are being dragged. The container colours an element markedindexing-separator, to show where a dragged row will land.- The line under the header and the group menu stops 20px short of each edge; with
useReactWindowit runs the full width while the pointer is over the first row. - The header watches the container with a
ResizeObserveras well as the window, so it re-lays out when a panel opens beside it. useReactWindowdefaults tofalseon the header andtrueon the body; set it the same on both and on the container, or the header lays out rows the body never renders.TableCellwrites itsvalueprop to the element as a non-standardvalueattribute, which drag and drop reads.- The group menu needs translations of its own for the select-all checkbox and the selection combo box.
- A
headerMenuentry whosedisabledis true renders nothing, rather than a greyed button, so an action that does not apply to the selection disappears from the toolbar.isBlockedis the greyed state: every button and its icon, clicks ignored, for while an operation on the selection runs. - The group menu lays its buttons out after a separator in a scroller that runs sideways when
they do not fit. Each button draws the icon fetched from
iconUrlbefore its label, withtitleas the hover tooltip, or the label whentitleis empty. On a tablet the icon sits above the label; on a phone the label is not drawn. - An entry with
withDropDowncalls itsonClickand then opens itsoptionsin a menu under the button. The menu is 354px wide when its options carry descriptions; withfixedDropdownStylesit is 161px wide and five rows high at most. - The group menu's info-panel button sits on a round background while
isInfoPanelVisible, and its icon is mirrored in a right-to-left interface; theisCloseablecross comes before it.
Sub-components
CSS variables
The table styles itself from the theme; the column widths are written as inline
grid-template-columns. The group menu reads one variable a consumer may set.
| Variable | Default | Effect |
|---|---|---|
--table-group-menu-checkbox-margin | 28px | margin-inline-start of the select-all checkbox or headerLabel, on desktop only; below it the margin is 24px |
The checkboxMargin prop writes the same variable onto the toolbar itself, so when it is set it
wins over a value set on a wrapper.
Accessibility
- Nothing here is a table to assistive technology: every part is a
<div>, with norole, nocolumnheader, noaria-sortand no row or column count. A screen reader is given a pile of text. - Sorting is a click on a
<div>; the sort arrow is anIconButton, which is not focusable either. There is no keyboard route to sorting, resizing or the settings menu. - Selection is whatever
Checkboxyou put in a cell — a real input, and the only reachable control in a row. - The group menu's action buttons are native
<button>s rendered byButton: reachable with Tab and activated with Enter and Space. Each one's accessible name is its label, even on a phone where the label is not drawn, andisBlockedsets the nativedisabled, which takes them out of the tab order. - Data a keyboard or screen-reader user has to work through is better served by a real
<table>of your own, styled with the kit'sText.
Test ids
Related
Rows— the portal's one-line-per-item list.ContextMenu— the menu every row renders.Checkbox— what selection is actually made of.
In this section
The following components are available:
| Component | Description |
|---|---|
| GroupMenuItem | GroupMenuItem is one action button of a TableGroupMenu, the toolbar that replaces the table header while rows are selected. |
| TableCell | TableCell is one cell of a TableRow: a fixed-height box that sits in the column the table's grid gives it. |
| TableHeaderCell | TableHeaderCell is the title of one column in a TableHeader, with its sort arrow and the handle that resizes it. |
| TableSettings | TableSettings is the cog at the end of a TableHeader that opens a list of the columns, each with a checkbox that shows or hides it. |
| TableBody | TableBody holds the rows of a table and, for a long list, renders only the rows in view and asks for the next page as the user scrolls. |
| TableContainer | TableContainer is the outer element of a table, the grid that the header, the group menu and the rows are laid out in. |
| TableGroupMenu | TableGroupMenu is the toolbar that takes the place of the table header while rows are selected, with a select-all checkbox and the actions that apply to the selection. |
| TableHeader | TableHeader is the row of column titles at the top of a table; it also decides the width of every column and writes them onto the table's grid. |
| TableRow | TableRow is one row of a table: its cells followed by a last cell with the row's context menu button. |