+ {
+ }
+}
diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor
new file mode 100644
index 0000000000..065ea8cba4
--- /dev/null
+++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor
@@ -0,0 +1,20 @@
+@using Microsoft.FluentUI.AspNetCore.Components
+@using FluentUI.Demo.Shared.SampleData
+
+
+
+ @(ceoItem?.IsCollapsed == true ? "Expand" : "Collapse") CEO
+
+
+
+
+
+ @(context.Item.FirstName + " " + context.Item.LastName)
+
+
+
+
+
+
+
diff --git a/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor.cs b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor.cs
new file mode 100644
index 0000000000..090f95aa86
--- /dev/null
+++ b/examples/Demo/Shared/Pages/DataGrid/Examples/DataGridHierarchicalOrgChart.razor.cs
@@ -0,0 +1,195 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.FluentUI.AspNetCore.Components;
+
+namespace FluentUI.Demo.Shared.Pages.DataGrid.Examples;
+
+public partial class DataGridHierarchicalOrgChart
+{
+ private static readonly Random Random = new();
+ private static readonly string[] FirstNames = { "John", "Jane", "Alex", "Emily", "Michael", "Sarah", "David", "Laura", "Robert", "Jessica", "James", "Olivia", "William", "Emma", "Benjamin", "Ava", "Lucas", "Sophia", "Henry", "Isabella", "Alexander", "Mia", "Ethan", "Charlotte", "Michael", "Amelia", "Daniel", "Harper", "Matthew" };
+ private static readonly string[] LastNames = { "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin" };
+ private static readonly string[] JobTitles = { "Software Engineer", "Project Manager", "Data Analyst", "Product Manager", "Graphic Designer", "HR Specialist", "Accountant", "Marketing Coordinator", "Sales Representative", "Customer Support", "IT Consultant", "Business Analyst", "Operations Manager", "UX Designer", "DevOps Engineer", "Quality Assurance Engineer", "Financial Analyst", "Content Writer", "Systems Administrator", "Network Engineer" };
+ private static readonly string[] Departments = { "Engineering", "Marketing", "Human Resources", "Sales", "IT", "Finance", "Operations", "Customer Support", "Design", "Product Management", "Quality Assurance", "Legal", "Research and Development", "Administration", "Business Development", "Logistics", "Procurement", "Compliance", "Corporate Strategy", "Public Relations" };
+ private static readonly string[] Locations = { "Washington", "London", "Tokyo", "Paris", "Berlin", "Ottawa", "Canberra", "Rome", "Madrid", "Beijing", "Moscow", "Brasília", "New Delhi", "Ottawa", "Buenos Aires", "Cairo", "Bangkok", "Seoul", "Jakarta", "Wellington" };
+ private static readonly string[] Companies = { "TechCorp", "GlobalSolutions", "BlueSky Enterprises", "Pinnacle Systems", "SynergySoft", "Apex Industries", "QuantumTech", "FutureWave", "Optima Consulting", "Vertex Innovations", "NextGen Solutions", "BrightPath", "Unified Technologies", "Visionary Labs", "EliteWorks", "PrimeTech", "Starline Group", "Skyline Enterprises", "FusionCorp" };
+
+ private PersonGridItem? ceoItem;
+ private readonly List items = [];
+
+ protected override void OnInitialized()
+ {
+ var allPeople = GeneratePersons(30).ToList();
+
+ // Level 0: CEO/Manager
+ var ceo = allPeople[0] with { FirstName = "Mads", LastName = "Torgersen", JobTitle = "CEO", Department = "Executive" };
+ ceoItem = new PersonGridItem
+ {
+ Item = ceo,
+ IsCollapsed = false
+ };
+ items.Add(ceoItem);
+
+ for (var i = 1; i <= 3; i++)
+ {
+ var manager = allPeople[i] with { JobTitle = "Director", Department = "Engineering" };
+ var managerItem = new PersonGridItem { Item = manager, Depth = 1, IsCollapsed = i != 2 };
+ ceoItem.Children.Add(managerItem);
+ items.Add(managerItem);
+
+ // Level 2: Employees
+ for (var j = 0; j < 4; j++)
+ {
+ var employee = allPeople[4 + ((i - 1) * 4) + j];
+ var employeeItem = new PersonGridItem { Item = employee, Depth = 2, IsHidden = i != 2 };
+ managerItem.Children.Add(employeeItem);
+ items.Add(employeeItem);
+ }
+ }
+ }
+
+ private static IEnumerable GeneratePersons(int count)
+ {
+ var people = new List(count);
+
+ for (var i = 0; i < count; i++)
+ {
+ var id = RandomNumber(10000, 99999).ToString(CultureInfo.InvariantCulture);
+ var firstName = FirstNames[Random.Next(FirstNames.Length)];
+ var lastName = LastNames[Random.Next(LastNames.Length)];
+ var male = Random.Next(2) == 0;
+ var initials = $"{firstName[0]}{lastName[0]}";
+ var birthDay = new DateTime(RandomNumber(1960, 2000), RandomNumber(1, 12), RandomNumber(1, 28));
+ var jobTitle = JobTitles[Random.Next(JobTitles.Length)];
+ var company = Companies[Random.Next(Companies.Length)];
+ var email = $"{firstName.ToLower()}.{lastName.ToLower()}@{company.ToLower()}.com";
+ var department = Departments[Random.Next(Departments.Length)];
+ var phone = GeneratePhoneNumber();
+ var location = Locations[Random.Next(Locations.Length)];
+
+ people.Add(new Person(id, firstName, lastName, male, initials, birthDay, email, jobTitle, department, company, phone, location));
+ }
+
+ return people;
+ }
+
+ private void ToggleCEO()
+ {
+ ceoItem?.IsCollapsed = !ceoItem.IsCollapsed;
+ }
+
+ public class PersonGridItem : HierarchicalGridItem
+ {
+ }
+
+ ///
+ private static string GeneratePhoneNumber()
+ {
+ return $"({RandomNumber(100, 999)}) {RandomNumber(100, 999)}-{RandomNumber(1000, 9999)}";
+ }
+
+ ///
+ private static int RandomNumber(int min, int max)
+ {
+ return Random.Next(min, max);
+ }
+
+ ///
+ /// Definition of a Person
+ ///
+ /// Id
+ /// First name
+ /// Last name
+ /// Male
+ /// Initials
+ /// Birth day
+ /// Email
+ /// Job title
+ /// Department
+ /// Company
+ /// Phone
+ /// Location
+ public record Person(string Id, string FirstName, string LastName, bool Male, string Initials, DateTime BirthDay, string Email, string JobTitle, string Department, string Company, string Phone, string Location)
+ {
+ ///
+ /// Gets the age of the person
+ ///
+ public int Age => DateTime.Today.Year - BirthDay.Year;
+
+ ///
+ public override string ToString()
+ {
+ return $"{Id} - {FirstName} {LastName}";
+ }
+ }
+
+ ///
+ /// Definition of an Employee
+ ///
+ /// Id
+ /// First name
+ /// Last name
+ /// Job title
+ public record Employee(string Id, string FirstName, string LastName, string JobTitle) { }
+
+ ///
+ /// Definition of a Department
+ ///
+ /// Department Id
+ /// Department name
+ /// List of employees
+ public record Department(string Id, string Name, IEnumerable Employees) { }
+
+ ///
+ /// Definition of a Company
+ ///
+ /// Id
+ /// Company name
+ /// List of departments
+ public record Company(string Id, string Name, IEnumerable Departments) { };
+
+ ///
+ /// Generates a list of companies with random data.
+ ///
+ /// Number of companies
+ /// Number of departments
+ /// Number of employees
+ ///
+ public static IEnumerable GetOrganization(int companyCount, int departmentCount, int employeeCount)
+ {
+ var companies = new List(companyCount);
+
+ for (var i = 0; i < companyCount; i++)
+ {
+ var companyId = "C" + RandomNumber(1000, 9999).ToString(CultureInfo.InvariantCulture);
+ var companyName = Companies[Random.Next(Companies.Length)];
+
+ var departments = new List(departmentCount);
+ for (var j = 0; j < departmentCount; j++)
+ {
+ var departmentId = "D" + RandomNumber(100, 999).ToString(CultureInfo.InvariantCulture);
+ var departmentName = Departments[Random.Next(Departments.Length)];
+
+ var employees = new List(employeeCount);
+ for (var k = 0; k < employeeCount; k++)
+ {
+ var employeeId = "E" + RandomNumber(10000, 99999).ToString(CultureInfo.InvariantCulture);
+ var firstName = FirstNames[Random.Next(FirstNames.Length)];
+ var lastName = LastNames[Random.Next(LastNames.Length)];
+ var jobTitle = JobTitles[Random.Next(JobTitles.Length)];
+
+ employees.Add(new Employee(employeeId, firstName, lastName, jobTitle));
+ }
+
+ departments.Add(new Department(departmentId, departmentName, employees));
+ }
+
+ companies.Add(new Company(companyId, companyName, departments));
+ }
+
+ return companies;
+ }
+}
diff --git a/examples/Demo/Shared/Pages/DataGrid/Pages/DataGridHierarchicalPage.razor b/examples/Demo/Shared/Pages/DataGrid/Pages/DataGridHierarchicalPage.razor
new file mode 100644
index 0000000000..eddafb860e
--- /dev/null
+++ b/examples/Demo/Shared/Pages/DataGrid/Pages/DataGridHierarchicalPage.razor
@@ -0,0 +1,20 @@
+@page "/datagrid-hierarchical"
+@using FluentUI.Demo.Shared.Pages.DataGrid.Examples
+
+DataGrid - Hierarchical data
+
+
+
+ The FluentDataGrid supports displaying hierarchical data.
+ By using the HierarchicalGridItem class (or a class derived from it) as the grid item type,
+ you can define parent-child relationships between items.
+
+ Set the HierarchicalToggle parameter to true on one of the columns to display the expand/collapse button.
+
+
+
+
+ The hierarchical data grid can display multiple levels of nesting. In this example, we show an organization chart with three levels: CEO, Directors, and Employees.
+
+ This example also shows how to programmatically expand and collapse rows by manually setting the IsCollapsed property on a HierarchicalGridItem instance.
+
diff --git a/examples/Demo/Shared/SampleData/DataSource.cs b/examples/Demo/Shared/SampleData/DataSource.cs
index ab3f3f8dcc..791f74d0f2 100644
--- a/examples/Demo/Shared/SampleData/DataSource.cs
+++ b/examples/Demo/Shared/SampleData/DataSource.cs
@@ -13,7 +13,7 @@ public class DataSource
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "This is just being used for sample data")]
public IQueryable People { get; } = _people.AsQueryable();
- public List Names = _people.Select(p => $"{p.FirstName} {p.LastName}").ToList();
+ public List Names = [.. _people.Select(p => $"{p.FirstName} {p.LastName}")];
public List Hits { get; } =
[
@@ -72,7 +72,7 @@ public Task GetCountriesAsync()
return Task.FromResult(_olympics_2024);
}
- private readonly static Person[] _people =
+ private static readonly Person[] _people =
[
new Person ( PersonId: 1, FirstName: "Jean", LastName: "Martin", CountryCode: "FR", BirthDate: new DateOnly(1985, 3, 16), Picture: ImageFaces[0] ),
new Person ( PersonId: 2, FirstName: "António", LastName: "Langa", CountryCode: "MZ", BirthDate: new DateOnly(1991, 12, 1), Picture: ImageFaces[1] ),
@@ -92,13 +92,12 @@ public class MonthItem
public override string ToString() => $"{Index:00} {Name}";
}
- public static MonthItem[] AllMonths = Enumerable.Range(0, 12)
+ public static MonthItem[] AllMonths = [.. Enumerable.Range(0, 12)
.Select(i => new MonthItem
{
Index = $"{i + 1:00}",
Name = GetMonthName(i)
- })
- .ToArray();
+ })];
private static string GetMonthName(int index)
{
@@ -109,200 +108,110 @@ private static string GetMonthName(int index)
.ElementAt(index % 12);
}
- // Content derived from https://www.kaggle.com/datasets/arjunprasadsarkhel/2021-olympics-in-tokyo,
- // which is licensed as https://creativecommons.org/licenses/by-sa/4.0/
- private readonly static Country[] _olympics_2021 =
- [
- new Country("ar", "Argentina", new Medals { Gold = 0, Silver = 1, Bronze = 2 }),
- new Country("am", "Armenia", new Medals { Gold = 0, Silver = 2, Bronze = 2 }),
- new Country("au", "Australia", new Medals { Gold = 17, Silver = 7, Bronze = 22 }),
- new Country("at", "Austria", new Medals { Gold = 1, Silver = 1, Bronze = 5 }),
- new Country("az", "Azerbaijan", new Medals { Gold = 0, Silver = 3, Bronze = 4 }),
- new Country("bs", "Bahamas", new Medals { Gold = 2, Silver = 0, Bronze = 0 }),
- new Country("bh", "Bahrain", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("by", "Belarus", new Medals { Gold = 1, Silver = 3, Bronze = 3 }),
- new Country("be", "Belgium", new Medals { Gold = 3, Silver = 1, Bronze = 3 }),
- new Country("bm", "Bermuda", new Medals { Gold = 1, Silver = 0, Bronze = 0 }),
- new Country("bw", "Botswana", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("br", "Brazil", new Medals { Gold = 7, Silver = 6, Bronze = 8 }),
- new Country("bg", "Bulgaria", new Medals { Gold = 3, Silver = 1, Bronze = 2 }),
- new Country("bf", "Burkina Faso", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("ca", "Canada", new Medals { Gold = 7, Silver = 6, Bronze = 11 }),
- new Country("tpe", "Chinese Taipei", new Medals { Gold = 2, Silver = 4, Bronze = 6 }),
- new Country("co", "Colombia", new Medals { Gold = 0, Silver = 4, Bronze = 1 }),
- new Country("ci", "Côte d'Ivoire", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("hr", "Croatia", new Medals { Gold = 3, Silver = 3, Bronze = 2 }),
- new Country("cu", "Cuba", new Medals { Gold = 7, Silver = 3, Bronze = 5 }),
- new Country("cz", "Czech Republic", new Medals { Gold = 4, Silver = 4, Bronze = 3 }),
- new Country("dk", "Denmark", new Medals { Gold = 3, Silver = 4, Bronze = 4 }),
- new Country("do", "Dominican Republic", new Medals { Gold = 0, Silver = 3, Bronze = 2 }),
- new Country("ec", "Ecuador", new Medals { Gold = 2, Silver = 1, Bronze = 0 }),
- new Country("eg", "Egypt", new Medals { Gold = 1, Silver = 1, Bronze = 4 }),
- new Country("ee", "Estonia", new Medals { Gold = 1, Silver = 0, Bronze = 1 }),
- new Country("et", "Ethiopia", new Medals { Gold = 1, Silver = 1, Bronze = 2 }),
- new Country("fj", "Fiji", new Medals { Gold = 1, Silver = 0, Bronze = 1 }),
- new Country("fi", "Finland", new Medals { Gold = 0, Silver = 0, Bronze = 2 }),
- new Country("fr", "France", new Medals { Gold = 10, Silver = 12, Bronze = 11 }),
- new Country("ge", "Georgia", new Medals { Gold = 2, Silver = 5, Bronze = 1 }),
- new Country("de", "Germany", new Medals { Gold = 10, Silver = 11, Bronze = 16 }),
- new Country("gh", "Ghana", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("gb", "Great Britain", new Medals { Gold = 22, Silver = 21, Bronze = 22 }),
- new Country("gr", "Greece", new Medals { Gold = 2, Silver = 1, Bronze = 1 }),
- new Country("gd", "Grenada", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("hk", "Hong Kong, China", new Medals { Gold = 1, Silver = 2, Bronze = 3 }),
- new Country("hu", "Hungary", new Medals { Gold = 6, Silver = 7, Bronze = 7 }),
- new Country("in", "India", new Medals { Gold = 1, Silver = 2, Bronze = 4 }),
- new Country("id", "Indonesia", new Medals { Gold = 1, Silver = 1, Bronze = 3 }),
- new Country("ie", "Ireland", new Medals { Gold = 2, Silver = 0, Bronze = 2 }),
- new Country("ir", "Islamic Republic of Iran", new Medals { Gold = 3, Silver = 2, Bronze = 2 }),
- new Country("il", "Israel", new Medals { Gold = 2, Silver = 0, Bronze = 2 }),
- new Country("it", "Italy", new Medals { Gold = 10, Silver = 10, Bronze = 20 }),
- new Country("jm", "Jamaica", new Medals { Gold = 4, Silver = 1, Bronze = 4 }),
- new Country("jp", "Japan", new Medals { Gold = 27, Silver = 14, Bronze = 17 }),
- new Country("jo", "Jordan", new Medals { Gold = 0, Silver = 1, Bronze = 1 }),
- new Country("kz", "Kazakhstan", new Medals { Gold = 0, Silver = 0, Bronze = 8 }),
- new Country("ke", "Kenya", new Medals { Gold = 4, Silver = 4, Bronze = 2 }),
- new Country("xk", "Kosovo", new Medals { Gold = 2, Silver = 0, Bronze = 0 }),
- new Country("kw", "Kuwait", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("kg", "Kyrgyzstan", new Medals { Gold = 0, Silver = 2, Bronze = 1 }),
- new Country("lv", "Latvia", new Medals { Gold = 1, Silver = 0, Bronze = 1 }),
- new Country("lt", "Lithuania", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("my", "Malaysia", new Medals { Gold = 0, Silver = 1, Bronze = 1 }),
- new Country("mx", "Mexico", new Medals { Gold = 0, Silver = 0, Bronze = 4 }),
- new Country("mn", "Mongolia", new Medals { Gold = 0, Silver = 1, Bronze = 3 }),
- new Country("ma", "Morocco", new Medals { Gold = 1, Silver = 0, Bronze = 0 }),
- new Country("na", "Namibia", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("nl", "Netherlands", new Medals { Gold = 10, Silver = 12, Bronze = 14 }),
- new Country("nz", "New Zealand", new Medals { Gold = 7, Silver = 6, Bronze = 7 }),
- new Country("ng", "Nigeria", new Medals { Gold = 0, Silver = 1, Bronze = 1 }),
- new Country("mk", "North Macedonia", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("no", "Norway", new Medals { Gold = 4, Silver = 2, Bronze = 2 }),
- new Country("cn", "People's Republic of China", new Medals { Gold = 38, Silver = 32, Bronze = 18 }),
- new Country("ph", "Philippines", new Medals { Gold = 1, Silver = 2, Bronze = 1 }),
- new Country("pl", "Poland", new Medals { Gold = 4, Silver = 5, Bronze = 5 }),
- new Country("pt", "Portugal", new Medals { Gold = 1, Silver = 1, Bronze = 2 }),
- new Country("pr", "Puerto Rico", new Medals { Gold = 1, Silver = 0, Bronze = 0 }),
- new Country("qa", "Qatar", new Medals { Gold = 2, Silver = 0, Bronze = 1 }),
- new Country("kr", "Republic of Korea", new Medals { Gold = 6, Silver = 4, Bronze = 10 }),
- new Country("md", "Republic of Moldova", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("roc", "ROC", new Medals { Gold = 20, Silver = 28, Bronze = 23 }),
- new Country("ro", "Romania", new Medals { Gold = 1, Silver = 3, Bronze = 0 }),
- new Country("sm", "San Marino", new Medals { Gold = 0, Silver = 1, Bronze = 2 }),
- new Country("sa", "Saudi Arabia", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("rs", "Serbia", new Medals { Gold = 3, Silver = 1, Bronze = 5 }),
- new Country("sk", "Slovakia", new Medals { Gold = 1, Silver = 2, Bronze = 1 }),
- new Country("si", "Slovenia", new Medals { Gold = 3, Silver = 1, Bronze = 1 }),
- new Country("za", "South Africa", new Medals { Gold = 1, Silver = 2, Bronze = 0 }),
- new Country("es", "Spain", new Medals { Gold = 3, Silver = 8, Bronze = 6 }),
- new Country("se", "Sweden", new Medals { Gold = 3, Silver = 6, Bronze = 0 }),
- new Country("ch", "Switzerland", new Medals { Gold = 3, Silver = 4, Bronze = 6 }),
- new Country("sy", "Syrian Arab Republic", new Medals { Gold = 0, Silver = 0, Bronze = 1 }),
- new Country("th", "Thailand", new Medals { Gold = 1, Silver = 0, Bronze = 1 }),
- new Country("tn", "Tunisia", new Medals { Gold = 1, Silver = 1, Bronze = 0 }),
- new Country("tr", "Turkey", new Medals { Gold = 2, Silver = 2, Bronze = 9 }),
- new Country("tm", "Turkmenistan", new Medals { Gold = 0, Silver = 1, Bronze = 0 }),
- new Country("ug", "Uganda", new Medals { Gold = 2, Silver = 1, Bronze = 1 }),
- new Country("ua", "Ukraine", new Medals { Gold = 1, Silver = 6, Bronze = 12 }),
- new Country("us", "United States of America", new Medals { Gold = 39, Silver = 41, Bronze = 33 }),
- new Country("uz", "Uzbekistan", new Medals { Gold = 3, Silver = 0, Bronze = 2 }),
- new Country("ve", "Venezuela", new Medals { Gold = 1, Silver = 3, Bronze = 0 }),
+ public static Country[] Continents = [
+ new Country("AF", "Africa", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
+ new Country("AS", "Asia", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
+ new Country("EU", "Europe", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
+ new Country("NA", "North America", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
+ new Country("OC", "Oceania", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
+ new Country("SA", "South America", null, new Medals { Gold = 0, Silver = 0, Bronze = 0}),
];
// Content derived from https://www.kaggle.com/datasets/berkayalan/paris-2024-olympics-medals
// which is licensed as MIT (https://www.mit.edu/~amini/LICENSE.md)
public static Country[] _olympics_2024 =
[
- new Country("al", "Albania", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
- new Country("dz", "Algeria", new Medals { Gold = 2, Silver = 0, Bronze = 1}),
- new Country("ar", "Argentina", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
- new Country("am", "Armenia", new Medals { Gold = 0, Silver = 3, Bronze = 1}),
- new Country("au", "Australia", new Medals { Gold = 18, Silver = 19, Bronze = 16}),
- new Country("at", "Austria", new Medals { Gold = 2, Silver = 0, Bronze = 3}),
- new Country("az", "Azerbaijan", new Medals { Gold = 2, Silver = 2, Bronze = 3}),
- new Country("be", "Belgium", new Medals { Gold = 3, Silver = 1, Bronze = 6}),
- new Country("bh", "Bahrain", new Medals { Gold = 2, Silver = 1, Bronze = 1}),
- new Country("bw", "Botswana", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
- new Country("br", "Brazil", new Medals { Gold = 3, Silver = 7, Bronze = 10}),
- new Country("bg", "Bulgaria", new Medals { Gold = 3, Silver = 1, Bronze = 3}),
- new Country("ca", "Canada", new Medals { Gold = 9, Silver = 7, Bronze = 11}),
- new Country("cl", "Chile", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
- new Country("cn", "People's Republic of China", new Medals { Gold = 40, Silver = 27, Bronze = 24}),
- new Country("ci", "Ivory Coast", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("co", "Colombia", new Medals { Gold = 0, Silver = 3, Bronze = 1}),
- new Country("cv", "Cape Verde", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("hr", "Croatia", new Medals { Gold = 2, Silver = 2, Bronze = 3}),
- new Country("cu", "Cuba", new Medals { Gold = 2, Silver = 1, Bronze = 6}),
- new Country("cy", "Cyprus", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
- new Country("cz", "Czech Republic", new Medals { Gold = 3, Silver = 0, Bronze = 2}),
- new Country("dk", "Denmark", new Medals { Gold = 2, Silver = 2, Bronze = 5}),
- new Country("dm", "Dominica", new Medals { Gold = 1, Silver = 0, Bronze = 0}),
- new Country("do", "Dominican Republic", new Medals { Gold = 1, Silver = 0, Bronze = 2}),
- new Country("ec", "Ecuador", new Medals { Gold = 1, Silver = 2, Bronze = 2}),
- new Country("eg", "Egypt", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
- new Country("xx", "Refugee Olympic Team", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("et", "Ethiopia", new Medals { Gold = 1, Silver = 3, Bronze = 0}),
- new Country("fj", "Fiji", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
- new Country("fr", "France", new Medals { Gold = 16, Silver = 26, Bronze = 22}),
- new Country("gb", "Great Britain", new Medals { Gold = 14, Silver = 22, Bronze = 29}),
- new Country("ge", "Georgia", new Medals { Gold = 3, Silver = 3, Bronze = 1}),
- new Country("de", "Germany", new Medals { Gold = 12, Silver = 13, Bronze = 8}),
- new Country("gr", "Greece", new Medals { Gold = 1, Silver = 1, Bronze = 6}),
- new Country("gd", "Grenada", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
- new Country("gt", "Guatemala", new Medals { Gold = 1, Silver = 0, Bronze = 1}),
- new Country("hk", "Hong Kong", new Medals { Gold = 2, Silver = 0, Bronze = 2}),
- new Country("hu", "Hungary", new Medals { Gold = 6, Silver = 7, Bronze = 6}),
- new Country("id", "Indonesia", new Medals { Gold = 2, Silver = 0, Bronze = 1}),
- new Country("in", "India", new Medals { Gold = 0, Silver = 1, Bronze = 5}),
- new Country("ie", "Ireland", new Medals { Gold = 4, Silver = 0, Bronze = 3}),
- new Country("ir", "Iran", new Medals { Gold = 3, Silver = 6, Bronze = 3}),
- new Country("il", "Israel", new Medals { Gold = 1, Silver = 5, Bronze = 1}),
- new Country("it", "Italy", new Medals { Gold = 12, Silver = 13, Bronze = 15}),
- new Country("jm", "Jamaica", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
- new Country("jo", "Jordan", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
- new Country("jp", "Japan", new Medals { Gold = 20, Silver = 12, Bronze = 13}),
- new Country("kz", "Kazakhstan", new Medals { Gold = 1, Silver = 3, Bronze = 3}),
- new Country("ke", "Kenya", new Medals { Gold = 4, Silver = 2, Bronze = 5}),
- new Country("kg", "Kyrgyzstan", new Medals { Gold = 0, Silver = 2, Bronze = 4}),
- new Country("kr", "South Korea", new Medals { Gold = 13, Silver = 9, Bronze = 10}),
- new Country("xk", "Kosovo", new Medals { Gold = 0, Silver = 1, Bronze = 1}),
- new Country("lc", "St Lucia", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
- new Country("lt", "Lithuania", new Medals { Gold = 0, Silver = 2, Bronze = 2}),
- new Country("my", "Malaysia", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
- new Country("md", "Moldova", new Medals { Gold = 0, Silver = 1, Bronze = 3}),
- new Country("mx", "Mexico", new Medals { Gold = 0, Silver = 3, Bronze = 2}),
- new Country("mn", "Mongolia", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
- new Country("ma", "Morocco", new Medals { Gold = 1, Silver = 0, Bronze = 1}),
- new Country("nl", "Netherlands", new Medals { Gold = 15, Silver = 7, Bronze = 12}),
- new Country("no", "Norway", new Medals { Gold = 4, Silver = 1, Bronze = 3}),
- new Country("nz", "New Zealand", new Medals { Gold = 10, Silver = 7, Bronze = 3}),
- new Country("pa", "Panama", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
- new Country("pe", "Peru", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("ph", "Philippines", new Medals { Gold = 2, Silver = 0, Bronze = 2}),
- new Country("pk", "Pakistan", new Medals { Gold = 1, Silver = 0, Bronze = 0}),
- new Country("pl", "Poland", new Medals { Gold = 1, Silver = 4, Bronze = 5}),
- new Country("pt", "Portugal", new Medals { Gold = 1, Silver = 2, Bronze = 1}),
- new Country("kp", "North Korea", new Medals { Gold = 0, Silver = 2, Bronze = 4}),
- new Country("pr", "Puerto Rico", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
- new Country("qa", "Qatar", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("ro", "Romania", new Medals { Gold = 3, Silver = 4, Bronze = 2}),
- new Country("za", "South Africa", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
- new Country("rs", "Serbia", new Medals { Gold = 3, Silver = 1, Bronze = 1}),
- new Country("sg", "Singapore", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("si", "Slovenia", new Medals { Gold = 2, Silver = 1, Bronze = 0}),
- new Country("es", "Spain", new Medals { Gold = 5, Silver = 4, Bronze = 9}),
- new Country("sk", "Slovakia", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
- new Country("se", "Sweden", new Medals { Gold = 4, Silver = 4, Bronze = 3}),
- new Country("ch", "Switzerland", new Medals { Gold = 1, Silver = 2, Bronze = 5}),
- new Country("th", "Thailand", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
- new Country("tj", "Tajikistan", new Medals { Gold = 0, Silver = 0, Bronze = 3}),
- new Country("tpe", "Chinese Taipei", new Medals { Gold = 2, Silver = 0, Bronze = 5}),
- new Country("tn", "Tunisia", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
- new Country("tr", "Turkey", new Medals { Gold = 0, Silver = 3, Bronze = 5}),
- new Country("ug", "Uganda", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
- new Country("ua", "Ukraine", new Medals { Gold = 3, Silver = 5, Bronze = 4}),
- new Country("us", "United States of America", new Medals { Gold = 40, Silver = 44, Bronze = 42}),
- new Country("uz", "Uzbekistan", new Medals { Gold = 8, Silver = 2, Bronze = 3}),
- new Country("zm", "Zambia", new Medals { Gold = 0, Silver = 0, Bronze = 1})
+ new Country("al", "Albania", "EU", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
+ new Country("dz", "Algeria", "AF", new Medals { Gold = 2, Silver = 0, Bronze = 1}),
+ new Country("ar", "Argentina", "SA", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
+ new Country("am", "Armenia", "AS", new Medals { Gold = 0, Silver = 3, Bronze = 1}),
+ new Country("au", "Australia", "OC", new Medals { Gold = 18, Silver = 19, Bronze = 16}),
+ new Country("at", "Austria", "EU", new Medals { Gold = 2, Silver = 0, Bronze = 3}),
+ new Country("az", "Azerbaijan", "AS", new Medals { Gold = 2, Silver = 2, Bronze = 3}),
+ new Country("be", "Belgium", "EU", new Medals { Gold = 3, Silver = 1, Bronze = 6}),
+ new Country("bh", "Bahrain", "AS", new Medals { Gold = 2, Silver = 1, Bronze = 1}),
+ new Country("bw", "Botswana", "AF", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
+ new Country("br", "Brazil", "SA", new Medals { Gold = 3, Silver = 7, Bronze = 10}),
+ new Country("bg", "Bulgaria", "EU", new Medals { Gold = 3, Silver = 1, Bronze = 3}),
+ new Country("ca", "Canada", "NA", new Medals { Gold = 9, Silver = 7, Bronze = 11}),
+ new Country("cl", "Chile", "SA", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
+ new Country("cn", "People's Republic of China", "AS", new Medals { Gold = 40, Silver = 27, Bronze = 24}),
+ new Country("ci", "Ivory Coast", "AF", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("co", "Colombia", "SA", new Medals { Gold = 0, Silver = 3, Bronze = 1}),
+ new Country("cv", "Cape Verde", "AF", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("hr", "Croatia", "EU", new Medals { Gold = 2, Silver = 2, Bronze = 3}),
+ new Country("cu", "Cuba", "NA", new Medals { Gold = 2, Silver = 1, Bronze = 6}),
+ new Country("cy", "Cyprus", "AS", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
+ new Country("cz", "Czech Republic", "EU", new Medals { Gold = 3, Silver = 0, Bronze = 2}),
+ new Country("dk", "Denmark", "EU", new Medals { Gold = 2, Silver = 2, Bronze = 5}),
+ new Country("dm", "Dominica", "NA", new Medals { Gold = 1, Silver = 0, Bronze = 0}),
+ new Country("do", "Dominican Republic", "NA", new Medals { Gold = 1, Silver = 0, Bronze = 2}),
+ new Country("ec", "Ecuador", "SA", new Medals { Gold = 1, Silver = 2, Bronze = 2}),
+ new Country("eg", "Egypt", "AF", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
+ new Country("xx", "Refugee Olympic Team", "", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("et", "Ethiopia", "AF", new Medals { Gold = 1, Silver = 3, Bronze = 0}),
+ new Country("fj", "Fiji", "OC", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
+ new Country("fr", "France", "EU", new Medals { Gold = 16, Silver = 26, Bronze = 22}),
+ new Country("gb", "Great Britain", "EU", new Medals { Gold = 14, Silver = 22, Bronze = 29}),
+ new Country("ge", "Georgia", "AS", new Medals { Gold = 3, Silver = 3, Bronze = 1}),
+ new Country("de", "Germany", "EU", new Medals { Gold = 12, Silver = 13, Bronze = 8}),
+ new Country("gr", "Greece", "EU", new Medals { Gold = 1, Silver = 1, Bronze = 6}),
+ new Country("gd", "Grenada", "NA", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
+ new Country("gt", "Guatemala", "NA", new Medals { Gold = 1, Silver = 0, Bronze = 1}),
+ new Country("hk", "Hong Kong", "AS", new Medals { Gold = 2, Silver = 0, Bronze = 2}),
+ new Country("hu", "Hungary", "EU", new Medals { Gold = 6, Silver = 7, Bronze = 6}),
+ new Country("id", "Indonesia", "AS", new Medals { Gold = 2, Silver = 0, Bronze = 1}),
+ new Country("in", "India", "AS", new Medals { Gold = 0, Silver = 1, Bronze = 5}),
+ new Country("ie", "Ireland", "EU", new Medals { Gold = 4, Silver = 0, Bronze = 3}),
+ new Country("ir", "Iran", "AS", new Medals { Gold = 3, Silver = 6, Bronze = 3}),
+ new Country("il", "Israel", "AS", new Medals { Gold = 1, Silver = 5, Bronze = 1}),
+ new Country("it", "Italy", "EU", new Medals { Gold = 12, Silver = 13, Bronze = 15}),
+ new Country("jm", "Jamaica", "NA", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
+ new Country("jo", "Jordan", "AS", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
+ new Country("jp", "Japan", "AS", new Medals { Gold = 20, Silver = 12, Bronze = 13}),
+ new Country("kz", "Kazakhstan", "AS", new Medals { Gold = 1, Silver = 3, Bronze = 3}),
+ new Country("ke", "Kenya", "AF", new Medals { Gold = 4, Silver = 2, Bronze = 5}),
+ new Country("kg", "Kyrgyzstan", "AS", new Medals { Gold = 0, Silver = 2, Bronze = 4}),
+ new Country("kr", "South Korea", "AS", new Medals { Gold = 13, Silver = 9, Bronze = 10}),
+ new Country("xk", "Kosovo", "EU", new Medals { Gold = 0, Silver = 1, Bronze = 1}),
+ new Country("lc", "St Lucia", "NA", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
+ new Country("lt", "Lithuania", "EU", new Medals { Gold = 0, Silver = 2, Bronze = 2}),
+ new Country("my", "Malaysia", "AS", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
+ new Country("md", "Moldova", "EU", new Medals { Gold = 0, Silver = 1, Bronze = 3}),
+ new Country("mx", "Mexico", "NA", new Medals { Gold = 0, Silver = 3, Bronze = 2}),
+ new Country("mn", "Mongolia", "AS", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
+ new Country("ma", "Morocco", "AF", new Medals { Gold = 1, Silver = 0, Bronze = 1}),
+ new Country("nl", "Netherlands", "EU", new Medals { Gold = 15, Silver = 7, Bronze = 12}),
+ new Country("no", "Norway", "EU", new Medals { Gold = 4, Silver = 1, Bronze = 3}),
+ new Country("nz", "New Zealand", "OC", new Medals { Gold = 10, Silver = 7, Bronze = 3}),
+ new Country("pa", "Panama", "NA", new Medals { Gold = 0, Silver = 1, Bronze = 0}),
+ new Country("pe", "Peru", "SA", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("ph", "Philippines", "AS", new Medals { Gold = 2, Silver = 0, Bronze = 2}),
+ new Country("pk", "Pakistan", "AS", new Medals { Gold = 1, Silver = 0, Bronze = 0}),
+ new Country("pl", "Poland", "EU", new Medals { Gold = 1, Silver = 4, Bronze = 5}),
+ new Country("pt", "Portugal", "EU", new Medals { Gold = 1, Silver = 2, Bronze = 1}),
+ new Country("kp", "North Korea", "AS", new Medals { Gold = 0, Silver = 2, Bronze = 4}),
+ new Country("pr", "Puerto Rico", "NA", new Medals { Gold = 0, Silver = 0, Bronze = 2}),
+ new Country("qa", "Qatar", "AS", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("ro", "Romania", "EU", new Medals { Gold = 3, Silver = 4, Bronze = 2}),
+ new Country("za", "South Africa", "AF", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
+ new Country("rs", "Serbia", "EU", new Medals { Gold = 3, Silver = 1, Bronze = 1}),
+ new Country("sg", "Singapore", "AS", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("si", "Slovenia", "EU", new Medals { Gold = 2, Silver = 1, Bronze = 0}),
+ new Country("es", "Spain", "EU", new Medals { Gold = 5, Silver = 4, Bronze = 9}),
+ new Country("sk", "Slovakia", "EU", new Medals { Gold = 0, Silver = 0, Bronze = 1}),
+ new Country("se", "Sweden", "EU", new Medals { Gold = 4, Silver = 4, Bronze = 3}),
+ new Country("ch", "Switzerland", "EU", new Medals { Gold = 1, Silver = 2, Bronze = 5}),
+ new Country("th", "Thailand", "AS", new Medals { Gold = 1, Silver = 3, Bronze = 2}),
+ new Country("tj", "Tajikistan", "AS", new Medals { Gold = 0, Silver = 0, Bronze = 3}),
+ new Country("tpe", "Chinese Taipei", "AS", new Medals { Gold = 2, Silver = 0, Bronze = 5}),
+ new Country("tn", "Tunisia", "AF", new Medals { Gold = 1, Silver = 1, Bronze = 1}),
+ new Country("tr", "Turkey", "AS", new Medals { Gold = 0, Silver = 3, Bronze = 5}),
+ new Country("ug", "Uganda", "AF", new Medals { Gold = 1, Silver = 1, Bronze = 0}),
+ new Country("ua", "Ukraine", "EU", new Medals { Gold = 3, Silver = 5, Bronze = 4}),
+ new Country("us", "United States of America", "NA", new Medals { Gold = 40, Silver = 44, Bronze = 42}),
+ new Country("uz", "Uzbekistan", "AS", new Medals { Gold = 8, Silver = 2, Bronze = 3}),
+ new Country("zm", "Zambia", "AF", new Medals { Gold = 0, Silver = 0, Bronze = 1})
];
}
diff --git a/examples/Demo/Shared/SampleData/Olympics.cs b/examples/Demo/Shared/SampleData/Olympics.cs
index 5da566510b..81ed4ad8f8 100644
--- a/examples/Demo/Shared/SampleData/Olympics.cs
+++ b/examples/Demo/Shared/SampleData/Olympics.cs
@@ -13,4 +13,5 @@ public record Medals
public int Total => Gold + Silver + Bronze;
}
-public record Country(string Code, string Name, Medals Medals);
+
+public record Country(string Code, string Name, string? ContinentCode, Medals Medals);
diff --git a/examples/Demo/Shared/Shared/DemoNavProvider.cs b/examples/Demo/Shared/Shared/DemoNavProvider.cs
index 02f956d994..b1765bf9ab 100644
--- a/examples/Demo/Shared/Shared/DemoNavProvider.cs
+++ b/examples/Demo/Shared/Shared/DemoNavProvider.cs
@@ -475,6 +475,11 @@ public DemoNavProvider()
icon: new Icons.Regular.Size20.Grid(),
title: "Remote data"
),
+ new NavLink (
+ href:"/datagrid-hierarchical",
+ icon: new Icons.Regular.Size20.Grid(),
+ title: "Hierarchical data"
+ ),
new NavLink (
href:"/datagrid-manual",
icon: new Icons.Regular.Size20.Grid(),
diff --git a/src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs b/src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
index aea06a9539..e327bbafc0 100644
--- a/src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
+++ b/src/Core/Components/DataGrid/Columns/ColumnBase.razor.cs
@@ -160,6 +160,13 @@ public abstract partial class ColumnBase
[Parameter]
public string MinWidth { get; set; } = "100px";
+ ///
+ /// If true, the column will include an expand/collapse toggle for hierarchical data.
+ /// This only applies if implements .
+ ///
+ [Parameter]
+ public bool HierarchicalToggle { get; set; }
+
///
/// Gets a reference to the enclosing .
///
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor b/src/Core/Components/DataGrid/FluentDataGrid.razor
index 9ddbcc2477..78a5ee4b7b 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor
@@ -117,7 +117,25 @@
string? tooltip = col.Tooltip ? @col.RawCellContent(item) : null;
- @((RenderFragment)(__builder => col.CellContent(__builder, item)))
+ @if (col.HierarchicalToggle && item is IHierarchicalGridItem hierarchicalItem )
+ {
+ var indent = hierarchicalItem.Depth * 16;
+
+
+ @if (hierarchicalItem.HasChildren)
+ {
+
+
+
+ }
+
+ @((RenderFragment)(__builder => col.CellContent(__builder, item)))
+
+ }
+ else
+ {
+ @((RenderFragment)(__builder => col.CellContent(__builder, item)))
+ }
}
@@ -269,7 +287,7 @@
}
-
+
private void RenderErrorContent(RenderTreeBuilder __builder)
{
if (_lastError == null)
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
index ea462b315e..4bb434e16c 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs
@@ -196,6 +196,12 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve
[Parameter]
public bool NoTabbing { get; set; }
+ ///
+ /// Event callback for when a hierarchical row is expanded or collapsed.
+ ///
+ [Parameter]
+ public EventCallback OnToggle { get; set; }
+
///
/// Gets or sets a value indicating whether the grid should automatically generate a header row and its type.
/// See
@@ -391,6 +397,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve
private bool _sortByAscending;
private bool _checkColumnOptionsPosition;
private bool _checkColumnResizePosition;
+ private bool _checkColumnResizing;
private bool _manualGrid;
// The associated ES6 module, which uses document-level event listeners
@@ -548,6 +555,12 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
_ = Module?.InvokeVoidAsync("autoFitGridColumns", _gridReference, _columns.Count).AsTask();
}
+
+ if (_checkColumnResizing && _gridReference is not null)
+ {
+ _checkColumnResizing = false;
+ _ = Module?.InvokeVoidAsync("enableColumnResizing", _gridReference, ResizeColumnOnAllRows).AsTask();
+ }
}
// Invoked by descendant columns at a special time during rendering
@@ -583,6 +596,16 @@ private void FinishCollectingColumns()
throw new Exception("You can use either the 'GridTemplateColumns' parameter on the grid or the 'Width' property at the column level, not both.");
}
+ if (_columns.Count(x => x.HierarchicalToggle) > 1)
+ {
+ throw new ArgumentException("Only one column can have 'HierarchicalToggle' set to true.");
+ }
+
+ if (_columns.Exists(x => x.HierarchicalToggle) && !_columns[0].HierarchicalToggle)
+ {
+ throw new ArgumentException("The 'HierarchicalToggle' parameter can only be set on the first column of the grid.");
+ }
+
// Always re-evaluate after collecting columns when using displaymode grid. A column might be added or hidden and the _internalGridTemplateColumns needs to reflect that.
if (DisplayMode == DataGridDisplayMode.Grid)
{
@@ -599,7 +622,7 @@ private void FinishCollectingColumns()
if (ResizableColumns)
{
- _ = Module?.InvokeVoidAsync("enableColumnResizing", _gridReference, ResizeColumnOnAllRows).AsTask();
+ _checkColumnResizing = true;
}
}
@@ -1194,5 +1217,18 @@ public async Task ResetColumnWidthsAsync()
await Module.InvokeVoidAsync("resetColumnWidths", _gridReference);
}
}
+
+ private async Task ToggleExpandedAsync(TGridItem item)
+ {
+ if (item is IHierarchicalGridItem hierarchicalItem)
+ {
+ hierarchicalItem.IsCollapsed = !hierarchicalItem.IsCollapsed;
+ if (OnToggle.HasDelegate)
+ {
+ await OnToggle.InvokeAsync(item);
+ }
+ await RefreshDataAsync();
+ }
+ }
}
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.css b/src/Core/Components/DataGrid/FluentDataGrid.razor.css
index 1256224f0a..e41d958037 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.css
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.css
@@ -77,6 +77,11 @@
opacity: var(--fluent-data-grid-header-opacity);
}
+[dir=rtl] * ::deep .resize-handle {
+ right: unset;
+ left: 0;
+}
+
.header {
padding: 0;
z-index: 3;
@@ -88,3 +93,4 @@
background-color: var(--neutral-fill-stealth-rest);
z-index: 2;
}
+
diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.js b/src/Core/Components/DataGrid/FluentDataGrid.razor.js
index a5f376177d..2c5d9e699f 100644
--- a/src/Core/Components/DataGrid/FluentDataGrid.razor.js
+++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.js
@@ -5,7 +5,10 @@ export function init(gridElement, autoFocus) {
return;
};
- enableColumnResizing(gridElement);
+ const controller = new AbortController();
+ const { signal } = controller;
+
+ enableColumnResizing(gridElement, true, signal);
let start = gridElement.querySelector('td:first-child');
@@ -31,43 +34,36 @@ export function init(gridElement, autoFocus) {
}
}
const keyDownHandler = event => {
- if (document.activeElement.tagName.toLowerCase() != 'table' && document.activeElement.tagName.toLowerCase() != 'td' && document.activeElement.tagName.toLowerCase() != 'th') {
- return;
- }
const columnOptionsElement = gridElement?.querySelector('.col-options');
- if (columnOptionsElement) {
+ if (columnOptionsElement && columnOptionsElement.contains(event.target)) {
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "ArrowUp") {
+ event.stopPropagation();
+ }
if (event.key === "Escape") {
gridElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
gridElement.focus();
}
- columnOptionsElement.addEventListener(
- "keydown",
- (event) => {
- if (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "ArrowUp") {
- event.stopPropagation();
- }
- }
- );
}
+
const columnResizeElement = gridElement?.querySelector('.col-resize');
- if (columnResizeElement) {
+ if (columnResizeElement && columnResizeElement.contains(event.target)) {
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "ArrowUp") {
+ event.stopPropagation();
+ }
if (event.key === "Escape") {
gridElement.dispatchEvent(new CustomEvent('closecolumnresize', { bubbles: true }));
gridElement.focus();
}
- columnResizeElement.addEventListener(
- "keydown",
- (event) => {
- if (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "ArrowUp") {
- event.stopPropagation();
- }
- }
- );
+ }
+
+ if (document.activeElement.tagName.toLowerCase() != 'table' && document.activeElement.tagName.toLowerCase() != 'td' && document.activeElement.tagName.toLowerCase() != 'th') {
+ return;
}
// check if start is a child of gridElement
if (start !== null && (gridElement.contains(start) || gridElement === start) && document.activeElement === start && document.activeElement.tagName.toLowerCase() !== 'fluent-text-field' && document.activeElement.tagName.toLowerCase() !== 'fluent-menu-item') {
const idx = start.cellIndex;
+ const isRTL = getComputedStyle(gridElement).direction === 'rtl';
if (event.key === "ArrowUp") {
// up arrow
@@ -88,13 +84,13 @@ export function init(gridElement, autoFocus) {
} else if (event.key === "ArrowLeft") {
// left arrow
event.preventDefault();
- const previousSibling = (document.body.dir === '' || document.body.dir === 'ltr') ? start.previousElementSibling : start.nextElementSibling;
+ const previousSibling = isRTL ? start.nextElementSibling : start.previousElementSibling;
keyboardNavigation(previousSibling);
event.stopPropagation();
} else if (event.key === "ArrowRight") {
// right arrow
event.preventDefault();
- const nextsibling = (document.body.dir === '' || document.body.dir === 'ltr') ? start.nextElementSibling : start.previousElementSibling;
+ const nextsibling = isRTL ? start.previousElementSibling : start.nextElementSibling;
keyboardNavigation(nextsibling);
event.stopPropagation();
}
@@ -120,20 +116,23 @@ export function init(gridElement, autoFocus) {
if (event.target.role !== "gridcell" && (event.key === "ArrowRight" || event.key === "ArrowLeft")) {
event.stopPropagation();
}
- }
+ },
+ { signal }
);
});
- document.body.addEventListener('click', bodyClickHandler);
- document.body.addEventListener('mousedown', bodyClickHandler); // Otherwise it seems strange that it doesn't go away until you release the mouse button
- gridElement.addEventListener('keydown', keyDownHandler);
+ document.body.addEventListener('click', bodyClickHandler, { signal });
+ document.body.addEventListener('mousedown', bodyClickHandler, { signal }); // Otherwise it seems strange that it doesn't go away until you release the mouse button
+ gridElement.addEventListener('keydown', keyDownHandler, { signal });
return {
stop: () => {
- document.body.removeEventListener('click', bodyClickHandler);
- document.body.removeEventListener('mousedown', bodyClickHandler);
- gridElement.removeEventListener('keydown', keyDownHandler);
- delete grids[gridElement];
+ controller.abort();
+
+ const index = grids.findIndex(grid => grid.id === gridElement.id);
+ if (index > -1) {
+ grids.splice(index, 1);
+ }
}
};
}
@@ -160,7 +159,7 @@ export function checkColumnPopupPosition(gridElement, selector) {
}
}
-export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true) {
+export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true, signal = null) {
const columns = [];
const headers = gridElement.querySelectorAll('.column-header.resizable');
@@ -168,7 +167,6 @@ export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true)
return;
}
- const isRTL = getComputedStyle(gridElement).direction === 'rtl';
const isGrid = gridElement.classList.contains('grid')
let tableHeight = gridElement.offsetHeight;
@@ -205,9 +203,9 @@ export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true)
const resizeTop = header.querySelector('.resize-handle').offsetTop;
// add a new resize div
- const div = createDiv(resizeHandleHeight, resizeTop, isRTL);
+ const div = createDiv(resizeHandleHeight, resizeTop);
header.appendChild(div);
- setListeners(div, isRTL);
+ setListeners(div, signal);
});
let initialWidths;
@@ -230,32 +228,15 @@ export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true)
});
}
- function setListeners(div, isRTL) {
+ function setListeners(div, signal) {
let pageX, curCol, curColWidth;
- div.addEventListener('pointerdown', function (e) {
- curCol = e.target.parentElement;
- pageX = e.pageX;
-
- const padding = paddingDiff(curCol);
-
- curColWidth = curCol.offsetWidth - padding;
- });
-
- div.addEventListener('pointerover', function (e) {
- e.target.style.borderInlineEnd = 'var(--fluent-data-grid-resize-handle-width) solid var(--fluent-data-grid-resize-handle-color)';
- e.target.previousElementSibling.style.visibility = 'hidden';
- });
-
- div.addEventListener('pointerup', removeBorder);
- div.addEventListener('pointercancel', removeBorder);
- div.addEventListener('pointerleave', removeBorder);
-
- document.addEventListener('pointermove', (e) =>
+ const moveHandler = (e) =>
requestAnimationFrame(() => {
gridElement.style.tableLayout = 'fixed';
if (curCol) {
+ const isRTL = getComputedStyle(gridElement).direction === 'rtl';
const diffX = isRTL ? pageX - e.pageX : e.pageX - pageX;
const column = columns.find(({ header }) => header === curCol);
@@ -276,17 +257,40 @@ export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true)
curCol.style.width = column.size;
}
}
- })
- );
+ });
+
+ const upHandler = function () {
+ gridElement.removeEventListener('pointermove', moveHandler);
+ gridElement.removeEventListener('pointerup', upHandler);
- document.addEventListener('pointerup', function () {
curCol = undefined;
curColWidth = undefined;
pageX = undefined;
- });
+ };
+
+ div.addEventListener('pointerdown', function (e) {
+ curCol = e.target.parentElement;
+ pageX = e.pageX;
+
+ const padding = paddingDiff(curCol);
+
+ curColWidth = curCol.offsetWidth - padding;
+
+ gridElement.addEventListener('pointermove', moveHandler, { signal });
+ gridElement.addEventListener('pointerup', upHandler, { signal });
+ }, { signal });
+
+ div.addEventListener('pointerover', function (e) {
+ e.target.style.borderInlineEnd = 'var(--fluent-data-grid-resize-handle-width) solid var(--fluent-data-grid-resize-handle-color)';
+ e.target.previousElementSibling.style.visibility = 'hidden';
+ }, { signal });
+
+ div.addEventListener('pointerup', removeBorder, { signal });
+ div.addEventListener('pointercancel', removeBorder, { signal });
+ div.addEventListener('pointerleave', removeBorder, { signal });
}
- function createDiv(height, top, isRTL) {
+ function createDiv(height, top) {
const div = document.createElement('div');
div.className = "actual-resize-handle";
div.style.top = top + 'px';
@@ -296,14 +300,8 @@ export function enableColumnResizing(gridElement, resizeColumnOnAllRows = true)
div.style.height = (height - 4) + 'px';
div.style.width = '6px';
div.style.opacity = 'var(--fluent-data-grid-header-opacity)'
+ div.style.insetInlineEnd = '0';
- if (isRTL) {
- div.style.left = '0';
- div.style.right = 'unset';
- } else {
- div.style.left = 'unset';
- div.style.right = '0';
- }
return div;
}
@@ -446,7 +444,10 @@ export function autoFitGridColumns(gridElement, columnCount) {
gridElement.style.gridTemplateColumns = gridTemplateColumns;
gridElement.classList.remove("auto-fit");
- grids[gridElement.id] = gridTemplateColumns;
+ const grid = grids.find(grid => grid.id === gridElement.id);
+ if (grid) {
+ grid.initialWidths = gridTemplateColumns;
+ }
}
function calculateVisibleRows(gridElement, rowHeight) {
diff --git a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
index 1a688cff8f..97234137ac 100644
--- a/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
+++ b/src/Core/Components/DataGrid/FluentDataGridCell.razor.css
@@ -105,3 +105,42 @@ td.grid-cell-placeholder:after {
.col-justify-end ::deep .col-title {
text-align: end;
}
+
+::deep .hierarchical-toggle-container {
+ height: 24px;
+ display: inline-flex;
+ vertical-align: middle;
+ align-items: center;
+ column-gap: calc(var(--design-unit) * 1px);
+ position: relative;
+ left: calc(var(--design-unit) * -1.5px);
+}
+
+::deep .hierarchical-expander {
+ display: inline-block;
+ width: 18px;
+}
+
+ ::deep .hierarchical-expander svg {
+ transition: transform 0.1s linear;
+ pointer-events: none;
+ }
+
+::deep .hierarchical-collapsed svg {
+ transform: rotate(0deg);
+}
+
+[dir=rtl] * ::deep .hierarchical-collapsed svg {
+ transform: rotate(180deg);
+}
+
+::deep .hierarchical-expanded svg {
+ transform: rotate(90deg);
+}
+
+::deep .hierarchical-expand-button {
+ width: 18px;
+ height: 24px;
+ min-width: 18px;
+ vertical-align: middle;
+}
diff --git a/src/Core/Components/DataGrid/FluentDataGridRow.razor.css b/src/Core/Components/DataGrid/FluentDataGridRow.razor.css
index 2fa3af2c47..cd511d9f29 100644
--- a/src/Core/Components/DataGrid/FluentDataGridRow.razor.css
+++ b/src/Core/Components/DataGrid/FluentDataGridRow.razor.css
@@ -8,3 +8,4 @@
}
+
diff --git a/src/Core/Components/DataGrid/HierarchicalGridItem.cs b/src/Core/Components/DataGrid/HierarchicalGridItem.cs
new file mode 100644
index 0000000000..ab1703c142
--- /dev/null
+++ b/src/Core/Components/DataGrid/HierarchicalGridItem.cs
@@ -0,0 +1,72 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// A base class for a grid item that can be part of a hierarchy.
+///
+/// The type of the item being displayed.
+/// The type of the grid item wrapper class.
+[DebuggerDisplay("Item = {Item}, Children = {Children.Count}, Depth = {Depth}, IsHidden = {IsHidden}")]
+public class HierarchicalGridItem : IHierarchicalGridItem
+ where TItem : notnull
+ where TGridItem : HierarchicalGridItem
+{
+ ///
+ /// Gets or sets the reference to the item that holds this row's values.
+ /// This name aligns with .
+ ///
+ public required TItem Item { get; init; }
+
+ ///
+ /// The children of this item in the hierarchy.
+ ///
+ public List Children { get; } = [];
+
+ ///
+ /// Gets a value indicating whether this item has children.
+ ///
+ public bool HasChildren => Children.Count > 0;
+
+ ///
+ /// The depth of this item in the hierarchy (0 for root).
+ ///
+ public int Depth { get; set; }
+
+ ///
+ /// Whether this item is currently hidden (e.g. because a parent is collapsed).
+ ///
+ public bool IsHidden { get; set; }
+
+ private bool _isCollapsed;
+
+ ///
+ /// Whether this item's children are currently collapsed.
+ ///
+ public bool IsCollapsed
+ {
+ get => _isCollapsed;
+ set
+ {
+ _isCollapsed = value;
+ UpdateHidden();
+ }
+ }
+
+ ///
+ /// Updates the property for this item's children.
+ ///
+ private void UpdateHidden()
+ {
+ var shouldHideChildren = IsHidden || IsCollapsed;
+ foreach (var child in Children)
+ {
+ child.IsHidden = shouldHideChildren;
+ child.UpdateHidden();
+ }
+ }
+}
diff --git a/src/Core/Components/DataGrid/HierarchicalGridUtilities.cs b/src/Core/Components/DataGrid/HierarchicalGridUtilities.cs
new file mode 100644
index 0000000000..3fcc0ead42
--- /dev/null
+++ b/src/Core/Components/DataGrid/HierarchicalGridUtilities.cs
@@ -0,0 +1,72 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Provides utility methods for managing hierarchical grid data.
+///
+public static class HierarchicalGridUtilities
+{
+ ///
+ /// Orders a flat list of grid items into a hierarchical structure based on parent relationships.
+ ///
+ /// The type of the grid item wrapper.
+ /// The type of the underlying data item.
+ /// The initial flat list of items.
+ /// Function to get the unique identifier of an item.
+ /// Function to get the parent identifier of an item.
+ /// Function to determine if an item is initially collapsed.
+ /// A list of items ordered hierarchically, including child relationships.
+ public static List OrderHierarchically(
+ List initialItems,
+ Func getId,
+ Func getParentId,
+ Func isCollapsed)
+ where TGridItem : HierarchicalGridItem
+ where TItem : notnull
+ {
+ var result = new List();
+
+ foreach (var item in initialItems.Where(it => !HasVisibleParent(it)))
+ {
+ item.Depth = 0;
+ item.IsCollapsed = isCollapsed(item.Item);
+ item.IsHidden = false;
+
+ result.Add(item);
+
+ AddChildViewModel(item.Item, item, 1, hidden: item.IsCollapsed);
+ }
+
+ return result;
+
+ void AddChildViewModel(TItem parentItem, TGridItem parentWrapper, int depth, bool hidden)
+ {
+ var parentId = getId(parentItem);
+ foreach (var child in initialItems.Where(it => getParentId(it.Item) == parentId))
+ {
+ child.Depth = depth;
+ child.IsCollapsed = isCollapsed(child.Item);
+ child.IsHidden = hidden;
+
+ parentWrapper.Children.Add(child);
+ result.Add(child);
+
+ AddChildViewModel(child.Item, child, depth + 1, hidden: child.IsHidden || child.IsCollapsed);
+ }
+ }
+
+ bool HasVisibleParent(TGridItem wrapper)
+ {
+ var parentId = getParentId(wrapper.Item);
+ if (string.IsNullOrEmpty(parentId))
+ {
+ return false;
+ }
+
+ return initialItems.Any(other => getId(other.Item) == parentId);
+ }
+ }
+}
diff --git a/src/Core/Components/DataGrid/IHierarchicalGridItem.cs b/src/Core/Components/DataGrid/IHierarchicalGridItem.cs
new file mode 100644
index 0000000000..d35ec768d8
--- /dev/null
+++ b/src/Core/Components/DataGrid/IHierarchicalGridItem.cs
@@ -0,0 +1,31 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// An interface for a grid item that can be part of a hierarchy.
+///
+public interface IHierarchicalGridItem
+{
+ ///
+ /// Gets or sets the depth of this item in the hierarchy (0 for root).
+ ///
+ int Depth { get; set; }
+
+ ///
+ /// Gets or sets whether this item is currently hidden (e.g. because a parent is collapsed).
+ ///
+ bool IsHidden { get; set; }
+
+ ///
+ /// Gets or sets whether this item's children are currently collapsed.
+ ///
+ bool IsCollapsed { get; set; }
+
+ ///
+ /// Gets a value indicating whether this item has children.
+ ///
+ bool HasChildren { get; }
+}