Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ public abstract partial class ColumnBase<TGridItem>
[Parameter]
public string? Width { get; set; }

/// <summary>
/// Gets or sets the minimal width of the column.
/// Defaults to 100px for a regular column and 50px for a select column.
/// When resizing a column, the user will not be able to make it smaller than this value.
/// Needs to be a valid CSS width value like '100px', '10%' or '0.5fr'.
/// </summary>
[Parameter]
public string MinWidth { get; set; } = "100px";

/// <summary>
/// Sets the column index for the current instance.
/// </summary>
Expand Down
63 changes: 51 additions & 12 deletions src/Core/Components/DataGrid/Columns/PropertyColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,7 @@ protected override void OnParametersSet()

if (!string.IsNullOrEmpty(Format))
{
// TODO: Consider using reflection to avoid having to box every value just to call IFormattable.ToString
// For example, define a method "string Type<U>(Func<TGridItem, U> property) where U: IFormattable", and
// then construct the closed type here with U=TProp when we know TProp implements IFormattable

// If the type is nullable, we're interested in formatting the underlying type
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
{
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}

_cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, formatProvider: null);
_cellTextFunc = CreateFormatter(compiledPropertyExpression, Format);
}
else
{
Expand Down Expand Up @@ -117,7 +106,57 @@ protected override void OnParametersSet()
}
}
}

#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.
private static Func<TGridItem, string?> CreateFormatter(Func<TGridItem, TProp> getter, string format)
{
var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp));

