+
+
+
+
+@code
+{
+ private IEnumerable? SelectedItems;
+ private IEnumerable? Items = new List();
+
+ // Read the Tree content
+ protected override void OnInitialized()
+ {
+ Items = GetCompanyOrganization();
+ }
+
+ // Example of a custom visibility function
+ private TreeSelectionVisibility GetTreeSelectionVisibility(ITreeViewItem item)
+ {
+ return item.Id.First() switch
+ {
+ // Company or Department => collapsed checkbox
+ 'C' => TreeSelectionVisibility.Collapse,
+ 'D' => TreeSelectionVisibility.Hidden,
+
+ // Employee or others => visible checkbox
+ 'E' => TreeSelectionVisibility.Visible,
+ _ => TreeSelectionVisibility.Visible
+ };
+ }
+
+ // Example of a tree with a company organization
+ // (5 companies containing 4 departments with 10 employees)
+ private TreeViewItem[] GetCompanyOrganization()
+ {
+ return SampleData.People
+ .GetOrganization(companyCount: 5, departmentCount: 4, employeeCount: 10)
+ .ToTreeViewItems(includeIcons: false)
+ .ToArray();
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/Examples/TreeViewWithUnlimitedItems.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/Examples/TreeViewWithUnlimitedItems.razor
new file mode 100644
index 0000000000..e936f5ff81
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/Examples/TreeViewWithUnlimitedItems.razor
@@ -0,0 +1,49 @@
+
+ Selected item: @SelectedItem?.Text
+
+
+
+
+@code
+{
+ private ITreeViewItem? SelectedItem;
+ private IEnumerable? Items = new List();
+
+ protected override async Task OnInitializedAsync()
+ {
+ Items = await GetItemsAsync();
+ }
+
+ // Generate a random number of items
+ // Including a "Fake" sub-item to simulate the [+]
+ private async Task> GetItemsAsync()
+ {
+ await Task.Delay(300); // Simulate a delay for loading items
+
+ var nbItems = Random.Shared.Next(3, 9);
+
+ return Enumerable.Range(1, nbItems)
+ .Select(i => new TreeViewItem()
+ {
+ Text = $"Item {Random.Shared.Next(1, 9999)}",
+ OnExpandedAsync = OnExpandedAsync,
+ Items = TreeViewItem.LoadingTreeViewItems("Loading..."), // "Fake" sub-item to simulate the [+]
+ }).ToArray();
+ }
+
+ // Handle the expanded event to load items
+ private async Task OnExpandedAsync(TreeViewItemExpandedEventArgs e)
+ {
+ if (e.Expanded)
+ {
+ e.CurrentItem.Items = await GetItemsAsync();
+ }
+ else
+ {
+ // Remove sub-items and add a "Fake" item to simulate the [+]
+ e.CurrentItem.Items = TreeViewItem.LoadingTreeViewItems("Loading...");
+ }
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/FluentTreeView.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/FluentTreeView.md
new file mode 100644
index 0000000000..2be8fd18fc
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/TreeView/FluentTreeView.md
@@ -0,0 +1,141 @@
+
+---
+title: TreeView
+route: /TreeView
+---
+
+# TreeView
+
+A hierarchical list structure component for displaying data in a collapsible and expandable way.
+
+Use this component when you need to present your users with a clear visual structure of content or data,
+allowing them to efficiently interact and navigate through the information.
+If the information is less hierarchical or node-based, consider using a list or table instead.
+
+When using this component, it is possible not to select an item.
+However, once an item has been selected, it is mandatory to keep an item selected.
+If you no longer want to select anything, you must do so by code: `SelectedId = null`.
+
+It is not possible to select multiple items at once. However, a custom development (see below) can be implemented to allow the selection of multiple items.
+
+You can create a Tree manually by nesting `FluentTreeItem` components or by using the `Items` property of `FluentTreeView` to dynamically generate a tree from a list of objects.
+If you use the `Items` property, you can also set the `ItemTemplate` property to specify how each item should be displayed.
+With these two ways of creating a tree, using the `Text` parameter will display the text of the element, adding an ellipsis `...` if the text is too long.
+Each element also has the properties `IconStart`, `IconEnd`, and `IconAside` to display an icon to the left, right, or at the end of the text.
+
+> [!NOTE]
+> When a user clicks on an item, it is selected and expanded/collapsed if children are present.
+> It is not possible to open an item without selecting it (except by using the keyboard, pressing the `Enter` or `Space` key)
+> or using the `Mutiple` selection feature (see below).
+
+## Manual TreeView
+
+In this example, we create a tree manually by nesting `FluentTreeItem` components.
+
+Using `SelectedId` parameter, we can select an item by passing its `Id` to the `FluentTreeView` component.
+When this parameter is bound to a variable, the selected item will be highlighted for other usages.
+
+Using `CurrentSelected` parameter, we can select an item by passing its `FluentTreeItem` to the `FluentTreeView` component.
+When this parameter is bound to a variable, the selected item will be highlighted for other usages.
+
+The event `OnExpandedChanged` is triggered when the user expands or collapses an item.
+
+{{ TreeViewDefault }}
+
+## Items parameter
+
+In this example, we create a tree dynamically by using the `Items` property of `FluentTreeView`.
+The `Items` parameter is a list of [**TreeViewItem**](/TreeView#class-treeviewitem) that represent the items in the tree.
+
+When a user selects an item, the `SelectedItem` parameter is updated with the `ITreeViewItem` of the selected item.
+
+{{ TreeViewItems }}
+
+## ItemTemplate
+
+Using an ItemTemplate, we can specify how each item should be displayed.
+`context` is the current item `ITreeViewItem` in the loop, and we can use its properties to display the item.
+
+```razor
+
+
+ @context.Text
+
+```
+
+{{ TreeViewItemTemplate }}
+
+**Note**:
+In some situation, the tree item elements may not catch the click event to select the item.
+To avoid this, you can use the `Style="pointer-events: none;"` property to disable the pointer events on the element
+and let the click event pass through to the parent element.
+
+## Unlimited Items
+
+If you have a very large number of items, you can use the `LazyLoadItems` parameter.
+This parameter tells the component to load items only when the node is expanded.
+Once the node is closed, the items are removed from the DOM and are not displayed.
+
+{{ TreeViewWithUnlimitedItems }}
+
+## Mutliple Selection
+
+The `FluentTreeView` component supports the multiple selection of items using the `SelectionMode` parameter.
+When this parameter is set to `TreeSelectionMode.Multiple`, a checkbox is displayed next to each item.
+
+Each time the user clicks on an item, the checkbox is checked or unchecked, and the parameter `SelectedItems`
+is updated with the list of selected items.
+
+We recommand to set the `HideSelection` parameter to `true` to hide the default selection of the item when the `MultiSelect`
+parameter is set. This is more user-friendly and allows the user to see the selected items more clearly.
+
+> [!NOTE]
+> This **Multiple Selection** feature is only available when the `Items` parameter is used to generate the tree.
+
+{{ TreeViewMultiSelect }}
+
+
+## Mutliple Selection with customized checkbox visibility
+
+You can customize the visibility of the checkbox using the `MultipleSelectionVisibility` parameter.
+This function allows you to show, hide (keeping the space) or hide and remove the checkbox, based on each `ITreeViewItem` objects.
+
+In this example, we use the `GetTreeSelectionVisibility` function to determine the visibility of the checkbox based
+on the first letter of the `Id` of the item. The result is a TreeView with a checkbox only for the `Employee` items (level 3).
+
+```csharp
+TreeSelectionVisibility GetTreeSelectionVisibility(ITreeViewItem item)
+{
+ return item.Id.First() switch
+ {
+ // Company or Department => collapsed checkbox
+ 'C' => TreeSelectionVisibility.Collapse,
+ 'D' => TreeSelectionVisibility.Hidden,
+
+ // Employee or others => visible checkbox
+ 'E' => TreeSelectionVisibility.Visible,
+ _ => TreeSelectionVisibility.Visible
+ };
+}
+```
+
+{{ TreeViewMultipleSelectionVisibility }}
+
+We don't have a possibility to customize the type of checkbox used in the `FluentTreeView` component.
+For example, if you want to use a mixed checkbox, you can use the `ItemTemplate` part to create your own checkbox logic.
+
+## API FluentTreeView
+
+{{ API Type=FluentTreeView }}
+
+## API FluentTreeItem
+
+{{ API Type=FluentTreeItem }}
+
+## Class TreeViewItem
+
+{{ API Type=TreeViewItem Properties=All }}
+
+## Migrating to v5
+
+{{ INCLUDE File=MigrationFluentTreeView }}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentTreeView.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentTreeView.md
new file mode 100644
index 0000000000..cee711207e
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentTreeView.md
@@ -0,0 +1,22 @@
+---
+title: Migration FluentTreeView
+route: /Migration/TreeView
+hidden: true
+---
+
+### New properties
+ `Appearance`, `HideSelection`, `MultipleSelectionVisibility`,
+ `SelectedId`, `SelectedItems`, `SelectionMode`, `Size` are new properties.
+
+### Removed properties💥
+ - The `RenderCollapsedNodes` property has been removed.
+ Use `LazyLoadItems` instead.
+
+ - The `FluentTreeItem.InitiallyExpanded` property has been removed. You can use the `FluentTreeItem.Expanded` parameter instead.
+ This parameter is now a two-way binding, so you can use it to control the expanded state of the item.
+
+ - The `FluentTreeItem.InitiallySelected` property has been removed. You can use the `FluentTreeView.SelectedId` or `FluentTreeView.SelectedItem`parameter instead.
+ These parametere are now a two-way binding.
+ The main reason is than the FluentTreeView supports only one selected item at a time (except using the `SelectionMode.Multiple` mode).
+
+ - The `FluentTreeItem.Disabled` property has been removed. The underline webcomponent does not support this property.
diff --git a/examples/Tools/FluentUI.Demo.SampleData/People.cs b/examples/Tools/FluentUI.Demo.SampleData/People.cs
index 2085ba9685..139c1728e6 100644
--- a/examples/Tools/FluentUI.Demo.SampleData/People.cs
+++ b/examples/Tools/FluentUI.Demo.SampleData/People.cs
@@ -149,4 +149,71 @@ public record Person(string Id, string FirstName, string LastName, bool Male, st
///
public int Age => DateTime.Today.Year - BirthDay.Year;
}
+
+ ///
+ /// 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/spelling.dic b/spelling.dic
index fc18c0dec5..ef868a5175 100644
--- a/spelling.dic
+++ b/spelling.dic
@@ -39,6 +39,7 @@ muhenkan
myid
noattribute
nonfile
+onchange
ondialogbeforetoggle
ondialogtoggle
ondropdownchange
diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json
index 663419ecd5..688db8d511 100644
--- a/src/Core.Scripts/package-lock.json
+++ b/src/Core.Scripts/package-lock.json
@@ -8,7 +8,7 @@
"name": "microsoft.fluentui.aspnetcore.components.assets",
"license": "ISC",
"dependencies": {
- "@fluentui/web-components": "3.0.0-beta.89"
+ "@fluentui/web-components": "3.0.0-beta.99"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.6.0",
@@ -486,9 +486,9 @@
}
},
"node_modules/@fluentui/web-components": {
- "version": "3.0.0-beta.89",
- "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/web-components/-/web-components-3.0.0-beta.89.tgz",
- "integrity": "sha1-KF8b8aZxs89FVR1VJ6XpI/CHKno=",
+ "version": "3.0.0-beta.99",
+ "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/web-components/-/web-components-3.0.0-beta.99.tgz",
+ "integrity": "sha1-fgPZRWISrU+AQs9yJOxCRKndJ1w=",
"dependencies": {
"@fluentui/tokens": "1.0.0-alpha.21",
"@microsoft/fast-web-utilities": "^6.0.0",
diff --git a/src/Core.Scripts/package.json b/src/Core.Scripts/package.json
index 408df7e36e..ad27271030 100644
--- a/src/Core.Scripts/package.json
+++ b/src/Core.Scripts/package.json
@@ -25,6 +25,6 @@
"typescript": "^5.4.4"
},
"dependencies": {
- "@fluentui/web-components": "3.0.0-beta.89"
+ "@fluentui/web-components": "3.0.0-beta.99"
}
}
diff --git a/src/Core.Scripts/src/FluentUICustomEvents.ts b/src/Core.Scripts/src/FluentUICustomEvents.ts
index 2ffccad047..dda3cf10e7 100644
--- a/src/Core.Scripts/src/FluentUICustomEvents.ts
+++ b/src/Core.Scripts/src/FluentUICustomEvents.ts
@@ -1,3 +1,5 @@
+import { Microsoft as FluentTreeView } from './Components/TreeView/FluentTreeView';
+
export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents {
/**
@@ -92,6 +94,33 @@ export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents {
});
}
+ export function TreeView(blazor: Blazor) {
+
+ // Event when an element is selected or deselected
+ blazor.registerCustomEventType('treechanged', {
+ browserEventName: 'change',
+ createEventArgs: (event: EventType) => {
+ return {
+ id: event.target.id,
+ selected: event.target.selected,
+ };
+ }
+ });
+
+ // Event when an element is expanded or collapsed
+ blazor.registerCustomEventType('treetoggle', {
+ browserEventName: 'toggle',
+ createEventArgs: (event: any) => {
+ return {
+ id: event.target.id,
+ type: event.type,
+ oldState: event.detail?.oldState ?? event.oldState,
+ newState: event.detail?.newState ?? event.newState,
+ };
+ }
+ });
+ }
+
// [^^^ Add your other custom events before this line ^^^]
}
diff --git a/src/Core.Scripts/src/FluentUIWebComponents.ts b/src/Core.Scripts/src/FluentUIWebComponents.ts
index d164e16f41..c46c4912b0 100644
--- a/src/Core.Scripts/src/FluentUIWebComponents.ts
+++ b/src/Core.Scripts/src/FluentUIWebComponents.ts
@@ -12,7 +12,6 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents {
// To generate these definitions, run the `_ExtractWebComponents.ps1` file
// and paste the output here.
-
FluentUIComponents.accordionDefinition.define(registry);
FluentUIComponents.accordionItemDefinition.define(registry);
FluentUIComponents.AnchorButtonDefinition.define(registry);
@@ -55,6 +54,8 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents {
FluentUIComponents.TextInputDefinition.define(registry);
FluentUIComponents.ToggleButtonDefinition.define(registry);
FluentUIComponents.TooltipDefinition.define(registry);
+ FluentUIComponents.TreeDefinition.define(registry);
+ FluentUIComponents.TreeItemDefinition.define(registry);
}
/**
diff --git a/src/Core.Scripts/src/Startup.ts b/src/Core.Scripts/src/Startup.ts
index 492aa904be..d43f1ab9be 100644
--- a/src/Core.Scripts/src/Startup.ts
+++ b/src/Core.Scripts/src/Startup.ts
@@ -54,6 +54,7 @@ export namespace Microsoft.FluentUI.Blazor.Startup {
FluentUICustomEvents.MenuItem(blazor);
FluentUICustomEvents.DropdownList(blazor);
FluentUICustomEvents.Tabs(blazor);
+ FluentUICustomEvents.TreeView(blazor);
// [^^^ Add your other custom events before this line ^^^]
// Finishing
diff --git a/src/Core.Scripts/src/d-ts/Blazor.d.ts b/src/Core.Scripts/src/d-ts/Blazor.d.ts
index dfd370f939..ff0d70e2d3 100644
--- a/src/Core.Scripts/src/d-ts/Blazor.d.ts
+++ b/src/Core.Scripts/src/d-ts/Blazor.d.ts
@@ -11,5 +11,4 @@ interface Blazor {
setLightTheme(): void,
setDarkTheme(): void,
}
-
}
diff --git a/src/Core/Components/Base/FluentSlot.cs b/src/Core/Components/Base/FluentSlot.cs
index d80c628884..45da56ad68 100644
--- a/src/Core/Components/Base/FluentSlot.cs
+++ b/src/Core/Components/Base/FluentSlot.cs
@@ -97,4 +97,14 @@ public static class FluentSlot
/// Slot for the title action elements (e.g. Close button). When the dialog type is set to non-modal and no title action is provided, a default title action button is rendered.
///
internal const string DialogTitleAction = "title-action";
+
+ ///
+ /// Slot for the expanded/collapsed element of a fluent-tree-item.
+ ///
+ internal const string Chevron = "chevron";
+
+ ///
+ /// Slot for the right-side element of a fluent-tree-item.
+ ///
+ internal const string Aside = "aside";
}
diff --git a/src/Core/Components/TreeView/FluentTreeItem.razor b/src/Core/Components/TreeView/FluentTreeItem.razor
new file mode 100644
index 0000000000..682630db66
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeItem.razor
@@ -0,0 +1,85 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@inherits FluentComponentBase
+
+
+
+ @* Expandable icons: _toggleTreeItem(this) manages expand/collapse, which does not work by default in the webcomponent *@
+ @if (IconCollapsed is not null || IconExpanded is not null)
+ {
+
+ @if (IconCollapsed is not null)
+ {
+
+ @(IconExpanded is null ? DefaultIcon_Expanded : null)
+ }
+
+ @if (IconExpanded is not null)
+ {
+
+ @(IconCollapsed is null ? DefaultIcon_Collapsed : null)
+ }
+
+ }
+
+ @* Text *@
+ @if (!string.IsNullOrEmpty(Text))
+ {
+
+ @Text
+
+ }
+
+ @* Custom content and sub-items *@
+ @ChildContent
+
+ @* Items *@
+ @if (OwnerTreeView is not null && Items is not null)
+ {
+ if (OwnerTreeView.LazyLoadItems && Items.Any() && !Expanded)
+ {
+ @* Lazy loading required a "fake" sub-item to simulate the [+] *@
+ @Localizer[Localization.LanguageResource.TreeItem_LoadingMessage]
+ }
+ else
+ {
+ foreach (var item in Items)
+ {
+ @FluentTreeItem.GetFluentTreeItem(OwnerTreeView, item)
+ }
+ }
+ }
+
+ @* Start *@
+ @if (IconStart is not null)
+ {
+
+
+
+ }
+
+ @* End *@
+ @if (IconEnd is not null)
+ {
+
+
+
+ }
+
+ @* Aside *@
+ @if (IconAside is not null)
+ {
+
+
+
+ }
+
diff --git a/src/Core/Components/TreeView/FluentTreeItem.razor.cs b/src/Core/Components/TreeView/FluentTreeItem.razor.cs
new file mode 100644
index 0000000000..74bd50d91c
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeItem.razor.cs
@@ -0,0 +1,371 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Represents a tree item in a .
+///
+public partial class FluentTreeItem : FluentComponentBase
+{
+ private const string DefaultIcon_CommonSvgAttributes = "width=\"12\" height=\"12\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" slot=\"chevron\"";
+ private const string DefaultIcon_CommonSvgPath = "";
+ private static readonly MarkupString DefaultIcon_Expanded = new($" ");
+ private static readonly MarkupString DefaultIcon_Collapsed = new($"");
+
+ ///
+ public FluentTreeItem()
+ {
+ Id = Identifier.NewId();
+ }
+
+ ///
+ protected string? ClassValue => DefaultClassBuilder
+ .Build();
+
+ ///
+ protected string? StyleValue => DefaultStyleBuilder
+ .AddStyle("--tree-item-height", Height, when: () => !string.IsNullOrEmpty(Height))
+ .Build();
+
+ ///
+ [CascadingParameter]
+ internal FluentTreeView? OwnerTreeView { get; set; }
+
+ ///
+ /// Gets or sets the size of the tree item.
+ /// Default is .
+ ///
+ [Parameter]
+ public TreeSize? Size { get; set; }
+
+ ///
+ /// Gets or sets the height of the tree item.
+ ///
+ [Parameter]
+ public string? Height { get; set; }
+
+ ///
+ /// Gets or sets the appearance of the tree item.
+ /// Default is .
+ ///
+ [Parameter]
+ public TreeAppearance? Appearance { get; set; }
+
+ ///
+ /// Gets or sets the list of sub-items to bind to the tree item
+ ///
+ [Parameter]
+ public IEnumerable? Items { get; set; }
+
+ ///
+ /// Gets or sets the text of the tree item.
+ /// If this text is too long, it will be truncated with ellipsis.
+ ///
+ [Parameter]
+ public string? Text { get; set; }
+
+ ///
+ /// Gets or sets the content to be rendered inside the component.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the start of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ [Parameter]
+ public Icon? IconStart { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the end of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ [Parameter]
+ public Icon? IconEnd { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the "far end" of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ [Parameter]
+ public Icon? IconAside { get; set; }
+
+ ///
+ /// Gets or sets the displayed to indicate the tree item is collapsed.
+ /// If this icon is not set, the will be used.
+ /// We recommend using an icon size of 16px.
+ ///
+ [Parameter]
+ public Icon? IconCollapsed { get; set; }
+
+ ///
+ /// Gets or sets the displayed to indicate the tree item is expanded.
+ /// If this icon is not set, the will be used.
+ /// A 90-degree rotation effect is applied to the icon.
+ /// Please select an icon that will look correct after rotation.
+ /// We recommend using an icon size of 16px.
+ ///
+ [Parameter]
+ public Icon? IconExpanded { get; set; }
+
+ ///
+ /// Returns if the tree item is expanded,
+ /// and if collapsed.
+ ///
+ [Parameter]
+ public bool Expanded { get; set; }
+
+ ///
+ /// Called whenever changes.
+ ///
+ [Parameter]
+ public EventCallback ExpandedChanged { get; set; }
+
+ ///
+ /// Called whenever the selected item changes.
+ ///
+ [Parameter]
+ public EventCallback SelectedChanged { get; set; }
+
+ ///
+ /// Gets the item associated with the current element, based on its Id.
+ ///
+ public ITreeViewItem? Item => OwnerTreeView is null || OwnerTreeView.Items is null
+ ? null
+ : TreeViewItem.FindItemById(OwnerTreeView.Items, Id);
+
+ ///
+ private bool IsSelected => string.CompareOrdinal(OwnerTreeView?.SelectedId, Id) == 0 ||
+ string.CompareOrdinal(OwnerTreeView?.SelectedItem?.Id, Id) == 0;
+
+ ///
+ protected override void OnInitialized()
+ {
+ if (OwnerTreeView is not null && !string.IsNullOrEmpty(Id))
+ {
+ OwnerTreeView.InternalItems.TryAdd(Id, this);
+ }
+ }
+
+ ///
+ internal async Task OnTreeChangedAsync(TreeItemChangedEventArgs args)
+ {
+ if (!string.Equals(Id, args.Id, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ // Selected?
+ var isSelected = args.Selected;
+
+ // Update the state
+ if (SelectedChanged.HasDelegate)
+ {
+ await SelectedChanged.InvokeAsync(isSelected);
+ }
+
+ // Update the FluentTree owner (only to inform the new selected item
+ if (OwnerTreeView is not null && isSelected)
+ {
+ // SelectedIdChanged
+ if (OwnerTreeView.SelectedIdChanged.HasDelegate &&
+ !string.Equals(OwnerTreeView.SelectedId, Id, StringComparison.Ordinal))
+ {
+ await OwnerTreeView.SelectedIdChanged.InvokeAsync(Id);
+ }
+
+ // CurrentSelectedItem
+ if (OwnerTreeView.CurrentSelectedChanged.HasDelegate &&
+ OwnerTreeView.CurrentSelected != this)
+ {
+ await OwnerTreeView.CurrentSelectedChanged.InvokeAsync(this);
+ }
+
+ // SelectedItem
+ if (OwnerTreeView.Items is not null &&
+ OwnerTreeView.SelectedItemChanged.HasDelegate)
+ {
+ var selectedItem = TreeViewItem.FindItemById(OwnerTreeView.Items, Id);
+
+ if (OwnerTreeView.SelectedItem != selectedItem)
+ {
+ await OwnerTreeView.SelectedItemChanged.InvokeAsync(selectedItem);
+ }
+ }
+
+ // OnSelectedChanged
+ if (OwnerTreeView.OnSelectedChanged.HasDelegate)
+ {
+ await OwnerTreeView.OnSelectedChanged.InvokeAsync(this);
+ }
+ }
+ }
+
+ ///
+ internal async Task OnTreeToggleAsync(TreeItemToggleEventArgs args)
+ {
+ // Only for the correct TreeItem
+ if (!string.Equals(Id, args.Id, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ // Expanded?
+ var isExpanded = string.Equals(args.NewState, "open", StringComparison.Ordinal);
+
+ // Update the state
+ if (isExpanded != Expanded)
+ {
+ Expanded = isExpanded;
+
+ if (ExpandedChanged.HasDelegate)
+ {
+ await InvokeAsync(async () => await ExpandedChanged.InvokeAsync(isExpanded));
+ }
+ }
+
+ // Update the FluentTree owner
+ if (OwnerTreeView is not null && OwnerTreeView.OnExpandedChanged.HasDelegate)
+ {
+ await InvokeAsync(async () => await OwnerTreeView.OnExpandedChanged.InvokeAsync(this));
+ }
+ }
+
+ ///
+ internal async Task OnCheckChangedHandlerAsync()
+ {
+ var checkedItem = TreeViewItem.FindItemById(OwnerTreeView?.Items, Id);
+
+ if (OwnerTreeView is null || checkedItem is null)
+ {
+ return;
+ }
+
+ var selectedItems = OwnerTreeView.SelectedItems?.ToList() ?? [];
+ var isSelected = selectedItems.Contains(checkedItem);
+
+ if (isSelected)
+ {
+ selectedItems.Remove(checkedItem);
+ }
+ else
+ {
+ selectedItems.Add(checkedItem);
+ }
+
+ if (OwnerTreeView.SelectedItemsChanged.HasDelegate)
+ {
+ await OwnerTreeView.SelectedItemsChanged.InvokeAsync(selectedItems);
+ }
+ }
+
+ ///
+ /// Renders a FluentTreeItem component, using a
+ ///
+ ///
+ ///
+ ///
+ internal static RenderFragment GetFluentTreeItem(FluentTreeView owner, ITreeViewItem item)
+ {
+ RenderFragment fluentTreeItem = builder =>
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, nameof(Id), item.Id);
+ builder.AddAttribute(2, nameof(Items), item.Items);
+ builder.AddAttribute(3, nameof(Text), owner.ItemTemplate is null && owner.SelectionMode == TreeSelectionMode.Single ? item.Text : null);
+ builder.AddAttribute(4, nameof(Expanded), item.Expanded);
+ builder.AddAttribute(5, nameof(IconStart), item.IconStart);
+ builder.AddAttribute(6, nameof(IconEnd), item.IconEnd);
+ builder.AddAttribute(7, nameof(IconAside), item.IconAside);
+ builder.AddAttribute(8, nameof(IconCollapsed), item.IconCollapsed);
+ builder.AddAttribute(9, nameof(IconExpanded), item.IconExpanded);
+
+ AddFluentTreeItemChildContent(builder, owner, item);
+
+ builder.AddAttribute(11, nameof(ExpandedChanged), EventCallback.Factory.Create(owner, async expanded =>
+ {
+ item.Expanded = expanded;
+ if (item.OnExpandedAsync != null)
+ {
+ await item.OnExpandedAsync(new TreeViewItemExpandedEventArgs(item, expanded));
+ }
+ }));
+
+ builder.CloseComponent();
+ };
+
+ return fluentTreeItem;
+ }
+
+ ///
+ private static void AddFluentTreeItemChildContent(RenderTreeBuilder builder, FluentTreeView owner, ITreeViewItem item)
+ {
+ switch (owner.SelectionMode)
+ {
+ case TreeSelectionMode.Single:
+ if (owner.ItemTemplate != null)
+ {
+ builder.AddAttribute(10, nameof(ChildContent), owner.ItemTemplate.Invoke(item));
+ }
+
+ break;
+
+ case TreeSelectionMode.Multiple:
+ builder.AddAttribute(10, nameof(ChildContent), (RenderFragment)(childBuilder =>
+ {
+ var visibility = owner.MultipleSelectionVisibility?.Invoke(item) ?? TreeSelectionVisibility.Visible;
+
+ // Checkbox
+ switch (visibility)
+ {
+ // Visible
+ case TreeSelectionVisibility.Visible:
+ childBuilder.OpenElement(0, "fluent-checkbox");
+ childBuilder.AddAttribute(1, "checked", owner.SelectedItems?.Contains(item) == true ? "true" : null);
+ childBuilder.AddAttribute(2, "onchange", EventCallback.Factory.Create(owner, async e =>
+ {
+ // Call the handler on the FluentTreeItem instance
+ var fluentTreeItem = owner.InternalItems.TryGetValue(item.Id, out var ti) ? ti : null;
+ if (fluentTreeItem != null)
+ {
+ await fluentTreeItem.OnCheckChangedHandlerAsync();
+ }
+ }));
+ childBuilder.AddAttribute(3, "tabindex", -1);
+ childBuilder.CloseElement();
+ break;
+
+ // Hidden
+ case TreeSelectionVisibility.Hidden:
+ childBuilder.OpenElement(0, "div");
+ childBuilder.AddAttribute(1, "class", "hidden-checkbox");
+ childBuilder.CloseElement();
+ break;
+ }
+
+ // Content
+ childBuilder.AddContent(4, owner.ItemTemplate?.Invoke(item) ?? (RenderFragment)(builder2 => builder2.AddContent(0, item.Text)));
+ }));
+
+ break;
+ }
+ }
+
+ ///
+ public override ValueTask DisposeAsync()
+ {
+ if (OwnerTreeView is not null && !string.IsNullOrEmpty(Id))
+ {
+ OwnerTreeView.InternalItems.TryRemove(Id, out _);
+ }
+
+ return base.DisposeAsync();
+ }
+}
diff --git a/src/Core/Components/TreeView/FluentTreeItem.razor.css b/src/Core/Components/TreeView/FluentTreeItem.razor.css
new file mode 100644
index 0000000000..a238eaa6bd
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeItem.razor.css
@@ -0,0 +1,53 @@
+fluent-tree-item > span[slot="chevron"],
+fluent-tree-item > span[slot="start"]:has(svg),
+fluent-tree-item > span[slot="end"]:has(svg),
+fluent-tree-item > span[slot="aside"]:has(svg) {
+ display: flex;
+}
+
+fluent-tree-item[expanded] > span[slot="chevron"] > svg[collapsed] {
+ display: none;
+}
+
+fluent-tree-item:not([expanded]) > span[slot="chevron"] > svg[expanded] {
+ display: none;
+}
+
+fluent-tree-item span[part="text"] {
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+fluent-tree-item::part(positioning-region) {
+ height: var(--tree-item-height, var(--spacingVerticalXXXL));
+}
+
+fluent-tree-item[size="small"]::part(positioning-region) {
+ height: var(--tree-item-height, var(--spacingVerticalXXL));
+}
+
+fluent-tree[hide-selection] fluent-tree-item[selected]::part(positioning-region) {
+ background-color: var(--colorSubtleBackground);
+ color: var(--colorNeutralForeground2);
+}
+
+fluent-tree-item > span[slot="chevron"],
+fluent-tree-item > span[slot="start"],
+fluent-tree-item > span[slot="end"],
+fluent-tree-item > span[slot="aside"],
+fluent-tree-item > span[part="text"] {
+ pointer-events: none;
+}
+
+fluent-tree-item fluent-checkbox {
+ margin-right: var(--spacingHorizontalXS);
+}
+
+fluent-tree-item .hidden-checkbox {
+ width: 16px; /* Default checkbox size */
+ height: 16px;
+ margin-right: var(--spacingHorizontalXS);
+}
diff --git a/src/Core/Components/TreeView/FluentTreeView.razor b/src/Core/Components/TreeView/FluentTreeView.razor
new file mode 100644
index 0000000000..e4a7af108f
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeView.razor
@@ -0,0 +1,25 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@inherits FluentComponentBase
+
+
+
+
+ @ChildContent
+
+ @if (Items != null)
+ {
+ foreach (var item in Items)
+ {
+ @FluentTreeItem.GetFluentTreeItem(this, item)
+ }
+ }
+
+
+
diff --git a/src/Core/Components/TreeView/FluentTreeView.razor.cs b/src/Core/Components/TreeView/FluentTreeView.razor.cs
new file mode 100644
index 0000000000..c1e6a7b69a
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeView.razor.cs
@@ -0,0 +1,174 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+using Microsoft.JSInterop;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Represents a tree view component.
+///
+public partial class FluentTreeView : FluentComponentBase
+{
+ private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "TreeView/FluentTreeView.razor.js";
+
+ internal ConcurrentDictionary InternalItems { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TreeItemChangedEventArgs))]
+ [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TreeItemToggleEventArgs))]
+ public FluentTreeView()
+ {
+ Id = Identifier.NewId();
+ }
+
+ ///
+ protected string? ClassValue => DefaultClassBuilder
+ .Build();
+
+ ///
+ protected string? StyleValue => DefaultStyleBuilder
+ .Build();
+
+ ///
+ /// Gets or sets the size of the tree. Default is .
+ ///
+ [Parameter]
+ public TreeSize? Size { get; set; } = TreeSize.Medium;
+
+ ///
+ /// Gets or sets the appearance of the tree. Default is .
+ ///
+ [Parameter]
+ public TreeAppearance? Appearance { get; set; } = TreeAppearance.Subtle;
+
+ ///
+ /// Gets or sets whether the tree view element is not highlighted to indicate that it is selected.
+ ///
+ [Parameter]
+ public bool HideSelection { get; set; }
+
+ ///
+ /// Gets or sets the list of items to bind to the tree.
+ ///
+ [Parameter]
+ public IEnumerable? Items { get; set; }
+
+ ///
+ /// Gets or sets the template for rendering tree items.
+ ///
+ [Parameter]
+ public RenderFragment? ItemTemplate { get; set; }
+
+ ///
+ /// Can only be used when the is defined.
+ /// Gets or sets whether the tree should use lazy loading when expanding nodes.
+ /// If True, the tree will only render the children of a node when it is expanded and will remove them when it is collapsed.
+ ///
+ [Parameter]
+ public bool LazyLoadItems { get; set; } = false;
+
+ ///
+ /// Gets or sets the content to be rendered inside the component.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Gets or sets the selected item id.
+ ///
+ [Parameter]
+ public string? SelectedId { get; set; }
+
+ ///
+ /// Called whenever the selected item changes.
+ ///
+ [Parameter]
+ public EventCallback SelectedIdChanged { get; set; }
+
+ ///
+ /// Gets or sets the selected item.
+ ///
+ [Parameter]
+ public FluentTreeItem? CurrentSelected { get; set; }
+
+ ///
+ /// Called whenever the selected changes.
+ ///
+ [Parameter]
+ public EventCallback CurrentSelectedChanged { get; set; }
+
+ ///
+ /// Gets or sets the selected item.
+ ///
+ [Parameter]
+ public ITreeViewItem? SelectedItem { get; set; }
+
+ ///
+ /// Called whenever the selected changes.
+ ///
+ [Parameter]
+ public EventCallback SelectedItemChanged { get; set; }
+
+ ///
+ /// Gets or sets whether the tree allows multiple selections.
+ /// This Multiple Selection feature is only available when the parameter is used to generate the tree.
+ /// By default, the tree allows only single selection.
+ ///
+ [Parameter]
+ public TreeSelectionMode SelectionMode { get; set; } = TreeSelectionMode.Single;
+
+ ///
+ /// Gets or sets the visibility of the multi-selection checkbox.
+ /// By default all items are visible.
+ ///
+ [Parameter]
+ public Func? MultipleSelectionVisibility { get; set; }
+
+ ///
+ /// Gets or sets the multi-selected items.
+ ///
+ [Parameter]
+ public IEnumerable? SelectedItems { get; set; }
+
+ ///
+ /// Called whenever the multi-selected changes.
+ ///
+ [Parameter]
+ public EventCallback?> SelectedItemsChanged { get; set; }
+
+ ///
+ /// Called whenever changes on an item within the tree.
+ /// You cannot update FluentTreeItem properties.
+ ///
+ [Parameter]
+ public EventCallback OnExpandedChanged { get; set; }
+
+ ///
+ /// Called whenever the selected item changes.
+ /// You cannot update FluentTreeItem properties.
+ ///
+ [Parameter]
+ public EventCallback OnSelectedChanged { get; set; }
+
+ ///
+ [ExcludeFromCodeCoverage(Justification = "JavaScript is not covered by unit tests")]
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender && SelectionMode != TreeSelectionMode.Single)
+ {
+ // Import the JavaScript module
+ var jsModule = await JSModule.ImportJavaScriptModuleAsync(JAVASCRIPT_FILE);
+
+ // Call a function from the JavaScript module
+ await jsModule.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TreeView.Initialize", Id, true);
+ }
+ }
+}
diff --git a/src/Core/Components/TreeView/FluentTreeView.razor.ts b/src/Core/Components/TreeView/FluentTreeView.razor.ts
new file mode 100644
index 0000000000..085d7396fc
--- /dev/null
+++ b/src/Core/Components/TreeView/FluentTreeView.razor.ts
@@ -0,0 +1,25 @@
+export namespace Microsoft.FluentUI.Blazor.TreeView {
+
+ /**
+ * Initializes the Fluent TreeView component.
+ * @param id
+ * @param multiple
+ */
+ export function Initialize(id: string, multiple: boolean) {
+ const treeView = document.getElementById(id);
+
+ if (treeView && multiple) {
+
+ treeView.addEventListener('keydown', (event: KeyboardEvent) => {
+
+ if (event.code === 'Space' && event.target instanceof HTMLElement) {
+
+ const checkbox = event.target.querySelector('fluent-checkbox') as HTMLElement;
+ if (checkbox) {
+ checkbox.click();
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/src/Core/Components/TreeView/ITreeViewItem.cs b/src/Core/Components/TreeView/ITreeViewItem.cs
new file mode 100644
index 0000000000..9cdeab0e5e
--- /dev/null
+++ b/src/Core/Components/TreeView/ITreeViewItem.cs
@@ -0,0 +1,71 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Interface for tree view items
+///
+public interface ITreeViewItem
+{
+ ///
+ /// Gets or sets the unique identifier of the tree item.
+ ///
+ string Id { get; set; }
+
+ ///
+ /// Gets or sets the text of the tree item.
+ /// If this text is too long, it will be truncated with ellipsis.
+ ///
+ string Text { get; set; }
+
+ ///
+ /// Gets or sets the sub-items of the tree item.
+ ///
+ IEnumerable? Items { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the start of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ Icon? IconStart { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the end of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ Icon? IconEnd { get; set; }
+
+ ///
+ /// Gets or sets the displayed at the "far end" of item content.
+ /// We recommend using an icon size of 16px.
+ ///
+ Icon? IconAside { get; set; }
+
+ ///
+ /// Gets or sets the displayed to indicate the tree item is collapsed.
+ /// If this icon is not set, the will be used.
+ /// We recommend using an icon size of 16px.
+ ///
+ Icon? IconCollapsed { get; set; }
+
+ ///
+ /// Gets or sets the displayed to indicate the tree item is expanded.
+ /// If this icon is not set, the will be used.
+ /// A 90-degree rotation effect is applied to the icon.
+ /// Please select an icon that will look correct after rotation.
+ /// We recommend using an icon size of 16px.
+ ///
+ Icon? IconExpanded { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the default state of the tree item.
+ ///
+ bool Expanded { get; set; }
+
+ ///
+ /// Gets or sets the action to be performed when the tree item is expanded or collapsed
+ ///
+ Func? OnExpandedAsync { get; set; }
+}
diff --git a/src/Core/Components/TreeView/TreeViewItem.cs b/src/Core/Components/TreeView/TreeViewItem.cs
new file mode 100644
index 0000000000..3f1a45dab0
--- /dev/null
+++ b/src/Core/Components/TreeView/TreeViewItem.cs
@@ -0,0 +1,125 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Implementation of
+///
+[DebuggerDisplay("{DebuggerDisplay}")]
+public class TreeViewItem : ITreeViewItem
+{
+ ///
+ /// Returns an array with a single that represents a loading state.
+ ///
+ /// The loading message
+ public static IEnumerable LoadingTreeViewItems(string loadingMessage) => [new TreeViewItem() { Text = loadingMessage }];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TreeViewItem()
+ {
+ Id = Identifier.NewId();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Text of the tree item
+ /// Sub-items of the tree item.
+ public TreeViewItem(string text, IEnumerable? items = null)
+ {
+ Id = Identifier.NewId();
+ Text = text;
+ Items = items;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Unique identifier of the tree item
+ /// Text of the tree item
+ /// Sub-items of the tree item.
+ public TreeViewItem(string id, string text, IEnumerable? items = null)
+ {
+ Id = id;
+ Text = text;
+ Items = items;
+ }
+
+ ///
+ public string Id { get; set; }
+
+ ///
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ public IEnumerable? Items { get; set; }
+
+ ///
+ public Icon? IconStart { get; set; }
+
+ ///
+ public Icon? IconEnd { get; set; }
+
+ ///
+ public Icon? IconAside { get; set; }
+
+ ///
+ public Icon? IconCollapsed { get; set; }
+
+ ///
+ public Icon? IconExpanded { get; set; }
+
+ ///
+ public bool Expanded { get; set; }
+
+ ///
+ public Func? OnExpandedAsync { get; set; }
+
+ ///
+ /// Returns the first item with the specified id in the tree view items.
+ ///
+ /// The tree view items to search in.
+ /// Identifier of the item to find.
+ ///
+ internal static ITreeViewItem? FindItemById(IEnumerable? items, string? id)
+ {
+ if (items == null)
+ {
+ return null;
+ }
+
+ foreach (var item in items)
+ {
+ if (string.Equals(item.Id, id, StringComparison.Ordinal))
+ {
+ return item;
+ }
+
+ var nestedItem = FindItemById(item.Items, id);
+ if (nestedItem != null)
+ {
+ return nestedItem;
+ }
+ }
+
+ return null;
+ }
+
+ internal string DebuggerDisplay
+ {
+ get
+ {
+ var count = Items?.Count() ?? 0;
+ return count > 0
+ ? $"[{Id}] {Text} (+ {count} sub-items)"
+ : $"[{Id}] {Text}";
+ }
+ }
+}
diff --git a/src/Core/Components/TreeView/TreeViewItemExpandedEventArgs.cs b/src/Core/Components/TreeView/TreeViewItemExpandedEventArgs.cs
new file mode 100644
index 0000000000..95ffa88ad2
--- /dev/null
+++ b/src/Core/Components/TreeView/TreeViewItemExpandedEventArgs.cs
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Class that contains the event arguments for the event.
+///
+public class TreeViewItemExpandedEventArgs
+{
+ ///
+ internal TreeViewItemExpandedEventArgs(ITreeViewItem item, bool expanded)
+ {
+ CurrentItem = item;
+ Expanded = expanded;
+ }
+
+ ///
+ /// Gets the that was expanded or collapsed.
+ ///
+ public ITreeViewItem CurrentItem { get; }
+
+ ///
+ /// Gets a value indicating whether the item was expanded or collapsed.
+ ///
+ public bool Expanded { get; }
+}
diff --git a/src/Core/Enums/TreeAppearance.cs b/src/Core/Enums/TreeAppearance.cs
new file mode 100644
index 0000000000..e906f10d22
--- /dev/null
+++ b/src/Core/Enums/TreeAppearance.cs
@@ -0,0 +1,31 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.ComponentModel;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Sets the visual appearance of the or the
+///
+public enum TreeAppearance
+{
+ ///
+ /// Sublte appearance
+ ///
+ [Description("subtle")]
+ Subtle,
+
+ ///
+ /// Subtle appearance with alpha
+ ///
+ [Description("subtle-alpha")]
+ SubtleAlpha,
+
+ ///
+ /// Transparent appearance
+ ///
+ [Description("transparant")]
+ Transparent,
+}
diff --git a/src/Core/Enums/TreeSelectionMode.cs b/src/Core/Enums/TreeSelectionMode.cs
new file mode 100644
index 0000000000..8275633495
--- /dev/null
+++ b/src/Core/Enums/TreeSelectionMode.cs
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Specifies the selection mode for a component.
+///
+public enum TreeSelectionMode
+{
+ ///
+ /// The user can select only one item at a time.
+ ///
+ Single,
+
+ ///
+ /// The user can select multiple items at a time, at any level.
+ ///
+ Multiple,
+}
diff --git a/src/Core/Enums/TreeSelectionVisibility.cs b/src/Core/Enums/TreeSelectionVisibility.cs
new file mode 100644
index 0000000000..c227707e54
--- /dev/null
+++ b/src/Core/Enums/TreeSelectionVisibility.cs
@@ -0,0 +1,26 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Specifies the selection mode for a component.
+///
+public enum TreeSelectionVisibility
+{
+ ///
+ /// The checkbox is visible.
+ ///
+ Visible,
+
+ ///
+ /// The checkbox is invisible (not drawn), but still affects layout as normal.
+ ///
+ Hidden,
+
+ ///
+ /// The checkbox is invisible (not drawn), and the space it would take up is collapsed.
+ ///
+ Collapse,
+}
diff --git a/src/Core/Enums/TreeSize.cs b/src/Core/Enums/TreeSize.cs
new file mode 100644
index 0000000000..5bd7fbe554
--- /dev/null
+++ b/src/Core/Enums/TreeSize.cs
@@ -0,0 +1,25 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.ComponentModel;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Sets the visual size of the or the
+///
+public enum TreeSize
+{
+ ///
+ /// Medium size
+ ///
+ [Description("medium")]
+ Medium,
+
+ ///
+ /// Small size
+ ///
+ [Description("small")]
+ Small,
+}
diff --git a/src/Core/Events/EventHandlers.cs b/src/Core/Events/EventHandlers.cs
index 9df1d174ac..c9727c574c 100644
--- a/src/Core/Events/EventHandlers.cs
+++ b/src/Core/Events/EventHandlers.cs
@@ -33,6 +33,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
[EventHandler("onmenuitemchange", typeof(MenuItemEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("ontabchange", typeof(TabChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("ondropdownchange", typeof(DropdownEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
+[EventHandler("ontreechanged", typeof(TreeItemChangedEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
+[EventHandler("ontreetoggle", typeof(TreeItemToggleEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}
diff --git a/src/Core/Events/TreeItemChangedEventArgs.cs b/src/Core/Events/TreeItemChangedEventArgs.cs
new file mode 100644
index 0000000000..0bf24c78ec
--- /dev/null
+++ b/src/Core/Events/TreeItemChangedEventArgs.cs
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Event arguments for the FluentTabs ActiveId changed event.
+///
+internal class TreeItemChangedEventArgs : EventArgs
+{
+ ///
+ /// Gets or sets the ID of the tree item.
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the item is selected.
+ ///
+ public bool Selected { get; set; }
+}
diff --git a/src/Core/Events/TreeItemToggleEventArgs.cs b/src/Core/Events/TreeItemToggleEventArgs.cs
new file mode 100644
index 0000000000..3901d2a61c
--- /dev/null
+++ b/src/Core/Events/TreeItemToggleEventArgs.cs
@@ -0,0 +1,12 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Event arguments for the TreeItem Expanded event.
+///
+internal class TreeItemToggleEventArgs : DialogToggleEventArgs
+{
+}
diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx
index e9b4b5e3e6..f59ab4878e 100644
--- a/src/Core/Localization/LanguageResource.resx
+++ b/src/Core/Localization/LanguageResource.resx
@@ -216,4 +216,7 @@
Close
+
+ Loading...
+
\ No newline at end of file
diff --git a/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Default.verified.razor.html b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Default.verified.razor.html
new file mode 100644
index 0000000000..5290394213
--- /dev/null
+++ b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Default.verified.razor.html
@@ -0,0 +1,4 @@
+
+
+ My text
+
\ No newline at end of file
diff --git a/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Icons.verified.razor.html b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Icons.verified.razor.html
new file mode 100644
index 0000000000..21a9036f4d
--- /dev/null
+++ b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_Icons.verified.razor.html
@@ -0,0 +1,21 @@
+
+
+ My text
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-custom.verified.razor.html b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-custom.verified.razor.html
new file mode 100644
index 0000000000..71a33c0944
--- /dev/null
+++ b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-custom.verified.razor.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+ My text
+
\ No newline at end of file
diff --git a/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-default.verified.razor.html b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-default.verified.razor.html
new file mode 100644
index 0000000000..a65a00ab30
--- /dev/null
+++ b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-custom-default.verified.razor.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ My text
+
\ No newline at end of file
diff --git a/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-default-custom.verified.razor.html b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-default-custom.verified.razor.html
new file mode 100644
index 0000000000..cc6c13f360
--- /dev/null
+++ b/tests/Core/Components/TreeView/FluentTreeItemTests.FluentTreeItem_IconsExpand-default-custom.verified.razor.html
@@ -0,0 +1,14 @@
+
+
+
+