diff --git a/Directory.Build.props b/Directory.Build.props index 7a784a92c0..37c032747d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,8 +12,8 @@ $(MSBuildThisFileDirectory) - 3.1.2 - 3.1.2 + 3.2.0 + 3.2.0 $(VersionFile) diff --git a/README.md b/README.md index 0801e8b3dc..8dd1003c80 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,13 @@ to your `index.html` or `_Layout.cshtml` file in the `` section like this: The file contains a number of CSS variables that are required to be defined for the components to work correctly. -### Project file -if you want to use icons and/or emoji, starting with version 2.1 you need add a `` to your project file. Within this group you can specify which icons and emoji are made available for usage and publication. Please refer to the [project setup](https://www.fluentui-blazor.net/ProjectSetup) document for more information. - - ### Code Please refer to the [code setup](https://www.fluentui-blazor.net/CodeSetup) document to learn what needs to be included in your `Program.cs` file so that all necessary services are available and setup in the correct way. +## Working with Icons and Emoji +We have additional packages available that include the complete Fluent UI System icons and Fluent UI Emoji collections. +Please refer to the [Icons and Emoji](https://www.fluentui-blazor.net/IconsAndEmoji) page for more information. ## Getting started by using project templates To make it easier to start a project that uses the Fluent UI Blazor components out of the box, we have created the diff --git a/WHATSNEW.md b/WHATSNEW.md index 5db09c21ea..ec395c2771 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,4 +1,7 @@ -## V3.1.1 +## V3.2.0 +- New NavMenu, NavGroup and NavLink components. **Breaking change** - See the [Upgrade guide](https://www.fluentui-blazor.net/UpgradeGuide) for details. See [NavMenu](https://www.fluentui-blazor.net/NavMenu) page for examples. + +## V3.1.1 - Fix [#776](https://github.com/microsoft/fluentui-blazor/issues/776): Icon throws exception when deployed to Azure - Fix [#755](https://github.com/microsoft/fluentui-blazor/issues/755): Icon throws exception when deployed to Azure - Fix [#789](https://github.com/microsoft/fluentui-blazor/issues/789): Navigation to "/" crashes with FluentNavMenu diff --git a/examples/Demo/Shared/Pages/Index/Index.razor b/examples/Demo/Shared/Pages/Index/Index.razor index ed2bc810c8..717e1dfe65 100644 --- a/examples/Demo/Shared/Pages/Index/Index.razor +++ b/examples/Demo/Shared/Pages/Index/Index.razor @@ -6,10 +6,10 @@

Welcome to the Fluent UI Blazor components library

-

This is the demo and documentation site for the next version (3.1.1) of the library

+

This is the demo and documentation site for the next version (3.2.0) of the library


- The previous demos and documentation site (2.4.3) is also still available. + The version 2 demos and documentation site is also still available.

diff --git a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCollapsible.razor b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCollapsible.razor index 306e13565b..533d9e5514 100644 --- a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCollapsible.razor +++ b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCollapsible.razor @@ -1,17 +1,15 @@ -@namespace FluentUI.Demo.Shared - -

Collapsible Navigation Example

+

Collapsible Navigation Example

- - - - - - - + Item 1 + Item 2 + + Item 3.1 + Item 3.2 + + Item 4
diff --git a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDataBound.razor b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDataBound.razor index 66dca1cd85..076c6a1666 100644 --- a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDataBound.razor +++ b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDataBound.razor @@ -1,20 +1,16 @@ -@namespace FluentUI.Demo.Shared - -@inject ILogger logger; - -

Navigation Examples

+

Navigation Examples

- - - - - - - - + + Item 1.1 + Item 1.2 + + + Item 2.1 + Item 2.2 + @@ -29,19 +25,7 @@ Item 2 -

Selected elements

- - Item 1.1 - - - Item 1.2 - - - Item 2.1 - - - Item 2.2 - +
@@ -50,11 +34,6 @@ bool MenuExpanded = true; bool Item1Expanded = true; bool Item2Expanded = true; - - bool Item1Point1Selected = false; - bool Item1Point2Selected = false; - bool Item2Point1Selected = false; - bool Item2Point2Selected = false; } diff --git a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDefault.razor b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDefault.razor index ab90b10a44..8ac20860d8 100644 --- a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDefault.razor +++ b/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuDefault.razor @@ -1,71 +1,65 @@ -@namespace FluentUI.Demo.Shared - -@inject ILogger logger; - -

Navigation Examples

+

Navigation Examples

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + Home + Item 2 + +

Item 3

+ + Item 3.1 + Item 3.2 + +
+ Item 4 + Item 5 + + Item 6.1 + Item 6.2 + + Item 6.3.1 Item 6.3.1 Item 6.3.1 + Item 6.3.2 + + Item 6.3.3.1 + Item 6.3.3.2 + + Item 6.3.3.3.1 + Item 6.3.3.3.2 + + + + +
+
+ + @code + { + bool expanded = true; + } - - - - + Item 1 + Item 2 + Item 3 + Item 4 - - - - + Item 1 + Item 2 + Item 3 + Item 4
@code { - void OnClick(MouseEventArgs e) { - logger.LogInformation("Item Clicked"); - } - - protected override void OnAfterRender(bool firstRender) { - if (firstRender) - { - - } - base.OnAfterRender(firstRender); + DemoLogger.WriteLine("NavMenu item clicked"); } } diff --git a/examples/Demo/Shared/Pages/NavMenu/NavMenuPage.razor b/examples/Demo/Shared/Pages/NavMenu/NavMenuPage.razor index 91ccf6a257..b162346cbf 100644 --- a/examples/Demo/Shared/Pages/NavMenu/NavMenuPage.razor +++ b/examples/Demo/Shared/Pages/NavMenu/NavMenuPage.razor @@ -1,29 +1,48 @@ @page "/NavMenu" +@using FluentUI.Demo.Shared.Pages.NavMenu.Examples + +

NavMenu, NavGroup and NavLink

+ +
+ IMPORTANT +

+ With version 3.2 a new, much improved, set of components to build side-bar menus has been added. +

+

+ + If you DO NOT want to upgrade to these new menu components, you can continue to use the pre-version 3.2 components. The only thing + you need to do is to change the name of the FluentNavMenu component in your application to FluentNavMenuTree. + +

+

+ Everything works exactely as before by changing the name of this FluentNavMenu component in your application. (This probably needs to be done + in one place only). The FluentNavMenuGroup and FluentNavMenuLink components have not been changed and do not need to be altered. +

+

+ We consider the FluentNavMenuTree, FluentNavMenuGroup and FluentNavMenuLink components as deprecated and will remove them in a future version. +

+

+ If you wish to upgrade to the new menu components, please refer to the Upgrade guide for more information. +

+
-

NavMenu, NavMenuGroup and NavMenuLink

- The FluentNavMenuGroup, FluentNavMenuLink and FluentNavMenu components can be used to build - hierarchical, collapsible and expandable menus. They can range from simple 1-level deep lists to complex multi-level menu - structures. + The FluentNavMenu, FluentNavGroup and FluentNavLink components can be used to build + hierarchical, collapsible and expandable side-bar menus. They can range from simple 1-level deep lists to complex multi-level menu + structures (with a max of 5 levels).

None of these components are particulary useful when used stand-alone.

- - - - - -

Examples

- This demo shows 3 different versions of a NavMenu (with FluentNavMenuGoups and FluentNavMenuLinks). + This demo shows 3 different versions of a NavMenu (with FluentNavGroups and FluentNavLink)s. From left to right:
    -
  • Menu with several sub-menus, icons, etc
  • +
  • Menu with several sub-menus, icons, etc. The first group (Item 3) uses both the Title as the TitleTemplate parameters
  • Menu without sub-menus but with icons
  • Menu with just text links
@@ -38,12 +57,13 @@ - An example data binding the Expanded parameter. + An example of binding to the Expanded parameter. - - - An example of intercepting menu actions to provide custom behavior. - - +

API Documentation

+ + + + + diff --git a/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCollapsible.razor b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCollapsible.razor new file mode 100644 index 0000000000..660d5f57ee --- /dev/null +++ b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCollapsible.razor @@ -0,0 +1,33 @@ +

Collapsible Navigation Example

+ + +
+ + + + + + + + + +
+
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce quis lorem lacus. + Ut id leo non enim feugiat ultrices. Proin vulputate volutpat urna nec iaculis. + Integer dui lacus, fermentum sit amet aliquet in, scelerisque vitae dui. + Nulla fringilla sagittis orci eu consectetur. Fusce eget dolor non lectus placerat + tincidunt. Pellentesque aliquam non odio ac porttitor. Nam finibus mattis sagittis. + Ut hendrerit porttitor massa in aliquam. Duis laoreet fringilla feugiat. + Sed maximus, urna in fringilla posuere, enim urna bibendum justo, vel molestie nibh orci nec lectus. + Etiam a varius justo. Aenean nisl ante, interdum eget vulputate eget, iaculis ut massa. + Suspendisse maximus sed purus id molestie. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+
+ +@code +{ + bool Expanded = true; +} \ No newline at end of file diff --git a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCustomActions.razor b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCustomActions.razor similarity index 96% rename from examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCustomActions.razor rename to examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCustomActions.razor index 1afe53dca6..ba11d8a38c 100644 --- a/examples/Demo/Shared/Pages/NavMenu/Examples/NavMenuCustomActions.razor +++ b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeCustomActions.razor @@ -1,11 +1,10 @@ @using System.Text; -@namespace FluentUI.Demo.Shared

Custom Actions Example

- + @* This item is handled by the MenuLevelHandler, which intercepts all items but will only handle ones with Id ending with _HandleAtMenuLevel @@ -48,7 +47,7 @@ - +

Log

diff --git a/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDataBound.razor b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDataBound.razor new file mode 100644 index 0000000000..891f81ac0a --- /dev/null +++ b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDataBound.razor @@ -0,0 +1,56 @@ +

Navigation Examples

+ + + + + + + + + + + + + + + +

Expanded elements

+ + Menu + + + Item 1 + + + Item 2 + + +

Selected elements

