Skip to content

Commit 40d8424

Browse files
authored
[dev-v5] Sync with v4 (#4212)
* Implement #4036 * Implement #4070 * Implement #4112 * Implement #4116 * Add extra test. Brings back code coverage to 100% for Row and Cell
1 parent 16469d7 commit 40d8424

19 files changed

+291
-75
lines changed

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

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

190+
/// <summary>
191+
/// Gets or sets the minimal width of the column.
192+
/// Defaults to 100px for a regular column and 50px for a select column.
193+
/// When resizing a column, the user will not be able to make it smaller than this value.
194+
/// Needs to be a valid CSS width value like '100px', '10%' or '0.5fr'.
195+
/// </summary>
196+
[Parameter]
197+
public string MinWidth { get; set; } = "100px";
198+
190199
/// <summary>
191200
/// Sets the column index for the current instance.
192201
/// </summary>

src/Core/Components/DataGrid/Columns/PropertyColumn.cs

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,7 @@ protected override void OnParametersSet()
6565

6666
if (!string.IsNullOrEmpty(Format))
6767
{
68-
// TODO: Consider using reflection to avoid having to box every value just to call IFormattable.ToString
69-
// For example, define a method "string Type<U>(Func<TGridItem, U> property) where U: IFormattable", and
70-
// then construct the closed type here with U=TProp when we know TProp implements IFormattable
71-
72-
// If the type is nullable, we're interested in formatting the underlying type
73-
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
74-
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
75-
{
76-
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
77-
}
78-
79-
_cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, formatProvider: null);
68+
_cellTextFunc = CreateFormatter(compiledPropertyExpression, Format);
8069
}
8170
else
8271
{
@@ -117,7 +106,57 @@ protected override void OnParametersSet()
117106
}
118107
}
119108
}
109+
120110
#pragma warning restore IL2072 // Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.
111+
private static Func<TGridItem, string?> CreateFormatter(Func<TGridItem, TProp> getter, string format)
112+
{
113+
var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp));
114+
115+
//Nullable struct
116+
if (Nullable.GetUnderlyingType(typeof(TProp)) is Type underlying &&
117+
typeof(IFormattable).IsAssignableFrom(underlying))
118+
{
119+
var method = closedType
120+
.GetMethod(nameof(CreateNullableValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
121+
.MakeGenericMethod(underlying);
122+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
123+
}
124+
125+
if (typeof(IFormattable).IsAssignableFrom(typeof(TProp)))
126+
{
127+
//Struct
128+
if (typeof(TProp).IsValueType)
129+
{
130+
var method = closedType
131+
.GetMethod(nameof(CreateValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
132+
.MakeGenericMethod(typeof(TProp));
133+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
134+
}
135+
136+
//Double cast required because CreateReferenceTypeFormatter required the TProp to be a reference type which implements IFormattable.
137+
return CreateReferenceTypeFormatter((Func<TGridItem, IFormattable?>)(object)getter, format);
138+
}
139+
140+
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
141+
}
142+
143+
private static Func<TGridItem, string?> CreateReferenceTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
144+
where T : class, IFormattable
145+
{
146+
return item => getter(item)?.ToString(format, null);
147+
}
148+
149+
private static Func<TGridItem, string?> CreateValueTypeFormatter<T>(Func<TGridItem, T> getter, string format)
150+
where T : struct, IFormattable
151+
{
152+
return item => getter(item).ToString(format, null);
153+
}
154+
155+
private static Func<TGridItem, string?> CreateNullableValueTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
156+
where T : struct, IFormattable
157+
{
158+
return item => getter(item)?.ToString(format, null);
159+
}
121160

122161
/// <inheritdoc />
123162
protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)

src/Core/Components/DataGrid/Columns/SelectColumn.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class SelectColumn<TGridItem> : ColumnBase<TGridItem>, IDisposable
3737
public SelectColumn()
3838
{
3939
Width = "50px";
40+
MinWidth = "50px";
4041
ChildContent = GetDefaultChildContent();
4142

4243
_itemsChanged = new(EventCallback.Factory.Create<object?>(this, UpdateSelectedItemsAsync));

src/Core/Components/DataGrid/FluentDataGrid.razor.ts

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
22

33
interface Grid {
44
id: string;
5-
columns: any[]; // or a more specific type if you have one
5+
columns: Column[]; // or a more specific type if you have one
66
initialWidths: string;
77
}
88

@@ -12,8 +12,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
1212
}
1313

1414
// Use a dictionary for grids for id-based access
15-
let grids: { [id: string]: Grid } = {};
16-
const minWidth = 100;
15+
let grids: Grid[] = []; // { [id: string]: Grid } = {};
1716

1817
export function Initialize(gridElement: HTMLElement, autoFocus: boolean) {
1918
if (gridElement === undefined || gridElement === null) {
@@ -150,7 +149,8 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
150149
document.body.removeEventListener('click', bodyClickHandler);
151150
document.body.removeEventListener('mousedown', bodyClickHandler);
152151
gridElement.removeEventListener('keydown', keyDownHandler);
153-
delete grids[gridElement.id];
152+
grids = grids.filter(grid => grid.id !== gridElement.id);
153+
154154
}
155155
};
156156
}
@@ -236,11 +236,13 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
236236
}
237237

