Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tables-grow-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cube-dev/ui-kit': minor
---

Add accessible nested tree rows, expansion state, hierarchy-aware operations, cascading ItemTable selection, grouped DataTable headers for pivoted results, and intrinsic table sizing with `isAutoHeight`.
12 changes: 10 additions & 2 deletions .size-limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ module.exports = [
}),
);
},
// 505 kB, raised from 501 kB, which the palette branch exceeded by 596 B
// 510 kB, raised from 505 kB for ItemTable/DataTable tree rows. The shared
// immutable hierarchy model, recursive operations, React Aria treegrid
// wiring, cascading selection and disclosure renderer add 2.97 kB locally
// (507.97 kB total). The Button-only budget below is unchanged, confirming
// that consumers which do not import the tables tree-shake the feature.
// Rounded to the next 5 kB step for the macOS/Linux zlib difference noted
// below.
//
// Previously 505 kB, raised from 501 kB, which the palette branch exceeded by 596 B
// locally. Measured with both sides rebuilt on the same machine: `main` at
// 500.29 kB, that branch at 501.60 kB, so the palette work itself is
// +1.31 kB — the color-seeded accent arrangement, `color-seed.ts`, and
Expand Down Expand Up @@ -110,7 +118,7 @@ module.exports = [
//
// Note when checking locally: `size-limit` bundles the built `./dist`, it
// does not build. Run `pnpm build` first or you will measure a stale bundle.
limit: '505kB',
limit: '510kB',
},
{
name: 'Tree shaking (just a Button)',
Expand Down
56 changes: 56 additions & 0 deletions src/components/data/DataTable/DataTable.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,62 @@ const scroller = () =>
document.querySelector<HTMLElement>('[data-element="Scroller"]')!;

describe('DataTable layout', () => {
it('fills a flex pane by default', async () => {
renderWithRoot(
<div
data-qa="TableHost"
style={{ display: 'flex', flexDirection: 'column', height: 420 }}
>
<DataTable
qa="SizingTable"
data={ROWS.slice(0, 4)}
columns={COLUMNS}
paginationMode="off"
/>
</div>,
);

const frame = screen.getByTestId('SizingTable');

await vi.waitFor(() =>
expect(Math.round(frame.getBoundingClientRect().height)).toBe(420),
);
});

it('shrink-wraps rows in auto-height mode despite a supplied height', async () => {
renderWithRoot(
<div
data-qa="TableHost"
style={{ display: 'flex', flexDirection: 'column', height: 420 }}
>
<DataTable
qa="SizingTable"
data={ROWS.slice(0, 4)}
columns={COLUMNS}
paginationMode="off"
height="300px"
isAutoHeight
/>
</div>,
);

const frame = screen.getByTestId('SizingTable');

await vi.waitFor(() =>
expect(frame.getBoundingClientRect().height).toBeGreaterThan(0),
);

const frameRect = frame.getBoundingClientRect();
const tableRect = grid().getBoundingClientRect();

expect(frameRect.height).toBeLessThan(300);
// The card/frame edge may contribute one border pixel; there must not be a
// leftover flexible track below the final row.
expect(Math.abs(frameRect.bottom - tableRect.bottom)).toBeLessThanOrEqual(
2,
);
});

it('does not overflow horizontally when the columns fit', async () => {
renderWithRoot(
<DataTable
Expand Down
125 changes: 114 additions & 11 deletions src/components/data/DataTable/DataTable.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ API keys. That is the one with selection, bulk actions, row menus and row links.
## It knows nothing about Cube

Measures, dimensions, pivots, drill-downs and calculated fields never reach this
component as concepts. They arrive as ordinary columns, `render` output and
`column.header.menu` content — which is what keeps Cloud's column-header menu,
and everything it knows about the query, in Cloud.
component as query concepts. They arrive as leaf columns, presentational column
groups, `render` output and `column.header.menu` content — which is what keeps
Cloud's column-header menu, and everything it knows about the query, in Cloud.

<Canvas of={DataTableStories.Default} />

Expand All @@ -49,9 +49,19 @@ and everything it knows about the query, in Cloud.
#### Data

- **`data`** `readonly T[]` — The rows.
- **`columns`** `CubeDataTableColumn<T>[]` — Same shape as `ItemTable`'s, plus `dataType`.
- **`columns`** `CubeDataTableColumnDefinition<T>[]` — Leaf columns use the same
shape as `ItemTable`'s, plus `dataType`; group nodes contain nested `children`.
- **`rowKey`** `string` (default: `'id'`) — Property to read the row identity from.
- **`getRowKey`** `(row: T, index: number) => Key` — Wins over `rowKey`.
- **`getRowChildren`** `(row: T) => readonly T[] | undefined` — Enables nested
treegrid rows; `data` contains roots.
- **`treeColumnKey`** `string` — Indented disclosure column. Defaults to the
first visible data column.
- **`expandedKeys`** `Key[]` — Controlled expansion.
- **`defaultExpandedKeys`** `Key[]` — Initial expansion. Nothing is expanded by
default.
- **`onExpand`** `(keys, info) => void` — Reports the resulting keys and toggled
row metadata.
- **`pinnedTopRows`** `readonly T[]` — Rows stuck to the top of the scroller. Ordinary rows as far as the columns are concerned.
- **`pinnedBottomRows`** `readonly T[]` — The same, at the bottom. This is where totals go.

Expand All @@ -71,10 +81,13 @@ and everything it knows about the query, in Cloud.
- **`size`** `SizeName` (default: `'small'`) — An analytical grid packs more rows than a list.
- **`rowSize`** `'small' | 'medium' | 'large'` — Row height as a named step: 28px / 32px / 40px. Rows only; the header keeps answering to `size`. Unset, the height comes from `size`.
- **`rowHeight`** `number` — An exact height, when none of the named steps is the answer. Wins over both `rowSize` and `size`.
- **`headerHeight`** `number`
- **`headerHeight`** `number` — Exact height of each header row in px.
- **`isStriped`** `boolean` (default: `true`) — Banding is what makes a wide row readable across.
- **`isHeaderHidden`** `boolean`
- **`isHeaderSticky`** `boolean` (default: `true`)
- **`isAutoHeight`** `boolean` (default: `false`) — Shrink-wraps the frame to
the rendered rows instead of filling its flex pane. It takes precedence over
root height/flex sizing and disables virtualization.
- **`isRowMoveAnimated`** `boolean` (default: `true`) — Slide rows to their new positions when the order changes, instead of teleporting them. Respects `prefers-reduced-motion`.
- **`showRowNumbers`** `boolean` (default: `false`) — A continuous count down the side, which stays continuous across pages.

Expand Down Expand Up @@ -135,7 +148,7 @@ see [Column Colors](#column-colors) for the last two.

#### Virtualization

- **`isVirtualized`** `boolean | 'auto'` (default: `'auto'`) — On when the grid is height-bounded and has more rows than the threshold.
- **`isVirtualized`** `boolean | 'auto'` (default: `'auto'`) — On when the grid is height-bounded and has more rows than the threshold. Ignored when `isAutoHeight` is enabled.
- **`virtualizeThreshold`** `number`
- **`overscan`** `number`

Expand All @@ -144,6 +157,9 @@ see [Column Colors](#column-colors) for the last two.
- **`getRowProps`** `(ctx: CubeTableRowContext<T>) => Record<string, any>` — Per-row mods, styles and height.
- **`storageKey`** `string` — Persists page size, column widths and column order.
- **`ariaLabel`** `string`
- **`isChecked`** `boolean` — Base checked-state modifier.
- **`place`** `string` — Tasty placement shorthand on the root frame.
- **`scrollMargin`** `string | number` — Scroll margin on the root frame.

#### `dataType`

Expand All @@ -167,9 +183,10 @@ Customizes the root frame. Every internal part of the grid is a **sub-element**
reachable from this one prop — the same contract, and the same sub-element
names, as [ItemTable](/docs/data-itemtable--docs#styling-properties).

`DataTable` adds one: `StateCell`, the full-span cell that carries the empty,
no-results and error content. It is deliberately not a `Cell`, so it takes none
of the row's banding or hover paint.
`DataTable` adds `StateCell`, the full-span cell that carries the empty,
no-results and error content, and `HeaderGroupCell`, a spanning cell in a
multi-row header. `StateCell` is deliberately not a `Cell`, so it takes none of
the row's banding or hover paint.

#### Targeted slots

Expand Down Expand Up @@ -212,6 +229,76 @@ body into a scroller, pins the header, and lets virtualization engage at all.
Cell-level, in addition to `ItemTable`'s: `cell-selected`, `last-column`,
`pinned`.

## Pivoted Results and Column Groups

Pass group nodes in `columns` to render real multi-row headers. Groups may nest
to any depth; their `children` eventually resolve to ordinary leaf columns, and
only those leaves participate in sorting, resizing, ranges and copying.

```jsx
const columns = [
{ key: 'region', title: 'Region', pin: 'start' },
{
key: 'organic',
title: 'Organic',
children: [
{ key: 'organic_orders', title: 'Orders', dataType: 'number' },
{ key: 'organic_revenue', title: 'Revenue', dataType: 'number' },
],
},
{
key: 'paid',
title: 'Paid',
children: [
{ key: 'paid_orders', title: 'Orders', dataType: 'number' },
{ key: 'paid_revenue', title: 'Revenue', dataType: 'number' },
],
},
];

<DataTable data={pivotRows} columns={columns} isAutoHeight />;
```

The data/query layer still owns the pivot calculation. DataTable only renders
the already-shaped rows and generated column hierarchy. Leaf resizing and
sorting remain available; drag column reordering is ignored while groups are
present because moving one leaf cannot preserve an arbitrary hierarchy.

<Canvas of={DataTableStories.Pivot} />

## Tree Rows

Tree mode uses the same nested-data contract as `ItemTable`:

```jsx
<DataTable
data={regions}
columns={columns}
getRowChildren={(row) => row.children}
treeColumnKey="region"
defaultExpandedKeys={['americas']}
/>
```

Multi-sort is recursive: every sibling collection is sorted by the same
precedence. Client pagination slices roots and keeps complete subtrees together;
server pages are rendered exactly as supplied and `total` counts roots. Pinned
rows remain ordinary non-hierarchical totals.

Only visible descendants participate in cell ranges and virtualization.
Collapsing a range endpoint makes the effective range inactive; a controlled or
uncontrolled range is restored when that endpoint becomes visible again. Client
row numbers remain continuous across root pages, while server tree pages restart
at 1 because preceding descendant counts are unknown.

The native table exposes `role="treegrid"`, `aria-level`, `aria-expanded`,
`aria-posinset` and `aria-setsize`. Right/Left expand, collapse or move to the
parent; Up/Down traverse visible rows, and typeahead reads `treeColumnKey`.
Customize the hierarchy chrome with `styles.TreeContent`, `styles.TreeToggle`
and `styles.TreeValue`.

<Canvas of={DataTableStories.TreeRows} />

## Examples

### Multi-Column Sorting
Expand Down Expand Up @@ -255,6 +342,20 @@ competing for a position.
Above `virtualizeThreshold` a bounded grid virtualizes on its own, so
`paginationMode="off"` is a reasonable default for a result the user scrolls.

For a compact result that should participate in normal page flow, set
`isAutoHeight`. The frame then ends at the last rendered row rather than filling
its flex pane. This renders the complete result and disables virtualization, so
keep larger results bounded instead.

```jsx
<DataTable
data={summaryRows}
columns={columns}
paginationMode="off"
isAutoHeight
/>
```

<Canvas of={DataTableStories.Unpaginated} />

### Sorting Moves Rows
Expand Down Expand Up @@ -429,8 +530,10 @@ elsewhere.
## Accessibility

The table is a `role="grid"` with document-absolute `aria-rowindex` /
`aria-colindex`, and `aria-rowcount` counts pinned rows. Every sorted column
carries `aria-sort`, not just the first.
`aria-colindex`, and `aria-rowcount` counts pinned rows and every visible header
row. Group cells use native `scope="colgroup"` / `colSpan`; ungrouped leaf
headers span the remaining rows. Every sorted leaf column carries `aria-sort`,
not just the first.

A header cell takes a tab stop when it is interactive — sortable, carrying a
menu, or reorderable. `Shift`+`F10` opens the column menu from the keyboard, and
Expand Down
Loading
Loading