[dev-v5][DataGrid] Add pinned (sticky/frozen) column support#4671
[dev-v5][DataGrid] Add pinned (sticky/frozen) column support#4671
Conversation
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/5cdc49d8-9764-4339-b8fa-6f9a9454f9e3 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
|
@copilot remove the obj\Debug folder and files from the src/Core.Scripts/ path. These are build artifacts and should not be included in the repo |
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/15853d21-e1e8-47ff-abe9-487b176c2352 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Done in e1acae8. Removed |
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/6c2b8e3b-c3f9-4e43-9827-7c473b6dbc66 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/1f05176a-ffeb-47aa-bf4a-df652edb999a Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/1f05176a-ffeb-47aa-bf4a-df652edb999a Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/1f05176a-ffeb-47aa-bf4a-df652edb999a Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/9c39db2d-7d9a-4278-bb79-73e76214f305 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/9c39db2d-7d9a-4278-bb79-73e76214f305 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/fluentui-blazor/sessions/9c39db2d-7d9a-4278-bb79-73e76214f305 Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com>
|
✅ All tests passed successfully Details on your Workflow / Core Tests page. |
Summary - Unit Tests Code CoverageSummary
CoverageMicrosoft.FluentUI.AspNetCore.Components - 98.9%
|
There was a problem hiding this comment.
Pull request overview
Adds pinned (sticky/frozen) column support to FluentDataGrid, enabling columns to remain visible while horizontally scrolling, with accompanying validation, styling, JS offset updates, docs, and tests.
Changes:
- Introduces
DataGridColumnPin+ a newPinparameter on columns, with grid-side validation and computed sticky offsets. - Updates rendering/CSS/JS to apply
position: stickyand keep left/right offsets correct during resizing. - Adds demo documentation/examples and expands test coverage/snapshots for pinned columns and updated header z-index behavior.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Core/Components/DataGrid/SelectColumnTests.SelectColumnTests_SingleStickySelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.SelectColumnTests_SingleSelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.SelectColumnTests_MultiSelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.SelectColumnTests_MultiSelect_Customized_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.FluentDataGrid_ColumSelect_SingleStickySelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.FluentDataGrid_ColumSelect_MultiSelect_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/SelectColumnTests.FluentDataGrid_ColumSelect_MultiSelect_Customized_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/HierarchicalSelectColumnTests.HierarchicalSelectColumnTests_Rendering.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.razor | Adds assertions ensuring header z-index moved from th to the button/menu layer. |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_StripedRows.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_StickyHeader.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_IsFixed.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_HeaderCellAsButtonWithMenu.verified.razor.html | Updates snapshot output (adds z-index layering on button/menu). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_Default.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_ColumnSortUISettings.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_ColumnResizeUISettings.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridTests.FluentDataGrid_ColumnOptionsUISettings.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| tests/Core/Components/DataGrid/FluentDataGridPinnedColumnTests.razor | Adds new unit/snapshot tests for pinned columns, offsets, and validation rules. |
| tests/Core/Components/DataGrid/FluentDataGridPinnedColumnTests.FluentDataGrid_PinnedColumn_Snapshot.verified.razor.html | Adds snapshot for pinned column rendering (classes + sticky offsets). |
| tests/Core/Components/DataGrid/FluentDataGridCellTests.FluentDataGridCell_DisableCellFocus.verified.razor.html | Updates snapshot output (removes inline header z-index styling). |
| src/Core/Enums/DataGridColumnPin.cs | Adds enum defining None/Left/Right pin states. |
| src/Core/Components/DataGrid/FluentDataGridCell.razor.cs | Applies pinned classes + sticky positioning/offset styles for pinned columns; adjusts min-width behavior and z-index usage. |
| src/Core/Components/DataGrid/FluentDataGrid.razor.ts | Adds pinned offset recalculation and improves min-width handling during resize; introduces UpdatePinnedColumnOffsets. |
| src/Core/Components/DataGrid/FluentDataGrid.razor.css | Adds pinned column styling (backgrounds, separators, stacking rules) and introduces --fluent-data-grid-pinned-background. |
| src/Core/Components/DataGrid/FluentDataGrid.razor.cs | Validates pinned configuration and computes initial pin offsets from explicit pixel widths. |
| src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs | Adds Pin parameter and internal computed PinOffsetPx for sticky offsets. |
| src/Core/Components/DataGrid/Columns/ColumnBase.razor | Adjusts header menu/button layering to use explicit z-index style on interactive elements. |
| src/Core.Scripts/.gitignore | Adds obj/ ignore for Core.Scripts. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridPinnedColumnsPage.md | Adds new documentation page for pinned columns. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/FluentDataGrid.md | Links the new pinned columns doc page and minor formatting tweaks. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Examples/DataGridPinnedColumns.razor | Adds a demo example showcasing pinned columns. |
Comments suppressed due to low confidence (2)
src/Core/Components/DataGrid/FluentDataGrid.razor.ts:476
- Pinned offsets are kept in sync during drag resize, but ResizeColumnDiscrete (keyboard/menu resize) doesn’t call UpdatePinnedColumnOffsets. Resizing a pinned (or adjacent) column via this path can leave pinned columns misaligned until a full re-init. Consider calling UpdatePinnedColumnOffsets after applying the new size / gridTemplateColumns here too.
if (column.header === headerBeingResized) {
const width = headerBeingResized!.offsetWidth + change;
//const width = headerBeingResized!.getBoundingClientRect().width + change;
if (change < 0) {
column.size = Math.max(getMinWidthPx(column.header), width) + 'px';
}
else {
column.size = width + 'px';
}
column.header.style.width = column.size;
}
if (isGrid) {
// for grid we need to recalculate all columns that are minmax
if (column.size.startsWith('minmax')) {
column.size = column.header.offsetWidth + 'px';
}
columns.push(column.size);
}
});
if (isGrid) {
gridElement.style.gridTemplateColumns = columns.join(' ');
}
}
src/Core/Components/DataGrid/FluentDataGrid.razor.ts:508
- ResizeColumnExact updates column widths but doesn’t call UpdatePinnedColumnOffsets, so pinned columns may keep stale left/right offsets after an exact resize. Consider invoking UpdatePinnedColumnOffsets before dispatching
closecolumnresize(and after updating gridTemplateColumns in Grid mode).
grids.find(grid => grid.id === gridElement.id)!.columns.forEach((column: any) => {
if (column.header === headerBeingResized) {
column.size = Math.max(getMinWidthPx(column.header), width) + 'px';
column.header.style.width = column.size;
}
if (isGrid) {
// for grid we need to recalculate all columns that are minmax
if (column.size.startsWith('minmax')) {
column.size = column.header.offsetWidth + 'px';
}
columns.push(column.size);
}
});
if (isGrid) {
gridElement.style.gridTemplateColumns = columns.join(' ');
}
gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
gridElement.focus();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private void ValidatePinnedColumnConstraints() | ||
| { | ||
| // Width must be an explicit pixel value. | ||
| foreach (var col in _columns.Where(c => c.Pin != DataGridColumnPin.None)) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(col.Width)) | ||
| { | ||
| throw new ArgumentException( | ||
| $"Column '{col.Title ?? col.Index.ToString(CultureInfo.InvariantCulture)}' has Pin set but no Width. " + | ||
| "Pinned columns require an explicit Width in pixels (e.g., '150px')."); | ||
| } | ||
|
|
||
| if (!col.Width!.Trim().EndsWith("px", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| throw new ArgumentException( | ||
| $"Column '{col.Title ?? col.Index.ToString(CultureInfo.InvariantCulture)}' has Pin set but Width '{col.Width}' is not in pixels. " + | ||
| "Pinned columns require an explicit Width in pixels (e.g., '150px')."); | ||
| } |
There was a problem hiding this comment.
Pinned-column width validation only checks that Width ends with "px", but does not verify the numeric part parses. Values like "abcpx" would pass validation, then ParsePixelWidth returns 0 and multiple pinned columns can overlap at offset 0. Consider validating that the value parses to a positive pixel number (and throw a descriptive ArgumentException when parsing fails).
| private void ValidatePinnedColumnConstraints() | |
| { | |
| // Width must be an explicit pixel value. | |
| foreach (var col in _columns.Where(c => c.Pin != DataGridColumnPin.None)) | |
| { | |
| if (string.IsNullOrWhiteSpace(col.Width)) | |
| { | |
| throw new ArgumentException( | |
| $"Column '{col.Title ?? col.Index.ToString(CultureInfo.InvariantCulture)}' has Pin set but no Width. " + | |
| "Pinned columns require an explicit Width in pixels (e.g., '150px')."); | |
| } | |
| if (!col.Width!.Trim().EndsWith("px", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| throw new ArgumentException( | |
| $"Column '{col.Title ?? col.Index.ToString(CultureInfo.InvariantCulture)}' has Pin set but Width '{col.Width}' is not in pixels. " + | |
| "Pinned columns require an explicit Width in pixels (e.g., '150px')."); | |
| } | |
| private static string GetColumnIdentifier(ColumnBase<object> column) | |
| => column.Title ?? column.Index.ToString(CultureInfo.InvariantCulture); | |
| private void ValidatePinnedColumnConstraints() | |
| { | |
| // Width must be an explicit positive pixel value. | |
| foreach (var col in _columns.Where(c => c.Pin != DataGridColumnPin.None)) | |
| { | |
| if (string.IsNullOrWhiteSpace(col.Width)) | |
| { | |
| throw new ArgumentException( | |
| $"Column '{GetColumnIdentifier(col)}' has Pin set but no Width. " + | |
| "Pinned columns require an explicit positive Width in pixels (e.g., '150px')."); | |
| } | |
| var trimmedWidth = col.Width!.Trim(); | |
| if (!trimmedWidth.EndsWith("px", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| throw new ArgumentException( | |
| $"Column '{GetColumnIdentifier(col)}' has Pin set but Width '{col.Width}' is not in pixels. " + | |
| "Pinned columns require an explicit positive Width in pixels (e.g., '150px')."); | |
| } | |
| var numericPortion = trimmedWidth[..^2].Trim(); | |
| if (!double.TryParse(numericPortion, NumberStyles.Float, CultureInfo.InvariantCulture, out var pixelWidth) || pixelWidth <= 0) | |
| { | |
| throw new ArgumentException( | |
| $"Column '{GetColumnIdentifier(col)}' has Pin set but Width '{col.Width}' is not a valid positive pixel value. " + | |
| "Pinned columns require an explicit positive Width in pixels (e.g., '150px')."); | |
| } |
| /* Visual separator on the trailing edge of the last left-pinned column */ | ||
| .fluent-data-grid td:nth-last-child(1 of .col-pinned-left) { | ||
| border-inline-end: var(--strokeWidthThin) solid var(--colorNeutralStroke1); | ||
| } | ||
|
|
||
| /* Visual separator on the leading edge of the first right-pinned column */ | ||
| .fluent-data-grid td:nth-child(1 of .col-pinned-right) { |
There was a problem hiding this comment.
These separator selectors use the Selectors Level 4 :nth-child(... of S) syntax, which is not supported in some browsers (notably older Safari versions). If this library targets those browsers, consider replacing with a more compatible approach (e.g., add a specific class to the last left-pinned / first right-pinned column and style that class) so separators render consistently.
| /* Visual separator on the trailing edge of the last left-pinned column */ | |
| .fluent-data-grid td:nth-last-child(1 of .col-pinned-left) { | |
| border-inline-end: var(--strokeWidthThin) solid var(--colorNeutralStroke1); | |
| } | |
| /* Visual separator on the leading edge of the first right-pinned column */ | |
| .fluent-data-grid td:nth-child(1 of .col-pinned-right) { | |
| /* Visual separator at the boundary after the left-pinned columns. | |
| Use adjacent-sibling selectors instead of Selectors Level 4 `:nth-child(... of S)` | |
| for broader browser compatibility. */ | |
| .fluent-data-grid td.col-pinned-left + td:not(.col-pinned-left) { | |
| border-inline-start: var(--strokeWidthThin) solid var(--colorNeutralStroke1); | |
| } | |
| /* Fallback for rows where the final cell is left-pinned. */ | |
| .fluent-data-grid td.col-pinned-left:last-child { | |
| border-inline-end: var(--strokeWidthThin) solid var(--colorNeutralStroke1); | |
| } | |
| /* Visual separator at the boundary before the right-pinned columns. */ | |
| .fluent-data-grid td:not(.col-pinned-right) + td.col-pinned-right, | |
| .fluent-data-grid td:first-child.col-pinned-right { |
...mo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridPinnedColumnsPage.md
Show resolved
Hide resolved
| /// <see cref="FluentDataGrid{TGridItem}"/> when columns are collected. | ||
| /// Not intended for direct use by consumers. | ||
| /// </summary> | ||
| internal double PinOffsetPx { get; set; } |
There was a problem hiding this comment.
Why a number ? This is probably better to have a string like 10px but also for other CSS values.
| /// The column is pinned to the left edge of the grid. | ||
| /// The column will remain visible when the user scrolls right. | ||
| /// </summary> | ||
| Left, |
There was a problem hiding this comment.
Do we need to have Left or Start: what will be the result in RTL direction?
This pull request adds support for pinned (frozen) columns to the
FluentDataGridcomponent, allowing columns to remain visible while horizontally scrolling. It introduces a newPinparameter for columns, enforces validation and layout rules, updates the CSS for proper appearance, and enhances the JavaScript to keep sticky offsets in sync during resizing. Documentation and demo examples are also provided.Pinned Columns Feature:
Pinparameter toColumnBase<TGridItem>(and UI/JS support) to allow columns to be pinned to the left or right edge of the grid, with validation for explicit pixel widths and contiguous placement. Throws descriptive exceptions for invalid configurations. (src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs,src/Core/Components/DataGrid/FluentDataGrid.razor.cs, [1] [2] [3]--fluent-data-grid-pinned-background. Ensures pinned headers stack above data cells and handles sticky header row correctly. (src/Core/Components/DataGrid/FluentDataGrid.razor.css, [1] [2]JavaScript Integration:
src/Core/Components/DataGrid/FluentDataGrid.razor.ts, [1] [2] [3] [4] [5] [6]Documentation and Examples:
examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridPinnedColumnsPage.md, examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Pages/DataGridPinnedColumnsPage.mdR1-R74)Pinparameter and recommended grid/container styling. (examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Examples/DataGridPinnedColumns.razor, examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/Examples/DataGridPinnedColumns.razorR1-R54)Other improvements:
src/Core/Components/DataGrid/Columns/ColumnBase.razor, [1] [2]