+ + Item 1.1 + + + Item 1.2 + + + Item 2.1 + + + Item 2.2 + + +
+
+ +@code { + bool MenuExpanded = true; + bool Item1Expanded = true; + bool Item2Expanded = true; + + bool Item1Point1Selected = false; + bool Item1Point2Selected = false; + bool Item2Point1Selected = false; + bool Item2Point2Selected = false; +} + + diff --git a/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDefault.razor b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDefault.razor new file mode 100644 index 0000000000..c36cccbce2 --- /dev/null +++ b/examples/Demo/Shared/Pages/NavMenuTree/Examples/NavMenuTreeDefault.razor @@ -0,0 +1,68 @@ +

Navigation Examples

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code +{ + + void OnClick(MouseEventArgs e) + { + DemoLogger.WriteLine("NavMenuTree item clicked"); + } + + protected override void OnAfterRender(bool firstRender) { + if (firstRender) + { + + } + base.OnAfterRender(firstRender); + } +} + + diff --git a/examples/Demo/Shared/Pages/NavMenuTree/NavMenuTreePage.razor b/examples/Demo/Shared/Pages/NavMenuTree/NavMenuTreePage.razor new file mode 100644 index 0000000000..4b4def70de --- /dev/null +++ b/examples/Demo/Shared/Pages/NavMenuTree/NavMenuTreePage.razor @@ -0,0 +1,76 @@ +@page "/NavMenuTree" + +@using FluentUI.Demo.Shared.Pages.NavMenuTree.Examples + +

NavMenuTree, NavMenuGroup and NavMenuLink

+ +
+ IMPORTANT +

+ With version 3.2 a new, much improved, set of components to build side-bar menus has been added. The demos shown here are using + components which are marked obsolete and will be removed in a future version. +

+

+ + If you DO NOT want to upgrade to these new menu components, you can continue to use the pre-version 3.2 components. The only thing + you need to do is to change the name of the FluentNavMenu component in your application to FluentNavMenuTree. + +

+

+ Everything works exactely as before by changing the name of this FluentNavMenu component in your application. (This probably needs to be done + in one place only). The FluentNavMenuGroup and FluentNavMenuLink components have not been changed and do not need to be altered. +

+

+ We consider the FluentNavMenuTree, FluentNavMenuGroup and FluentNavMenuLink components as deprecated and will remove them in a future version. +

+

+ If you wish to upgrade to the new menu components, please refer to the Upgrade guide for more information. +

+
+

+ The FluentNavMenuGroup, FluentNavMenuLink and FluentNavMenu components can be used to build + hierarchical, collapsible and expandable menus. They can range from simple 1-level deep lists to complex multi-level menu + structures. +

+

+ None of these components are particulary useful when used stand-alone. +

+ +

Examples

+ + + + This demo shows 3 different versions of a NavMenu (with FluentNavMenuGoups and FluentNavMenuLinks). + From left to right: +
    +
  • Menu with several sub-menus, icons, etc
  • +
  • Menu without sub-menus but with icons
  • +
  • Menu with just text links
  • +
+
+
+ + + + More complete example which show how menu works with together with a text section when it collapses. + + + + + + An example data binding the Expanded parameter. + + + + + + An example of intercepting menu actions to provide custom behavior. + + + +

API Documentation

+ + + + + \ No newline at end of file diff --git a/examples/Demo/Shared/Shared/DemoMainLayout.razor b/examples/Demo/Shared/Shared/DemoMainLayout.razor index 221bcb0a05..fbd73a253f 100644 --- a/examples/Demo/Shared/Shared/DemoMainLayout.razor +++ b/examples/Demo/Shared/Shared/DemoMainLayout.razor @@ -53,7 +53,7 @@
diff --git a/examples/Demo/Shared/Shared/DemoNavMenu.razor b/examples/Demo/Shared/Shared/DemoNavMenu.razor new file mode 100644 index 0000000000..d32edddab8 --- /dev/null +++ b/examples/Demo/Shared/Shared/DemoNavMenu.razor @@ -0,0 +1,106 @@ +
+ + +

Home

