[dev-v5][DataGrid] Add master/detail row expansion#5011
Conversation
Adds a RowDetails template parameter that lets any row be expanded via a chevron toggle to reveal detail content spanning all columns directly below it, independent of the existing hierarchical view (which requires parent/child rows of the same item type and columns). The RowDetails template's context is the row's item, so it can host a child FluentDataGrid filtered by that item, nested to any depth. Also fixes a CSS isolation issue where the ::deep tr selector used for CSS Grid display mode could match rows of a nested/child FluentDataGrid, and introduces DataGridCellType.RowDetails so the detail cell isn't constrained by the fixed row height/grid-column styling applied to normal cells.
dev-v5 doesn't use Blazor's per-component CSS isolation (no [b-xxxx] scope attributes are emitted anywhere in the bundled stylesheet); DataGrid styles are plain global CSS instead. The ::deep combinator I had carried over from the dev branch fix is meaningless in that model and was left unprocessed by the build, silently invalidating the whole selector and breaking `display: contents` for every grid (not just nested ones). Replaced it with plain child combinators, which is all that's needed since there's no scope boundary to cross. Also fixes the RowDetails toggle button starting its own line instead of staying beside the first column's value: unlike the HierarchicalToggle container (which wraps the entire cell content), the RowDetails toggle is a standalone element rendered before the cell's own content, so it needs display: inline-flex rather than the block-level flex used for hierarchical-toggle-container.
There was a problem hiding this comment.
Pull request overview
Adds a new master/detail row expansion feature to FluentDataGrid<TGridItem> via a RowDetails template, enabling per-row expandable detail content (including nested grids) without relying on the existing hierarchical (parent/child) model.
Changes:
- Introduces
RowDetailsrendering, toggle UI, and a programmatic expand/collapse API surface onFluentDataGrid<TGridItem>. - Adds
DataGridCellType.RowDetailsand styling adjustments so the details row can span all columns and size to content. - Adds bUnit coverage plus new demo documentation/examples for master/detail and multi-level nesting.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Core/Components/DataGrid/FluentDataGridRowDetailsTests.razor | Adds bUnit tests covering RowDetails toggling, API methods, and Virtualize guard. |
| src/Core/Enums/DataGridCellType.cs | Adds RowDetails enum member for the new details cell type. |
| src/Core/Components/DataGrid/FluentDataGridRow.razor.cs | Prevents row click/double-click handlers from acting on the details row. |
| src/Core/Components/DataGrid/FluentDataGridCell.razor.css | Adds styling for the details cell and toggle container inline layout. |
| src/Core/Components/DataGrid/FluentDataGridCell.razor.cs | Skips fixed-height / grid-column styling for RowDetails cells. |
| src/Core/Components/DataGrid/FluentDataGridCell.razor | Renders RowDetails cells as <td> like default cells. |
| src/Core/Components/DataGrid/FluentDataGrid.razor.css | Restricts display: contents selector to avoid impacting nested grids. |
| src/Core/Components/DataGrid/FluentDataGrid.razor.cs | Adds RowDetails, OnRowDetailsToggle, Virtualize guard, and new expansion-state APIs. |
| src/Core/Components/DataGrid/FluentDataGrid.razor | Renders the toggle in the first column and conditionally renders the spanning details row. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridMasterDetailPage.md | Adds documentation page describing the new master/detail feature. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Examples/DataGridMasterDetail.razor | Adds master/detail demo example with nested grid. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Examples/DataGridMasterDetailTwoLevels.razor | Adds two-level nested RowDetails demo example. |
- Give the RowDetails toggle button its own aria-label (DataGrid_ToggleRowDetails) instead of reusing "Toggle nesting", which described the unrelated hierarchical-view expander and was misleading for screen reader users. - Make ExpandAllRowDetailsAsync/CollapseAllRowDetailsAsync raise OnRowDetailsToggle for each row whose expansion state actually changes, matching the callback's documented "raised when a row is expanded or collapsed" contract (previously only the single-row toggle methods raised it). - Add tests for both, plus a regression test locking in the dedicated aria-label.
|
@microsoft-github-policy-service agree |
|
Great addition. In fact this is something I was searching before as well. This isn't just limited to nested The only weird thing in my opinion is the mouse houver effect for the entire block. |
With ShowHover enabled, hovering anywhere inside a RowDetails block (which can contain arbitrary, potentially large content such as a nested grid) highlighted the entire details cell, since the hover background rule only excluded header/sticky-header/loading rows. Excluded row-details-row the same way.
|
Thanks, glad it's useful! And good catch on the hover — you're right, that's a real issue. The hover highlight rule (FluentDataGridRow.razor.css) only excluded header/sticky-header/loading rows from the ShowHover background, so a RowDetails block (which can hold arbitrary, potentially large content) got the whole cell highlighted on hover instead of behaving like a normal thin row. Fixed in cb67cfa by excluding row-details-row the same way those other special rows already are. |
Adds an "Any content as detail" example showing that the RowDetails template isn't limited to a nested FluentDataGrid: it can host any content bound to the master row's item. Here a customer row expands into a contact card (FluentCard/FluentStack/FluentProgressBar) instead of a child grid.
|
Following up on your comment about RowDetails not being limited to a nested FluentDataGrid — added a dedicated example illustrating exactly that in 0a419eb. New "Any content as detail" section on the /DataGrid/MasterDetail page: expanding a customer row now shows a contact card (FluentCard + FluentStack + FluentProgressBar) instead of a nested grid, bound to that row's item — no FluentDataGrid involved at all. Should make it clearer to readers that the template is a free-form RenderFragment, not something tied to grids specifically. |
|
I think I like this (have not had a chance to look at it closely and in detail). Perhaps you can add some screenshots to the PR description? One thing that worries me a bit is that we end up with two solutions for (sort of) the same thing. We need to think carefully about how we can/should position this once we get to the point where we want to merge this in (not saying we are going to, also not saying we are not going to) With .NET 11 we get the option to have inequal heights with Validation. We should investigate if we can then support this new option (and Multiline) in the that version (#ifdef's or something like that) |
|
Screenshots attached — one per example on /DataGrid/MasterDetail: Single-level (customers → orders) Hierarchical view: parent and children are the same TItem, rendered through the same columns in a single grid — it's for recursive/self-referential data (org charts, categories/subcategories). Good call on .NET 11 — I hadn't looked into the unequal-heights/Validation work there. I don't know that API surface yet, but I'm glad to dig into whether RowDetails (and MultiLine) could ride on it once it's available, if/when that becomes relevant here. |
Pull Request
📖 Description
Adds a
RowDetailstemplate parameter that lets any row be expanded via a chevron toggle to reveal detail content spanning all columns directly below it, independent of the existing hierarchical view (which requires parent/child rows of the same item type and columns). TheRowDetailstemplate's context is the row's item, so it can host a childFluentDataGridfiltered by that item, nested to any depth.This is a new feature (additive, non-breaking): existing grids are unaffected unless
RowDetailsis set.New public API on
FluentDataGrid<TGridItem>:RowDetails(RenderFragment<TGridItem>) — the detail template parameter.OnRowDetailsToggle(EventCallback<TGridItem>) — raised when a row's details are expanded or collapsed.ToggleRowDetailsAsync,ExpandRowDetailsAsync,CollapseRowDetailsAsync,ExpandAllRowDetailsAsync,CollapseAllRowDetailsAsync,IsRowDetailsExpanded— programmatic control of expansion state.DataGridCellType.RowDetails— new enum member identifying the detail cell.RowDetailscannot be combined withVirtualize(throwsInvalidOperationException, matching the existingMultiLine/Virtualizeguard).📸 Screenshots
CSS notes for reviewers
FluentDataGrid'sdisplay: gridmode relies ondisplay: contentson<tr>so cells participate directly in the table's CSS Grid. The original selector matched anytrunder.grid, so a nestedFluentDataGridplaced insideRowDetailshad its own rows absorbed into the parent's grid layout instead of staying contained. Fixed by restricting the rule to this grid's own directthead/tbodychildren (.fluent-data-grid.grid > thead > tr,.fluent-data-grid.grid > tbody > tr) so a nested grid's rows — several DOM levels deeper — aren't matched.Also added
display: inline-flexfor the new toggle's container: unlikeHierarchicalToggle(which wraps the entire cell content, so being block-level doesn't matter), theRowDetailstoggle is a standalone element rendered before the cell's own content, so it needs to stay inline with it rather than starting its own line.👩💻 Reviewer Notes
Two demo pages are included under
/DataGrid/MasterDetail:/DataGrid/Hierarchicalexample where parent/child share the same item type and columns.FluentDataGridcan itself define aRowDetailstemplate, so nesting depth is unlimited.Suggested smoke test: open
/DataGrid/MasterDetail, expand a row in each example, confirm the child grid renders in its own area below the master row (not overlapping subsequent rows), confirm the toggle chevron sits beside the first column's value (not above/below it), collapse it again, and confirm sorting/columns in the child grid work independently of the master grid.Areas to focus review on:
FluentDataGrid.razor/.razor.cs— theRowDetailsrendering and the new expand/collapse API (state keyed byItemKey, so it survives sorting/refresh).FluentDataGrid.razor.css— the> thead > tr/> tbody > trselector change (previously a baretrdescendant selector).FluentDataGridCell.razor.cs—DataGridCellType.RowDetailsskips the fixed row-height andgrid-columnstyles so the detail cell's height follows its content.FluentDataGridRow.razor.cs— click/double-click handling now ignores therow-details-rowclass so interacting with a nested grid doesn't bubble upOnRowClick/OnRowDoubleClickon the master grid.📑 Test Plan
Added
tests/Core/Components/DataGrid/FluentDataGridRowDetailsTests.razor(6 new bUnit tests):ExpandAllRowDetailsAsync/CollapseAllRowDetailsAsyncaffect all loaded rows.ExpandRowDetailsAsync/CollapseRowDetailsAsync/IsRowDetailsExpandedon a single item (including idempotency).OnRowDetailsToggleis raised with the correct item.RowDetails+VirtualizethrowsInvalidOperationException.Full suite run locally: 3752 passed, 1 failed, 0 skipped — the one failure (
FluentNumberTests.FluentNumber_Culture_FR) is pre-existing on a cleandev-v5checkout with no changes (culture/environment-dependent) and unrelated to this change.Also manually verified in the running demo app (
FluentUI.Demo/FluentUI.Demo.Client) that: the nested child grid renders in its own row without overlapping the master grid's subsequent rows, the toggle chevron sits correctly beside the column value, and both demo pages behave correctly end to end.✅ Checklist
General
Component-specific
⏭ Next Steps
RowDetailsTemplatevariant that also receives the row's expanded/collapsed state if reviewers want the template itself to react to it (currently onlyOnRowDetailsToggleexposes this, from the grid side).aria-expandedattribute already set on the toggle button.