//Nullable struct
if (Nullable.GetUnderlyingType(typeof(TProp)) is Type underlying &&
typeof(IFormattable).IsAssignableFrom(underlying))
{
var method = closedType
.GetMethod(nameof(CreateNullableValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(underlying);
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
}

if (typeof(IFormattable).IsAssignableFrom(typeof(TProp)))
{
//Struct
if (typeof(TProp).IsValueType)
{
var method = closedType
.GetMethod(nameof(CreateValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(typeof(TProp));
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
}

//Double cast required because CreateReferenceTypeFormatter required the TProp to be a reference type which implements IFormattable.
return CreateReferenceTypeFormatter((Func<TGridItem, IFormattable?>)(object)getter, format);
}

throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}

private static Func<TGridItem, string?> CreateReferenceTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
where T : class, IFormattable
{
return item => getter(item)?.ToString(format, null);
}

private static Func<TGridItem, string?> CreateValueTypeFormatter<T>(Func<TGridItem, T> getter, string format)
where T : struct, IFormattable
{
return item => getter(item).ToString(format, null);
}

private static Func<TGridItem, string?> CreateNullableValueTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
where T : struct, IFormattable
{
return item => getter(item)?.ToString(format, null);
}

/// <inheritdoc />
protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
Expand Down
1 change: 1 addition & 0 deletions src/Core/Components/DataGrid/Columns/SelectColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class SelectColumn<TGridItem> : ColumnBase<TGridItem>, IDisposable
public SelectColumn()
{
Width = "50px";
MinWidth = "50px";
ChildContent = GetDefaultChildContent();

_itemsChanged = new(EventCallback.Factory.Create<object?>(this, UpdateSelectedItemsAsync));
Expand Down
66 changes: 41 additions & 25 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {

interface Grid {
id: string;
columns: any[]; // or a more specific type if you have one
columns: Column[]; // or a more specific type if you have one
initialWidths: string;
}

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

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

export function Initialize(gridElement: HTMLElement, autoFocus: boolean) {
if (gridElement === undefined || gridElement === null) {
Expand Down Expand Up @@ -150,7 +149,8 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
document.body.removeEventListener('click', bodyClickHandler);
document.body.removeEventListener('mousedown', bodyClickHandler);
gridElement.removeEventListener('keydown', keyDownHandler);
delete grids[gridElement.id];
grids = grids.filter(grid => grid.id !== gridElement.id);

}
};
}
Expand Down Expand Up @@ -236,11 +236,13 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
}

const id = gridElement.id;
grids[id] = {
id,
columns,
initialWidths,
};
if (!grids.find((grid: Grid) => grid.id === id)) {
grids.push({
id,
columns,
initialWidths,
});
}

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

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

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

export function ResetColumnWidths(gridElement: HTMLElement) {
const isGrid = gridElement.classList.contains('grid');
const grid = grids[gridElement.id];
const grid = grids.find(grid => grid.id = gridElement.id);
if (!grid) {
return;
}
Expand All @@ -370,6 +372,7 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
}

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

Expand All @@ -383,50 +386,62 @@ export namespace Microsoft.FluentUI.Blazor.DataGrid {
else {
headerBeingResized = gridElement.querySelector('.column-header[col-index="' + column + '"]') as HTMLElement | null;
}

grids[gridElement.id].columns.forEach((column: any) => {
grids.find(grid => grid.id = gridElement.id)!.columns.forEach((column: any) => {
if (column.header === headerBeingResized) {
const width = headerBeingResized!.getBoundingClientRect().width + change;
const width = headerBeingResized!.offsetWidth + change;
//const width = headerBeingResized!.getBoundingClientRect().width + change;

if (change < 0) {
column.size = Math.max(minWidth, width) + 'px';
column.size = Math.max(parseInt(column.header.style.minWidth), width) + 'px';
}
else {
column.size = width + 'px';
}
column.header.style.width = column.size;
}
else {

if (isGrid) {
// for grid we need to recalculate all columns that are minmax
if (column.size.startsWith('minmax')) {
column.size = parseInt(column.header.clientWidth, 10) + 'px';
}
columns.push(column.size);
}
columns.push(column.size);
});

gridElement.style.gridTemplateColumns = columns.join(' ');
if (isGrid) {
gridElement.style.gridTemplateColumns = columns.join(' ');
}
}

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

if (!headerBeingResized) {
return;
}

grids[gridElement.id].columns.forEach((column: any) => {
grids.find(grid => grid.id = gridElement.id)!.columns.forEach((column: any) => {
if (column.header === headerBeingResized) {
column.size = Math.max(minWidth, width) + 'px';
column.size = Math.max(parseInt(column.header.style.minWidth), width) + 'px';
column.header.style.width = column.size;
}
else {

if (isGrid) {
// for grid we need to recalculate all columns that are minmax
if (column.size.startsWith('minmax')) {
column.size = parseInt(column.header.clientWidth, 10) + 'px';
}
column.header.style.width = column.size;
columns.push(column.size);
}
columns.push(column.size);
});

gridElement.style.gridTemplateColumns = columns.join(' ');
if (isGrid) {
gridElement.style.gridTemplateColumns = columns.join(' ');
}

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

if (grids[gridElement.id]) {
grids[gridElement.id].initialWidths = gridTemplateColumns;
const grid = grids.find(grid => grid.id = gridElement.id);
if (grid) {
grid.initialWidths = gridTemplateColumns;
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Core/Components/DataGrid/FluentDataGridCell.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public partial class FluentDataGridCell<TGridItem> : FluentComponentBase
/// <summary>
/// Gets a reference to the column that this cell belongs to.
/// </summary>
private ColumnBase<TGridItem>? Column => Grid._columns.ElementAtOrDefault(GridColumn - 1);
public ColumnBase<TGridItem>? Column => Grid._columns.ElementAtOrDefault(GridColumn - 1);

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

Expand All @@ -39,6 +39,7 @@ public FluentDataGridCell(LibraryConfiguration configuration) : base(configurati
.AddStyle("grid-column", GridColumn.ToString(CultureInfo.InvariantCulture), () => !Grid.EffectiveLoadingValue && (Grid.Items is not null || Grid.ItemsProvider is not null) && Grid.DisplayMode == DataGridDisplayMode.Grid)
.AddStyle("text-align", "center", Column is SelectColumn<TGridItem>)
.AddStyle("align-content", "center", Column is SelectColumn<TGridItem>)
.AddStyle("min-width", Column?.MinWidth, Owner.RowType is DataGridRowType.Header or DataGridRowType.StickyHeader)
.AddStyle("padding-top", "10px", Column is SelectColumn<TGridItem> && (Grid.RowSize == DataGridRowSize.Medium || Owner.RowType == DataGridRowType.Header))
.AddStyle("padding-top", "6px", Column is SelectColumn<TGridItem> && Grid.RowSize == DataGridRowSize.Small && Owner.RowType == DataGridRowType.Default)
.AddStyle("width", Column?.Width, !string.IsNullOrEmpty(Column?.Width) && Grid.DisplayMode == DataGridDisplayMode.Table)
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Components/DataGrid/FluentDataGridRow.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ public FluentDataGridRow(LibraryConfiguration configuration) : base(configuratio
/// </summary>
protected FluentDataGrid<TGridItem> Grid => InternalGridContext.Grid;

/// <summary>
/// Gets the columns associated with this data grid row.
/// </summary>
public IReadOnlyList<ColumnBase<TGridItem>> Columns => Grid._columns;

/// <summary>
/// Sets the RowIndex for this row.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions tests/Core/Components/DataGrid/FluentDataGridCellTests.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
Assert.Null(cell.Instance.ChildContent);
}

[Fact]
public void FluentDataGridCell_Properties_CompileCorrectly()
{
// This test verifies that our new public properties compile correctly
// by checking that they exist and are public using reflection

var cellType = typeof(FluentDataGridCell<object>);

// Verify that the Column property exists on DataGridCell
var columnProperty = cellType.GetProperty("Column");
Assert.NotNull(columnProperty);
Assert.True(columnProperty.CanRead);
Assert.True(columnProperty.GetMethod?.IsPublic);
}

[Fact]
public async Task FluentDataGridCell_HandleOnCellClickAsync_InvokesCallbacks()
{
Expand Down
Loading
Loading