+
+ + + What's new + Upgrade guide + @* Project setup *@ + Code setup + Design tokens + Reboot + Icons and Emoji + DialogService + MessageService + ToastService + Project templates + Fluent UI Form + Blazor Form + + + + FluentComponentBase + FluentInputBase + Clear cache + + + + Header + Footer + BodyContent + Grid + Layout + MainLayout + Spacer + Splitter + Stack + + + + Accordion + Anchor + Anchored Region + + Badge + CounterBadge + PresenceBadge + + Autocomplete + Breadcrumb + Button + Card + Checkbox + CodeEditor + Combobox + Data grid + Date & Time + Dialog + Divider + Drag and Drop + Emoji + Flipper + Highlighter + Horizontal Scroll + Icon + InputFile + Label + Listbox + Menu + MenuButton + MessageBar + MessageBox + NavMenu + NavMenuTree + Number Field + Option + Overflow + Overlay + Panel + Popover + Progress + Progress Ring + Radio + Radio Group + Search + Select + Skeleton + Slider + SplashScreen + Switch + Tabs + TextArea + Text Field + Toast + Toolbar + Tooltip + Tree View + + + + MarkdownSection + TableOfContents + +
+
\ No newline at end of file diff --git a/examples/Demo/Shared/Shared/DemoNavMenu.razor.css b/examples/Demo/Shared/Shared/DemoNavMenu.razor.css new file mode 100644 index 0000000000..7348dbe7be --- /dev/null +++ b/examples/Demo/Shared/Shared/DemoNavMenu.razor.css @@ -0,0 +1,3 @@ +::deep fluent-tree-item { + margin: 5px 0; +} diff --git a/examples/Demo/Shared/Shared/DemoNavMenuList.razor b/examples/Demo/Shared/Shared/DemoNavMenuList.razor index eef71058e5..3bc9f8b523 100644 --- a/examples/Demo/Shared/Shared/DemoNavMenuList.razor +++ b/examples/Demo/Shared/Shared/DemoNavMenuList.razor @@ -8,9 +8,9 @@
  • What's new?
  • -
  • + @*
  • Project setup -
  • + *@
  • Code setup
  • diff --git a/examples/Demo/Shared/Shared/DemoNavMenuTree.razor b/examples/Demo/Shared/Shared/DemoNavMenuTree.razor index e4842e482d..90b9a7da01 100644 --- a/examples/Demo/Shared/Shared/DemoNavMenuTree.razor +++ b/examples/Demo/Shared/Shared/DemoNavMenuTree.razor @@ -1,5 +1,5 @@
    - +

    Home

    @@ -8,7 +8,7 @@

    More information

    - + @* *@ @@ -110,5 +110,5 @@ -
    +
    \ No newline at end of file diff --git a/examples/Demo/Shared/wwwroot/docs/IconsAndEmoji.md b/examples/Demo/Shared/wwwroot/docs/IconsAndEmoji.md index 30a35b068c..7a4251fe5e 100644 --- a/examples/Demo/Shared/wwwroot/docs/IconsAndEmoji.md +++ b/examples/Demo/Shared/wwwroot/docs/IconsAndEmoji.md @@ -1,3 +1,5 @@ +>**If you are upgrading from a previous version of the library and you were alreasy using icons and/or emoji, please see the [Upgrade Guide](https://www.fluentui-blazor.net/UpgradeGuide) for more information.** + Starting with v3, the assets for the icons and emoji are removed from the library package and are provided through additional (separate) packages for both the icon and emoji resources. The components, and icons that are used by the library itself, are still part of the package. Adding the [Microsoft.Fast.Components.FluentUI.Icons package](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Icons) and/or [Microsoft.Fast.Components.FluentUI.Emojis package](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Emojis) @@ -5,16 +7,24 @@ is enough to make the resources available to your code. We use the [.NET trimming capabilities](https://learn.microsoft.com/aspnet/core/blazor/host-and-deploy/configure-trimmer) to publish only those assests that are actually being used in your program. Usually this results in some very small DLL's that only contain the resources that are actually being used in your application. -We still have support for both the **complete** [Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons) and the [Fluent Emoji](https://github.com/microsoft/fluentui-emoji) libraries. +We support the **complete** [Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons) and [Fluent Emoji](https://github.com/microsoft/fluentui-emoji) collections. ## Getting Started -To use the **Fluent UI System Icons** in your application, you will need to install the [Microsoft.Fast.Components.FluentUI.Icons](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Icons/) NuGet package in the project are using the main library. +To use the **Fluent UI System Icons** in your application, you will need to install the [Microsoft.Fast.Components.FluentUI.Icons](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Icons/) NuGet package in the project which is using the main library. + +```shell +dotnet add package Microsoft.Fast.Components.FluentUI.Icons +``` +To use the **Fluent UI Emoji** in your application, you need to install the [Microsoft.Fast.Components.FluentUI.Emojis](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Emojis/) NuGet package in the project which is using the main library. + +```shell +dotnet add package Microsoft.Fast.Components.FluentUI.Emojis +``` -To use the **Fluent UI Emoji** in your application, you need to install the [Microsoft.Fast.Components.FluentUI.Emojis](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI.Emojis/) NuGet package in the project are using the main library. -#### `FluentIcon` component +#### Using the `FluentIcon` component To use the icons, you add a `FluentIcon` component in your code like this: @@ -58,7 +68,7 @@ After adding the class, you can start using this custom icon like a "normal" Flu ``` -#### `FluentEmoji` component +#### Using the `FluentEmoji` component To use the emoji, you add a `FleuntEmoji` component in your code like this: diff --git a/examples/Demo/Shared/wwwroot/docs/UpgradeGuide.md b/examples/Demo/Shared/wwwroot/docs/UpgradeGuide.md index 740734e98d..45225821c1 100644 --- a/examples/Demo/Shared/wwwroot/docs/UpgradeGuide.md +++ b/examples/Demo/Shared/wwwroot/docs/UpgradeGuide.md @@ -1,10 +1,29 @@ -## Breaking changes - The `FluentDataGrid` component is, as you may know, a `QuickGrid` in disguise. We - aligned the underlying code even more to the productized version that will ship with - .NET 8. Where we previously aligned parameter names to the `fluent-datagrid` Web - Component, we will now align to the `QuickGrid` naming. This should make - integrating/copying `QuickGrid` component examples in your own environment easier and - will make it easier for us to keep the code up-to-date. Changes that need to be made in parameter names from v2 are: +## Breaking changes v3.2.0 + +### The pre-v3.2 `FluentNavMenu` has been renamed to `FluentNavMenuTree` +A new `FluentNavMenu` component has been added. + +If you want to **upgrade** your previous menu code, the following changes need to be made: + +* Change all occurrences of `` to `` +* Change `FluentNavMenuLink` from a self-closing tag to a tag with a closing tag +* Move the `FluentNavMenuLink` `Text` parameter content to in between the opening and closing tag +* Change any `@onclick` occurrences to `OnClick` + +* Change all occurrences of `FluentNavMenuGroup` to `FluentNavGroup' +* Replace the `Text` parameter with `Title` + +If you want to **keep** your previous menu code, the following change needs to be made: +* Rename `FluentNavMenu` to `FluentNavMenuTree` + + +## Breaking changes v3.0.0 +The `FluentDataGrid` component is, as you may know, a `QuickGrid` in disguise. We +aligned the underlying code even more to the productized version that will ship with +.NET 8. Where we previously aligned parameter names to the `fluent-datagrid` Web +Component, we will now align to the `QuickGrid` naming. This should make +integrating/copying `QuickGrid` component examples in your own environment easier and +will make it easier for us to keep the code up-to-date. Changes that need to be made in parameter names from v2 are: * RowsData -> Items * RowsDataProvider -> ItemsProvider * RowsDataSize -> ItemSize diff --git a/examples/Demo/Shared/wwwroot/docs/WhatsNew.md b/examples/Demo/Shared/wwwroot/docs/WhatsNew.md index 6b84563f29..386ee3d954 100644 --- a/examples/Demo/Shared/wwwroot/docs/WhatsNew.md +++ b/examples/Demo/Shared/wwwroot/docs/WhatsNew.md @@ -1,4 +1,7 @@ -## V3.1.1 +## V3.2.0 +- New NavMenu, NavGroup and NavLink components. **Breaking change** - See the [Upgrade guide](https://www.fluentui-blazor.net/UpgradeGuide) for details. See [NavMenu](https://www.fluentui-blazor.net/NavMenu) page for examples. + +## V3.1.1 - Fix [#776](https://github.com/microsoft/fluentui-blazor/issues/776): Icon throws exception when deployed to Azure - Fix [#755](https://github.com/microsoft/fluentui-blazor/issues/755): Icon throws exception when deployed to Azure - Fix [#789](https://github.com/microsoft/fluentui-blazor/issues/789): Navigation to "/" crashes with FluentNavMenu diff --git a/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor new file mode 100644 index 0000000000..66ab387d57 --- /dev/null +++ b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor @@ -0,0 +1,7 @@ +@namespace Microsoft.Fast.Components.FluentUI +@inherits FluentComponentBase +@using System.Globalization + +
    + @ChildContent +
    \ No newline at end of file diff --git a/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.cs b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.cs new file mode 100644 index 0000000000..96c3d9faf5 --- /dev/null +++ b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.cs @@ -0,0 +1,62 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Fast.Components.FluentUI.Utilities; +using Microsoft.JSInterop; + + +namespace Microsoft.Fast.Components.FluentUI; +public partial class FluentCollapsibleRegion : FluentComponentBase +{ + private bool _expanded; + + protected string? StyleValue => + new StyleBuilder(Style) + .AddStyle("max-height", MaxHeight, !string.IsNullOrEmpty(MaxHeight)) + .AddStyle("height", "auto", Expanded) + .AddStyle("height", "0", !Expanded) + .Build(); + + protected string? ClassValue => + new CssBuilder(Class) + .AddClass("fluent-collapsible-region-container") + .Build(); + + /// + /// If true, the region is expaned, otherwise it is collapsed. + /// + [Parameter] + public bool Expanded + { + get => _expanded; + set + { + if (_expanded == value) + { + return; + } + _expanded = value; + _ = ExpandedChanged.InvokeAsync(_expanded); + } + } + + /// + /// Explicitly sets the height for the Collapse element to override the css default. + /// + [Parameter] + public string? MaxHeight { get; set; } + + /// + /// Child content of component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Callback for when the Expanded property changes. + /// + [Parameter] + public EventCallback ExpandedChanged { get; set; } + +} diff --git a/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.css b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.css new file mode 100644 index 0000000000..2f5758665a --- /dev/null +++ b/src/Core/Components/CollapsibleRegion/FluentCollapsibleRegion.razor.css @@ -0,0 +1,3 @@ +.fluent-collapsible-region-container { + overflow: hidden; +} \ No newline at end of file diff --git a/src/Core/Components/Divider/FluentDivider.razor b/src/Core/Components/Divider/FluentDivider.razor index eace7ea01f..4b6d2c2b52 100644 --- a/src/Core/Components/Divider/FluentDivider.razor +++ b/src/Core/Components/Divider/FluentDivider.razor @@ -1,6 +1,7 @@ @namespace Microsoft.Fast.Components.FluentUI @inherits FluentComponentBase ") { } } + public class ChevronLeft : Icon { public ChevronLeft() : base("ChevronLeft", IconVariant.Regular, IconSize.Size12, "") { } } + public class ChevronRight : Icon { public ChevronRight() : base("ChevronRight", IconVariant.Regular, IconSize.Size12, "") { } } + public class ChevronUp : Icon { public ChevronUp() : base("ChevronUp", IconVariant.Regular, IconSize.Size12, "") { } } + + } + } internal static partial class Regular { [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/src/Core/Components/MainLayout/FluentMainLayout.razor b/src/Core/Components/MainLayout/FluentMainLayout.razor index 1a4e7c3867..91ca93c839 100644 --- a/src/Core/Components/MainLayout/FluentMainLayout.razor +++ b/src/Core/Components/MainLayout/FluentMainLayout.razor @@ -5,15 +5,17 @@ @Header - - + + @SubHeader - + - - @NavMenuContent - - + @if (NavMenuContent != null) + { + + @NavMenuContent + + } @Body diff --git a/src/Core/Components/NavMenu/FluentNavBase.cs b/src/Core/Components/NavMenu/FluentNavBase.cs new file mode 100644 index 0000000000..c1779964f0 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavBase.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Fast.Components.FluentUI; +using Microsoft.Fast.Components.FluentUI.Utilities; + +namespace Microsoft.Fast.Components.FluentUI; + +/// +/// Base class for and . +/// +public abstract class FluentNavBase : FluentComponentBase +{ + /// + /// URL for the group. + /// + [Parameter] + public string? Href { get; set; } + + /// + /// The target attribute specifies where to open the group, if Href is specified. + /// Possible values: _blank | _self | _parent | _top. + /// + [Parameter] + public string? Target { get; set; } + + /// + /// Icon to use if set. + /// + [Parameter] + public Icon? Icon { get; set; } + + /// + /// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Accent; + + /// + /// If true, the button will be disabled. + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Gets or sets the content to be shown. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Class names to use to indicate the item is active, separated by space. + /// + [Parameter] + public string ActiveClass { get; set; } = "active"; + + /// + /// Gets or sets how the link should be matched. Defaults to . + /// + [Parameter] + public NavLinkMatch Match { get; set; } = NavLinkMatch.Prefix; + + [CascadingParameter(Name = "NavMenuExpanded")] + public bool NavMenuExpanded { get; private set; } + + /// + /// Returns if the item has an set. + /// + internal bool HasIcon => Icon is not null; + + /// + /// The callback to invoke when the item is clicked. + /// + [Parameter] + public EventCallback OnClick { get; set; } + + /// + /// If true, force browser to redirect outside component router-space. + /// + [Parameter] + public bool ForceLoad { get; set; } + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + + protected async Task OnClickHandler(MouseEventArgs ev) + { + if (Disabled) + { + return; + } + if (!string.IsNullOrEmpty(Href)) + { + NavigationManager.NavigateTo(Href, ForceLoad); + } + else + { + await OnClick.InvokeAsync(ev); + } + } +} + diff --git a/src/Core/Components/NavMenu/FluentNavGroup.razor b/src/Core/Components/NavMenu/FluentNavGroup.razor new file mode 100644 index 0000000000..baac263994 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavGroup.razor @@ -0,0 +1,70 @@ +@namespace Microsoft.Fast.Components.FluentUI +@inherits FluentNavBase +@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Routing + + +@if (NavMenuExpanded || HasIcon) +{ +
    +
    +
    + @if (!string.IsNullOrEmpty(Href)) + { + + @_renderContent + + @_renderButton + } + else + { + + } +
    +
    + + + @ChildContent + + +
    +} +@code { + private void RenderContent(RenderTreeBuilder __builder) + { + @if (Icon is not null) + { + + } + else + { + + } +
    + @Title + @TitleTemplate +
    + } + + private void RenderButton(RenderTreeBuilder __builder) + { + @if (!HideExpander) + { + + + } + } +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavGroup.razor.cs b/src/Core/Components/NavMenu/FluentNavGroup.razor.cs new file mode 100644 index 0000000000..8a5b7a3af6 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavGroup.razor.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Fast.Components.FluentUI.Utilities; + +namespace Microsoft.Fast.Components.FluentUI; + +public partial class FluentNavGroup : FluentNavBase +{ + private readonly RenderFragment _renderContent; + private readonly RenderFragment _renderButton; + + protected string? ClassValue => + new CssBuilder("fluent-nav-group") + .AddClass("fluent-nav-item") + .AddClass("expanded", Expanded) + .AddClass("disabled", Disabled) + .AddClass(Class) + .Build(); + + internal string? StyleValue => new StyleBuilder(Style) + .AddStyle("margin", $"{Gap} 0" , !string.IsNullOrEmpty(Gap)) + .Build(); + + + protected string? ButtonClassValue => + new CssBuilder("expand-collapse-button") + .AddClass("rotate", Expanded) + .Build(); + + protected string? ExpandIconClassValue => + new CssBuilder("fluent-nav-expand-icon") + .Build(); + + internal Dictionary Attributes + { + get => Disabled ? new Dictionary() : new Dictionary + { + { "href", Href }, + { "target", Target }, + { "rel", !string.IsNullOrWhiteSpace(Target) ? "noopener noreferrer" : string.Empty } + }; + } + /// + /// The text to display for the group. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// If true, expands the nav group, otherwise collapse it. + /// Two-way bindable + /// + [Parameter] + public bool Expanded { get; set; } + + /// + /// If true, hides expand button at the end of the NavGroup. + /// + [Parameter] + public bool HideExpander { get; set; } + + /// + /// Explicitly sets the height for the Collapse element to override the css default. + /// + [Parameter] + public string? MaxHeight { get; set; } + + /// + /// Defines the vertical spacing between the NavGroup and adjecent items. + /// Needs to be a valid CSS value. Defaults to 10px. + /// + [Parameter] + public string? Gap { get; set; } + + /// + /// If set, overrides the default expand icon. + /// + [Parameter] + public Icon ExpandIcon { get; set; } = new CoreIcons.Regular.Size12.ChevronRight(); + + /// + /// Allows for specific markup and styling to be applied for the group title + /// When using this, the containded s and s need to be placed in a ChildContent tag. + /// When specifying both Title and TitleTemplate, both will be rendered. + /// + [Parameter] + public RenderFragment? TitleTemplate { get; set; } + + /// + /// Gets or sets a callback that is triggered whenever changes. + /// + + [Parameter] + public EventCallback ExpandedChanged { get; set; } + + public FluentNavGroup() + { + _renderContent = RenderContent; + _renderButton = RenderButton; + } + + private Task ToggleExpandedAsync() => SetExpandedAsync(!Expanded); + + private async Task HandleExpanderKeyDownAsync(KeyboardEventArgs args) + { + Task handler = args.Code switch + { + "NumpadEnter" => SetExpandedAsync(!Expanded), + "NumpadArrowRight" => SetExpandedAsync(true), + "NumpadArrowLeft" => SetExpandedAsync(false), + "Enter" => SetExpandedAsync(!Expanded), + "ArrowRight" => SetExpandedAsync(true), + "ArrowLeft" => SetExpandedAsync(false), + _ => Task.CompletedTask + }; + await handler; + } + + private async Task SetExpandedAsync(bool value) + { + if (value == Expanded) + { + return; + } + + Expanded = value; + if (ExpandedChanged.HasDelegate) + { + await ExpandedChanged.InvokeAsync(value); + } + } +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavGroup.razor.css b/src/Core/Components/NavMenu/FluentNavGroup.razor.css new file mode 100644 index 0000000000..a7eed56927 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavGroup.razor.css @@ -0,0 +1,60 @@ +::deep .fluent-nav-link { + width: 100%; + color: inherit; + align-items: center; + text-decoration: none; + display: grid; + grid-template-columns: 30px auto 40px; +} + +::deep .fluent-nav-icon { + margin-inline-end: calc(var(--design-unit) * 2px + 2px); + min-width: 20px; +} + +::deep .fluent-nav-group.disabled { + color: var(--neutral-fill-secondary-rest) !important; + pointer-events: none; +} + + ::deep .fluent-nav-group.disabled .fluent-nav-icon { + fill: var(--neutral-stroke-rest) !important; + } + + +/* Group expand/collapse */ +::deep .expand-collapse-button { + position: absolute; + right: calc(var(--design-unit) * 2px); + left: unset; + border-radius: calc(var(--control-corner-radius) * 1px); + width: calc((((var(--base-height-multiplier) / 2) * var(--design-unit)) + ((var(--design-unit) * var(--density)) / 2) + (var(--design-unit) * 2)) * 1px); + height: calc((((var(--base-height-multiplier) / 2) * var(--design-unit)) + ((var(--design-unit) * var(--density)) / 2) + (var(--design-unit) * 2)) * 1px); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +[dir="rtl"] * .expand-collapse-button { + position: absolute; + left: calc(var(--design-unit) * 2px); + right: unset; +} + +::deep .expand-collapse-button:hover { + background: var(--tree-item-expand-collapse-hover) +} + +::deep .expand-collapse-button svg { + transition: transform 0.1s linear 0s; + pointer-events: none; +} + +[dir="rtl"] * ::deep .expand-collapse-button svg { + transform: rotate(180deg); +} + +::deep .rotate.expand-collapse-button svg { + transform: rotate(90deg); +} diff --git a/src/Core/Components/NavMenu/FluentNavLink.razor b/src/Core/Components/NavMenu/FluentNavLink.razor new file mode 100644 index 0000000000..49381059ba --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavLink.razor @@ -0,0 +1,45 @@ +@namespace Microsoft.Fast.Components.FluentUI +@inherits FluentNavBase +@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Routing + +
    +
    +
    + @if (!OnClick.HasDelegate && !string.IsNullOrEmpty(Href)) + { + + @_renderContent + + } + else + { +
    + @_renderContent +
    + } +
    +
    +
    + +@code { + private void RenderContent(RenderTreeBuilder __builder) + { + @if (Icon is not null) + { + + } + else + { + + } +
    + @ChildContent +
    + + + } +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavLink.razor.cs b/src/Core/Components/NavMenu/FluentNavLink.razor.cs new file mode 100644 index 0000000000..812a4963d3 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavLink.razor.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Fast.Components.FluentUI.Utilities; + +namespace Microsoft.Fast.Components.FluentUI; + +public partial class FluentNavLink : FluentNavBase +{ + private readonly RenderFragment _renderContent; + + internal string? ClassValue => new CssBuilder(Class) + .AddClass("fluent-nav-item") + .Build(); + + internal string? LinkClassValue => new CssBuilder("fluent-nav-link") + .AddClass($"disabled", Disabled) + .Build(); + + internal Dictionary Attributes + { + get => Disabled ? new Dictionary() : new Dictionary + { + { "href", Href }, + { "target", Target }, + { "rel", !string.IsNullOrWhiteSpace(Target) ? "noopener noreferrer" : string.Empty } + }; + } + + public FluentNavLink() + { + _renderContent = RenderContent; + } +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavLink.razor.css b/src/Core/Components/NavMenu/FluentNavLink.razor.css new file mode 100644 index 0000000000..615539adc0 --- /dev/null +++ b/src/Core/Components/NavMenu/FluentNavLink.razor.css @@ -0,0 +1,24 @@ +::deep .fluent-nav-link { + font-weight: 400; + color: inherit; + align-items: center; + text-decoration: none; + display: grid; + grid-template-columns: 30px auto 40px; +} + +::deep .fluent-nav-icon { + margin-inline-end: calc(var(--design-unit) * 2px + 2px); + min-width: 20px; +} + +::deep .fluent-nav-link.disabled { + color: var(--neutral-fill-secondary-rest) !important; + cursor: not-allowed ; + pointer-events: none; +} + + +::deep .fluent-nav-link.disabled .fluent-nav-icon { + fill: var(--neutral-stroke-rest) !important; +} diff --git a/src/Core/Components/NavMenu/FluentNavMenu.razor b/src/Core/Components/NavMenu/FluentNavMenu.razor index 2055c23304..2fd23c6f52 100644 --- a/src/Core/Components/NavMenu/FluentNavMenu.razor +++ b/src/Core/Components/NavMenu/FluentNavMenu.razor @@ -1,39 +1,32 @@ @namespace Microsoft.Fast.Components.FluentUI @inherits FluentComponentBase -
    - - - - - - @if (Collapsible) +
    + + @if (Collapsible) + { +
    +
    +
    + @if (ExpanderContent is not null) { - - @if (ExpanderContent is not null) - { -
    - @ExpanderContent -
    - } - else - { - - } -
    + + @ExpanderContent + + } + else + { + } - @ChildContent - - - - +
    +
    +
    + } + @ChildContent
    \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavMenu.razor.cs b/src/Core/Components/NavMenu/FluentNavMenu.razor.cs index 40f39e5786..b013ee5a76 100644 --- a/src/Core/Components/NavMenu/FluentNavMenu.razor.cs +++ b/src/Core/Components/NavMenu/FluentNavMenu.razor.cs @@ -1,50 +1,37 @@ using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; using Microsoft.Fast.Components.FluentUI.Utilities; namespace Microsoft.Fast.Components.FluentUI; -public partial class FluentNavMenu : FluentComponentBase, INavMenuItemsOwner, IDisposable +public partial class FluentNavMenu : FluentComponentBase { private const string WIDTH_COLLAPSED_MENU = "40px"; - private bool _disposed; - private bool _hasChildIcons => ((INavMenuItemsOwner)this).HasChildIcons; - private readonly Dictionary _allItems = new(); - private readonly List _childItems = new(); - private readonly string _expandCollapseTreeItemId = Identifier.NewId(); - private FluentTreeItem? _currentlySelectedTreeItem; - private FluentTreeItem? _previousSuccessfullySelectedTreeItem; - protected string? ClassValue => new CssBuilder(Class) - .AddClass("navmenu") - .AddClass("collapsed", Collapsed) - .AddClass("navmenu-parent-element") + internal string? ClassValue => new CssBuilder("fluent-nav-menu") + .AddClass(Class) + .AddClass("collapsed", () => !Expanded) .Build(); - protected string? StyleValue => new StyleBuilder(Style) + internal string? StyleValue => new StyleBuilder(Style) + .AddStyle("margin", Margin, !string.IsNullOrEmpty(Margin)) .AddStyle("width", $"{Width}px", () => Expanded && Width.HasValue) + .AddStyle("width", "100%", () => Expanded && !Width.HasValue) .AddStyle("width", WIDTH_COLLAPSED_MENU, () => !Expanded) .AddStyle("min-width", WIDTH_COLLAPSED_MENU, () => !Expanded) .Build(); - - /// - /// Gets or sets the content to be rendered inside the component. - /// - [Parameter] - public RenderFragment? ChildContent { get; set; } - + /// - /// Gets or sets the content to be rendered for the expander icon - /// when the menu is collapsible. The default icon will be used if + /// Gets or sets the content to be rendered for the collapse icon + /// when the menu is collapsible. The default icon will be used if /// this is not specified. /// [Parameter] public RenderFragment? ExpanderContent { get; set; } /// - /// Gets or sets the title of the navigation menu - /// Default to "Navigation menu" + /// Gets or sets the title of the navigation menu using the aria-label attribute. + /// Defaults to "Navigation menu" /// [Parameter] public string? Title { get; set; } = "Navigation menu"; @@ -72,27 +59,16 @@ public partial class FluentNavMenu : FluentComponentBase, INavMenuItemsOwner, ID public EventCallback ExpandedChanged { get; set; } /// - /// Called when the user attempts to execute the default action of a menu item. + /// Adjust the vertical spacing between navlinks. /// [Parameter] - public EventCallback OnAction { get; set; } + public string? Margin { get; set; } - /// - /// If set to then the tree will - /// expand when it is created. - /// - [Parameter] - public bool InitiallyExpanded { get; set; } - /// - /// If true, the menu will re-navigate to the current page when the user clicks on the currently selected menu item. - /// [Parameter] - public bool ReNavigate { get; set; } = false; - - /// - public bool Collapsed => !Expanded; + public RenderFragment? ChildContent { get; set; } + /// /// Navigation manager /// @@ -104,139 +80,16 @@ public FluentNavMenu() Id = Identifier.NewId(); } - /// - IEnumerable INavMenuItemsOwner.GetChildItems() => _childItems; - - /// - void INavMenuItemsOwner.Register(FluentNavMenuItemBase child) - { - _childItems.Add(child); - StateHasChanged(); - } - - /// - void INavMenuItemsOwner.Unregister(FluentNavMenuItemBase child) - { - _childItems.Remove(child); - StateHasChanged(); - } - - void IDisposable.Dispose() - { - // Do not change this code. Put clean-up code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - internal void Register(FluentNavMenuItemBase item) - { - _allItems[item.Id!] = item; - StateHasChanged(); - } - - internal async Task MenuItemExpandedChangedAsync(INavMenuItemsOwner menuItem) - { - if (menuItem.Id == _expandCollapseTreeItemId) - { - return; - } - - if (menuItem.Expanded && !Expanded) - { - await SetExpandedAsync(value: true); - } - } - - internal void Unregister(FluentNavMenuItemBase item) - { - _allItems.Remove(item.Id!); - StateHasChanged(); - } - - protected override async Task OnInitializedAsync() - { - NavigationManager.LocationChanged += HandleNavigationManagerLocationChanged; - - if (InitiallyExpanded) - { - await SetExpandedAsync(true); - } - } - - protected override void OnAfterRender(bool firstRender) - { - if (firstRender) - { - SelectMenuItemForCurrentUrl(); - } - } - - private void SelectMenuItemForCurrentUrl() - { - HandleNavigationManagerLocationChanged(null, new LocationChangedEventArgs(NavigationManager.Uri, isNavigationIntercepted: false)); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - NavigationManager.LocationChanged -= HandleNavigationManagerLocationChanged; - _allItems.Clear(); - _childItems.Clear(); - } - - _disposed = true; - } - - private Task ToggleCollapsedAsync() => SetExpandedAsync(!Expanded); - - private async void HandleNavigationManagerLocationChanged(object? sender, LocationChangedEventArgs e) - { - FluentNavMenuItemBase? menuItem = null; - - string localPath = new Uri(NavigationManager.Uri).LocalPath; - if (string.IsNullOrEmpty(localPath)) - localPath = "/"; - - if (localPath == "/") - { - if (_allItems.Count > 0) menuItem = _allItems.Values.ElementAt(0); - } - else - { - // This will match the first item that has a Href that matches the current URL exactly - menuItem = _allItems.Values - .Where(x => !string.IsNullOrEmpty(x.Href)) - .FirstOrDefault(x => x.Href != "/" && localPath.Equals((x.Href!), StringComparison.InvariantCultureIgnoreCase)); - - // If not found, try to match the first item that has a Href (ending in a "/") that starts with the current URL - // URL: https://.../Panel/Panel2 starts with Href: https://.../Panel + "/" - // Extra "/" is needed to avoid matching https://.../Panels with https://.../Panel - if (menuItem is null) - { - menuItem = _allItems.Values - .Where(x => !string.IsNullOrEmpty(x.Href)) - .FirstOrDefault(x => x.Href != "/" && localPath.StartsWith((x.Href! + "/"), StringComparison.InvariantCultureIgnoreCase)); - } - } - if (menuItem is not null) - { - _currentlySelectedTreeItem = menuItem.TreeItem; - _previousSuccessfullySelectedTreeItem = menuItem.TreeItem; - await _currentlySelectedTreeItem.SetSelectedAsync(true); - } - } + private Task ToggleExpandedAsync() => SetExpandedAsync(!Expanded); private async Task HandleExpandCollapseKeyDownAsync(KeyboardEventArgs args) { Task handler = args.Code switch { - "Enter" => SetExpandedAsync(true), + "NumpadEnter" => SetExpandedAsync(!Expanded), + "NumpadArrowRight" => SetExpandedAsync(true), + "NumpadArrowLeft" => SetExpandedAsync(false), + "Enter" => SetExpandedAsync(value: !Expanded), "ArrowRight" => SetExpandedAsync(true), "ArrowLeft" => SetExpandedAsync(false), _ => Task.CompletedTask @@ -258,115 +111,6 @@ private async Task SetExpandedAsync(bool value) } StateHasChanged(); - } - - private async Task HandleCurrentSelectedChangedAsync(FluentTreeItem? treeItem) - { - // If an already activated menu item is clicked again, then it will - // match the previously selected one but have Selected == false. - // In this case, the user has indicated they wish to re-trigger - // its action. For the case of a simple navigation, this will be the same - // page and therefore do nothing. - // But for a nav menu with custom actions like showing a dialog etc, it will - // re-trigger and repeat that action. - - // itemWasClickedWhilstAlreadySelected will never be true as treeItem is null when the treeItem is clicked again - // left the code in for now but this should probably be removed - //bool itemWasClickedWhilstAlreadySelected = treeItem?.Selected == false && treeItem == _previousSuccessfullySelectedTreeItem; - //if (itemWasClickedWhilstAlreadySelected) - //{ - // await TryActivateMenuItemAsync(treeItem); - // return; - //} - - if (treeItem is null && _previousSuccessfullySelectedTreeItem is not null && ReNavigate) - { - await TryActivateMenuItemAsync(_previousSuccessfullySelectedTreeItem, true); - return; - } - // If the user has selected a different item, then it will not match the previously - // selected item, and it will have Selected == true. - // So try to activate the new one instead of the old one. - // If it succeeds then keep it selected, if it fails then revert to the last successfully selected - // tree item. This prevents the user from selecting an item with no Href or custom action. - if (treeItem?.Selected == true && _allItems.TryGetValue(treeItem.Id!, out FluentNavMenuItemBase? menuItem)) - { - bool activated = await TryActivateMenuItemAsync(treeItem); - if (activated) - { - _currentlySelectedTreeItem = treeItem; - _previousSuccessfullySelectedTreeItem = treeItem; - } - else - { - _currentlySelectedTreeItem = _previousSuccessfullySelectedTreeItem; - } - } - // At this point we have either - // 1) Succeeded, - // 2) Failed and reverted to a previously successful item without re-executing its action, - // 3) Failed and have no previously successful item to revert to. - // If we have no currently selected item then we fall back to selecting whichever matches the current - // URI. - if (_currentlySelectedTreeItem is null) - { - SelectMenuItemForCurrentUrl(); - } - - // Now we need to ensure the currently selected item has Selected=true, and - // the previous has Selected=false - if (_currentlySelectedTreeItem?.Selected == false) - { - await _currentlySelectedTreeItem.SetSelectedAsync(true); - } - - if (_currentlySelectedTreeItem?.Selected == true) - { - if (_previousSuccessfullySelectedTreeItem?.Selected == true && _previousSuccessfullySelectedTreeItem != _currentlySelectedTreeItem) - { - await _previousSuccessfullySelectedTreeItem.SetSelectedAsync(false); - } - _previousSuccessfullySelectedTreeItem = _currentlySelectedTreeItem; - } - - // If we still don't have a currently selected item, then make sure the invalid one - // the user tried to select is not selected. - if (treeItem?.Selected == true && treeItem != _currentlySelectedTreeItem) - { - await treeItem.SetSelectedAsync(false); - } - } - - private async ValueTask TryActivateMenuItemAsync(FluentTreeItem? treeItem, bool renavigate = false) - { - if (treeItem is null) - { - return false; - } - - if (!_allItems.TryGetValue(treeItem.Id!, out FluentNavMenuItemBase? menuItem)) - { - return false; - } - - NavMenuActionArgs? actionArgs = new NavMenuActionArgs(target: menuItem, renavigate: renavigate); - if (OnAction.HasDelegate) - { - await OnAction.InvokeAsync(actionArgs); - } - - if (!actionArgs.Handled) - { - await menuItem.ExecuteAsync(actionArgs); - } - - if (actionArgs.Handled) - { - await menuItem.SetSelectedAsync(true); - } - - return actionArgs.Handled; } - -} +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/FluentNavMenu.razor.css b/src/Core/Components/NavMenu/FluentNavMenu.razor.css index ca6e73f0da..32525ff060 100644 --- a/src/Core/Components/NavMenu/FluentNavMenu.razor.css +++ b/src/Core/Components/NavMenu/FluentNavMenu.razor.css @@ -1,95 +1,107 @@ -/* - NavMenu -*/ -.navmenu { - background-color: var(--neutral-layer-1); +/* NavMenu */ +.fluent-nav-menu { + flex-direction: column; + align-items: stretch; } - -/* - NavMenu expander -*/ -::deep .navmenu-expander::part(positioning-region) { - background-color: var(--neutral-fill-stealth-rest); -} - -::deep .navmenu-expander::part(content-region) { - margin-inline-start: calc(var(--design-unit) * 2px); +::deep .fluent-nav-item { + margin: calc(var(--design-unit) * 1px + 1px) 0; -webkit-user-select: none; user-select: none; } -::deep .navmenu-expander::part(positioning-region):hover { - background-color: var(--neutral-fill-secondary-rest); -} - -::deep .navmenu-expander.selected::after { - display: none; + /* Hover and active highlighting */ + ::deep .fluent-nav-item .positioning-region:hover:not(:has(.disabled)) { + cursor: pointer; + background: var(--neutral-fill-secondary-rest); + } + + ::deep .fluent-nav-item .positioning-region:has(a.active) { + background: var(--neutral-fill-secondary-rest); + } + + /* Active item indicator */ + ::deep .fluent-nav-item .positioning-region:has(a.active)::before { + content: ""; + display: block; + position: absolute; + left: 0px; + right: unset; + width: 3px; + height: calc(((var(--base-height-multiplier) + var(--density)) * var(--design-unit) / 2) * 1px); + background: var(--accent-fill-rest); + border-radius: calc(var(--control-corner-radius) * 1px); + margin: calc(var(--design-unit) * 2px) 2px; + z-index: 5; + } + +[dir='rtl'] * ::deep .fluent-nav-item .positioning-region:has(a.active)::before { + left: unset; + right: 0px; } - - -/* - Child Elements (Groups and Items) -*/ -::deep .navmenu-child-element::part(content-region) { - -webkit-user-select: none; - user-select: none; +::deep .content-region { + display: flex; + align-items: center; + white-space: nowrap; + width: 100%; margin-inline-start: calc(var(--design-unit) * 2px); - margin-inline-end: calc(var(--design-unit) * 2px); } -::deep .navmenu .treeitem-text, ::deep .navmenu fluent-tree-item.navmenu-child-element::part(start) { - pointer-events: none; +/* Nav items */ +::deep .fluent-nav-item .positioning-region { + display: flex; + position: relative; + box-sizing: border-box; + background: var(--neutral-fill-stealth-rest); + border: calc(var(--stroke-width) * 1px) solid transparent; + border-radius: calc(var(--control-corner-radius) * 1px); + min-height: calc(((var(--base-height-multiplier) + var(--density)) * var(--design-unit) + 1) * 1px); } -::deep .navmenu.collapsed .treeitem-text, ::deep .navmenu.collapsed fluent-tree-item.navmenu-child-element::part(expand-collapse-button) { - display: none; +::deep .fluent-nav-group .fluent-nav-item:last-of-type, .fluent-nav-menu .fluent-nav-item:last-of-type { + margin-bottom: 0; } - - -/* - Groups -*/ -::deep .navmenu-group::part(content-region) { - margin-inline-end: var(--expand-collapse-button-size); +/* level indenting */ +::deep .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item > .positioning-region { + padding-inline-start: 24px; } +::deep .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item > .positioning-region { + padding-inline-start: 48px; +} +::deep .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item > .positioning-region { + padding-inline-start: 72px; +} -/* - Group expander -*/ -::deep .navmenu .navmenu-group::part(expand-collapse-button) { - right: calc(var(--design-unit) * 2px); - left: unset; - margin-inline-end: calc(var(--expand-collapse-button-size) * -1); +::deep .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item > .positioning-region { + padding-inline-start: 96px; } -[dir="rtl"] * ::deep .navmenu-group::part(expand-collapse-button) { - left: calc(var(--design-unit) * 2px); - right: unset; - margin-inline-start: calc(var(--expand-collapse-button-size) - (var(--design-unit) * 2px)); +::deep .fluent-nav-text { + overflow: hidden; + text-overflow: ellipsis; } +/* collapsed */ +::deep.collapsed .fluent-nav-text { + display: none; +} -/* - Group items -*/ -::deep .navmenu-group .navmenu-child-element::part(content-region) { - padding-inline-start: calc(var(--design-unit) * 2px); +::deep.collapsed .expand-collapse-button { + display: none; } -/* Hide any items inside groups of a collapsed nav menu*/ -::deep .navmenu.collapsed > .navmenu-parent-element::part(items) { +::deep.collapsed .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item { display: none; } + ::deep.collapsed .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item { + display: none; + } -/* - Nav links -*/ -::deep .navmenu .navmenu-link::part(content-region) { - margin-inline-start: calc(var(--design-unit) * 2px); -} \ No newline at end of file + ::deep.collapsed .fluent-nav-group * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item * .fluent-nav-menu > .fluent-nav-item { + display: none; + } diff --git a/src/Core/Components/NavMenu/FluentNavMenuGroup.razor b/src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor similarity index 86% rename from src/Core/Components/NavMenu/FluentNavMenuGroup.razor rename to src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor index 188d0806a2..613effa0ab 100644 --- a/src/Core/Components/NavMenu/FluentNavMenuGroup.razor +++ b/src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor @@ -17,11 +17,11 @@ Text="@Text"> @if (HasIcon) { - + } else if (SiblingHasIcon) { - + } @if (GetShouldRenderChildContent()) { diff --git a/src/Core/Components/NavMenu/FluentNavMenuGroup.razor.cs b/src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor.cs similarity index 96% rename from src/Core/Components/NavMenu/FluentNavMenuGroup.razor.cs rename to src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor.cs index 57b2bc2934..370ef3574a 100644 --- a/src/Core/Components/NavMenu/FluentNavMenuGroup.razor.cs +++ b/src/Core/Components/NavMenuTree/FluentNavMenuGroup.razor.cs @@ -3,8 +3,11 @@ namespace Microsoft.Fast.Components.FluentUI; +//[Obsolete("This component has been replaced with the FluentNavGroup and will be removed in a future version.")] public partial class FluentNavMenuGroup : FluentNavMenuItemBase, INavMenuItemsOwner, IDisposable { + internal const string ICON_WIDTH = "20px"; + private readonly List _childItems = new(); private bool HasChildIcons => ((INavMenuItemsOwner)this).HasChildIcons; private bool Visible => NavMenu.Expanded || HasIcon; diff --git a/src/Core/Components/NavMenu/FluentNavMenuItemBase.cs b/src/Core/Components/NavMenuTree/FluentNavMenuItemBase.cs similarity index 96% rename from src/Core/Components/NavMenu/FluentNavMenuItemBase.cs rename to src/Core/Components/NavMenuTree/FluentNavMenuItemBase.cs index 480fc803fa..fd91201cbc 100644 --- a/src/Core/Components/NavMenu/FluentNavMenuItemBase.cs +++ b/src/Core/Components/NavMenuTree/FluentNavMenuItemBase.cs @@ -67,7 +67,9 @@ public abstract class FluentNavMenuItemBase : FluentComponentBase, IDisposable public int? Width { get; set; } [CascadingParameter] - protected FluentNavMenu NavMenu { get; private set; } = default!; +#pragma warning disable CS0618 // Type or member is obsolete + protected FluentNavMenuTree NavMenu { get; private set; } = default!; +#pragma warning restore CS0618 // Type or member is obsolete [CascadingParameter(Name = "NavMenuExpanded")] protected bool NavMenuExpanded { get; private set; } diff --git a/src/Core/Components/NavMenu/FluentNavMenuLink.razor b/src/Core/Components/NavMenuTree/FluentNavMenuLink.razor similarity index 100% rename from src/Core/Components/NavMenu/FluentNavMenuLink.razor rename to src/Core/Components/NavMenuTree/FluentNavMenuLink.razor diff --git a/src/Core/Components/NavMenu/FluentNavMenuLink.razor.cs b/src/Core/Components/NavMenuTree/FluentNavMenuLink.razor.cs similarity index 88% rename from src/Core/Components/NavMenu/FluentNavMenuLink.razor.cs rename to src/Core/Components/NavMenuTree/FluentNavMenuLink.razor.cs index 91873e6bf7..dee03a617d 100644 --- a/src/Core/Components/NavMenu/FluentNavMenuLink.razor.cs +++ b/src/Core/Components/NavMenuTree/FluentNavMenuLink.razor.cs @@ -2,6 +2,7 @@ namespace Microsoft.Fast.Components.FluentUI; +//[Obsolete("This component has been replaced with the FluentNavLink and will be removed in a future version.")] public partial class FluentNavMenuLink : FluentNavMenuItemBase, IDisposable { protected string? ClassValue => new CssBuilder(Class) diff --git a/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor new file mode 100644 index 0000000000..2055c23304 --- /dev/null +++ b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor @@ -0,0 +1,39 @@ +@namespace Microsoft.Fast.Components.FluentUI +@inherits FluentComponentBase + +
    + + + + + + @if (Collapsible) + { + + @if (ExpanderContent is not null) + { +
    + @ExpanderContent +
    + } + else + { + + } +
    + } + @ChildContent +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.cs b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.cs new file mode 100644 index 0000000000..764229bfc6 --- /dev/null +++ b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.cs @@ -0,0 +1,373 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Fast.Components.FluentUI.Utilities; + +namespace Microsoft.Fast.Components.FluentUI; + +//[Obsolete("This component has been replaced with the FluentNavMenu and will be removed in a future version.")] +public partial class FluentNavMenuTree : FluentComponentBase, INavMenuItemsOwner, IDisposable +{ + private const string WIDTH_COLLAPSED_MENU = "40px"; + private bool _disposed; + private bool _hasChildIcons => ((INavMenuItemsOwner)this).HasChildIcons; + private readonly Dictionary _allItems = new(); + private readonly List _childItems = new(); + private readonly string _expandCollapseTreeItemId = Identifier.NewId(); + private FluentTreeItem? _currentlySelectedTreeItem; + private FluentTreeItem? _previousSuccessfullySelectedTreeItem; + + protected string? ClassValue => new CssBuilder(Class) + .AddClass("navmenu") + .AddClass("collapsed", Collapsed) + .AddClass("navmenu-parent-element") + .Build(); + + protected string? StyleValue => new StyleBuilder(Style) + .AddStyle("width", $"{Width}px", () => Expanded && Width.HasValue) + .AddStyle("width", WIDTH_COLLAPSED_MENU, () => !Expanded) + .AddStyle("min-width", WIDTH_COLLAPSED_MENU, () => !Expanded) + .Build(); + + /// + /// Gets or sets the content to be rendered inside the component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Gets or sets the content to be rendered for the expander icon + /// when the menu is collapsible. The default icon will be used if + /// this is not specified. + /// + [Parameter] + public RenderFragment? ExpanderContent { get; set; } + + /// + /// Gets or sets the title of the navigation menu + /// Default to "Navigation menu" + /// + [Parameter] + public string? Title { get; set; } = "Navigation menu"; + + /// + /// Gets or sets the width of the menu (in pixels). + /// + [Parameter] + public int? Width { get; set; } + + /// + /// Gets or sets whether or not the menu can be collapsed. + /// + [Parameter] + public bool Collapsible { get; set; } + + /// + [Parameter] + public bool Expanded { get; set; } = true; + + /// + /// Event callback for when the property changes. + /// + [Parameter] + public EventCallback ExpandedChanged { get; set; } + + /// + /// Called when the user attempts to execute the default action of a menu item. + /// + [Parameter] + public EventCallback OnAction { get; set; } + + /// + /// If set to then the tree will + /// expand when it is created. + /// + [Parameter] + public bool InitiallyExpanded { get; set; } + + /// + /// If true, the menu will re-navigate to the current page when the user clicks on the currently selected menu item. + /// + [Parameter] + public bool ReNavigate { get; set; } = false; + + /// + public bool Collapsed => !Expanded; + + /// + /// Navigation manager + /// + [Inject] + protected NavigationManager NavigationManager { get; private set; } = null!; + + public FluentNavMenuTree() + { + Id = Identifier.NewId(); + } + + /// + IEnumerable INavMenuItemsOwner.GetChildItems() => _childItems; + + /// + void INavMenuItemsOwner.Register(FluentNavMenuItemBase child) + { + _childItems.Add(child); + StateHasChanged(); + } + + /// + void INavMenuItemsOwner.Unregister(FluentNavMenuItemBase child) + { + _childItems.Remove(child); + StateHasChanged(); + } + + void IDisposable.Dispose() + { + // Do not change this code. Put clean-up code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + internal void Register(FluentNavMenuItemBase item) + { + _allItems[item.Id!] = item; + StateHasChanged(); + } + + internal async Task MenuItemExpandedChangedAsync(INavMenuItemsOwner menuItem) + { + if (menuItem.Id == _expandCollapseTreeItemId) + { + return; + } + + if (menuItem.Expanded && !Expanded) + { + await SetExpandedAsync(value: true); + } + } + + internal void Unregister(FluentNavMenuItemBase item) + { + _allItems.Remove(item.Id!); + StateHasChanged(); + } + + protected override async Task OnInitializedAsync() + { + NavigationManager.LocationChanged += HandleNavigationManagerLocationChanged; + + if (InitiallyExpanded) + { + await SetExpandedAsync(true); + } + } + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + SelectMenuItemForCurrentUrl(); + } + } + + private void SelectMenuItemForCurrentUrl() + { + HandleNavigationManagerLocationChanged(null, new LocationChangedEventArgs(NavigationManager.Uri, isNavigationIntercepted: false)); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + NavigationManager.LocationChanged -= HandleNavigationManagerLocationChanged; + _allItems.Clear(); + _childItems.Clear(); + } + + _disposed = true; + } + + private Task ToggleCollapsedAsync() => SetExpandedAsync(!Expanded); + + private async void HandleNavigationManagerLocationChanged(object? sender, LocationChangedEventArgs e) + { + FluentNavMenuItemBase? menuItem = null; + + string localPath = new Uri(NavigationManager.Uri).LocalPath; + if (string.IsNullOrEmpty(localPath)) + localPath = "/"; + + if (localPath == "/") + { + if (_allItems.Count > 0) menuItem = _allItems.Values.ElementAt(0); + } + else + { + // This will match the first item that has a Href that matches the current URL exactly + menuItem = _allItems.Values + .Where(x => !string.IsNullOrEmpty(x.Href)) + .FirstOrDefault(x => x.Href != "/" && localPath.Equals((x.Href!), StringComparison.InvariantCultureIgnoreCase)); + + // If not found, try to match the first item that has a Href (ending in a "/") that starts with the current URL + // URL: https://.../Panel/Panel2 starts with Href: https://.../Panel + "/" + // Extra "/" is needed to avoid matching https://.../Panels with https://.../Panel + if (menuItem is null) + { + menuItem = _allItems.Values + .Where(x => !string.IsNullOrEmpty(x.Href)) + .FirstOrDefault(x => x.Href != "/" && localPath.StartsWith((x.Href! + "/"), StringComparison.InvariantCultureIgnoreCase)); + } + } + if (menuItem is not null) + { + _currentlySelectedTreeItem = menuItem.TreeItem; + _previousSuccessfullySelectedTreeItem = menuItem.TreeItem; + await _currentlySelectedTreeItem.SetSelectedAsync(true); + } + } + + private async Task HandleExpandCollapseKeyDownAsync(KeyboardEventArgs args) + { + Task handler = args.Code switch + { + "Enter" => SetExpandedAsync(true), + "ArrowRight" => SetExpandedAsync(true), + "ArrowLeft" => SetExpandedAsync(false), + _ => Task.CompletedTask + }; + await handler; + } + + private async Task SetExpandedAsync(bool value) + { + if (value == Expanded) + { + return; + } + + Expanded = value; + if (ExpandedChanged.HasDelegate) + { + await ExpandedChanged.InvokeAsync(value); + } + + StateHasChanged(); + } + + private async Task HandleCurrentSelectedChangedAsync(FluentTreeItem? treeItem) + { + // If an already activated menu item is clicked again, then it will + // match the previously selected one but have Selected == false. + // In this case, the user has indicated they wish to re-trigger + // its action. For the case of a simple navigation, this will be the same + // page and therefore do nothing. + // But for a nav menu with custom actions like showing a dialog etc, it will + // re-trigger and repeat that action. + + // itemWasClickedWhilstAlreadySelected will never be true as treeItem is null when the treeItem is clicked again + // left the code in for now but this should probably be removed + //bool itemWasClickedWhilstAlreadySelected = treeItem?.Selected == false && treeItem == _previousSuccessfullySelectedTreeItem; + //if (itemWasClickedWhilstAlreadySelected) + //{ + // await TryActivateMenuItemAsync(treeItem); + // return; + //} + + if (treeItem is null && _previousSuccessfullySelectedTreeItem is not null && ReNavigate) + { + await TryActivateMenuItemAsync(_previousSuccessfullySelectedTreeItem, true); + return; + } + // If the user has selected a different item, then it will not match the previously + // selected item, and it will have Selected == true. + // So try to activate the new one instead of the old one. + // If it succeeds then keep it selected, if it fails then revert to the last successfully selected + // tree item. This prevents the user from selecting an item with no Href or custom action. + if (treeItem?.Selected == true && _allItems.TryGetValue(treeItem.Id!, out FluentNavMenuItemBase? menuItem)) + { + bool activated = await TryActivateMenuItemAsync(treeItem); + if (activated) + { + _currentlySelectedTreeItem = treeItem; + _previousSuccessfullySelectedTreeItem = treeItem; + } + else + { + _currentlySelectedTreeItem = _previousSuccessfullySelectedTreeItem; + } + } + + // At this point we have either + // 1) Succeeded, + // 2) Failed and reverted to a previously successful item without re-executing its action, + // 3) Failed and have no previously successful item to revert to. + // If we have no currently selected item then we fall back to selecting whichever matches the current + // URI. + if (_currentlySelectedTreeItem is null) + { + SelectMenuItemForCurrentUrl(); + } + + // Now we need to ensure the currently selected item has Selected=true, and + // the previous has Selected=false + if (_currentlySelectedTreeItem?.Selected == false) + { + await _currentlySelectedTreeItem.SetSelectedAsync(true); + } + + if (_currentlySelectedTreeItem?.Selected == true) + { + if (_previousSuccessfullySelectedTreeItem?.Selected == true && _previousSuccessfullySelectedTreeItem != _currentlySelectedTreeItem) + { + await _previousSuccessfullySelectedTreeItem.SetSelectedAsync(false); + } + _previousSuccessfullySelectedTreeItem = _currentlySelectedTreeItem; + } + + // If we still don't have a currently selected item, then make sure the invalid one + // the user tried to select is not selected. + if (treeItem?.Selected == true && treeItem != _currentlySelectedTreeItem) + { + await treeItem.SetSelectedAsync(false); + } + } + + private async ValueTask TryActivateMenuItemAsync(FluentTreeItem? treeItem, bool renavigate = false) + { + if (treeItem is null) + { + return false; + } + + if (!_allItems.TryGetValue(treeItem.Id!, out FluentNavMenuItemBase? menuItem)) + { + return false; + } + + NavMenuActionArgs? actionArgs = new NavMenuActionArgs(target: menuItem, renavigate: renavigate); + if (OnAction.HasDelegate) + { + await OnAction.InvokeAsync(actionArgs); + } + + if (!actionArgs.Handled) + { + await menuItem.ExecuteAsync(actionArgs); + } + + if (actionArgs.Handled) + { + await menuItem.SetSelectedAsync(true); + } + + return actionArgs.Handled; + } + +} diff --git a/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.css b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.css new file mode 100644 index 0000000000..23b0d4d066 --- /dev/null +++ b/src/Core/Components/NavMenuTree/FluentNavMenuTree.razor.css @@ -0,0 +1,94 @@ +/* + NavMenu +*/ +.navmenu { + background-color: var(--neutral-layer-1); +} + +/* + NavMenu expander +*/ +::deep .navmenu-expander::part(positioning-region) { + background-color: var(--neutral-fill-stealth-rest); +} + +::deep .navmenu-expander::part(content-region) { + margin-inline-start: calc(var(--design-unit) * 2px); + -webkit-user-select: none; + user-select: none; +} + +::deep .navmenu-expander::part(positioning-region):hover { + background-color: var(--neutral-fill-secondary-rest); +} + +::deep .navmenu-expander.selected::after { + display: none; +} + + + +/* + Child Elements (Groups and Items) +*/ +::deep .navmenu-child-element::part(content-region) { + -webkit-user-select: none; + user-select: none; + margin-inline-start: calc(var(--design-unit) * 2px); + margin-inline-end: calc(var(--design-unit) * 2px); +} + +::deep .navmenu .treeitem-text, ::deep .navmenu fluent-tree-item.navmenu-child-element::part(start) { + pointer-events: none; +} + +::deep .navmenu.collapsed .treeitem-text, ::deep .navmenu.collapsed fluent-tree-item.navmenu-child-element::part(expand-collapse-button) { + display: none; +} + + + +/* + Groups +*/ +::deep .navmenu-group::part(content-region) { + margin-inline-end: var(--expand-collapse-button-size); +} + + + +/* + Group expander +*/ +::deep .navmenu .navmenu-group::part(expand-collapse-button) { + right: calc(var(--design-unit) * 2px); + left: unset; + margin-inline-end: calc(var(--expand-collapse-button-size) * -1); +} + +[dir="rtl"] * ::deep .navmenu-group::part(expand-collapse-button) { + left: calc(var(--design-unit) * 2px); + right: unset; + margin-inline-start: calc(var(--expand-collapse-button-size) - (var(--design-unit) * 2px)); +} + + +/* + Group items +*/ +::deep .navmenu-group .navmenu-child-element::part(content-region) { + padding-inline-start: calc(var(--design-unit) * 2px); +} + +/* Hide any items inside groups of a collapsed nav menu*/ +::deep .navmenu.collapsed > .navmenu-parent-element::part(items) { + display: none; +} + + +/* + Nav links +*/ +::deep .navmenu .navmenu-link::part(content-region) { + margin-inline-start: calc(var(--design-unit) * 2px); +} \ No newline at end of file diff --git a/src/Core/Components/NavMenu/INavMenuItemsOwner.cs b/src/Core/Components/NavMenuTree/INavMenuItemsOwner.cs similarity index 100% rename from src/Core/Components/NavMenu/INavMenuItemsOwner.cs rename to src/Core/Components/NavMenuTree/INavMenuItemsOwner.cs diff --git a/src/Core/Enums/Color.cs b/src/Core/Enums/Color.cs index b347790366..31b848bcfd 100644 --- a/src/Core/Enums/Color.cs +++ b/src/Core/Enums/Color.cs @@ -71,6 +71,12 @@ public enum Color [Description("var(--neutral-layer-1)")] Lightweight, + /// + /// Use the --neutral-stroke-rest CSS variable color, adapts to light/dark mode. + /// + [Description("var(--neutral-stroke-rest)")] + Disabled, + /// /// Supply an HTML hex color string value (#rrggbb or #rgb) for the CustomColor parameter. /// diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Default.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Default.verified.html new file mode 100644 index 0000000000..91478a34cb --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Default.verified.html @@ -0,0 +1,21 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified (2).html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified (2).html new file mode 100644 index 0000000000..6d41c49e26 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified (2).html @@ -0,0 +1,20 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified.html new file mode 100644 index 0000000000..39c558143d --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Disabled.verified.html @@ -0,0 +1,20 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Empty.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Empty.verified.html new file mode 100644 index 0000000000..469d4b508d --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Empty.verified.html @@ -0,0 +1,20 @@ + +
    +
    +
    + +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Gap.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Gap.verified.html new file mode 100644 index 0000000000..3e6730937b --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Gap.verified.html @@ -0,0 +1,20 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_HideExpander.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_HideExpander.verified.html new file mode 100644 index 0000000000..705d4cd30e --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_HideExpander.verified.html @@ -0,0 +1,15 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Href.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Href.verified.html new file mode 100644 index 0000000000..52b6fd2bdd --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Href.verified.html @@ -0,0 +1,20 @@ + +
    +
    +
    + + +
    Group title +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Icon.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Icon.verified.html new file mode 100644 index 0000000000..0ff11a7609 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_Icon.verified.html @@ -0,0 +1,22 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_IconAndIconColor.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_IconAndIconColor.verified.html new file mode 100644 index 0000000000..1e7bc5b5ae --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_IconAndIconColor.verified.html @@ -0,0 +1,22 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_TitleTemplate.verified.html b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_TitleTemplate.verified.html new file mode 100644 index 0000000000..91d63a0657 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.FluentNavGroup_TitleTemplate.verified.html @@ -0,0 +1,21 @@ + +
    +
    +
    + +
    +
    +
    +
    NavGroups and NavLinks here
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavGroupTests.cs b/tests/Core/NavMenu/FluentNavGroupTests.cs new file mode 100644 index 0000000000..4cf8245a4a --- /dev/null +++ b/tests/Core/NavMenu/FluentNavGroupTests.cs @@ -0,0 +1,147 @@ +using Microsoft.Fast.Components.FluentUI.Tests.Extensions; +using Xunit; + +namespace Microsoft.Fast.Components.FluentUI.Tests.NavMenu; + +public class FluentNavGroupTests : TestBase +{ + [Fact] + public void FluentNavGroup_Default() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Title, "Group title"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_Empty() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_HideExpander() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.HideExpander, true); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_Disabled() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Disabled, true); + parameters.Add(p => p.Title, "Group title"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_TitleTemplate() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.TitleTemplate, "

    Group title

    "); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_Gap() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Title, "Group title"); + parameters.Add(p => p.Gap, "20px"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_Href() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Title, "Group title"); + parameters.Add(p => p.Href, "/NavMenu"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_Icon() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Title, "Group title"); + parameters.Add(p => p.Icon, SampleIcons.Info); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavGroup_IconAndIconColor() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Title, "Group title"); + parameters.Add(p => p.Icon, SampleIcons.Info); + parameters.Add(p => p.IconColor, Color.Neutral); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + +} diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Default.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Default.verified.html new file mode 100644 index 0000000000..17d77eeee3 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Default.verified.html @@ -0,0 +1,11 @@ + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ExtendedTtitle.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ExtendedTtitle.verified.html new file mode 100644 index 0000000000..3b7ccb8f99 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ExtendedTtitle.verified.html @@ -0,0 +1,13 @@ + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ForceLoad.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ForceLoad.verified.html new file mode 100644 index 0000000000..08d6cf5258 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_ForceLoad.verified.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Href.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Href.verified.html new file mode 100644 index 0000000000..08d6cf5258 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Href.verified.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Icon.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Icon.verified.html new file mode 100644 index 0000000000..61c7275191 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Icon.verified.html @@ -0,0 +1,13 @@ + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_IconAndIconColor.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_IconAndIconColor.verified.html new file mode 100644 index 0000000000..d891dbbbb2 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_IconAndIconColor.verified.html @@ -0,0 +1,13 @@ + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Match.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Match.verified.html new file mode 100644 index 0000000000..08d6cf5258 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Match.verified.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_OnClick.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_OnClick.verified.html new file mode 100644 index 0000000000..17d77eeee3 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_OnClick.verified.html @@ -0,0 +1,11 @@ + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Target.verified.html b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Target.verified.html new file mode 100644 index 0000000000..2d91ef5729 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.FluentNavLink_Target.verified.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavLinkTests.cs b/tests/Core/NavMenu/FluentNavLinkTests.cs new file mode 100644 index 0000000000..b0c3822111 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavLinkTests.cs @@ -0,0 +1,144 @@ +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Fast.Components.FluentUI.Tests.Extensions; +using Xunit; + +namespace Microsoft.Fast.Components.FluentUI.Tests.NavMenu; + +public class FluentNavLinkTests : TestBase +{ + [Fact] + public void FluentNavLink_Default() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_ExtendedTtitle() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.AddChildContent("

    NavLink text

    "); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_Href() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Href, "/NavMenu"); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_Target() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Href, "/NavMenu"); + parameters.Add(p => p.Target, "_blank"); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_Match() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Href, "/NavMenu"); + parameters.Add(p => p.Match, NavLinkMatch.All); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_ForceLoad() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Href, "/NavMenu"); + parameters.Add(p => p.ForceLoad, true); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_Icon() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Icon, SampleIcons.Info); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_IconAndIconColor() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.NavMenuExpanded, true); + parameters.Add(p => p.Icon, SampleIcons.Info); + parameters.Add(p => p.IconColor, Color.Neutral); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavLink_OnClick() + { + Action onClickHandler = _ => { }; + + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.OnClick, onClickHandler); + parameters.AddChildContent("NavLink text"); + }); + + // Assert + cut.Verify(); + } + + //ActiveClass + //Match + //Target +} diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Collapsible.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Collapsible.verified.html new file mode 100644 index 0000000000..c4a556086e --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Collapsible.verified.html @@ -0,0 +1,11 @@ + +
    +
    +
    +
    + +
    +
    +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CollapsibleCustomTitle.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CollapsibleCustomTitle.verified.html new file mode 100644 index 0000000000..d6b657561e --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CollapsibleCustomTitle.verified.html @@ -0,0 +1,11 @@ + +
    +
    +
    +
    + +
    +
    +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CustomTitle.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CustomTitle.verified.html new file mode 100644 index 0000000000..39ec9a18d1 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_CustomTitle.verified.html @@ -0,0 +1,2 @@ + +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Default.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Default.verified.html new file mode 100644 index 0000000000..bbf9fd18df --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Default.verified.html @@ -0,0 +1,2 @@ + +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_ExpanderContent.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_ExpanderContent.verified.html new file mode 100644 index 0000000000..ab9e5b4c22 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_ExpanderContent.verified.html @@ -0,0 +1,10 @@ + +
    +
    +
    +
    +
    custom expander
    +
    +
    +
    +
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Margin.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Margin.verified.html new file mode 100644 index 0000000000..a3874c3e52 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Margin.verified.html @@ -0,0 +1,2 @@ + +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_NotExpanded.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_NotExpanded.verified.html new file mode 100644 index 0000000000..40aabf93c6 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_NotExpanded.verified.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Width.verified.html b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Width.verified.html new file mode 100644 index 0000000000..f1c083ae5b --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.FluentNavMenu_Width.verified.html @@ -0,0 +1,2 @@ + +
    NavGroups and NavLinks here
    \ No newline at end of file diff --git a/tests/Core/NavMenu/FluentNavMenuTests.cs b/tests/Core/NavMenu/FluentNavMenuTests.cs new file mode 100644 index 0000000000..c587c458c2 --- /dev/null +++ b/tests/Core/NavMenu/FluentNavMenuTests.cs @@ -0,0 +1,119 @@ +using Xunit; + +namespace Microsoft.Fast.Components.FluentUI.Tests.NavMenu; + +public class FluentNavMenuTests : TestBase +{ + [Fact] + public void FluentNavMenu_Default() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_Collapsible() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Collapsible, true); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_NotExpanded() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Expanded, false); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_CustomTitle() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Title, "Custom title"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + + [Fact] + public void FluentNavMenu_CollapsibleCustomTitle() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Collapsible, true); + parameters.Add(p => p.Title, "Custom title"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_Width() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Width, 300); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_Margin() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Margin, "5px 15px"); + parameters.AddChildContent("NavGroups and NavLinks here"); + }); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentNavMenu_ExpanderContent() + { + // Arrange & Act + var cut = TestContext.RenderComponent(parameters => + { + parameters.Add(p => p.Collapsible, true); + parameters.Add(p => p.ExpanderContent, "
    custom expander
    "); + }); + + // Assert + cut.Verify(); + } +}