Skip to content
Data TableGuides

Row Expand

comins-table@0.2.2

Starting with 0.2.2, .render is the standard UI customization entry point. Legacy properties remain supported throughout 0.x, with removal targeted for 1.0.0. See the migration rules.

Row Expand adds a controlled full-width Detail region immediately after a flat owner business Row. The application owns the expanded IDs and Detail content; Comins Table keeps sorting, pagination, loading, movement, selection, clipboard, and callbacks addressed to owner Rows only.

Open /examples/row-expand after npm run dev for fixed, automatic, and tall Detail examples. Each sample demonstrates one expanded owner at a time.

export type CominsRowDetailParams<TData> = {
row: CominsEventRow<TData>;
};
export type CominsRowDetailHeight = number | "auto";
export type CominsRowDetailProps<TData> = {
estimatedRowDetailHeight?: number;
expandedRowIds?: readonly CominsRowId[];
getRowDetailHeight?: (
params: CominsRowDetailParams<TData>,
) => CominsRowDetailHeight;
isRowExpandable?: (params: CominsRowDetailParams<TData>) => boolean;
onChangeExpandedRowIds?: (rowIds: CominsRowId[]) => void;
rowDetail?: { render?: (
params: CominsRowDetailParams<TData>,
) => React.ReactNode };
};

rowDetail.render enables the flat Row Expand surface. Without it, all other Detail props remain inert.

const [expandedRowIds, setExpandedRowIds] = useState<readonly string[]>([]);
<CominsTable
columns={columns}
data={rows}
expandedRowIds={expandedRowIds}
getRowId={(row) => row.id}
onChangeExpandedRowIds={setExpandedRowIds}
rowDetail={{ render: ({ row }) => <Detail row={row.data} /> }}
/>;

expandedRowIds defaults to an empty array. A disclosure calls onChangeExpandedRowIds with the next deduplicated owner ID array; the application writes it back to keep the UI controlled. If the callback is omitted for an otherwise expandable Row, the read-only controlled disclosure reflects the supplied state but is disabled. Its effective Detail remains mounted when its ID is present.

The library accepts multiple IDs so applications can keep several Details open. To use an accordion-style single expansion policy, normalize the callback before writing the controlled state:

const [expandedRowIds, setExpandedRowIds] = useState<readonly CominsRowId[]>([]);
<CominsTable
expandedRowIds={expandedRowIds}
onChangeExpandedRowIds={(nextIds) => setExpandedRowIds(nextIds.slice(-1))}
/>

An empty callback value still collapses the current Detail. The Row Expand Playground uses this application-owned policy without adding a Table mode prop.

IDs remain dormant when their owner is filtered out, paged out, not loaded, or temporarily unavailable. They are preserved in subsequent callback input and become effective again when the owner returns. An ID is effective only when the current flat data contains the owner and isRowExpandable does not return false. This is a separate contract from read-only control: a non-expandable Row renders neither a disclosure nor a Detail, even when a change callback and controlled ID are present.

getRowDetailHeight accepts a finite positive CSS pixel height or "auto". Only a finite positive number is fixed: its Detail keeps that inline height. Missing values, invalid numeric values, and "auto" use automatic measurement, so the rendered Detail has no inline height.

<CominsTable
estimatedRowDetailHeight={300}
getRowDetailHeight={({ row }) => (row.id === "large" ? 480 : "auto")}
rowDetail={{ render: ({ row }) => <Detail row={row.data} /> }}
/>

Before an automatic Detail has a matching-width measurement, estimatedRowDetailHeight is used when it is a finite positive value; otherwise the current resolved rowHeight is the estimate. Measurements are cached by owner ID and width. A width mismatch returns to that estimate until the shared ResizeObserver reports the new border-box height. Fixed Details are not observed and never use estimatedRowDetailHeight.

Each effective Detail is a semantic sibling of its owner:

<tr data-comins-row-data-index="0">...</tr>
<tr data-detail-for="row-id">
<td colspan="current visible Column count">
<div role="region" aria-labelledby="owner-disclosure-id">...</div>
</td>
</tr>

The owner disclosure exposes aria-expanded. aria-controls is present only while the controlled region is mounted. The region is labelled by that disclosure. Its accessible name is exactly Expand <row-id> details or Collapse <row-id> details. Native Enter and Space activation is preserved, while disclosure keydown events do not enter owner Cell or Row keyboard and clipboard handling. Interactive Detail content stays in the normal tab order. When a controlled collapse unmounts a Detail that contains focus, focus returns to the owner disclosure.

The Detail cell spans the effective visible Column count after Column hide, restore, and reorder. It is one non-sticky full-width cell and scrolls horizontally with the body.

Surface Row Expand behavior
Sorting Owner and Detail move together; controlled IDs do not change.
Pagination Off-page IDs stay dormant and are preserved.
Infinite Scroll and Lazy Load Offsets, limits, thresholds, and counts use owner business Rows only.
Row movement The owner Slot moves as one unit; Detail is never a target.
Row, Cell, and range selection Detail has no Row or Cell address and cannot enter a range.
Copy and paste Detail content is neither a clipboard source nor target.
Context menu and double-click Owner callbacks ignore Detail content and disclosure events.
Loading, empty, filler, Summary, and infinite-loading Rows Structural Rows have no disclosure or Detail.
Tree Grid Unsupported; the runtime wrapper strips untyped flat Detail props.

Uniform fixed-height Rows keep the arithmetic path. Row-specific or automatic heights and expanded Details use the private height index. See automatic Row height.

The owner and its optional Detail are one private virtual Slot. This keeps a Detail taller than the viewport mounted while the outer body viewport scrolls through it, without turning Detail content into a business Row.

When virtual content is shorter than the body viewport, the final Row keeps its bottom separator. Once the content reaches the viewport boundary, the outer Table frame supplies the terminal border without a duplicate Cell border.

Prefer a finite fixed height for bounded panels, large lists, or nested application widgets, and give large inner content its own scroll or virtualization. Use "auto" for bounded content that genuinely needs measured height. One shared ResizeObserver observes mounted automatic Rows and Detail blocks; fixed heights do not allocate measurements.

  • Tree Grid Row Details are unsupported.
  • Nested managed Details are unsupported. An application may render ordinary content inside one Detail, but Comins Table does not manage a second Detail hierarchy.

Legacy renderRowDetail remains supported in 0.x; see the migration guide.


Runnable example · Source guide for this version