Skip to content

Commit 6fae06c

Browse files
Copilotvnbaaij
andauthored
[dev-v5][DataGrid] Add pinned (sticky/frozen) column support (#4671)
* feat: add pinned (sticky) column support to FluentDataGrid 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> * chore: remove Core.Scripts build artifact and add .gitignore for obj/ 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> * chore: remove tracked Core.Scripts build artifact 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> * chore: start datagrid header layering fix 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> * fix: move datagrid header popup z-index to menu layer 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> * test: update datagrid snapshot expectations 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> * chore: start pinned resize min-width fix 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> * fix: preserve pinned column min-width on resize 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> * Example adjustments and CSS tweaks * refactor: avoid repeated computed style lookup for min width 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> * Update docs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vnbaaij <1761079+vnbaaij@users.noreply.github.com> Co-authored-by: Vincent Baaij <vnbaaij@outlook.com>
1 parent 3e8d499 commit 6fae06c

32 files changed

Lines changed: 871 additions & 48 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,4 @@ all-icons.json
424424
all-emojis.json
425425
/global.json
426426
/src/Core.Scripts/src/BuildConstants.ts
427+
/src/Core.Scripts/obj/
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<div style="overflow-x: auto;">
2+
<FluentDataGrid Items="@employees"
3+
Style="min-width: max-content;"
4+
ResizableColumns="true"
5+
DisplayMode="DataGridDisplayMode.Grid">
6+
<PropertyColumn Title="ID" Property="@(e => e.Id)" Width="10%" Pin="DataGridColumnPin.Start" Sortable="true" />
7+
<PropertyColumn Title="Full Name" Property="@(e => e.FullName)" Width="160px" Pin="DataGridColumnPin.Start" Sortable="true" />
8+
<PropertyColumn Title="Department" Property="@(e => e.Department)" Sortable="true" />
9+
<PropertyColumn Title="Location" Property="@(e => e.Location)" Sortable="true" />
10+
<PropertyColumn Title="Start Date" Property="@(e => e.StartDate)" Sortable="true" />
11+
<PropertyColumn Title="Salary" Property="@(e => e.Salary)" Width="120px" Sortable="true" Align="DataGridCellAlignment.End" />
12+
<TemplateColumn Title="Actions" Width="120px" Pin="DataGridColumnPin.End">
13+
<FluentButton IconStart="@(new Icons.Regular.Size16.Edit())" Appearance="ButtonAppearance.Subtle" Title="Edit" @onclick="@(() => selectedName = context.FullName + " (edit)")" />
14+
<FluentButton IconStart="@(new Icons.Regular.Size16.Delete())" Appearance="ButtonAppearance.Subtle" Title="Delete" @onclick="@(() => selectedName = context.FullName + " (delete)")" />
15+
</TemplateColumn>
16+
</FluentDataGrid>
17+
</div>
18+
19+
@if (!string.IsNullOrEmpty(selectedName))
20+
{
21+
<p style="margin-top: 1rem;">Last action: <strong>@selectedName</strong></p>
22+
}
23+
24+
@code {
25+
string selectedName = string.Empty;
26+
27+
record Employee(int Id, string FullName, string Department, string Location, string StartDate, string Salary);
28+
29+
IQueryable<Employee> employees = new[]
30+
{
31+
new Employee(1, "Denis Voituron", "Engineering", "Brussels", "2019-03-01", "$120,000"),
32+
new Employee(2, "Vincent Baaij", "Engineering", "Amsterdam", "2018-07-15", "$130,000"),
33+
new Employee(3, "Harry Mars", "Executive", "Medina", "1975-04-04", "$1,000,000"),
34+
new Employee(4, "Bruno Styles", "Executive", "Bellevue", "1992-02-17", "$950,000"),
35+
new Employee(5, "Taylor Eilish", "Developer Relations", "Portland", "2007-01-22", "$200,000"),
36+
new Employee(6, "Billie Swift", "Languages", "Seattle", "2005-08-01", "$180,000"),
37+
new Employee(7, "Jacky Bond", "Framework", "Seattle", "2010-06-14", "$190,000"),
38+
new Employee(8, "James Chan", "Framework", "Cambridge", "2009-03-30", "$185,000"),
39+
new Employee(9, "John Cage", "Community", "San Diego", "2011-11-01", "$160,000"),
40+
new Employee(10, "Nick Travolta", "Engineering", "New York", "2016-05-20", "$155,000"),
41+
}.AsQueryable();
42+
}

examples/Demo/FluentUI.Demo.Client/Documentation/Components/DataGrid/FluentDataGrid.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ component and is the one normally put onto a page. Internally it uses the`<Fluen
1111
the grid. It is possible (technically speaking) to use these components manually, but that is **not** the recommended way of working with the DataGrid.
1212

1313
## Rendering
14+
1415
The DataGrid uses standard HTML table elements for rendering the grid. It supports 2 different display modes through the `DisplayMode`
1516
parameter: `DataGridDisplayMode.Grid` (default) and `DataGridDisplayMode.Table`.
17+
1618
- With the `Grid` mode, the `GridTableColumns` parameter can be
1719
used to specify column widths in fractions. It basically provides an HTML table element with a `display: grid;` style.
1820
- With the `Table` mode, it uses standard HTML table elements and rendering. Column widths are best specified through the `Width` parameter on the columns.
1921

2022
> [!NOTE] Specifically when using `Virtualize`, it is **highly recommended** to use the `Table` display mode as the `Grid` mode can exhibit odd scrolling behavior.
2123
22-
2324
## Accessibility
2425

2526
You can use the <kbd>Arrow</kbd> keys to navigate through a DataGrid. When a header cell is focused and the column is sortable, you can use the
@@ -45,7 +46,7 @@ again will toggle the sort direction. When `HeaderCellAsButtonWithMenu` is true,
4546
A sort can be removed by right clicking (or by pressing <kbd>Shift</kbd> + <kbd>r</kbd>) on the header column (with exception of
4647
the default sort).
4748

48-
_The minimal width for a sortable column is 75 pixels._
49+
*The minimal width for a sortable column is 75 pixels.*
4950

5051
## Row size
5152

@@ -76,19 +77,20 @@ The following values can be localized:
7677
- DataGrid_SortMenuDescending
7778
- DataGrid_ToggleNesting
7879

79-
8080
## Using the DataGrid component with EF Core
8181

8282
If you want to use the `FluentDataGrid` with data provided through EF Core, you need to install an additional package so the grid knows how to resolve queries asynchronously for efficiency.
8383

8484
### Installation
85+
8586
Install the package by running the command:
8687

8788
```cshtml
8889
dotnet add package Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapterCopy
8990
```
9091

9192
### Usage
93+
9294
In your `Program.cs` file you need to add the following after the `builder.Services.AddFluentUIComponents();` line:
9395

9496
```csharp
@@ -100,27 +102,33 @@ builder.Services.AddDataGridEntityFrameworkAdapter();Copy
100102
If you want to use the `FluentDataGrid` with data provided through OData, you need to install an additional package so the grid knows how to resolve queries asynchronously for efficiency.
101103

102104
### Installation
105+
103106
Install the package by running the command:
104107

105108
```cshtml
106109
dotnet add package Microsoft.FluentUI.AspNetCore.Components.DataGrid.ODataAdapterCopy
107110
```
108111

109112
### Usage
113+
110114
In your `Program.cs` file you need to add the following after the `builder.Services.AddFluentUIComponents();` line:
111115

112116
```csharp
113117
builder.Services.AddDataGridODataAdapter();
114118
```
115119

116120
## Examples
121+
117122
The following examples show how to use the DataGrid component in different scenarios:
118123

119124
### Basics
125+
120126
- [Getting started](/DataGrid/GettingStarted)
121127
- [Typical grid usage](/DataGrid/Typical)
122-
128+
123129
### Layout
130+
131+
- [Pinned columns](/DataGrid/PinnedColumns)
124132
- [Loading and empty content](/DataGrid/LoadingAndEmptyContent)
125133
- [Auto fit columns](/DataGrid/AutoFit)
126134
- [Auto items per page](/DataGrid/AutoItemsPerPage)
@@ -129,24 +137,26 @@ The following examples show how to use the DataGrid component in different scena
129137
- [Table with scrollbars](/DataGrid/TableScrollbars)
130138

131139
### Sorting
140+
132141
- [Custom comparer for sorting](/DataGrid/CustomComparerSort)
133142
- [Custom sorting](/DataGrid/CustomSort)
134-
143+
135144
### Columns
145+
136146
- [Single/Multi select](/DataGrid/MultiSelect)
137147
- [Dynamic columns](/DataGrid/DynamicColumns)
138148
- [Column headers](/DataGrid/HeaderGeneration)
139149
- [Template columns](/DataGrid/TemplateColumns)
140150
- [Template columns 2](/DataGrid/TemplateColumns2)
141151

142152
### Advanced
153+
143154
- [Custom comparer](/DataGrid/CustomComparerSort)
144155
- [Virtualized grid](/DataGrid/Virtualize)
145156
- [Remote data](/DataGrid/RemoteData)
146157
- [Hierarchical grid](/DataGrid/HierarchicalDataGrid)
147158
- [Manual grid](/DataGrid/ManualDataGrid)
148159

149-
150160
## Migrating to v5
151161

152162
{{ INCLUDE File=MigrationFluentDataGrid }}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: Pinned columns
3+
route: /DataGrid/PinnedColumns
4+
---
5+
6+
# Pinned columns
7+
8+
Columns can be pinned (frozen) to the start or end edge of the grid so that they remain visible
9+
while the user scrolls horizontally through wider datasets. Using `Start`/`End` instead of
10+
`Left`/`Right` means pinned columns automatically work correctly in both LTR and RTL layouts.
11+
12+
## Parameters
13+
14+
Set the `Pin` parameter on any `PropertyColumn` or `TemplateColumn`:
15+
16+
| Value | Behavior |
17+
|---|---|
18+
| `DataGridColumnPin.None` | Default — column scrolls normally |
19+
| `DataGridColumnPin.Start` | Column stays anchored to the start edge |
20+
| `DataGridColumnPin.End` | Column stays anchored to the end edge |
21+
22+
## Rules
23+
24+
* **Explicit width required.** Every pinned column must declare a `Width`.
25+
Pixel and non-pixel CSS units are supported. After the grid renders, sticky offsets are
26+
recomputed from the rendered header widths so pinned columns stay aligned.
27+
* **Start-pinned columns must be contiguous at the start.** Each start-pinned column must
28+
immediately follow another start-pinned column, or be the very first column.
29+
* **End-pinned columns must be contiguous at the end.** Each end-pinned column must
30+
immediately precede another end-pinned column, or be the very last column.
31+
* Violating the missing-width or ordering rules throws an `ArgumentException` with a descriptive message.
32+
33+
## Scrollable container
34+
35+
Sticky positioning only activates inside a scrollable ancestor. Wrap the grid in a container with
36+
`overflow-x: auto` and give the grid `Style="min-width: max-content;"` so that a horizontal scroll
37+
bar appears when columns overflow the container:
38+
39+
```razor
40+
<div style="overflow-x: auto;">
41+
<FluentDataGrid Items="@employees" Style="min-width: max-content;">
42+
<PropertyColumn Title="ID" Property="@(e => e.Id)" Width="60px" Pin="DataGridColumnPin.Start" />
43+
<PropertyColumn Title="Name" Property="@(e => e.Name)" Width="160px" Pin="DataGridColumnPin.Start" />
44+
<PropertyColumn Title="City" Property="@(e => e.City)" />
45+
<TemplateColumn Title="Actions" Width="120px" Pin="DataGridColumnPin.End">
46+
...
47+
</TemplateColumn>
48+
</FluentDataGrid>
49+
</div>
50+
```
51+
52+
## Theming the pinned background
53+
54+
Pinned cells receive a solid background to prevent scrolling content from showing through. The
55+
color defaults to `--colorNeutralBackground2` and can be overridden per-grid with the CSS custom
56+
property `--fluent-data-grid-pinned-background`:
57+
58+
```css
59+
.my-grid {
60+
--fluent-data-grid-pinned-background: var(--colorNeutralBackground2);
61+
}
62+
```
63+
64+
## Notes
65+
66+
* Column resizing keeps pinned columns aligned as widths change.
67+
* Virtualization and paging are fully compatible because each rendered row's cells carry the
68+
same `position: sticky` styling regardless of which page or scroll position is active.
69+
* RTL layouts are fully supported: start and end automatically map to the correct physical
70+
direction based on the document's writing mode.
71+
72+
## Example
73+
74+
Demonstrates pinned (frozen) columns using `Pin="DataGridColumnPin.Start"` and `Pin="DataGridColumnPin.End"`.
75+
76+
The two leftmost columns and the Actions column remain visible while the rest scroll horizontally. The grid is
77+
wrapped in a scrollable container to enable sticky positioning and horizontal scrolling when needed. Resize the window
78+
or the columns in the grid to see the effect.
79+
80+
**Pinned columns require an explicit `Width`**.
81+
82+
{{ DataGridPinnedColumns }}

src/Core/Components/DataGrid/Columns/ColumnBase.razor

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
else if (Grid.HeaderCellAsButtonWithMenu)
3030
{
3131
string? tooltip = Tooltip ? (HeaderTooltip ?? Title) : null;
32+
string headerPopupLayerStyle = $"position: relative; z-index: {ZIndex.DataGridHeaderPopup};";
33+
string headerButtonStyle = $"width: calc(100% - 10px); {headerPopupLayerStyle}";
3234

3335
@if (AnyColumnActionEnabled)
3436
{
35-
<FluentButton Id="@_columnId" Appearance="ButtonAppearance.Subtle" Class="col-sort-button" Style="width: calc(100% - 10px);" @onclick="@HandleColumnHeaderClickedAsync" aria-label="@tooltip" title="@tooltip" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(this))">
37+
<FluentButton Id="@_columnId" Appearance="ButtonAppearance.Subtle" Class="col-sort-button" Style="@headerButtonStyle" @onclick="@HandleColumnHeaderClickedAsync" aria-label="@tooltip" title="@tooltip" @oncontextmenu="@(() => Grid.RemoveSortByColumnAsync(this))">
3638
<div class="col-title-text" title="@tooltip">
3739
@HeaderTitleContent
3840
</div>
@@ -62,7 +64,7 @@
6264
</div>
6365
</div>
6466
}
65-
<FluentMenu @ref="@_menu" Trigger="@_columnId">
67+
<FluentMenu @ref="@_menu" Trigger="@_columnId" Style="@headerPopupLayerStyle">
6668
<FluentMenuList>
6769
@if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault())
6870
{

src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,25 @@ public abstract partial class ColumnBase<TGridItem>
187187
[Parameter]
188188
public string? Width { get; set; }
189189

190+
/// <summary>
191+
/// Gets or sets whether this column is pinned (frozen) to the start or end edge of the grid,
192+
/// so it remains visible when the user scrolls horizontally.
193+
/// Pinned columns require an explicit <see cref="Width"/>.
194+
/// Sticky offsets are recomputed from rendered header widths after the grid is rendered.
195+
/// Start-pinned columns must be contiguous at the start of the column list;
196+
/// end-pinned columns must be contiguous at the end.
197+
/// </summary>
198+
[Parameter]
199+
public DataGridColumnPin Pin { get; set; } = DataGridColumnPin.None;
200+
201+
/// <summary>
202+
/// The sticky start or end CSS offset seeded by
203+
/// <see cref="FluentDataGrid{TGridItem}"/> when columns are collected and later updated from
204+
/// rendered widths by JavaScript.
205+
/// Not intended for direct use by consumers.
206+
/// </summary>
207+
internal string PinOffset { get; set; } = "0px";
208+
190209
/// <summary>
191210
/// Gets or sets the minimal width of the column.
192211
/// Defaults to 100px for a regular column and 50px for a select column.

0 commit comments

Comments
 (0)