diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 997831e740..760b59595d 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1444,6 +1444,12 @@ Gets or sets the content to be rendered for each row in the table. + + + Gets or sets whether the selection of rows is restricted to the checkbox cells (true) or if the whole row can be clicked to toggled the selection of rows (false). + ⚠ Setting this to true will stop the from being invoked, when a cell in this column is clicked. + + Gets or sets whether the [All] checkbox is disabled (not clickable). @@ -1725,6 +1731,11 @@ Gets or sets a callback when a row is clicked. + + + Gets or sets a callback when a cell is clicked. + + Gets or sets a callback when a row is double-clicked. @@ -1855,6 +1866,9 @@ Gets or sets the owning component + + + Gets or sets the reference to the item that holds this row's values. diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor index ca78863f01..4bba452177 100644 --- a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridMultiSelect.razor @@ -4,6 +4,9 @@ + @if (UseSelectedItems) @@ -11,9 +14,13 @@ @* Sample using SelectedItems *@
Using SelectedItems
- - + @@ -30,9 +37,13 @@ else @* Sample using Property and OnSelect *@
Using Property and OnSelect
- - + ? SelectColumn; + bool UseSelectedItems = true; DataGridSelectMode Mode = DataGridSelectMode.Single; IEnumerable SelectedItems = People.Where(p => p.Selected); + bool RestrictToCheckbox = false; record Person(int PersonId, string Name, DateOnly BirthDate) { @@ -72,7 +86,6 @@ else private void ResetSelectItems() { People.ToList().ForEach(i => i.Selected = false); - People.First().Selected = true; - SelectedItems = People.Where(p => p.Selected); + SelectColumn?.ClearSelection(); } } diff --git a/src/Core/Components/DataGrid/Columns/SelectColumn.cs b/src/Core/Components/DataGrid/Columns/SelectColumn.cs index 9e0fed801c..c879bc8dc2 100644 --- a/src/Core/Components/DataGrid/Columns/SelectColumn.cs +++ b/src/Core/Components/DataGrid/Columns/SelectColumn.cs @@ -38,6 +38,13 @@ public SelectColumn() [Parameter] public RenderFragment ChildContent { get; set; } + /// + /// Gets or sets whether the selection of rows is restricted to the checkbox cells (true) or if the whole row can be clicked to toggled the selection of rows (false). + /// ⚠ Setting this to true will stop the from being invoked, when a cell in this column is clicked. + /// + [Parameter] + public bool RestrictToCheckbox { get; set; } = false; + /// /// Gets or sets whether the [All] checkbox is disabled (not clickable). /// diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor index 2021c63008..74d3d6a92b 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor @@ -92,9 +92,9 @@ var rowStyle = RowStyle?.Invoke(item) ?? null; Loading = false; - @for (var colIndex = 0; colIndex < _columns.Count; colIndex++) + @for (var colIndex = 0; colIndex < Columns.Count; colIndex++) { - var col = _columns[colIndex]; + var col = Columns[colIndex]; string? tooltip = col.Tooltip ? @col.RawCellContent(item) : null; @@ -110,9 +110,9 @@ string? _rowsDataSize = $"height: {ItemSize}px"; - @for (var colIndex = 0; colIndex < _columns.Count; colIndex++) + @for (var colIndex = 0; colIndex < Columns.Count; colIndex++) { - var col = _columns[colIndex]; + var col = Columns[colIndex]; @((RenderFragment)(__builder => col.RenderPlaceholderContent(__builder, placeholderContext))) @@ -124,9 +124,9 @@ private void RenderColumnHeaders(RenderTreeBuilder __builder) { - @for (var colIndex = 0; colIndex < _columns.Count; colIndex++) + @for (var colIndex = 0; colIndex < Columns.Count; colIndex++) { - var col = _columns[colIndex]; + var col = Columns[colIndex]; string CellId = Identifier.NewId(); if (_sortByColumn == col) col.ShowSortIcon = true; diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 0768a0efdc..9ebbb75812 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 cell is clicked. + /// + [Parameter] + public EventCallback> OnCellClick { get; set; } + /// /// Gets or sets a callback when a row is double-clicked. /// @@ -175,7 +181,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve /// /// Gets the first (optional) SelectColumn /// - internal IEnumerable> SelectColumns => _columns.Where(col => col is SelectColumn).Cast< SelectColumn>(); + internal IEnumerable> SelectColumns => Columns.Where(col => col is SelectColumn).Cast< SelectColumn>(); private ElementReference? _gridReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; @@ -187,7 +193,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns // This happens on every render so that the column list can be updated dynamically private readonly InternalGridContext _internalGridContext; - private readonly List> _columns; + internal readonly List> Columns; private bool _collectingColumns; // Columns might re-render themselves arbitrarily. We only want to capture them at a defined time. // Tracking state for options and sorting @@ -229,7 +235,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(DataGridRowFocusEventArgs))] public FluentDataGrid() { - _columns = []; + Columns = []; _internalGridContext = new(this); _currentPageItemsChanged = new(EventCallback.Factory.Create(this, RefreshDataCoreAsync)); _renderColumnHeaders = RenderColumnHeaders; @@ -279,7 +285,7 @@ protected override Task OnParametersSetAsync() // We don't want to trigger the first data load until we've collected the initial set of columns, // because they might perform some action like setting the default sort order, so it would be wasteful // to have to re-query immediately - return (_columns.Count > 0 && mustRefreshData) ? RefreshDataCoreAsync() : Task.CompletedTask; + return (Columns.Count > 0 && mustRefreshData) ? RefreshDataCoreAsync() : Task.CompletedTask; } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -309,7 +315,7 @@ internal void AddColumn(ColumnBase column, SortDirection? initialSort { if (_collectingColumns) { - _columns.Add(column); + Columns.Add(column); if (isDefaultSortColumn && _sortByColumn is null && initialSortDirection.HasValue) { @@ -322,23 +328,23 @@ internal void AddColumn(ColumnBase column, SortDirection? initialSort private void StartCollectingColumns() { - _columns.Clear(); + Columns.Clear(); _collectingColumns = true; } private void FinishCollectingColumns() { _collectingColumns = false; - _manualGrid = _columns.Count == 0; + _manualGrid = Columns.Count == 0; - if (!string.IsNullOrWhiteSpace(GridTemplateColumns) && _columns.Where(x => x is not SelectColumn).Any(x => !string.IsNullOrWhiteSpace(x.Width))) + if (!string.IsNullOrWhiteSpace(GridTemplateColumns) && Columns.Where(x => x is not SelectColumn).Any(x => !string.IsNullOrWhiteSpace(x.Width))) { throw new Exception("You can use either the 'GridTemplateColumns' parameter on the grid or the 'Width' property at the column level, not both."); } - if (string.IsNullOrWhiteSpace(_internalGridTemplateColumns) && _columns.Any(x => !string.IsNullOrWhiteSpace(x.Width))) + if (string.IsNullOrWhiteSpace(_internalGridTemplateColumns) && Columns.Any(x => !string.IsNullOrWhiteSpace(x.Width))) { - _internalGridTemplateColumns = string.Join(" ", _columns.Select(x => x.Width ?? "1fr")); + _internalGridTemplateColumns = string.Join(" ", Columns.Select(x => x.Width ?? "1fr")); } if (ResizableColumns) @@ -376,7 +382,7 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto) { - var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); + var column = Columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); if (column is not null) { @@ -393,9 +399,9 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto) { - if (index >= 0 && index < _columns.Count) + if (index >= 0 && index < Columns.Count) { - return SortByColumnAsync(_columns[index], direction); + return SortByColumnAsync(Columns[index], direction); } return Task.CompletedTask; diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor b/src/Core/Components/DataGrid/FluentDataGridCell.razor index 5e05d90a24..e94605961d 100644 --- a/src/Core/Components/DataGrid/FluentDataGridCell.razor +++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor @@ -6,6 +6,7 @@ grid-column=@GridColumn class="@Class" style="@StyleValue" + @onclick="@HandleOnCellClickAsync" @attributes="AdditionalAttributes"> @ChildContent diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs b/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs index ebd89b844e..b1847779d0 100644 --- a/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor.cs @@ -61,4 +61,23 @@ protected override void OnInitialized() public void Dispose() => Owner.Unregister(this); + /// + internal async Task HandleOnCellClickAsync() + { + if (GridContext.Grid.OnCellClick.HasDelegate) + { + await GridContext.Grid.OnCellClick.InvokeAsync(this); + } + + if (CellType == DataGridCellType.Default && Owner.Owner.Grid.SelectColumns.Any(selColumn => selColumn.RestrictToCheckbox)) + { + foreach (var selColumn in Owner.Owner.Grid.SelectColumns) + { + if (selColumn != null && selColumn.RestrictToCheckbox is true && Owner.Owner.Grid.Columns.IndexOf(selColumn) == GridColumn - 1) + { + await selColumn.AddOrRemoveSelectedItemAsync(Item); + } + } + } + } } diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs index 7954c4e983..b2856d5007 100644 --- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.cs @@ -13,7 +13,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public partial class FluentDataGridRow : FluentComponentBase, IHandleEvent, IDisposable { internal string RowId { get; set; } = string.Empty; - private readonly Dictionary> cells = []; + internal Dictionary> Cells { get; } = []; /// /// Gets or sets the reference to the item that holds this row's values. @@ -56,7 +56,7 @@ public partial class FluentDataGridRow : FluentComponentBase, IHandle /// Gets or sets the owning component /// [CascadingParameter] - private InternalGridContext Owner { get; set; } = default!; + internal InternalGridContext Owner { get; set; } = default!; protected string? ClassValue => new CssBuilder(Class) .AddClass("hover", when: Owner.Grid.ShowHover) @@ -80,18 +80,18 @@ internal void Register(FluentDataGridCell cell) { cell.CellId = $"c{Owner.GetNextCellId()}"; - cells.Add(cell.CellId, cell); + Cells.Add(cell.CellId, cell); } internal void Unregister(FluentDataGridCell cell) { - cells.Remove(cell.CellId!); + Cells.Remove(cell.CellId!); } private async Task HandleOnCellFocusAsync(DataGridCellFocusEventArgs args) { var cellId = args.CellId; - if (cells.TryGetValue(cellId!, out var cell)) + if (Cells.TryGetValue(cellId!, out var cell)) { if (cell != null && cell.CellType == DataGridCellType.Default) { @@ -114,7 +114,10 @@ internal async Task HandleOnRowClickAsync(string rowId) { foreach (var selColumn in Owner.Grid.SelectColumns) { - await selColumn.AddOrRemoveSelectedItemAsync(Item); + if (!selColumn.RestrictToCheckbox) + { + await selColumn.AddOrRemoveSelectedItemAsync(Item); + } } } } @@ -144,7 +147,10 @@ internal async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e) { foreach (var selColumn in Owner.Grid.SelectColumns) { - await selColumn.AddOrRemoveSelectedItemAsync(Item); + if (!selColumn.RestrictToCheckbox) + { + await selColumn.AddOrRemoveSelectedItemAsync(Item); + } } } } diff --git a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor index d0acb17bc3..d6295373e5 100644 --- a/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor +++ b/tests/Core/DataGrid/FluentDataGridColumSelectTests.razor @@ -318,6 +318,88 @@ cut.Verify(); } + [Fact] + public async Task FluentDataGrid_ColumSelect_SingleSelect_SelectedItems_RestrictToCheckbox() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click Row 0 Cell 1 + await ClickOnCellAsync(cut, row: 0, cell: 1); + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click and select Row 0 Cell 0 + await ClickOnCellAsync(cut, row: 0, cell: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click Row 1 Cell 1 + await ClickOnCellAsync(cut, row: 1, cell: 1); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click and select Row 1 Cell 0 + await ClickOnCellAsync(cut, row: 1, cell: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + } + + [Fact] + public async Task FluentDataGrid_ColumSelect_MultiSelect_SelectedItems_RestrictToCheckbox() + { + IEnumerable SelectedItems = Array.Empty(); + + // Arrange + var cut = Render( + @ + + + + ); + + // Pre-Assert + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click Row 0 Cell 1 + await ClickOnCellAsync(cut, row: 0, cell: 1); + Assert.Empty(cut.FindAll("svg[row-selected]")); + Assert.Empty(SelectedItems); + + // Act - Click and select Row 0 Cell 0 + await ClickOnCellAsync(cut, row: 0, cell: 0); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click Row 1 Cell 1 + await ClickOnCellAsync(cut, row: 1, cell: 1); + Assert.Single(cut.FindAll("svg[row-selected]")); + Assert.Single(SelectedItems); + + // Act - Click and select Row 1 Cell 0 + await ClickOnCellAsync(cut, row: 1, cell: 0); + Assert.Equal(2, cut.FindAll("svg[row-selected]").Count); + Assert.Equal(2, SelectedItems.Count()); + } + /// /// Simulate a click on the DataGrid row number . /// @@ -331,6 +413,20 @@ cut.FindComponent>().Render(); } + /// + /// Simulate a click on the DataGrid cell number in row number . + /// + /// + /// + /// + /// + private async Task ClickOnCellAsync(IRenderedFragment cut, int row, int cell) + { + var item = cut.FindComponents>().ElementAt(row + 1).FindComponents>().ElementAt(cell); + await item.Instance.HandleOnCellClickAsync(); + cut.FindComponent>().Render(); + } + /// /// Simulate a click on the All Checkbox. ///