From 1aae32f210e987d1bccb53cd723ab558f9b091a1 Mon Sep 17 00:00:00 2001 From: Kevin Becker Date: Tue, 20 Aug 2024 14:42:16 -0400 Subject: [PATCH] [DataGrid] Add AutoFit feature to size columns automatically as good as possible (#2496) * Add AutoFit feature to FluentDataGrid component Introduced the AutoFit property to the FluentDataGrid component, enabling columns to auto-fit the grid width using JavaScript. Updated XML documentation and added a new demo section in DataGridPage.razor to showcase this feature. Created DataGridAutoFit.razor component for demonstration purposes. Modified FluentDataGrid.razor and FluentDataGrid.razor.cs to implement AutoFit functionality, including invoking a JavaScript function to adjust column widths. Simplified SortByColumnAsync methods using ternary operators. Updated CSS in FluentDataGrid.razor.css and FluentDataGridCell.razor.css to support AutoFit, ensuring proper styling and visibility of grid cells. Added autoFitGridColumns JavaScript function in FluentDataGrid.razor.js. Removed redundant CSS rules and adjusted formatting for consistency. * Provide better comment for the AutoFit feature and fix merge conflict artifacts. * Add AutoFit parameter to FluentDataGrid component Updated DataGridPage.razor to correct a typo and provide additional information about the AutoFit parameter. Removed GridTemplateColumns="auto-fit" from DataGridAutoFit.razor as it is now handled internally. Modified OnParametersSetAsync and OnAfterRenderAsync in FluentDataGrid.razor.cs to support AutoFit. Improved autoFitGridColumns function in FluentDataGrid.razor.js. * Place `Parameter` attribute on separate line for `AutoFit`. Also fixed the `Parameter` attribute for `LoadingContent`. --------- Co-authored-by: Kevin Becker Co-authored-by: Denis Voituron Co-authored-by: Vincent Baaij --- ...crosoft.FluentUI.AspNetCore.Components.xml | 10 ++++ .../Shared/Pages/DataGrid/DataGridPage.razor | 30 +++++++++- .../DataGrid/Examples/DataGridAutoFit.razor | 57 +++++++++++++++++++ .../Components/DataGrid/FluentDataGrid.razor | 1 - .../DataGrid/FluentDataGrid.razor.cs | 51 ++++++++++------- .../DataGrid/FluentDataGrid.razor.css | 4 +- .../DataGrid/FluentDataGrid.razor.js | 22 ++++++- .../DataGrid/FluentDataGridCell.razor.css | 35 ++++++------ 8 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 examples/Demo/Shared/Pages/DataGrid/Examples/DataGridAutoFit.razor diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 922f2f9c2e..adf9db9f59 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -1891,6 +1891,16 @@ A default fragment is used if loading content is not specified. + + + Sets to automatically fit the columns to the available width as best it can. + + + + + Gets the first (optional) SelectColumn + + Constructs an instance of . diff --git a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor index 9698a7fdc2..a0c6b2f6c8 100644 --- a/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor +++ b/examples/Demo/Shared/Pages/DataGrid/DataGridPage.razor @@ -299,13 +299,41 @@ fluent-data-grid-row:has([row-selected]) { Example of using an outside div and the Style parameter to achieve a table like DataGrid with infinite horizontal scrollbars to display all content on all devices.

- If you use this in combination with a sticky header, the style of the header will be lost for the columns that are renderded out of the view initially. + If you use this in combination with a sticky header, the style of the header will be lost for the columns that are rendered out of the view initially. You can fix this by adding the following Style to your data grid: Style="min-width: max-content;"

+ + +

+ The example and code below show what you need to add to one of your Blazor page components to implement auto-fit. +

+

+ The AutoFit parameter is used to automatically adjust the column widths to fit the content. It only runs on + the first render and does not update when the content changes. +

+

+ The column widths are calculated with the process below: +

    +
  • + Loop through the columns and find the biggest width of each cell of the column +
  • +
  • + Build the GridTemplateColumns string using the fr unit +
  • +
+

+

+ This does not work + when Virtualization is turned on. The GridTemplateColumns parameter is ignored + when AutoFit is set to true. This is untested in MAUI. +

+
+
+

Documentation

diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridAutoFit.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridAutoFit.razor new file mode 100644 index 0000000000..c3bf9a049f --- /dev/null +++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridAutoFit.razor @@ -0,0 +1,57 @@ +@using System.ComponentModel.DataAnnotations + + + +

With auto-fit

+ + + + + + +
+ +

Without auto-fit

+ + + + + + +
+
+ +@code { + public class Person + { + public Person(int personId, string name, DateOnly birthDate, string bio) + { + PersonId = personId; + Name = name; + BirthDate = birthDate; + Bio = bio; + } + + [Display(Name = "Identity")] + public int PersonId { get; set; } + + [Display(Name = "Name")] + public string Name { get; set; } + + [Display(Name = "Birth date")] + public DateOnly BirthDate { get; set; } + + [Display(Name = "Biography")] + public string Bio { get; set; } + } + + IQueryable people = new[] + { + new Person(10895, "Jean Martin", new DateOnly(1825, 11, 29), "Born on November 29, 1825, in Paris, France, is renowned as the founder of modern neurology."), + new Person(10944, "António Langa", new DateOnly(1972, 5, 15), "Born on May 15, 1972, in Columbia, South Carolina, is a distinguished former professional basketball player."), + new Person(11203, "Julie Smith", new DateOnly(1944, 11, 25), "Born on November 25, 1944, in Annapolis, Maryland, is an acclaimed American mystery writer celebrated for her rich storytelling."), + new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27), "Nur Sari is a fictional character known for her extraordinary contributions to the field of renewable energy."), + new Person(11898, "Jose Hernandez", new DateOnly(1962, 8, 7), "Born on August 7, 1962, in French Camp, California, is a Mexican-American engineer."), + new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9), ""), + }.AsQueryable(); +} diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor index 6f1f1b41df..bc357fb780 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor @@ -126,7 +126,6 @@ } - private void RenderColumnHeaders(RenderTreeBuilder __builder) { @for (var colIndex = 0; colIndex < _columns.Count; colIndex++) diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index 171c84080f..b6574581c1 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -8,6 +8,7 @@ using Microsoft.FluentUI.AspNetCore.Components.Extensions; using Microsoft.FluentUI.AspNetCore.Components.Infrastructure; using Microsoft.JSInterop; + using System.Diagnostics.CodeAnalysis; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -215,6 +216,17 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve [Parameter] public RenderFragment? LoadingContent { get; set; } + /// + /// Sets to automatically fit the columns to the available width as best it can. + /// + [Parameter] + public bool AutoFit { get; set; } + + /// + /// Gets the first (optional) SelectColumn + /// + internal IEnumerable> SelectColumns => _columns.Where(col => col is SelectColumn).Cast>(); + private ElementReference? _gridReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; @@ -293,7 +305,14 @@ protected override void OnInitialized() /// protected override Task OnParametersSetAsync() { - _internalGridTemplateColumns = GridTemplateColumns; + if (AutoFit) + { + _internalGridTemplateColumns = "auto-fit"; + } + else + { + _internalGridTemplateColumns = GridTemplateColumns; + } // The associated pagination state may have been added/removed/replaced _currentPageItemsChanged.SubscribeOrMove(Pagination?.CurrentPageItemsChanged); @@ -337,6 +356,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { Console.WriteLine("[FluentDataGrid] " + ex.Message); } + + if (AutoFit && _gridReference is not null) + { + _ = Module?.InvokeVoidAsync("autoFitGridColumns", _gridReference, _columns.Count).AsTask(); + } } if (_checkColumnOptionsPosition && _displayOptionsForColumn is not null) @@ -420,12 +444,7 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect { var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); - if (column is not null) - { - return SortByColumnAsync(column, direction); - } - - return Task.CompletedTask; + return column is not null ? SortByColumnAsync(column, direction) : Task.CompletedTask; } /// @@ -435,12 +454,7 @@ 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) - { - return SortByColumnAsync(_columns[index], direction); - } - - return Task.CompletedTask; + return index >= 0 && index < _columns.Count ? SortByColumnAsync(_columns[index], direction) : Task.CompletedTask; } /// @@ -622,14 +636,13 @@ private string AriaSortValue(ColumnBase column) private string? GridClass() { var value = $"{Class} {(_pendingDataLoadCancellationTokenSource is null ? null : "loading")}".Trim(); - if (string.IsNullOrEmpty(value)) - { - return null; - } - else + + if (AutoFit) { - return value; + value += " auto-fit"; } + + return string.IsNullOrEmpty(value) ? null : value; } private static string? ColumnClass(ColumnBase column) => column.Align switch diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.css b/src/Core/Components/DataGrid/FluentDataGrid.razor.css index b0f0334439..28c17768ab 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.css +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.css @@ -5,7 +5,7 @@ fluent-data-grid { ::deep .empty-content-row, ::deep .loading-content-row { width: 100%; - height: 100% !important ; + height: 100% !important; display: flex; justify-content: center; align-items: center; @@ -13,7 +13,6 @@ fluent-data-grid { ::deep .empty-content-cell, ::deep .loading-content-cell { - font-weight: 600; } @@ -55,7 +54,6 @@ fluent-data-grid { align-items: center; } - ::deep .col-width-draghandle { height: auto; width: calc(var(--design-unit) * 1px + 2px); diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.js b/src/Core/Components/DataGrid/FluentDataGrid.razor.js index d43e3c046b..2991e70d07 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.js +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.js @@ -91,7 +91,6 @@ export function checkColumnOptionsPosition(gridElement) { } } - export function enableColumnResizing(gridElement) { if (gridElement === latestGridElement) return; @@ -159,7 +158,7 @@ export function enableColumnResizing(gridElement) { } export function resetColumnWidths(gridElement) { - + gridElement.gridTemplateColumns = initialColumnsWidths; } @@ -216,6 +215,25 @@ export function resizeColumnDiscrete(gridElement, column, change) { .join(' '); } +export function autoFitGridColumns(gridElement, columnCount) { + let gridTemplateColumns = ''; + + for (var i = 0; i < columnCount; i++) { + const columnWidths = Array + .from(gridElement.querySelectorAll(`[grid-column="${i + 1}"]`)) + .flatMap((x) => x.offsetWidth); + + const maxColumnWidth = Math.max(...columnWidths); + + gridTemplateColumns += ` ${maxColumnWidth}fr`; + } + + gridElement.setAttribute("grid-template-columns", gridTemplateColumns); + gridElement.classList.remove("auto-fit"); + + initialColumnsWidths = gridTemplateColumns; +} + export function resizeColumnExact(gridElement, column, width) { let headers = gridElement.querySelectorAll('.column-header.resizable'); diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css index 0b71c25e35..3bb056b485 100644 --- a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css +++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css @@ -2,6 +2,12 @@ fluent-data-grid-cell { text-overflow: ellipsis; } +.auto-fit fluent-data-grid-cell { + text-overflow: unset; + overflow: visible; + visibility: hidden; +} + .multiline-text { white-space: inherit; overflow: auto; @@ -18,14 +24,12 @@ fluent-data-grid-cell { } .column-header.col-justify-end, .column-header.col-justify-right { - padding-right: 1px; padding-left: 1px; justify-content: end; } .column-header.col-justify-center { - padding-left: 1px; padding-right: 1px; justify-content: center; @@ -36,20 +40,20 @@ fluent-data-grid-cell { flex-grow: 1; } - .column-header > ::deep .keycapture .col-sort-container { - display: flex; - flex-grow: 1; - } + .column-header > ::deep .keycapture .col-sort-container { + display: flex; + flex-grow: 1; + } - .column-header > ::deep .keycapture .col-sort-container .col-sort-button { - flex-grow: 1; - padding-inline-end: 5px; - } + .column-header > ::deep .keycapture .col-sort-container .col-sort-button { + flex-grow: 1; + padding-inline-end: 5px; + } - .column-header > ::deep .keycapture .col-sort-container .col-sort-button::part(content) { - overflow: hidden; - text-overflow: ellipsis; - } + .column-header > ::deep .keycapture .col-sort-container .col-sort-button::part(content) { + overflow: hidden; + text-overflow: ellipsis; + } .column-header.col-justify-end ::deep .col-title-text, .column-header.col-justify-right ::deep .col-title-text { text-align: end; @@ -109,6 +113,3 @@ fluent-data-grid-cell { white-space: nowrap; font-weight: 600; } - - -