From a113c59c2bfa3ec7e7d46f0ca37081d3bb9263ab Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 14:34:43 +0200 Subject: [PATCH 01/26] Add SelectRowColumn column --- ...crosoft.FluentUI.AspNetCore.Components.xml | 56 ++++++++++++++ .../Shared/Pages/DataGrid/DataGridPage.razor | 6 ++ .../Examples/DataGridMultiSelect.razor | 29 ++++++++ .../DataGrid/Columns/SelectRowColumn.cs | 74 +++++++++++++++++++ .../DataGrid/FluentDataGrid.razor.cs | 17 ++++- .../DataGrid/FluentDataGridRow.razor | 1 + .../DataGrid/FluentDataGridRow.razor.cs | 17 +++++ 7 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor create mode 100644 src/Core/Components/DataGrid/Columns/SelectRowColumn.cs diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 69d3706942..f65bc76f07 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1359,6 +1359,47 @@ + + + Represents a column whose cells render a selected checkbox updated when the user click on a row. + + The type of data represented by each row in the grid. + + + + Gets or sets the content to be rendered for each row in the table. + + + + + Gets or sets the Icon to be rendered when the row is non selected. + + + + + Gets or sets the Icon to be rendered when the row is selected. + + + + + Gets or sets the action to be executed when the row is selected or unselected. + This action is required to update you data model. + + + + + Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. + + + + + + + + + + + Holds the name of a property and the direction to sort by. @@ -1500,6 +1541,11 @@ Gets or sets a callback when a row is focused. + + + Gets or sets a callback when a row is clicked. + + Optionally defines a class to be applied to a rendered row. @@ -1511,6 +1557,11 @@ Do not use to dynamically update a row style after rendering as this will interfere with the script that use this attribute. Use instead. + + + Gets or sets a value indicating whether the grid should show a hover effect on rows. + + If specified, grids render this fragment when there is no content. @@ -1527,6 +1578,11 @@ A default fragment is used if loading content is not specified. + + + Gets the first (optional) SelectRowColumn + + Constructs an instance of . diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index 43672bada2..e0d647c7f8 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -5,6 +5,12 @@ @App.PageTitle("DataGrid") + + + Example ... + + +

Data grid

The <FluentDataGrid> component is used to display tabular data. The <FluentDataGridRow> diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor new file mode 100644 index 0000000000..b0e21564d5 --- /dev/null +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -0,0 +1,29 @@ + + + + + + + +Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) + +Refresh + +@code { + + + record Person(int PersonId, string Name, DateOnly BirthDate) + { + public bool Selected { get; set; } + }; + + IQueryable people = new[] + { + new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António Langa", new DateOnly(1991, 12, 1)), + new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose Hernandez", new DateOnly(2011, 5, 3)), + new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9)), + }.AsQueryable(); +} diff --git a/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs b/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs new file mode 100644 index 0000000000..68c5862c69 --- /dev/null +++ b/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +///

+/// Represents a column whose cells render a selected checkbox updated when the user click on a row. +/// +/// The type of data represented by each row in the grid. +public class SelectRowColumn : ColumnBase +{ + public SelectRowColumn() + { + RenderFragment DefaultChildContent = (context) => new RenderFragment(async (builder) => + { + var selected = Property.Invoke(context); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); + builder.AddAttribute(2, "row-selected", selected); + builder.CloseComponent(); + }); + + Width = "50px"; + ChildContent = DefaultChildContent; + } + + /// + /// Gets or sets the content to be rendered for each row in the table. + /// + [Parameter] public RenderFragment ChildContent { get; set; } + + /// + /// Gets or sets the Icon to be rendered when the row is non selected. + /// + [Parameter] + public required Icon IconUnchecked { get; set; } //= new Icons.Regular.Size20.Circle(); + + /// + /// Gets or sets the Icon to be rendered when the row is selected. + /// + [Parameter] + public required Icon IconChecked { get; set; } //= new Icons.Regular.Size20.Circle(); + + /// + /// Gets or sets the action to be executed when the row is selected or unselected. + /// This action is required to update you data model. + /// + [Parameter] + [EditorRequired] + public Action SelectChanged { get; set; } = (item) => { }; + + /// + /// Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. + /// + [Parameter] + public Func Property { get; set; } = (item) => false; + + /// + [Parameter] + public override GridSort? SortBy { get; set; } + + /// + protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) + => builder.AddContent(0, ChildContent(item)); + + protected internal override string? RawCellContent(TGridItem item) + { + return TooltipText?.Invoke(item); + } + + /// + protected override bool IsSortableByDefault() => SortBy is not null; +} diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index a750723d8f..7db084207f 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 row is clicked. + /// + [Parameter] + public EventCallback> OnRowClick { get; set; } + /// /// Optionally defines a class to be applied to a rendered row. /// @@ -135,7 +141,9 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve ///
[Parameter] public Func? RowStyle { get; set; } - + /// + /// Gets or sets a value indicating whether the grid should show a hover effect on rows. + /// [Parameter] public bool ShowHover { get; set; } /// @@ -153,10 +161,16 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve /// A default fragment is used if loading content is not specified. /// [Parameter] public RenderFragment? LoadingContent { get; set; } + [Inject] private IServiceProvider Services { get; set; } = default!; [Inject] private IJSRuntime JSRuntime { get; set; } = default!; [Inject] private IKeyCodeService KeyCodeService { get; set; } = default!; + /// + /// Gets the first (optional) SelectRowColumn + /// + internal SelectRowColumn? SelectedRowColumn => _columns.FirstOrDefault(col => col is SelectRowColumn) as SelectRowColumn; + private ElementReference? _gridReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; private int _ariaBodyRowCount; @@ -219,7 +233,6 @@ public FluentDataGrid() _renderEmptyContent = RenderEmptyContent; _renderLoadingContent = RenderLoadingContent; - // As a special case, we don't issue the first data load request until we've collected the initial set of columns // This is so we can apply default sort order (or any future per-column options) before loading data // We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor b/src/Core/Components/DataGrid/FluentDataGridRow.razor index 348db03e70..a719b0e97c 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor @@ -9,6 +9,7 @@ row-index=@RowIndex grid-template-columns=@GridTemplateColumns row-type=@RowType.ToAttributeValue() + @onclick="@(e => HandleOnRowClickAsync(RowId))" @attributes="AdditionalAttributes" @oncellfocus=HandleOnCellFocusAsync> @ChildContent diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 7adb2e9ec1..d64b778990 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -99,6 +99,23 @@ private async Task HandleOnCellFocusAsync(DataGridCellFocusEventArgs args) } } + private async Task HandleOnRowClickAsync(string rowId) + { + if (Owner.Rows.TryGetValue(rowId, out var row)) + { + if (row != null && row.RowType == DataGridRowType.Default) + { + await Owner.Grid.OnRowClick.InvokeAsync(row); + + if (Owner.Grid.SelectedRowColumn != null && row.Item != null) + { + Owner.Grid.SelectedRowColumn.SelectChanged.Invoke(row.Item); + StateHasChanged(); + } + } + } + } + Task IHandleEvent.HandleEventAsync( EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg); } From 81177cd5d44a2f606dd4f7ef1094282ca3f85dd9 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 14:49:45 +0200 Subject: [PATCH 02/26] Including Icons --- .../Demo/Shared/Pages/DataGrid/DataGridPage.razor | 12 ++++++------ .../DataGrid/Examples/DataGridMultiSelect.razor | 2 +- .../Components/DataGrid/Columns/SelectRowColumn.cs | 4 ++-- src/Core/Components/Icons/CoreIcons.cs | 2 ++ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index e0d647c7f8..a98a8685ee 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -5,12 +5,6 @@ @App.PageTitle("DataGrid") - - - Example ... - - -

Data grid

The <FluentDataGrid> component is used to display tabular data. The <FluentDataGridRow> @@ -59,6 +53,12 @@ + + + The same example, adding a SelectRowColumn column, to allow multi-select rows. + + +

diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index b0e21564d5..bf5ec8bf50 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,5 +1,5 @@  - + diff --git a/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs b/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs index 68c5862c69..09d9983f0e 100644 --- a/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs @@ -34,13 +34,13 @@ public SelectRowColumn() /// Gets or sets the Icon to be rendered when the row is non selected. /// [Parameter] - public required Icon IconUnchecked { get; set; } //= new Icons.Regular.Size20.Circle(); + public required Icon IconUnchecked { get; set; } = new CoreIcons.Regular.Size20.CheckboxUnchecked().WithColor(Color.FillInverse); ///