238238
const id = gridElement.id;
239-
grids[id] = {
240-
id,
241-
columns,
242-
initialWidths,
243-
};
239+
if (!grids.find((grid: Grid) => grid.id === id)) {
240+
grids.push({
241+
id,
242+
columns,
243+
initialWidths,
244+
});
245+
}
244246

245247
function setListeners(div: HTMLElement, isRTL: boolean) {
246248
let pageX: number | undefined, curCol: HTMLElement | undefined, curColWidth: number | undefined;
@@ -273,7 +275,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
273275
const diffX = isRTL ? (pageX! - e.pageX) : (e.pageX - pageX!);
274276
const column: Column = columns.find(({ header }) => header === curCol)!;
275277

276-
column.size = parseInt(Math.max(minWidth, curColWidth! + diffX) as any, 10) + 'px';
278+
column.size = parseInt(Math.max(parseInt((column.header as HTMLElement).style.minWidth), curColWidth! + diffX) as any, 10) + 'px';
277279

278280
columns.forEach((col) => {
279281
if (col.size.startsWith('minmax')) {
@@ -345,7 +347,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
345347

346348
export function ResetColumnWidths(gridElement: HTMLElement) {
347349
const isGrid = gridElement.classList.contains('grid');
348-
const grid = grids[gridElement.id];
350+
const grid = grids.find(grid => grid.id = gridElement.id);
349351
if (!grid) {
350352
return;
351353
}
@@ -370,6 +372,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
370372
}
371373

372374
export function ResizeColumnDiscrete(gridElement: HTMLElement, column: string | undefined, change: number) {
375+
const isGrid = gridElement.classList.contains('grid');
373376
const columns: any[] = [];
374377
let headerBeingResized: HTMLElement | null | undefined;
375378

@@ -383,50 +386,62 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
383386
else {
384387
headerBeingResized = gridElement.querySelector('.column-header[col-index="' + column + '"]') as HTMLElement | null;
385388
}
386-
387-
grids[gridElement.id].columns.forEach((column: any) => {
389+
grids.find(grid => grid.id = gridElement.id)!.columns.forEach((column: any) => {
388390
if (column.header === headerBeingResized) {
389-
const width = headerBeingResized!.getBoundingClientRect().width + change;
391+
const width = headerBeingResized!.offsetWidth + change;
392+
//const width = headerBeingResized!.getBoundingClientRect().width + change;
390393

391394
if (change < 0) {
392-
column.size = Math.max(minWidth, width) + 'px';
395+
column.size = Math.max(parseInt(column.header.style.minWidth), width) + 'px';
393396
}
394397
else {
395398
column.size = width + 'px';
396399
}
400+
column.header.style.width = column.size;
397401
}
398-
else {
402+
403+
if (isGrid) {
404+
// for grid we need to recalculate all columns that are minmax
399405
if (column.size.startsWith('minmax')) {
400406
column.size = parseInt(column.header.clientWidth, 10) + 'px';
401407
}
408+
columns.push(column.size);
402409
}
403-
columns.push(column.size);
404410
});
405411

406-
gridElement.style.gridTemplateColumns = columns.join(' ');
412+
if (isGrid) {
413+
gridElement.style.gridTemplateColumns = columns.join(' ');
414+
}
407415
}
408416

409417
export function ResizeColumnExact(gridElement: HTMLElement, column: string, width: number) {
418+
const isGrid = gridElement.classList.contains('grid');
410419
const columns: any[] = [];
411420
let headerBeingResized = gridElement.querySelector('.column-header[col-index="' + column + '"]') as HTMLElement | null;
412421

413422
if (!headerBeingResized) {
414423
return;
415424
}
416425

417-
grids[gridElement.id].columns.forEach((column: any) => {
426+
grids.find(grid => grid.id = gridElement.id)!.columns.forEach((column: any) => {
418427
if (column.header === headerBeingResized) {
419-
column.size = Math.max(minWidth, width) + 'px';
428+
column.size = Math.max(parseInt(column.header.style.minWidth), width) + 'px';
429+
column.header.style.width = column.size;
420430
}
421-
else {
431+
432+
if (isGrid) {
433+
// for grid we need to recalculate all columns that are minmax
422434
if (column.size.startsWith('minmax')) {
423435
column.size = parseInt(column.header.clientWidth, 10) + 'px';
424436
}
437+
column.header.style.width = column.size;
438+
columns.push(column.size);
425439
}
426-
columns.push(column.size);
427440
});
428441

429-
gridElement.style.gridTemplateColumns = columns.join(' ');
442+
if (isGrid) {
443+
gridElement.style.gridTemplateColumns = columns.join(' ');
444+
}
430445

431446
gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
432447
gridElement.focus();
@@ -448,8 +463,9 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
448463
gridElement.style.gridTemplateColumns = gridTemplateColumns;
449464
gridElement.classList.remove('auto-fit');
450465

451-
if (grids[gridElement.id]) {
452-
grids[gridElement.id].initialWidths = gridTemplateColumns;
466+
const grid = grids.find(grid => grid.id = gridElement.id);
467+
if (grid) {
468+
grid.initialWidths = gridTemplateColumns;
453469
}
454470
}
455471

src/Core/Components/DataGrid/FluentDataGridCell.razor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public partial class FluentDataGridCell<TGridItem> : FluentComponentBase
1717
/// <summary>
1818
/// Gets a reference to the column that this cell belongs to.
1919
/// </summary>
20-
private ColumnBase<TGridItem>? Column => Grid._columns.ElementAtOrDefault(GridColumn - 1);
20+
public ColumnBase<TGridItem>? Column => Grid._columns.ElementAtOrDefault(GridColumn - 1);
2121

2222
internal string CellId { get; set; } = string.Empty;
2323

@@ -39,6 +39,7 @@ public FluentDataGridCell(LibraryConfiguration configuration) : base(configurati
3939
.AddStyle("grid-column", GridColumn.ToString(CultureInfo.InvariantCulture), () => !Grid.EffectiveLoadingValue && (Grid.Items is not null || Grid.ItemsProvider is not null) && Grid.DisplayMode == DataGridDisplayMode.Grid)
4040
.AddStyle("text-align", "center", Column is SelectColumn<TGridItem>)
4141
.AddStyle("align-content", "center", Column is SelectColumn<TGridItem>)
42+
.AddStyle("min-width", Column?.MinWidth, Owner.RowType is DataGridRowType.Header or DataGridRowType.StickyHeader)
4243
.AddStyle("padding-top", "10px", Column is SelectColumn<TGridItem> && (Grid.RowSize == DataGridRowSize.Medium || Owner.RowType == DataGridRowType.Header))
4344
.AddStyle("padding-top", "6px", Column is SelectColumn<TGridItem> && Grid.RowSize == DataGridRowSize.Small && Owner.RowType == DataGridRowType.Default)
4445
.AddStyle("width", Column?.Width, !string.IsNullOrEmpty(Column?.Width) && Grid.DisplayMode == DataGridDisplayMode.Table)

src/Core/Components/DataGrid/FluentDataGridRow.razor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ public FluentDataGridRow(LibraryConfiguration configuration) : base(configuratio
8686
/// </summary>
8787
protected FluentDataGrid<TGridItem> Grid => InternalGridContext.Grid;
8888

89+
/// <summary>
90+
/// Gets the columns associated with this data grid row.
91+
/// </summary>
92+
public IReadOnlyList<ColumnBase<TGridItem>> Columns => Grid._columns;
93+
8994
/// <summary>
9095
/// Sets the RowIndex for this row.
9196
/// </summary>

tests/Core/Components/DataGrid/FluentDataGridCellTests.razor

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@
4242
Assert.Null(cell.Instance.ChildContent);
4343
}
4444

45+
[Fact]
46+
public void FluentDataGridCell_Properties_CompileCorrectly()
47+
{
48+
// This test verifies that our new public properties compile correctly
49+
// by checking that they exist and are public using reflection
50+
51+
var cellType = typeof(FluentDataGridCell<object>);
52+
53+
// Verify that the Column property exists on DataGridCell
54+
var columnProperty = cellType.GetProperty("Column");
55+
Assert.NotNull(columnProperty);
56+
Assert.True(columnProperty.CanRead);
57+
Assert.True(columnProperty.GetMethod?.IsPublic);
58+
}
59+
4560
[Fact]
4661
public async Task FluentDataGridCell_HandleOnCellClickAsync_InvokesCallbacks()
4762
{

0 commit comments

Comments
 (0)