Skip to content

Commit f173450

Browse files
Merge pull request #121 from datum-cloud/feat/grouped-table-style-parity
feat(grouped-table): data-table style parity + slot className overrides
2 parents 917f447 + eb8bb48 commit f173450

5 files changed

Lines changed: 70 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@datum-cloud/datum-ui": minor
3+
---
4+
5+
GroupedTable: align header/body/cell styling with `data-table`, restyle the group band as a muted section header (subtle fill, matching the column header), and drop cell truncation. Adds slot-level `className` overrides mirroring data-table — `tableClassName`, `headerRowClassName`, `headerCellClassName`, `groupHeaderClassName` (`string | (group) => string`), `bodyClassName`, `rowClassName` (`string | (row) => string`), `cellClassName` (`string | (cell) => string`), and `toolbarClassName`.

packages/datum-ui/src/components/features/grouped-table/__tests__/grouped-table.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,16 @@ describe('groupedTable', () => {
138138
expect(onRowSelectionChange).toHaveBeenCalled()
139139
expect(screen.getAllByLabelText('Select row')[0]!).not.toBeChecked()
140140
})
141+
142+
it('applies a string cellClassName to every data cell', () => {
143+
const { container } = render(<GroupedTable columns={columns} groups={groups} cellClassName="custom-cell" />)
144+
// 3 rows (alpha, beta, gamma) x 2 columns
145+
expect(container.querySelectorAll('td.custom-cell')).toHaveLength(6)
146+
})
147+
148+
it('applies groupHeaderClassName(group) to each group band', () => {
149+
render(<GroupedTable columns={columns} groups={groups} groupHeaderClassName={g => `band-${g.id}`} />)
150+
expect(screen.getByText('Group One').closest('button')).toHaveClass('band-g1')
151+
expect(screen.getByText('Group Two').closest('button')).toHaveClass('band-g2')
152+
})
141153
})

packages/datum-ui/src/components/features/grouped-table/components/grouped-toolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState } from 'react'
2+
import { cn } from '../../../../utils/cn'
23
import { Input } from '../../../base/input'
34

45
const DEFAULT_DEBOUNCE_MS = 300
@@ -33,13 +34,12 @@ export function GroupedToolbar({
3334
}, [value, debounceMs, search, onSearchChange])
3435

3536
return (
36-
<div className="pb-3" data-slot="gt-toolbar">
37+
<div className={cn('pb-3', className)} data-slot="gt-toolbar">
3738
<Input
3839
placeholder={placeholder}
3940
value={value}
4041
onChange={e => setValue(e.target.value)}
4142
aria-label={placeholder}
42-
className={className}
4343
data-slot="gt-search"
4444
/>
4545
</div>

packages/datum-ui/src/components/features/grouped-table/grouped-table.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ function renderColGroup<TData>(resolvedColumns: ColumnDef<TData, unknown>[]): Re
4747
)
4848
}
4949

50+
/** Resolve a static or per-item className override (mirrors data-table). */
51+
function resolveClassName<T>(value: string | ((item: T) => string) | undefined, item: T): string | undefined {
52+
return typeof value === 'function' ? value(item) : value
53+
}
54+
5055
export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
5156
const {
5257
columns,
@@ -73,6 +78,14 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
7378
isLoading,
7479
empty,
7580
className,
81+
toolbarClassName,
82+
tableClassName,
83+
headerRowClassName,
84+
headerCellClassName,
85+
groupHeaderClassName,
86+
bodyClassName,
87+
rowClassName,
88+
cellClassName,
7689
} = props
7790

7891
const [sorting, setSorting] = useControllableState(sortingProp, [], onSortingChange)
@@ -119,6 +132,7 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
119132