/// Gets or sets the Icon to be rendered when the row is selected. /// [Parameter] - public required Icon IconChecked { get; set; } //= new Icons.Regular.Size20.Circle(); + public required Icon IconChecked { get; set; } = new CoreIcons.Filled.Size20.CheckboxChecked(); /// /// Gets or sets the action to be executed when the row is selected or unselected. diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index 57793bd073..d5e5dfc2ac 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -39,6 +39,7 @@ public class CheckmarkCircle : Icon { public CheckmarkCircle() : base("Checkmark public class DismissCircle : Icon { public DismissCircle() : base("DismissCircle", IconVariant.Filled, IconSize.Size20, "") { } } public class Info : Icon { public Info() : base("Info", IconVariant.Filled, IconSize.Size20, "") { } } public class Warning : Icon { public Warning() : base("Warning", IconVariant.Filled, IconSize.Size20, "") { } } + public class CheckboxChecked : Icon { public CheckboxChecked() : base("CheckboxChecked", IconVariant.Filled, IconSize.Size20, "") { } } } } @@ -94,6 +95,7 @@ public class ChevronDoubleLeft : Icon { public ChevronDoubleLeft() : base("Chevr public class ChevronDoubleRight : Icon { public ChevronDoubleRight() : base("ChevronDoubleRight", IconVariant.Regular, IconSize.Size20, "") { } } public class Dismiss : Icon { public Dismiss() : base("Dismiss", IconVariant.Regular, IconSize.Size20, "") { } } public class DismissCircle : Icon { public DismissCircle() : base("DismissCircle", IconVariant.Regular, IconSize.Size20, "") { } } + public class CheckboxUnchecked : Icon { public CheckboxUnchecked() : base("CheckboxUnchecked", IconVariant.Regular, IconSize.Size20, "") { } } } } From 3e233e201e85eccaf12dce9a5afb16ca8e7752c3 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 15:19:58 +0200 Subject: [PATCH 03/26] Update the colum name to SelectColumn and use a EventCallback --- ...crosoft.FluentUI.AspNetCore.Components.xml | 20 +++++++++---------- .../Examples/DataGridMultiSelect.razor | 4 ++-- .../{SelectRowColumn.cs => SelectColumn.cs} | 7 +++---- .../DataGrid/FluentDataGrid.razor.cs | 4 ++-- .../DataGrid/FluentDataGridRow.razor.cs | 4 ++-- 5 files changed, 19 insertions(+), 20 deletions(-) rename src/Core/Components/DataGrid/Columns/{SelectRowColumn.cs => SelectColumn.cs} (93%) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index f65bc76f07..b69b091a8b 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1359,45 +1359,45 @@ - + Represents a column whose cells render a selected checkbox updated when the user click on a row. The type of data represented by each row in the grid. - + Gets or sets the content to be rendered for each row in the table. - + Gets or sets the Icon to be rendered when the row is non selected. - + Gets or sets the Icon to be rendered when the row is selected. - + Gets or sets the action to be executed when the row is selected or unselected. This action is required to update you data model. - + Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. - + - + - + @@ -1580,7 +1580,7 @@ - Gets the first (optional) SelectRowColumn + Gets the first (optional) SelectColumn diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index bf5ec8bf50..d75a238ebf 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,5 +1,5 @@  - + @@ -19,7 +19,7 @@ Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) IQueryable people = new[] { - new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)), + new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)) { Selected = true }, new Person(10944, "António Langa", new DateOnly(1991, 12, 1)), new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)), new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)), diff --git a/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs similarity index 93% rename from src/Core/Components/DataGrid/Columns/SelectRowColumn.cs rename to src/Core/Components/DataGrid/Columns/SelectColumn.cs index 09d9983f0e..64514e880f 100644 --- a/src/Core/Components/DataGrid/Columns/SelectRowColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -7,9 +7,9 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// Represents a column whose cells render a selected checkbox updated when the user click on a row. /// /// The type of data represented by each row in the grid. -public class SelectRowColumn : ColumnBase +public class SelectColumn : ColumnBase { - public SelectRowColumn() + public SelectColumn() { RenderFragment DefaultChildContent = (context) => new RenderFragment(async (builder) => { @@ -47,8 +47,7 @@ public SelectRowColumn() /// This action is required to update you data model. /// [Parameter] - [EditorRequired] - public Action SelectChanged { get; set; } = (item) => { }; + public EventCallback OnSelect { get; set; } /// /// Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 7db084207f..b414926615 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -167,9 +167,9 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Inject] private IKeyCodeService KeyCodeService { get; set; } = default!; /// - /// Gets the first (optional) SelectRowColumn + /// Gets the first (optional) SelectColumn /// - internal SelectRowColumn? SelectedRowColumn => _columns.FirstOrDefault(col => col is SelectRowColumn) as SelectRowColumn; + internal SelectColumn? SelectedRowColumn => _columns.FirstOrDefault(col => col is SelectColumn) as SelectColumn; private ElementReference? _gridReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index d64b778990..916b5e1081 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -107,9 +107,9 @@ private async Task HandleOnRowClickAsync(string rowId) { await Owner.Grid.OnRowClick.InvokeAsync(row); - if (Owner.Grid.SelectedRowColumn != null && row.Item != null) + if (Owner.Grid.SelectedRowColumn != null && Owner.Grid.SelectedRowColumn.OnSelect.HasDelegate && row.Item != null) { - Owner.Grid.SelectedRowColumn.SelectChanged.Invoke(row.Item); + await Owner.Grid.SelectedRowColumn.OnSelect.InvokeAsync(row.Item); StateHasChanged(); } } From ef233bc7c9e97fa960769daf7365004601121c8b Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 15:27:36 +0200 Subject: [PATCH 04/26] Adding TGridItem="Person" --- .../Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index d75a238ebf..314c44d708 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,5 +1,5 @@  - + From 5524506c90d94c161d1ee8100f821fe0d202b8d0 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 17:19:58 +0200 Subject: [PATCH 05/26] Add SelectAllChanged --- ...crosoft.FluentUI.AspNetCore.Components.xml | 10 ++++ .../Examples/DataGridMultiSelect.razor | 7 +-- .../DataGrid/Columns/SelectColumn.cs | 54 ++++++++++++++++++- .../DataGrid/FluentDataGridRow.razor.cs | 6 +-- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index b69b091a8b..4a72d0d29e 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1386,6 +1386,13 @@ This action is required to update you data model.
+ + + Gets or sets the action to be executed when the [All] checkbox is clicked. + When this action is defined, the [All] checkbox is displayed. + This action is required to update you data model. + + Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. @@ -1394,6 +1401,9 @@ + + + diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 314c44d708..253612e6b8 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,5 +1,8 @@  - + @@ -7,8 +10,6 @@ Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) -Refresh - @code { diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 64514e880f..a526148a22 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Web; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -11,7 +12,7 @@ public class SelectColumn : ColumnBase { public SelectColumn() { - RenderFragment DefaultChildContent = (context) => new RenderFragment(async (builder) => + RenderFragment DefaultChildContent = (context) => new RenderFragment((builder) => { var selected = Property.Invoke(context); @@ -28,7 +29,8 @@ public SelectColumn() /// /// Gets or sets the content to be rendered for each row in the table. /// - [Parameter] public RenderFragment ChildContent { get; set; } + [Parameter] + public RenderFragment ChildContent { get; set; } /// /// Gets or sets the Icon to be rendered when the row is non selected. @@ -49,6 +51,17 @@ public SelectColumn() [Parameter] public EventCallback OnSelect { get; set; } + [Parameter] + public bool SelectAll { get; set; } = false; + + /// + /// Gets or sets the action to be executed when the [All] checkbox is clicked. + /// When this action is defined, the [All] checkbox is displayed. + /// This action is required to update you data model. + /// + [Parameter] + public EventCallback SelectAllChanged { get; set; } + /// /// Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. /// @@ -59,6 +72,29 @@ public SelectColumn() [Parameter] public override GridSort? SortBy { get; set; } + internal async Task InverseSelectionAsync(TGridItem item) + { + await OnSelect.InvokeAsync(item); + } + + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (SelectAllChanged.HasDelegate) + { + HeaderContent = new RenderFragment((builder) => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", SelectAll ? IconChecked : IconUnchecked); + builder.AddAttribute(2, "all-selected", SelectAll); + builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); + builder.CloseComponent(); + }); + } + } + /// protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) => builder.AddContent(0, ChildContent(item)); @@ -70,4 +106,18 @@ protected internal override void CellContent(RenderTreeBuilder builder, TGridIte /// protected override bool IsSortableByDefault() => SortBy is not null; + + private async Task OnClickAllAsync(MouseEventArgs e) + { + if (Grid.Items == null) + { + return; + } + + SelectAll = !SelectAll; + if (SelectAllChanged.HasDelegate) + { + await SelectAllChanged.InvokeAsync(SelectAll); + } + } } diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 916b5e1081..a4930f8397 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -107,10 +107,10 @@ private async Task HandleOnRowClickAsync(string rowId) { await Owner.Grid.OnRowClick.InvokeAsync(row); - if (Owner.Grid.SelectedRowColumn != null && Owner.Grid.SelectedRowColumn.OnSelect.HasDelegate && row.Item != null) + var selectedColumn = Owner.Grid.SelectedRowColumn; + if (selectedColumn != null && selectedColumn.OnSelect.HasDelegate && row.Item != null) { - await Owner.Grid.SelectedRowColumn.OnSelect.InvokeAsync(row.Item); - StateHasChanged(); + await selectedColumn.InverseSelectionAsync(row.Item); } } } From fc2f2258ddaab0420fbcc77ee556a08dc4d33f02 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 17:35:50 +0200 Subject: [PATCH 06/26] Add a bindable SelectAll property --- .../Microsoft.FluentUI.AspNetCore.Components.xml | 3 +++ .../Pages/DataGrid/Examples/DataGridMultiSelect.razor | 4 ++-- src/Core/Components/DataGrid/Columns/SelectColumn.cs | 10 ++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 4a72d0d29e..4fa0e8f5e4 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1401,6 +1401,9 @@ + + + diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 253612e6b8..e2db43bfd7 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -2,7 +2,7 @@ + SelectAllChanged="@(all => people.ToList().ForEach(p => p.Selected = (all == true)))" /> @@ -11,7 +11,7 @@ Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) @code { - + bool AllSelected = false; record Person(int PersonId, string Name, DateOnly BirthDate) { diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index a526148a22..feb73ab72e 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -52,7 +52,7 @@ public SelectColumn() public EventCallback OnSelect { get; set; } [Parameter] - public bool SelectAll { get; set; } = false; + public bool? SelectAll { get; set; } = false; /// /// Gets or sets the action to be executed when the [All] checkbox is clicked. @@ -60,7 +60,7 @@ public SelectColumn() /// This action is required to update you data model. /// [Parameter] - public EventCallback SelectAllChanged { get; set; } + public EventCallback SelectAllChanged { get; set; } /// /// Gets or sets the function to be executed to display the checked/unchecked icon, depending of you data model. @@ -72,8 +72,10 @@ public SelectColumn() [Parameter] public override GridSort? SortBy { get; set; } + /// internal async Task InverseSelectionAsync(TGridItem item) { + SelectAll = null; await OnSelect.InvokeAsync(item); } @@ -87,7 +89,7 @@ protected override void OnParametersSet() HeaderContent = new RenderFragment((builder) => { builder.OpenComponent>(0); - builder.AddAttribute(1, "Value", SelectAll ? IconChecked : IconUnchecked); + builder.AddAttribute(1, "Value", SelectAll == true ? IconChecked : IconUnchecked); builder.AddAttribute(2, "all-selected", SelectAll); builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); builder.CloseComponent(); @@ -114,7 +116,7 @@ private async Task OnClickAllAsync(MouseEventArgs e) return; } - SelectAll = !SelectAll; + SelectAll = !(SelectAll == true); if (SelectAllChanged.HasDelegate) { await SelectAllChanged.InvokeAsync(SelectAll); From ac38417ecffa5c24de20db09f1c4e4ebfdc2af33 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 17:46:13 +0200 Subject: [PATCH 07/26] Remove AllSelected demo var --- .../Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index e2db43bfd7..b6ed4d69d2 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -11,8 +11,6 @@ Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) @code { - bool AllSelected = false; - record Person(int PersonId, string Name, DateOnly BirthDate) { public bool Selected { get; set; } From 4bf3bbf13f1cbbf5bbed496abe6e3646aa01cdc4 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 18:18:43 +0200 Subject: [PATCH 08/26] Add Style --- .../Demo/Shared/Pages/DataGrid/DataGridPage.razor | 14 +++++++++++++- .../DataGrid/Examples/DataGridMultiSelect.razor | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index a98a8685ee..8f4152772e 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -55,7 +55,19 @@ - The same example, adding a SelectRowColumn column, to allow multi-select rows. + The same example, adding a SelectColumn, to allow multi-select rows. + +
+ By default, FluentUI only recommends selecting the checkbox for the selected line. + But you can use a CSS style like this one to grey the background of selected lines. + +
+fluent-data-grid-row:has([row-selected]) {
+    background-color: var(--neutral-fill-stealth-hover)
+}
+
+
+
diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index b6ed4d69d2..064c88c684 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,4 +1,10 @@ - + + + Date: Fri, 26 Apr 2024 18:19:11 +0200 Subject: [PATCH 09/26] Fix the sample --- .../Pages/DataGrid/Examples/DataGridMultiSelect.razor | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 064c88c684..b6ed4d69d2 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,10 +1,4 @@ - - - + Date: Fri, 26 Apr 2024 23:11:12 +0200 Subject: [PATCH 10/26] Add IconIndeterminate --- ...crosoft.FluentUI.AspNetCore.Components.xml | 50 +++++- .../Examples/DataGridMultiSelect.razor | 20 ++- .../DataGrid/Columns/SelectColumn.cs | 147 +++++++++++++++--- .../DataGrid/FluentDataGridRow.razor.cs | 9 +- src/Core/Components/Icons/CoreIcons.cs | 1 + src/Core/Enums/DataGridSelectMode.cs | 18 +++ 6 files changed, 210 insertions(+), 35 deletions(-) create mode 100644 src/Core/Enums/DataGridSelectMode.cs diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 4fa0e8f5e4..a83cb96b0b 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1370,6 +1370,16 @@ Gets or sets the content to be rendered for each row in the table.
+ + + Gets or sets the list of selected items. + + + + + Gets or sets the selection mode (Single or Multiple). + + Gets or sets the Icon to be rendered when the row is non selected. @@ -1380,12 +1390,23 @@ Gets or sets the Icon to be rendered when the row is selected. + + + Gets or sets the Icon to be rendered when some but not all rows are selected. + + Gets or sets the action to be executed when the row is selected or unselected. This action is required to update you data model. + + + Gets or sets the value indicating whether the [All] checkbox is selected. + Null is undefined. + + Gets or sets the action to be executed when the [All] checkbox is clicked. @@ -1401,18 +1422,33 @@ - + + + + - + + + + + + + + + + + + + Holds the name of a property and the direction to sort by. @@ -11840,6 +11876,16 @@ A sticky header row. + + + Allow only one selected row. + + + + + Allow multiple selected rows. + + The standard theme values for light and dark mode. diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index b6ed4d69d2..26325dcf41 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,22 +1,34 @@ - + + SelectAllChanged="@(all => People.ToList().ForEach(p => p.Selected = (all == true)))" /> -Selected: @String.Join("; ", people.Where(p => p.Selected).Select(p => p.Name)) +
+ SelectedItems: + @String.Join("; ", SelectedItems.Select(p => p.Name)) +
+
+ Peoples: + @String.Join("; ", People.Where(p => p.Selected).Select(p => p.Name)) +
@code { + List SelectedItems = new(People.Where(p => p.Selected)); + record Person(int PersonId, string Name, DateOnly BirthDate) { public bool Selected { get; set; } }; - IQueryable people = new[] + static IQueryable People = new[] { new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)) { Selected = true }, new Person(10944, "António Langa", new DateOnly(1991, 12, 1)), diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index feb73ab72e..1b0db048a9 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -10,20 +10,12 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// The type of data represented by each row in the grid. public class SelectColumn : ColumnBase { + private List _selectedItems = new List(); + public SelectColumn() { - RenderFragment DefaultChildContent = (context) => new RenderFragment((builder) => - { - var selected = Property.Invoke(context); - - builder.OpenComponent>(0); - builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); - builder.AddAttribute(2, "row-selected", selected); - builder.CloseComponent(); - }); - Width = "50px"; - ChildContent = DefaultChildContent; + ChildContent = GetDefaultChildContent(); } /// @@ -32,6 +24,31 @@ public SelectColumn() [Parameter] public RenderFragment ChildContent { get; set; } + /// + /// Gets or sets the list of selected items. + /// + [Parameter] + public List SelectedItems + { + get => _selectedItems; + set + { + if (_selectedItems != value) + { + _selectedItems = value; + } + } + } + + [Parameter] + public EventCallback> SelectedItemsChanged { get; set; } + + /// + /// Gets or sets the selection mode (Single or Multiple). + /// + [Parameter] + public DataGridSelectMode SelectMode { get; set; } = DataGridSelectMode.Single; + /// /// Gets or sets the Icon to be rendered when the row is non selected. /// @@ -44,6 +61,12 @@ public SelectColumn() [Parameter] public required Icon IconChecked { get; set; } = new CoreIcons.Filled.Size20.CheckboxChecked(); + /// + /// Gets or sets the Icon to be rendered when some but not all rows are selected. + /// + [Parameter] + public Icon? IconIndeterminate { get; set; } = new CoreIcons.Filled.Size20.CheckboxIndeterminate(); + /// /// Gets or sets the action to be executed when the row is selected or unselected. /// This action is required to update you data model. @@ -51,6 +74,10 @@ public SelectColumn() [Parameter] public EventCallback OnSelect { get; set; } + /// + /// Gets or sets the value indicating whether the [All] checkbox is selected. + /// Null is undefined. + /// [Parameter] public bool? SelectAll { get; set; } = false; @@ -73,27 +100,85 @@ public SelectColumn() public override GridSort? SortBy { get; set; } /// - internal async Task InverseSelectionAsync(TGridItem item) + internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) + { + if (item != null) + { + if (SelectedItems.Contains(item)) + { + SelectedItems.Remove(item); + } + else + { + SelectedItems.Add(item); + } + + if (SelectedItemsChanged.HasDelegate) + { + await SelectedItemsChanged.InvokeAsync(SelectedItems); + } + + SelectAll = null; + await OnSelect.InvokeAsync(item); + + RefreshHeaderContent(); + } + } + + /// + private RenderFragment GetDefaultChildContent() { - SelectAll = null; - await OnSelect.InvokeAsync(item); + return (item) => new RenderFragment((builder) => + { + var selected = SelectedItems.Contains(item) || Property.Invoke(item); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); + builder.AddAttribute(2, "row-selected", selected); + builder.CloseComponent(); + }); } - /// - protected override void OnParametersSet() + /// + private RenderFragment GetHeaderContent() { - base.OnParametersSet(); + var iconAllChecked = (SelectAll == null && IconIndeterminate != null) + ? IconIndeterminate + : (SelectAll == true ? IconChecked : IconUnchecked); - if (SelectAllChanged.HasDelegate) + return new RenderFragment((builder) => { - HeaderContent = new RenderFragment((builder) => + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", iconAllChecked); + builder.AddAttribute(2, "all-selected", SelectAll); + builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); + builder.CloseComponent(); + }); + } + + /// + private void RefreshHeaderContent() + { + if (SelectMode == DataGridSelectMode.Multiple) + { + if (!SelectedItems.Any()) { - builder.OpenComponent>(0); - builder.AddAttribute(1, "Value", SelectAll == true ? IconChecked : IconUnchecked); - builder.AddAttribute(2, "all-selected", SelectAll); - builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); - builder.CloseComponent(); - }); + SelectAll = false; + } + + HeaderContent = GetHeaderContent(); + } + } + + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + if (SelectMode == DataGridSelectMode.Multiple) + { + SelectAll = SelectedItems.Any() ? null : false; + RefreshHeaderContent(); } } @@ -101,6 +186,7 @@ protected override void OnParametersSet() protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) => builder.AddContent(0, ChildContent(item)); + /// protected internal override string? RawCellContent(TGridItem item) { return TooltipText?.Invoke(item); @@ -109,6 +195,7 @@ protected internal override void CellContent(RenderTreeBuilder builder, TGridIte /// protected override bool IsSortableByDefault() => SortBy is not null; + /// private async Task OnClickAllAsync(MouseEventArgs e) { if (Grid.Items == null) @@ -116,10 +203,20 @@ private async Task OnClickAllAsync(MouseEventArgs e) return; } + // SelectAllChanged SelectAll = !(SelectAll == true); if (SelectAllChanged.HasDelegate) { await SelectAllChanged.InvokeAsync(SelectAll); } + + // SelectedItems + SelectedItems.Clear(); + if (SelectAll == true) + { + SelectedItems.AddRange(Grid.Items); + } + + RefreshHeaderContent(); } } diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index a4930f8397..7871fb540b 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -103,14 +103,15 @@ private async Task HandleOnRowClickAsync(string rowId) { if (Owner.Rows.TryGetValue(rowId, out var row)) { + + await Owner.Grid.OnRowClick.InvokeAsync(row); + if (row != null && row.RowType == DataGridRowType.Default) { - await Owner.Grid.OnRowClick.InvokeAsync(row); - var selectedColumn = Owner.Grid.SelectedRowColumn; - if (selectedColumn != null && selectedColumn.OnSelect.HasDelegate && row.Item != null) + if (selectedColumn != null) { - await selectedColumn.InverseSelectionAsync(row.Item); + await selectedColumn.AddOrRemoveSelectedItemAsync(Item); } } } diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index d5e5dfc2ac..93bedfc599 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -40,6 +40,7 @@ public class DismissCircle : Icon { public DismissCircle() : base("DismissCircle public class Info : Icon { public Info() : base("Info", IconVariant.Filled, IconSize.Size20, "") { } } public class Warning : Icon { public Warning() : base("Warning", IconVariant.Filled, IconSize.Size20, "") { } } public class CheckboxChecked : Icon { public CheckboxChecked() : base("CheckboxChecked", IconVariant.Filled, IconSize.Size20, "") { } } + public class CheckboxIndeterminate : Icon { public CheckboxIndeterminate() : base("CheckboxIndeterminate", IconVariant.Filled, IconSize.Size20, "") { } } } } diff --git a/src/Core/Enums/DataGridSelectMode.cs b/src/Core/Enums/DataGridSelectMode.cs new file mode 100644 index 0000000000..3a95208388 --- /dev/null +++ b/src/Core/Enums/DataGridSelectMode.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +public enum DataGridSelectMode +{ + /// + /// Allow only one selected row. + /// + Single, + + /// + /// Allow multiple selected rows. + /// + Multiple, +} From 0b47a0732a987b5ba89684fd7ed69e239f499130 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 23:14:42 +0200 Subject: [PATCH 11/26] Replace by IEnumerable --- .../DataGrid/Examples/DataGridMultiSelect.razor | 2 +- .../Components/DataGrid/Columns/SelectColumn.cs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 26325dcf41..31ecf72ab7 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -21,7 +21,7 @@ @code { - List SelectedItems = new(People.Where(p => p.Selected)); + IEnumerable SelectedItems = People.Where(p => p.Selected); record Person(int PersonId, string Name, DateOnly BirthDate) { diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 1b0db048a9..8a30018974 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -10,7 +10,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// The type of data represented by each row in the grid. public class SelectColumn : ColumnBase { - private List _selectedItems = new List(); + private readonly List _selectedItems = new List(); public SelectColumn() { @@ -28,20 +28,21 @@ public SelectColumn() /// Gets or sets the list of selected items. /// [Parameter] - public List SelectedItems + public IEnumerable SelectedItems { get => _selectedItems; set { if (_selectedItems != value) { - _selectedItems = value; + _selectedItems.Clear(); + _selectedItems.AddRange(value); } } } [Parameter] - public EventCallback> SelectedItemsChanged { get; set; } + public EventCallback> SelectedItemsChanged { get; set; } /// /// Gets or sets the selection mode (Single or Multiple). @@ -106,11 +107,11 @@ internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) { if (SelectedItems.Contains(item)) { - SelectedItems.Remove(item); + _selectedItems.Remove(item); } else { - SelectedItems.Add(item); + _selectedItems.Add(item); } if (SelectedItemsChanged.HasDelegate) @@ -211,10 +212,10 @@ private async Task OnClickAllAsync(MouseEventArgs e) } // SelectedItems - SelectedItems.Clear(); + _selectedItems.Clear(); if (SelectAll == true) { - SelectedItems.AddRange(Grid.Items); + _selectedItems.AddRange(Grid.Items); } RefreshHeaderContent(); From 5ed9696ff2f5d0164e307ed541c9b463f08cddd7 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Fri, 26 Apr 2024 23:26:24 +0200 Subject: [PATCH 12/26] Add DataGridSelectMode.Single --- .../DataGrid/Examples/DataGridMultiSelect.razor | 17 +++++++++++++++-- .../Components/DataGrid/Columns/SelectColumn.cs | 7 ++++++- .../DataGrid/FluentDataGridRow.razor.cs | 1 - 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 31ecf72ab7..853b3c92b5 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,10 +1,22 @@  @@ -21,6 +33,7 @@ @code { + DataGridSelectMode Mode = DataGridSelectMode.Single; IEnumerable SelectedItems = People.Where(p => p.Selected); record Person(int PersonId, string Name, DateOnly BirthDate) diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 8a30018974..ba44703a16 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -111,6 +111,11 @@ internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) } else { + if (SelectMode == DataGridSelectMode.Single) + { + _selectedItems.Clear(); + } + _selectedItems.Add(item); } @@ -199,7 +204,7 @@ protected internal override void CellContent(RenderTreeBuilder builder, TGridIte /// private async Task OnClickAllAsync(MouseEventArgs e) { - if (Grid.Items == null) + if (Grid.Items == null || SelectMode != DataGridSelectMode.Multiple) { return; } diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 7871fb540b..c9dde0640b 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -103,7 +103,6 @@ private async Task HandleOnRowClickAsync(string rowId) { if (Owner.Rows.TryGetValue(rowId, out var row)) { - await Owner.Grid.OnRowClick.InvokeAsync(row); if (row != null && row.RowType == DataGridRowType.Default) From c8daac0789695bc7df653b7223efae6ca0708add Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 12:07:57 +0200 Subject: [PATCH 13/26] Separated the SelectedItems and Property / OnSelect (not yet completed) --- ...crosoft.FluentUI.AspNetCore.Components.xml | 3 - .../Examples/DataGridMultiSelect.razor | 85 ++++++++------ .../DataGrid/Columns/SelectColumn.cs | 111 ++++++++++++------ 3 files changed, 129 insertions(+), 70 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index a83cb96b0b..9ad67b0649 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1434,9 +1434,6 @@ - - - diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 853b3c92b5..621f62b83c 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -1,39 +1,58 @@ - - - - - - - -
- SelectedItems: - @String.Join("; ", SelectedItems.Select(p => p.Name)) -
-
- Peoples: - @String.Join("; ", People.Where(p => p.Selected).Select(p => p.Name)) -
+ + + + + +@if (UseSelectedItems) +{ + @* Sample using SelectedItems *@ +
Using SelectedItems
+ + + + + + + + +
+ SelectedItems: + @String.Join("; ", SelectedItems.Select(p => p.Name)) +
+} +else +{ + @* Sample using Property and OnSelect *@ +
Using Property and OnSelect
+ + + + + + + + +
+ Peoples: + @String.Join("; ", People.Where(p => p.Selected).Select(p => p.Name)) +
+} @code { + bool UseSelectedItems = false; DataGridSelectMode Mode = DataGridSelectMode.Single; + IEnumerable SelectedItems = People.Where(p => p.Selected); record Person(int PersonId, string Name, DateOnly BirthDate) diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index ba44703a16..9c18c41ee0 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -10,6 +10,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// The type of data represented by each row in the grid. public class SelectColumn : ColumnBase { + private DataGridSelectMode _selectMode = DataGridSelectMode.Single; private readonly List _selectedItems = new List(); public SelectColumn() @@ -48,7 +49,21 @@ public IEnumerable SelectedItems /// Gets or sets the selection mode (Single or Multiple). ///
[Parameter] - public DataGridSelectMode SelectMode { get; set; } = DataGridSelectMode.Single; + public DataGridSelectMode SelectMode + { + get => _selectMode; + set + { + _selectMode = value; + + if (value == DataGridSelectMode.Single) + { + KeepOnlyFirstSelectedItemAsync().Wait(); + } + + RefreshHeaderContent(); + } + } /// /// Gets or sets the Icon to be rendered when the row is non selected. @@ -73,7 +88,7 @@ public IEnumerable SelectedItems /// This action is required to update you data model. /// [Parameter] - public EventCallback OnSelect { get; set; } + public EventCallback<(TGridItem Item, bool Selected)> OnSelect { get; set; } /// /// Gets or sets the value indicating whether the [All] checkbox is selected. @@ -108,15 +123,21 @@ internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) if (SelectedItems.Contains(item)) { _selectedItems.Remove(item); + await CallOnSelect(item, false); } else { if (SelectMode == DataGridSelectMode.Single) { + foreach (var previous in _selectedItems) + { + await CallOnSelect(previous, false); + } _selectedItems.Clear(); } _selectedItems.Add(item); + await CallOnSelect(item, true); } if (SelectedItemsChanged.HasDelegate) @@ -125,10 +146,40 @@ internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) } SelectAll = null; - await OnSelect.InvokeAsync(item); - RefreshHeaderContent(); } + + Task CallOnSelect(TGridItem item, bool isSelected) + { + return OnSelect.HasDelegate + ? OnSelect.InvokeAsync((item, isSelected)) + : Task.CompletedTask; + } + } + + private async Task KeepOnlyFirstSelectedItemAsync() + { + if (_selectedItems.Count() <= 1) + { + return; + } + + // Unselect all except the first + foreach (var item in _selectedItems.Skip(1)) + { + await OnSelect.InvokeAsync((item, false)); + } + + // Keep the first selected item + _selectedItems.RemoveRange(1, _selectedItems.Count - 1); + + if (SelectedItemsChanged.HasDelegate) + { + await SelectedItemsChanged.InvokeAsync(_selectedItems); + } + + // Indeterminate + SelectAll = null; } /// @@ -148,44 +199,36 @@ private RenderFragment GetDefaultChildContent() /// private RenderFragment GetHeaderContent() { - var iconAllChecked = (SelectAll == null && IconIndeterminate != null) - ? IconIndeterminate - : (SelectAll == true ? IconChecked : IconUnchecked); + SelectAll = SelectedItems.Any() ? null : false; - return new RenderFragment((builder) => + switch (SelectMode) { - builder.OpenComponent>(0); - builder.AddAttribute(1, "Value", iconAllChecked); - builder.AddAttribute(2, "all-selected", SelectAll); - builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); - builder.CloseComponent(); - }); - } + case DataGridSelectMode.Single: + return new RenderFragment((builder) => { }); - /// - private void RefreshHeaderContent() - { - if (SelectMode == DataGridSelectMode.Multiple) - { - if (!SelectedItems.Any()) - { - SelectAll = false; - } + case DataGridSelectMode.Multiple: + var iconAllChecked = (SelectAll == null && IconIndeterminate != null) + ? IconIndeterminate + : (SelectAll == true ? IconChecked : IconUnchecked); - HeaderContent = GetHeaderContent(); + return new RenderFragment((builder) => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", iconAllChecked); + builder.AddAttribute(2, "all-selected", SelectAll); + builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); + builder.CloseComponent(); + }); + + default: + return new RenderFragment((builder) => { }); } } - /// - protected override void OnInitialized() + /// + private void RefreshHeaderContent() { - base.OnInitialized(); - - if (SelectMode == DataGridSelectMode.Multiple) - { - SelectAll = SelectedItems.Any() ? null : false; - RefreshHeaderContent(); - } + HeaderContent = GetHeaderContent(); } /// From fc271b7960abd2c5b19b046fe86fc164240e3939 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 12:10:22 +0200 Subject: [PATCH 14/26] Update --- .../Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index 621f62b83c..f69c7a7828 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -13,7 +13,6 @@ @@ -50,7 +49,7 @@ else } @code { - bool UseSelectedItems = false; + bool UseSelectedItems = true; DataGridSelectMode Mode = DataGridSelectMode.Single; IEnumerable SelectedItems = People.Where(p => p.Selected); From 1b83879a29aa50cfbecf19e6eb867c93210436c4 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 16:57:27 +0200 Subject: [PATCH 15/26] Update the 2 examples --- .../Examples/DataGridMultiSelect.razor | 9 ++- .../DataGrid/Columns/SelectColumn.cs | 55 ++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index f69c7a7828..ca78863f01 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -2,6 +2,7 @@ @@ -32,7 +33,6 @@ else i.Selected = false); + People.First().Selected = true; + 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 9c18c41ee0..8b4332ca9a 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -145,7 +145,6 @@ internal async Task AddOrRemoveSelectedItemAsync(TGridItem? item) await SelectedItemsChanged.InvokeAsync(SelectedItems); } - SelectAll = null; RefreshHeaderContent(); } @@ -180,6 +179,10 @@ private async Task KeepOnlyFirstSelectedItemAsync() // Indeterminate SelectAll = null; + if (SelectAllChanged.HasDelegate) + { + await SelectAllChanged.InvokeAsync(SelectAll); + } } /// @@ -187,7 +190,17 @@ private RenderFragment GetDefaultChildContent() { return (item) => new RenderFragment((builder) => { - var selected = SelectedItems.Contains(item) || Property.Invoke(item); + var selected = _selectedItems.Contains(item) || Property.Invoke(item); + + // Sync with SelectedItems list + if (selected && !_selectedItems.Contains(item)) + { + _selectedItems.Add(item); + } + else if (!selected && _selectedItems.Contains(item)) + { + _selectedItems.Remove(item); + } builder.OpenComponent>(0); builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); @@ -199,17 +212,16 @@ private RenderFragment GetDefaultChildContent() /// private RenderFragment GetHeaderContent() { - SelectAll = SelectedItems.Any() ? null : false; - switch (SelectMode) { case DataGridSelectMode.Single: return new RenderFragment((builder) => { }); case DataGridSelectMode.Multiple: - var iconAllChecked = (SelectAll == null && IconIndeterminate != null) + var selectedAll = GetSelectAll(); + var iconAllChecked = (selectedAll == null && IconIndeterminate != null) ? IconIndeterminate - : (SelectAll == true ? IconChecked : IconUnchecked); + : (selectedAll == true ? IconChecked : IconUnchecked); return new RenderFragment((builder) => { @@ -231,6 +243,30 @@ private void RefreshHeaderContent() HeaderContent = GetHeaderContent(); } + private bool? GetSelectAll() + { + // Using SelectedItems only + if (InternalGridContext != null && Grid.Items != null) + { + if (!SelectedItems.Any()) + { + return false; + } + else if (SelectedItems.Count() == Grid.Items.Count()) + { + return true; + } + else + { + return null; + } + } + else + { + return null; + } + } + /// protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) => builder.AddContent(0, ChildContent(item)); @@ -247,7 +283,7 @@ protected internal override void CellContent(RenderTreeBuilder builder, TGridIte /// private async Task OnClickAllAsync(MouseEventArgs e) { - if (Grid.Items == null || SelectMode != DataGridSelectMode.Multiple) + if (Grid == null || Grid.Items == null || SelectMode != DataGridSelectMode.Multiple) { return; } @@ -266,6 +302,11 @@ private async Task OnClickAllAsync(MouseEventArgs e) _selectedItems.AddRange(Grid.Items); } + if (SelectedItemsChanged.HasDelegate) + { + await SelectedItemsChanged.InvokeAsync(SelectedItems); + } + RefreshHeaderContent(); } } From 0229a577dd999fcaf54a893a09d3fae4c412a702 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 17:27:40 +0200 Subject: [PATCH 16/26] Add Titles --- ...crosoft.FluentUI.AspNetCore.Components.xml | 30 ++++++++++++++ .../DataGrid/Columns/SelectColumn.cs | 40 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 9ad67b0649..774e4060ef 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1385,16 +1385,46 @@ Gets or sets the Icon to be rendered when the row is non selected. + + + Gets or sets the Icon title display as a tooltip and used with Accessibility. + The default text is "Row unselected". + + Gets or sets the Icon to be rendered when the row is selected. + + + Gets or sets the Icon title display as a tooltip and used with Accessibility. + The default text is "Row selected". + + Gets or sets the Icon to be rendered when some but not all rows are selected. + + + Gets or sets the Icon title display as a tooltip and used with Accessibility. + The default text is "All rows are selected.". + + + + + Gets or sets the Icon title display as a tooltip and used with Accessibility. + The default text is "No rows are selected.". + + + + + Gets or sets the Icon title display as a tooltip and used with Accessibility. + The default text is "Some rows are selected.". + + Gets or sets the action to be executed when the row is selected or unselected. diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 8b4332ca9a..20c0dd6201 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -71,18 +71,53 @@ public DataGridSelectMode SelectMode [Parameter] public required Icon IconUnchecked { get; set; } = new CoreIcons.Regular.Size20.CheckboxUnchecked().WithColor(Color.FillInverse); + /// + /// Gets or sets the Icon title display as a tooltip and used with Accessibility. + /// The default text is "Row unselected". + /// + [Parameter] + public string TitleUnchecked { get; set; } = "Row unselected"; + /// /// Gets or sets the Icon to be rendered when the row is selected. /// [Parameter] public required Icon IconChecked { get; set; } = new CoreIcons.Filled.Size20.CheckboxChecked(); + /// + /// Gets or sets the Icon title display as a tooltip and used with Accessibility. + /// The default text is "Row selected". + /// + [Parameter] + public string TitleChecked { get; set; } = "Row selected."; + /// /// Gets or sets the Icon to be rendered when some but not all rows are selected. /// [Parameter] public Icon? IconIndeterminate { get; set; } = new CoreIcons.Filled.Size20.CheckboxIndeterminate(); + /// + /// Gets or sets the Icon title display as a tooltip and used with Accessibility. + /// The default text is "All rows are selected.". + /// + [Parameter] + public string TitleAllChecked { get; set; } = "All rows are selected."; + + /// + /// Gets or sets the Icon title display as a tooltip and used with Accessibility. + /// The default text is "No rows are selected.". + /// + [Parameter] + public string TitleAllUnchecked { get; set; } = "No rows are selected."; + + /// + /// Gets or sets the Icon title display as a tooltip and used with Accessibility. + /// The default text is "Some rows are selected.". + /// + [Parameter] + public string TitleAllIndeterminate { get; set; } = "Some rows are selected."; + /// /// Gets or sets the action to be executed when the row is selected or unselected. /// This action is required to update you data model. @@ -204,6 +239,7 @@ private RenderFragment GetDefaultChildContent() builder.OpenComponent>(0); builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); + builder.AddAttribute(1, "Title", selected ? TitleChecked : TitleUnchecked); builder.AddAttribute(2, "row-selected", selected); builder.CloseComponent(); }); @@ -229,6 +265,10 @@ private RenderFragment GetHeaderContent() builder.AddAttribute(1, "Value", iconAllChecked); builder.AddAttribute(2, "all-selected", SelectAll); builder.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, OnClickAllAsync)); + builder.AddAttribute(4, "Style", "margin-left: 12px;"); + builder.AddAttribute(5, "Title", iconAllChecked == IconIndeterminate + ? TitleAllIndeterminate + : (iconAllChecked == IconChecked ? TitleAllChecked : TitleAllUnchecked)); builder.CloseComponent(); }); From 489af95465d9ed28ac518abbfce5cc716d021f03 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 17:34:06 +0200 Subject: [PATCH 17/26] Add OnRowDoubleClick --- ...crosoft.FluentUI.AspNetCore.Components.xml | 14 +++++++++++ .../DataGrid/FluentDataGrid.razor.cs | 6 +++++ .../DataGrid/FluentDataGridRow.razor | 1 + .../DataGrid/FluentDataGridRow.razor.cs | 23 ++++++++++++++++--- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 774e4060ef..73e4aecd4c 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1622,6 +1622,11 @@ Gets or sets a callback when a row is clicked. + + + Gets or sets a callback when a row is double-clicked. + + Optionally defines a class to be applied to a rendered row. @@ -1764,6 +1769,15 @@ Gets or sets the owning component + + + + + + + + + A callback that provides data for a . diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index d0ecb6e3c8..8c9c4f241b 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -130,6 +130,12 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Parameter] public EventCallback> OnRowClick { get; set; } + /// + /// Gets or sets a callback when a row is double-clicked. + /// + [Parameter] + public EventCallback> OnRowDoubleClick { get; set; } + /// /// Optionally defines a class to be applied to a rendered row. /// diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor b/src/Core/Components/DataGrid/FluentDataGridRow.razor index a719b0e97c..3cb7c7fa33 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor @@ -10,6 +10,7 @@ grid-template-columns=@GridTemplateColumns row-type=@RowType.ToAttributeValue() @onclick="@(e => HandleOnRowClickAsync(RowId))" + @ondblclick="@(e => HandleOnRowDoubleClickAsync(RowId))" @attributes="AdditionalAttributes" @oncellfocus=HandleOnCellFocusAsync> @ChildContent diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index c9dde0640b..c779595798 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -99,11 +99,15 @@ private async Task HandleOnCellFocusAsync(DataGridCellFocusEventArgs args) } } + /// private async Task HandleOnRowClickAsync(string rowId) { if (Owner.Rows.TryGetValue(rowId, out var row)) { - await Owner.Grid.OnRowClick.InvokeAsync(row); + if (Owner.Grid.OnRowClick.HasDelegate) + { + await Owner.Grid.OnRowClick.InvokeAsync(row); + } if (row != null && row.RowType == DataGridRowType.Default) { @@ -116,6 +120,19 @@ private async Task HandleOnRowClickAsync(string rowId) } } - Task IHandleEvent.HandleEventAsync( - EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg); + /// + private async Task HandleOnRowDoubleClickAsync(string rowId) + { + if (Owner.Rows.TryGetValue(rowId, out var row)) + { + if (Owner.Grid.OnRowDoubleClick.HasDelegate) + { + await Owner.Grid.OnRowDoubleClick.InvokeAsync(row); + } + } + } + + /// + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) + => callback.InvokeAsync(arg); } From 3c1a8acfb0690adaaee65039128de23d3a60dd3b Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 17:52:59 +0200 Subject: [PATCH 18/26] Add Enter key --- ...crosoft.FluentUI.AspNetCore.Components.xml | 5 +++- .../DataGrid/FluentDataGrid.razor.cs | 2 +- .../DataGrid/FluentDataGridRow.razor | 1 + .../DataGrid/FluentDataGridRow.razor.cs | 25 ++++++++++++++++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 73e4aecd4c..50de9bb26d 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1659,7 +1659,7 @@ A default fragment is used if loading content is not specified. - + Gets the first (optional) SelectColumn @@ -1775,6 +1775,9 @@ + + + diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 8c9c4f241b..65d2ae6157 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -175,7 +175,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve /// /// Gets the first (optional) SelectColumn /// - internal SelectColumn? SelectedRowColumn => _columns.FirstOrDefault(col => col is SelectColumn) as SelectColumn; + internal IEnumerable> SelectColumns => _columns.Where(col => col is SelectColumn).Cast< SelectColumn>(); private ElementReference? _gridReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor b/src/Core/Components/DataGrid/FluentDataGridRow.razor index 3cb7c7fa33..809c817810 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor @@ -9,6 +9,7 @@ row-index=@RowIndex grid-template-columns=@GridTemplateColumns row-type=@RowType.ToAttributeValue() + @onkeydown="@(e => HandleOnRowKeyDownAsync(RowId, e))" @onclick="@(e => HandleOnRowClickAsync(RowId))" @ondblclick="@(e => HandleOnRowDoubleClickAsync(RowId))" @attributes="AdditionalAttributes" diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index c779595798..e3ecbce865 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure; using Microsoft.FluentUI.AspNetCore.Components.Utilities; @@ -111,10 +112,9 @@ private async Task HandleOnRowClickAsync(string rowId) if (row != null && row.RowType == DataGridRowType.Default) { - var selectedColumn = Owner.Grid.SelectedRowColumn; - if (selectedColumn != null) + foreach (var selColumn in Owner.Grid.SelectColumns) { - await selectedColumn.AddOrRemoveSelectedItemAsync(Item); + await selColumn.AddOrRemoveSelectedItemAsync(Item); } } } @@ -132,6 +132,25 @@ private async Task HandleOnRowDoubleClickAsync(string rowId) } } + /// + private async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e) + { + // Enter when a SelectColumn is defined. + if (e.Code == "Enter" || e.Code == "NumpadEnter") + { + if (Owner.Rows.TryGetValue(rowId, out var row)) + { + if (row != null && row.RowType == DataGridRowType.Default) + { + foreach (var selColumn in Owner.Grid.SelectColumns) + { + await selColumn.AddOrRemoveSelectedItemAsync(Item); + } + } + } + } + } + /// Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg); From 04881be63f49170d9158637fcb498f1a5c4bc8f0 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 18:07:57 +0200 Subject: [PATCH 19/26] Update keys to select/unselect --- ...icrosoft.FluentUI.AspNetCore.Components.xml | 18 ++++++++++++++++++ .../DataGrid/Columns/SelectColumn.cs | 12 ++++++++++++ .../DataGrid/FluentDataGridRow.razor.cs | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 50de9bb26d..b32f6e381d 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1365,6 +1365,16 @@ The type of data represented by each row in the grid. + + + List of keys to press, to select/unselect a row. + + + + + Initializes a new instance of . + + Gets or sets the content to be rendered for each row in the table. @@ -1375,6 +1385,11 @@ Gets or sets the list of selected items. + + + Gets or sets a callback when list of selected items changed. + + Gets or sets the selection mode (Single or Multiple). @@ -1464,6 +1479,9 @@ + + + diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 20c0dd6201..00459fe632 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -10,9 +10,17 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// The type of data represented by each row in the grid. public class SelectColumn : ColumnBase { + /// + /// List of keys to press, to select/unselect a row. + /// + public static string[] KEYBOARD_SELECT_KEYS = ["Enter", "NumpadEnter"]; + private DataGridSelectMode _selectMode = DataGridSelectMode.Single; private readonly List _selectedItems = new List(); + /// + /// Initializes a new instance of . + /// public SelectColumn() { Width = "50px"; @@ -42,6 +50,9 @@ public IEnumerable SelectedItems } } + /// + /// Gets or sets a callback when list of selected items changed. + /// [Parameter] public EventCallback> SelectedItemsChanged { get; set; } @@ -283,6 +294,7 @@ private void RefreshHeaderContent() HeaderContent = GetHeaderContent(); } + /// private bool? GetSelectAll() { // Using SelectedItems only diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index e3ecbce865..41d1cd0bfe 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -136,7 +136,7 @@ private async Task HandleOnRowDoubleClickAsync(string rowId) private async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e) { // Enter when a SelectColumn is defined. - if (e.Code == "Enter" || e.Code == "NumpadEnter") + if (SelectColumn.KEYBOARD_SELECT_KEYS.Contains(e.Code)) { if (Owner.Rows.TryGetValue(rowId, out var row)) { From d360e7edf6c79bed9b5d2d9dfebd9756102f58b3 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 28 Apr 2024 18:27:49 +0200 Subject: [PATCH 20/26] Update doc --- examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index 8f4152772e..77ca6a0864 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -247,6 +247,8 @@ fluent-data-grid-row:has([row-selected]) { + +

The FluentDataGridRow and FluentDataGridCell API's are usually not used directely From 323f2c3f637200d045510086fdb85c780bee5829 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Mon, 29 Apr 2024 11:37:54 +0200 Subject: [PATCH 21/26] Add first Unit Test --- src/Core/BindAttributes.cs | 4 ++ .../DataGrid/FluentDataGridRow.razor.cs | 6 +-- .../FluentDataGridColumSelectTests.razor | 47 +++++++++++++++++++ .../FluentDataGridColumSelectTests.razor.cs | 32 +++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/Core/DataGrid/FluentDataGridColumSelectTests.razor create mode 100644 tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs diff --git a/src/Core/BindAttributes.cs b/src/Core/BindAttributes.cs index d27eab4567..0ff1cffe6b 100644 --- a/src/Core/BindAttributes.cs +++ b/src/Core/BindAttributes.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Components; +// Specifies that types that are ordinarily visible only within the current assembly +// are visible to the UnitTest assembly. +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.FluentUI.AspNetCore.Components.Tests")] + namespace Microsoft.FluentUI.AspNetCore.Components; // Checkbox like items diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 41d1cd0bfe..7954c4e983 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -101,7 +101,7 @@ private async Task HandleOnCellFocusAsync(DataGridCellFocusEventArgs args) } ///

- private async Task HandleOnRowClickAsync(string rowId) + internal async Task HandleOnRowClickAsync(string rowId) { if (Owner.Rows.TryGetValue(rowId, out var row)) { @@ -121,7 +121,7 @@ private async Task HandleOnRowClickAsync(string rowId) } /// - private async Task HandleOnRowDoubleClickAsync(string rowId) + internal async Task HandleOnRowDoubleClickAsync(string rowId) { if (Owner.Rows.TryGetValue(rowId, out var row)) { @@ -133,7 +133,7 @@ private async Task HandleOnRowDoubleClickAsync(string rowId) } /// - private async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e) + internal async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e) { // Enter when a SelectColumn is defined. if (SelectColumn.KEYBOARD_SELECT_KEYS.Contains(e.Code)) diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor new file mode 100644 index 0000000000..c7f01a82f6 --- /dev/null +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor @@ -0,0 +1,47 @@ +@using Xunit; +@inherits TestContext +@code +{ + [Fact] + public async Task FluentDataGrid_ColumSelect_SingleSelect() + { + IEnumerable SelectedItems = People.Where(p => p.Selected); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click and select Row 0 + await ClickOnRowAsync(cut, row: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click and select Row 1 + await ClickOnRowAsync(cut, row: 1); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + } + + /// + /// Simulate a click on the DataGrid row number . + /// + /// + /// + /// + private async Task ClickOnRowAsync(IRenderedFragment cut, int row) + { + var item = cut.FindComponents>().ElementAt(row + 1); + await item.Instance.HandleOnRowClickAsync(item.Instance.RowId); + cut.FindComponent>().Render(); + } +} diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs new file mode 100644 index 0000000000..7d28a689dd --- /dev/null +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Bunit; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.FluentUI.AspNetCore.Components.Tests.DataGrid; + +public partial class FluentDataGridColumSelectTests +{ + public FluentDataGridColumSelectTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + + // Register Service + var keycodeService = new KeyCodeService(); + Services.AddScoped(factory => keycodeService); + } + + private record Person(int PersonId, string Name, DateOnly BirthDate) + { + public bool Selected { get; set; } + }; + + private static readonly IQueryable People = new[] + { + new Person(1, "Jean Martin", new DateOnly(1985, 3, 16)), + new Person(2, "Kenji Sato", new DateOnly(2004, 1, 9)), + new Person(3, "Julie Smith", new DateOnly(1958, 10, 10)), + }.AsQueryable(); +} From e275763d8d59dcdcaf4e141c9678b1d0afb93b6b Mon Sep 17 00:00:00 2001 From: dvoituron Date: Mon, 29 Apr 2024 11:59:16 +0200 Subject: [PATCH 22/26] Add Unit Tests --- .../DataGrid/Columns/SelectColumn.cs | 2 +- ..._MultiSelect_Rendering.verified.razor.html | 44 +++++++ ...SingleSelect_Rendering.verified.razor.html | 38 ++++++ .../FluentDataGridColumSelectTests.razor | 121 +++++++++++++++++- 4 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_MultiSelect_Rendering.verified.razor.html create mode 100644 tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 00459fe632..f255be3b1b 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -333,7 +333,7 @@ protected internal override void CellContent(RenderTreeBuilder builder, TGridIte protected override bool IsSortableByDefault() => SortBy is not null; /// - private async Task OnClickAllAsync(MouseEventArgs e) + internal async Task OnClickAllAsync(MouseEventArgs e) { if (Grid == null || Grid.Items == null || SelectMode != DataGridSelectMode.Multiple) { diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_MultiSelect_Rendering.verified.razor.html b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_MultiSelect_Rendering.verified.razor.html new file mode 100644 index 0000000000..87edd83d2b --- /dev/null +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_MultiSelect_Rendering.verified.razor.html @@ -0,0 +1,44 @@ + + + + + + + +
+
Name
+
+
+
+ + + + + Jean Martin + + + + + + Kenji Sato + + + + + + Julie Smith + +
\ No newline at end of file diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html new file mode 100644 index 0000000000..17903a27bd --- /dev/null +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html @@ -0,0 +1,38 @@ + + + + + +
+
Name
+
+
+
+ + + + + Jean Martin + + + + + + Kenji Sato + + + + + + Julie Smith + +
diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor index c7f01a82f6..0223226157 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor @@ -2,21 +2,87 @@ @inherits TestContext @code { + [Fact] + public void FluentDataGrid_ColumSelect_SingleSelect_Rendering() + { + IEnumerable SelectedItems = new[] { People.ElementAt(1) }; + + // Arrange + var cut = Render( + @ + + + + ); + + cut.Verify(); + } + [Fact] public async Task FluentDataGrid_ColumSelect_SingleSelect() { - IEnumerable SelectedItems = People.Where(p => p.Selected); + IEnumerable SelectedItems = Array.Empty(); // Arrange var cut = Render( - @ + @ - + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click and select Row 0 + await ClickOnRowAsync(cut, row: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click and select Row 1 + await ClickOnRowAsync(cut, row: 1); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + } + + [Fact] + public void FluentDataGrid_ColumSelect_MultiSelect_Rendering() + { + IEnumerable SelectedItems = new[] { People.ElementAt(1), People.ElementAt(2) }; + + // Arrange + var cut = Render( + @ + + ); + cut.Verify(); + } + + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); Assert.Empty(SelectedItems); @@ -28,8 +94,45 @@ // Act - Click and select Row 1 await ClickOnRowAsync(cut, row: 1); + Assert.Equal(2, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(2, SelectedItems.Count()); + + // Act - Click and unselect Row 0 + await ClickOnRowAsync(cut, row: 0); Assert.Single(cut.FindAll("svg[row-selected]")); Assert.Single(SelectedItems); + + } + + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectAll() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click on All checkbox to select all + await ClickOnAllAsync(cut); + Assert.Equal(3, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(3, SelectedItems.Count()); + + // Act - Click on All checkbox to unselect all + await ClickOnAllAsync(cut); + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + } /// @@ -44,4 +147,16 @@ await item.Instance.HandleOnRowClickAsync(item.Instance.RowId); cut.FindComponent>().Render(); } + + /// + /// Simulate a click on the All Checkbox. + /// + /// + /// + private async Task ClickOnAllAsync(IRenderedFragment cut) + { + var col = cut.FindComponent>(); + await col.Instance.OnClickAllAsync(new MouseEventArgs()); + cut.FindComponent>().Render(); + } } From 56d3a89f0c37cc2f2b26ec3c6c448814d7f2b227 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Mon, 29 Apr 2024 12:16:42 +0200 Subject: [PATCH 23/26] Add Unit Tests for Property attribute --- .../FluentDataGridColumSelectTests.razor | 114 +++++++++++++++++- .../FluentDataGridColumSelectTests.razor.cs | 2 +- 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor index 0223226157..c714fbc11b 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor @@ -15,13 +15,13 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); cut.Verify(); } [Fact] - public async Task FluentDataGrid_ColumSelect_SingleSelect() + public async Task FluentDataGrid_ColumSelect_SingleSelect_SelectedItems() { IEnumerable SelectedItems = Array.Empty(); @@ -50,6 +50,37 @@ Assert.Single(SelectedItems); } + [Fact] + public async Task FluentDataGrid_ColumSelect_SingleSelect_Property() + { + var items = new List(People).AsQueryable(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(items.Where(i => i.Selected)); + + // Act - Click and select Row 0 + await ClickOnRowAsync(cut, row: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(items.Where(i => i.Selected)); + + // Act - Click and select Row 1 + await ClickOnRowAsync(cut, row: 1); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(items.Where(i => i.Selected)); + } + [Fact] public void FluentDataGrid_ColumSelect_MultiSelect_Rendering() { @@ -63,13 +94,13 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); cut.Verify(); } [Fact] - public async Task FluentDataGrid_ColumSelect_MultiSelect() + public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectedItems() { IEnumerable SelectedItems = Array.Empty(); @@ -81,7 +112,7 @@ @bind-SelectedItems="@SelectedItems" /> - ); + ); // Pre-Assert Assert.Empty(cut.FindAll("svg[row-selected]")); @@ -105,7 +136,44 @@ } [Fact] - public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectAll() + public async Task FluentDataGrid_ColumSelect_MultiSelect_Property() + { + var items = new List(People).AsQueryable(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(items.Where(i => i.Selected)); + + // Act - Click and select Row 0 + await ClickOnRowAsync(cut, row: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(items.Where(i => i.Selected)); + + // Act - Click and select Row 1 + await ClickOnRowAsync(cut, row: 1); + Assert.Equal(2, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(2, items.Where(i => i.Selected).Count()); + + // Act - Click and unselect Row 0 + await ClickOnRowAsync(cut, row: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(items.Where(i => i.Selected)); + + } + + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectAll_SelectedItems() { IEnumerable SelectedItems = Array.Empty(); @@ -135,6 +203,40 @@ } + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectAll_Property() + { + var items = new List(People).AsQueryable(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(items.Where(i => i.Selected)); + + // Act - Click on All checkbox to select all + await ClickOnAllAsync(cut); + Assert.Equal(3, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(3, items.Where(i => i.Selected).Count()); + + // Act - Click on All checkbox to unselect all + await ClickOnAllAsync(cut); + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(items.Where(i => i.Selected)); + + } + /// /// Simulate a click on the DataGrid row number . /// diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs index 7d28a689dd..42f2901c4d 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor.cs @@ -23,7 +23,7 @@ private record Person(int PersonId, string Name, DateOnly BirthDate) public bool Selected { get; set; } }; - private static readonly IQueryable People = new[] + private readonly IQueryable People = new[] { new Person(1, "Jean Martin", new DateOnly(1985, 3, 16)), new Person(2, "Kenji Sato", new DateOnly(2004, 1, 9)), From cf19e3ff2cb3ed23b69edec1375fbebe7b2005ab Mon Sep 17 00:00:00 2001 From: dvoituron Date: Mon, 29 Apr 2024 12:26:49 +0200 Subject: [PATCH 24/26] Add SwitchMultiToSingleSelect --- .../FluentDataGridColumSelectTests.razor | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor index c714fbc11b..60f8c8bbcf 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]")); @@ -237,6 +237,36 @@ } + [Fact] + public void FluentDataGrid_ColumSelect_SwitchMultiToSingleSelect() + { + IEnumerable selectedItems = new[] { People.ElementAt(1), People.ElementAt(2) }; + + // Arrange + var cut = Render( + @ + + + + ); + + // Before the switch + Assert.Equal(2, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(2, selectedItems.Count()); + + // Act + cut.FindComponent>().Instance.SelectMode = DataGridSelectMode.Single; + cut.FindComponent>().Render(); + + var x = cut.Markup; + + // After the switch + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(selectedItems); + } + /// /// Simulate a click on the DataGrid row number . /// From 6473d6eaf0c5215edd4a776e03de5c51fc449229 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Mon, 29 Apr 2024 12:42:25 +0200 Subject: [PATCH 25/26] Update the doc --- .../Shared/Pages/DataGrid/DataGridPage.razor | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index 77ca6a0864..e4f73a0340 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -39,6 +39,10 @@ width is done in steps of 10 pixels at a time. You can reset to the original initial column widths by pressing Shift + r.

+

+ When a row cell is focused and the grid contains a SelectColumn column, you can use the Enter key to select or unselect the current row. +

+

Sorting

The DataGrid supports sorting by clicking on the column headers. The default sort direction is ascending. Clicking on the same column header again will toggle the sort direction. @@ -55,7 +59,25 @@ - The same example, adding a SelectColumn, to allow multi-select rows. +

The same example, adding a SelectColumn, to allow multi-select rows.

+

To utilize the new SelectColumn feature in the Fluent DataGrid, there are two approaches available:

+ +

+ Automatic Management via SelectedItems +

    +
  • Provide a list of data via the Items property.
  • +
  • Let the grid handle selected rows entirely through the SelectedItems property.
  • +
+

+

+ Manual Management via Property and OnSelect: +

    +
  • Control how selected lines are saved manually.
  • +
  • Utilize the Property, OnSelect, and SelectAll attributes.
  • +
+ This method offers more flexibility but requires additional configuration, making it particularly useful when + using Virtualize or directly managing a custom IsSelected property. +

By default, FluentUI only recommends selecting the checkbox for the selected line. From c9277208133cbc508864b13918bef7a233d008f0 Mon Sep 17 00:00:00 2001 From: dvoituron Date: Tue, 30 Apr 2024 10:06:14 +0200 Subject: [PATCH 26/26] Update Single Icons and Doc (PR comments) --- ...crosoft.FluentUI.AspNetCore.Components.xml | 1 + .../Shared/Pages/DataGrid/DataGridPage.razor | 6 ++-- .../DataGrid/Columns/SelectColumn.cs | 36 ++++++++++++++++--- src/Core/Components/Icons/CoreIcons.cs | 2 ++ ...SingleSelect_Rendering.verified.razor.html | 6 ++-- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index b32f6e381d..921bdf0ec6 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1420,6 +1420,7 @@ Gets or sets the Icon to be rendered when some but not all rows are selected. + Only when is Multiple. diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index e4f73a0340..4185276a1f 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -60,7 +60,7 @@

The same example, adding a SelectColumn, to allow multi-select rows.

-

To utilize the new SelectColumn feature in the Fluent DataGrid, there are two approaches available:

+

To utilize the SelectColumn feature in the Fluent DataGrid, there are two approaches available:

Automatic Management via SelectedItems @@ -80,8 +80,8 @@

- By default, FluentUI only recommends selecting the checkbox for the selected line. - But you can use a CSS style like this one to grey the background of selected lines. + By default the Fluent Design System recommends to only use the checkbox to indicate selected rows. + It is possible to change this behavior by using a CSS style like this to set a background on selected rows:
 fluent-data-grid-row:has([row-selected]) {
diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs
index f255be3b1b..bb854a2bb2 100644
--- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs
+++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs
@@ -15,6 +15,11 @@ public class SelectColumn : ColumnBase
     /// 
public static string[] KEYBOARD_SELECT_KEYS = ["Enter", "NumpadEnter"]; + private readonly Icon IconUnselectedMultiple = new CoreIcons.Regular.Size20.CheckboxUnchecked().WithColor(Color.FillInverse); + private readonly Icon IconSelectedMultiple = new CoreIcons.Filled.Size20.CheckboxChecked(); + private readonly Icon IconUnselectedSingle = new CoreIcons.Regular.Size20.RadioButton().WithColor(Color.FillInverse); + private readonly Icon IconSelectedSingle = new CoreIcons.Filled.Size20.RadioButton(); + private DataGridSelectMode _selectMode = DataGridSelectMode.Single; private readonly List _selectedItems = new List(); @@ -80,7 +85,7 @@ public DataGridSelectMode SelectMode /// Gets or sets the Icon to be rendered when the row is non selected. ///
[Parameter] - public required Icon IconUnchecked { get; set; } = new CoreIcons.Regular.Size20.CheckboxUnchecked().WithColor(Color.FillInverse); + public Icon? IconUnchecked { get; set; } /// /// Gets or sets the Icon title display as a tooltip and used with Accessibility. @@ -93,7 +98,7 @@ public DataGridSelectMode SelectMode /// Gets or sets the Icon to be rendered when the row is selected. /// [Parameter] - public required Icon IconChecked { get; set; } = new CoreIcons.Filled.Size20.CheckboxChecked(); + public Icon? IconChecked { get; set; } /// /// Gets or sets the Icon title display as a tooltip and used with Accessibility. @@ -104,6 +109,7 @@ public DataGridSelectMode SelectMode /// /// Gets or sets the Icon to be rendered when some but not all rows are selected. + /// Only when is Multiple. /// [Parameter] public Icon? IconIndeterminate { get; set; } = new CoreIcons.Filled.Size20.CheckboxIndeterminate(); @@ -202,6 +208,26 @@ Task CallOnSelect(TGridItem item, bool isSelected) } } + private Icon GetIcon(bool? selected) + { + if (selected == true) + { + return IconChecked ?? SelectMode switch + { + DataGridSelectMode.Single => IconSelectedSingle, + _ => IconSelectedMultiple + }; + } + else + { + return IconUnchecked ?? SelectMode switch + { + DataGridSelectMode.Single => IconUnselectedSingle, + _ => IconUnselectedMultiple + }; + } + } + private async Task KeepOnlyFirstSelectedItemAsync() { if (_selectedItems.Count() <= 1) @@ -249,7 +275,7 @@ private RenderFragment GetDefaultChildContent() } builder.OpenComponent>(0); - builder.AddAttribute(1, "Value", selected ? IconChecked : IconUnchecked); + builder.AddAttribute(1, "Value", GetIcon(selected)); builder.AddAttribute(1, "Title", selected ? TitleChecked : TitleUnchecked); builder.AddAttribute(2, "row-selected", selected); builder.CloseComponent(); @@ -268,7 +294,7 @@ private RenderFragment GetHeaderContent() var selectedAll = GetSelectAll(); var iconAllChecked = (selectedAll == null && IconIndeterminate != null) ? IconIndeterminate - : (selectedAll == true ? IconChecked : IconUnchecked); + : GetIcon(selectedAll); return new RenderFragment((builder) => { @@ -279,7 +305,7 @@ private RenderFragment GetHeaderContent() builder.AddAttribute(4, "Style", "margin-left: 12px;"); builder.AddAttribute(5, "Title", iconAllChecked == IconIndeterminate ? TitleAllIndeterminate - : (iconAllChecked == IconChecked ? TitleAllChecked : TitleAllUnchecked)); + : (iconAllChecked == GetIcon(true) ? TitleAllChecked : TitleAllUnchecked)); builder.CloseComponent(); }); diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index 93bedfc599..194aa7baea 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -41,6 +41,7 @@ public class Info : Icon { public Info() : base("Info", IconVariant.Filled, Icon public class Warning : Icon { public Warning() : base("Warning", IconVariant.Filled, IconSize.Size20, "") { } } public class CheckboxChecked : Icon { public CheckboxChecked() : base("CheckboxChecked", IconVariant.Filled, IconSize.Size20, "") { } } public class CheckboxIndeterminate : Icon { public CheckboxIndeterminate() : base("CheckboxIndeterminate", IconVariant.Filled, IconSize.Size20, "") { } } + public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } }; } } @@ -97,6 +98,7 @@ public class ChevronDoubleRight : Icon { public ChevronDoubleRight() : base("Che public class Dismiss : Icon { public Dismiss() : base("Dismiss", IconVariant.Regular, IconSize.Size20, "") { } } public class DismissCircle : Icon { public DismissCircle() : base("DismissCircle", IconVariant.Regular, IconSize.Size20, "") { } } public class CheckboxUnchecked : Icon { public CheckboxUnchecked() : base("CheckboxUnchecked", IconVariant.Regular, IconSize.Size20, "") { } } + public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } }; } } diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html index 17903a27bd..a2b8b67e5d 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.FluentDataGrid_ColumSelect_SingleSelect_Rendering.verified.razor.html @@ -12,7 +12,7 @@ Jean Martin @@ -21,7 +21,7 @@ Kenji Sato @@ -30,7 +30,7 @@ Julie Smith