diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 31112d231f..005c8155b7 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1449,6 +1449,12 @@ Gets or sets whether the [All] checkbox is disabled (not clickable). + + + Gets or sets whether the selection of rows is restricted to the SelectColumn (false) or if the whole row can be clicked to toggled the selection (true). + Default is True. + + Gets or sets the template for the [All] checkbox column template. @@ -1720,6 +1726,11 @@ Gets or sets a callback when a row is focused. + + + Gets or sets a callback when a cell is clicked. + + Gets or sets a callback when a row is clicked. @@ -1855,6 +1866,14 @@ Gets or sets the owning component + + + Gets a reference to the column that this cell belongs to. + + + + + Gets or sets the reference to the item that holds this row's values. diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index ca78863f01..099c11d656 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -4,6 +4,9 @@ + @if (UseSelectedItems) @@ -11,9 +14,10 @@ @* Sample using SelectedItems *@
Using SelectedItems
- + @@ -30,9 +34,10 @@ else @* Sample using Property and OnSelect *@
Using Property and OnSelect
- + SelectedItems = People.Where(p => p.Selected); diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 9e0fed801c..24882ac088 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -44,6 +44,13 @@ public SelectColumn() [Parameter] public bool SelectAllDisabled { get; set; } = false; + /// + /// Gets or sets whether the selection of rows is restricted to the SelectColumn (false) or if the whole row can be clicked to toggled the selection (true). + /// Default is True. + /// + [Parameter] + public bool SelectFromEntireRow { get; set; } = true; + /// /// Gets or sets the template for the [All] checkbox column template. /// @@ -309,8 +316,12 @@ private RenderFragment GetDefaultChildContent() builder.OpenComponent>(0); builder.AddAttribute(1, "Value", GetIcon(selected)); - builder.AddAttribute(1, "Title", selected ? TitleChecked : TitleUnchecked); - builder.AddAttribute(2, "row-selected", selected); + builder.AddAttribute(2, "Title", selected ? TitleChecked : TitleUnchecked); + builder.AddAttribute(3, "row-selected", selected); + if (!SelectFromEntireRow) + { + builder.AddAttribute(4, "style", "cursor: pointer;"); + } builder.CloseComponent(); }); } diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 1db0749077..04917057be 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -124,6 +124,12 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Parameter] public EventCallback> OnCellFocus { get; set; } + /// + /// Gets or sets a callback when a cell is clicked. + /// + [Parameter] + public EventCallback> OnCellClick { get; set; } + /// /// Gets or sets a callback when a row is clicked. /// @@ -187,7 +193,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns // This happens on every render so that the column list can be updated dynamically private readonly InternalGridContext _internalGridContext; - private readonly List> _columns; + internal readonly List> _columns; private bool _collectingColumns; // Columns might re-render themselves arbitrarily. We only want to capture them at a defined time. // Tracking state for options and sorting diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor b/src/Core/Components/DataGrid/FluentDataGridCell.razor index 5e05d90a24..e94605961d 100644 --- a/src/Core/Components/DataGrid/FluentDataGridCell.razor +++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor @@ -6,6 +6,7 @@ grid-column=@GridColumn class="@Class" style="@StyleValue" + @onclick="@HandleOnCellClickAsync" @attributes="AdditionalAttributes"> @ChildContent diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs b/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs index ebd89b844e..ad3fa3cada 100644 --- a/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs @@ -41,7 +41,7 @@ public partial class FluentDataGridCell : FluentComponentBase /// Gets or sets the owning component. ///
[CascadingParameter(Name = "OwningRow")] - public FluentDataGridRow Owner { get; set; } = default!; + internal FluentDataGridRow Owner { get; set; } = default!; /// /// Gets or sets the owning component @@ -49,6 +49,11 @@ public partial class FluentDataGridCell : FluentComponentBase [CascadingParameter] private InternalGridContext GridContext { get; set; } = default!; + /// + /// Gets a reference to the column that this cell belongs to. + /// + private ColumnBase? Column => Owner.Owner.Grid._columns.ElementAtOrDefault(GridColumn - 1); + protected string? StyleValue => new StyleBuilder(Style) .AddStyle("height", $"{GridContext.Grid.ItemSize:0}px", () => !GridContext.Grid.Loading && GridContext.Grid.Virtualize && Owner.RowType == DataGridRowType.Default) .AddStyle("align-content", "center", () => !GridContext.Grid.Loading && GridContext.Grid.Virtualize && Owner.RowType == DataGridRowType.Default && string.IsNullOrEmpty(Style)) @@ -59,6 +64,22 @@ protected override void OnInitialized() Owner.Register(this); } + /// + internal async Task HandleOnCellClickAsync() + { + if (GridContext.Grid.OnCellClick.HasDelegate) + { + await GridContext.Grid.OnCellClick.InvokeAsync(this); + } + + // If the cell is a checkbox cell, add or remove the item from the selected items list. + var selectColumn = Column as SelectColumn; + if (CellType == DataGridCellType.Default && selectColumn != null && selectColumn.SelectFromEntireRow == false) + { + await selectColumn.AddOrRemoveSelectedItemAsync(Item); + } + } + public void Dispose() => Owner.Unregister(this); } diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 7954c4e983..3051b6f0f3 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -56,7 +56,7 @@ public partial class FluentDataGridRow : FluentComponentBase, IHandle /// Gets or sets the owning component /// [CascadingParameter] - private InternalGridContext Owner { get; set; } = default!; + internal InternalGridContext Owner { get; set; } = default!; protected string? ClassValue => new CssBuilder(Class) .AddClass("hover", when: Owner.Grid.ShowHover) @@ -112,7 +112,7 @@ internal async Task HandleOnRowClickAsync(string rowId) if (row != null && row.RowType == DataGridRowType.Default) { - foreach (var selColumn in Owner.Grid.SelectColumns) + foreach (var selColumn in Owner.Grid.SelectColumns.Where(i => i.SelectFromEntireRow)) { await selColumn.AddOrRemoveSelectedItemAsync(Item); } diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor index d0acb17bc3..bb612c3e0f 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor @@ -15,7 +15,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); cut.Verify(); } @@ -33,7 +33,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -64,7 +64,7 @@ OnSelect="@(e => e.Item.Selected = e.Selected)" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -94,7 +94,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); cut.Verify(); } @@ -112,7 +112,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -149,7 +149,7 @@ OnSelect="@(e => e.Item.Selected = e.Selected)" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -185,7 +185,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -219,7 +219,7 @@ SelectAllChanged="@(all => items.ToList().ForEach(p => p.Selected = (all == true)))" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -250,7 +250,7 @@ @bind-SelectedItems="@selectedItems" /> - ); + ); // Before the switch Assert.Equal(2, cut.FindAll("svg[row-selected]").Count); @@ -281,7 +281,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -313,22 +313,102 @@ - ); + ); cut.Verify(); } + [Fact] + public async Task FluentDataGrid_ColumSelect_SingleSelect_NotSelectFromEntireRow() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Act - Click on the second cell => no selection + await ClickOnRowAsync(cut, row: 0, col: 1); + Assert.Empty(SelectedItems); + + // Act - Click on the first cell => select the row + await ClickOnRowAsync(cut, row: 0, col: 0); + Assert.Single(SelectedItems); + Assert.Equal(1, SelectedItems.First().PersonId); + + // Act - Click on the second cell => keep the selection + await ClickOnRowAsync(cut, row: 1, col: 1); + Assert.Single(SelectedItems); + Assert.Equal(1, SelectedItems.First().PersonId); + } + + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect_NotSelectFromEntireRow() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Act - Click on the second cell => no selection + await ClickOnRowAsync(cut, row: 0, col: 1); + Assert.Empty(SelectedItems); + + // Act - Click on the first cell => select the row + await ClickOnRowAsync(cut, row: 0, col: 0); + Assert.Single(SelectedItems); + Assert.Equal(1, SelectedItems.First().PersonId); + + // Act - Click on the second cell => keep the selection + await ClickOnRowAsync(cut, row: 1, col: 1); + Assert.Single(SelectedItems); + Assert.Equal(1, SelectedItems.First().PersonId); + + // Act - Click on the first cell => select another row + await ClickOnRowAsync(cut, row: 1, col: 0); + Assert.Equal(2, SelectedItems.Count()); + Assert.Equal(1, SelectedItems.ElementAt(0).PersonId); + Assert.Equal(2, SelectedItems.ElementAt(1).PersonId); + } + /// /// Simulate a click on the DataGrid row number . /// /// /// + /// /// - private async Task ClickOnRowAsync(IRenderedFragment cut, int row) + private async Task ClickOnRowAsync(IRenderedFragment cut, int row, int? col = null) { - var item = cut.FindComponents>().ElementAt(row + 1); - await item.Instance.HandleOnRowClickAsync(item.Instance.RowId); - cut.FindComponent>().Render(); + if (col == null) + { + var item = cut.FindComponents>().ElementAt(row + 1); + await item.Instance.HandleOnRowClickAsync(item.Instance.RowId); + cut.FindComponent>().Render(); + } + else + { + var item = cut.FindComponents>() + .Where(i => i.Instance.GridColumn == col + 1) + .ElementAt(row + 1); + await item.Instance.HandleOnCellClickAsync(); + cut.FindComponent>().Render(); + } } ///