120133
const slices = useMemo(
121134
() => groups.map(g => ({
135+
group: g,
122136
id: g.id,
123137
title: g.title,
124138
meta: g.meta,
@@ -139,6 +153,7 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
139153
onSearchChange={setSearch}
140154
placeholder={searchPlaceholder}
141155
debounceMs={searchDebounceMs}
156+
className={toolbarClassName}
142157
/>
143158
)}
144159
<div className={cn('w-full rounded-md border', scrollable ? 'overflow-x-auto' : 'overflow-hidden')}>
@@ -155,13 +170,13 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
155170

156171
return renderShell(
157172
<>
158-
<table className="w-full table-fixed text-sm">
173+
<table className={cn('w-full table-fixed text-sm', tableClassName)}>
159174
{renderColGroup(resolvedColumns)}
160175
<TableHeader>
161176
{headerGroups.map(hg => (
162-
<TableRow key={hg.id}>
177+
<TableRow key={hg.id} className={headerRowClassName}>
163178
{hg.headers.map(header => (
164-
<TableHead key={header.id} scope="col">
179+
<TableHead key={header.id} scope="col" className={headerCellClassName}>
165180
{header.isPlaceholder
166181
? null
167182
: flexRender(header.column.columnDef.header, header.getContext())}
@@ -172,38 +187,42 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
172187
</TableHeader>
173188
</table>
174189

175-
{visibleSlices.map((slice, i) => {
190+
{visibleSlices.map((slice) => {
176191
const open = isSearching ? true : isOpen(slice.id)
177192
return (
178193
<Collapsible key={slice.id} open={open} onOpenChange={() => toggle(slice.id)}>
179194
<CollapsibleTrigger
180195
className={cn(
181-
'flex w-full items-center gap-2 bg-muted/40 px-3 py-2 text-left text-sm font-semibold hover:bg-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
182-
i > 0 && 'border-t',
196+
'flex h-10 w-full items-center gap-2 border-b bg-muted/40 px-2 text-left align-middle text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
197+
resolveClassName(groupHeaderClassName, slice.group),
183198
)}
184199
>
185200
<Icon
186201
icon={ChevronRight}
187202
aria-hidden
188-
className={cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90')}
203+
className={cn('size-4 shrink-0 transition-transform', open && 'rotate-90')}
189204
/>
190205
<span>{slice.title}</span>
191206
{slice.meta != null && (
192-
<span className="ml-auto flex items-center gap-2 font-medium text-muted-foreground">{slice.meta}</span>
207+
<span className="ml-auto flex items-center gap-2 font-medium">{slice.meta}</span>
193208
)}
194209
</CollapsibleTrigger>
195210

196211
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
197212
<table
198-
className="w-full table-fixed border-t text-sm"
213+
className={cn('w-full table-fixed text-sm', tableClassName)}
199214
aria-label={typeof slice.title === 'string' ? slice.title : undefined}
200215
>
201216
{renderColGroup(resolvedColumns)}
202-
<TableBody>
217+
<TableBody className={bodyClassName}>
203218
{slice.rows.map((row: Row<TData>) => (
204-
<TableRow key={row.id} data-state={row.getIsSelected() ? 'selected' : undefined}>
219+
<TableRow
220+
key={row.id}
221+
data-state={row.getIsSelected() ? 'selected' : undefined}
222+
className={resolveClassName(rowClassName, row)}
223+
>
205224
{row.getVisibleCells().map(cell => (
206-
<TableCell key={cell.id} className="truncate">
225+
<TableCell key={cell.id} className={resolveClassName(cellClassName, cell)}>
207226
{flexRender(cell.column.columnDef.cell, cell.getContext())}
208227
</TableCell>
209228
))}

packages/datum-ui/src/components/features/grouped-table/types.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'
1+
import type { Cell, ColumnDef, Row, RowSelectionState, SortingState } from '@tanstack/react-table'
22
import type { ReactNode } from 'react'
33
import type { SelectionColumnOptions } from '../data-table/types'
44
import type { ActionItem } from '../more-actions/types'
@@ -61,5 +61,24 @@ export interface GroupedTableProps<TData> {
6161

6262
/** Rendered when there are no groups, every group is empty, or search clears everything. */
6363
empty?: ReactNode
64+
65+
// ── styling overrides (mirrors data-table; merged on top of defaults) ──
66+
/** Root wrapper. */
6467
className?: string
68+
/** Search toolbar wrapper (when `enableSearch`). */
69+
toolbarClassName?: string
70+
/** Every `<table>` element (shared header + each group body). */
71+
tableClassName?: string
72+
/** The column-header `<tr>`. */
73+
headerRowClassName?: string
74+
/** Each column-header `<th>`. */
75+
headerCellClassName?: string
76+
/** Each group's collapsible header band; receives the group for per-group styling. */
77+
groupHeaderClassName?: string | ((group: GroupedTableGroup<TData>) => string)
78+
/** Each group's `<tbody>`. */
79+
bodyClassName?: string
80+
/** Each data `<tr>`; receives the row for per-row styling. */
81+
rowClassName?: string | ((row: Row<TData>) => string)
82+
/** Each data `<td>`; receives the cell for per-cell styling. */
83+
cellClassName?: string | ((cell: Cell<TData, unknown>) => string)
6584
}

0 commit comments

Comments
 (0)