mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
361 lines
14 KiB
C#
361 lines
14 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Microsoft.JSInterop;
|
|
using Radzen.Blazor.Rendering;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Radzen.Blazor
|
|
{
|
|
/// <summary>
|
|
/// A horizontal menu component with support for nested submenus, icons, and responsive behavior.
|
|
/// RadzenMenu provides a classic menu bar for navigation, typically used in application headers or toolbars.
|
|
/// Displays menu items horizontally with dropdown submenus.
|
|
/// Supports multi-level nested menus via RadzenMenuItem child items, automatic navigation via Path property or custom Click handlers,
|
|
/// icons displayed alongside menu item text, responsive design that automatically collapses to a hamburger menu on small screens (configurable),
|
|
/// click-to-open or hover-to-open interaction modes, keyboard navigation (Arrow keys, Enter, Escape) for accessibility, and visual separators between menu items.
|
|
/// Use for application navigation bars, command menus, or toolbar-style interfaces. Menu items are defined using RadzenMenuItem components as child content.
|
|
/// </summary>
|
|
/// <example>
|
|
/// Basic menu with navigation:
|
|
/// <code>
|
|
/// <RadzenMenu>
|
|
/// <RadzenMenuItem Text="Home" Path="/" Icon="home" />
|
|
/// <RadzenMenuItem Text="Data">
|
|
/// <RadzenMenuItem Text="Orders" Path="/orders" />
|
|
/// <RadzenMenuItem Text="Customers" Path="/customers" />
|
|
/// </RadzenMenuItem>
|
|
/// <RadzenMenuItem Text="Reports" Path="/reports" />
|
|
/// </RadzenMenu>
|
|
/// </code>
|
|
/// Menu with click handlers:
|
|
/// <code>
|
|
/// <RadzenMenu Click=@OnMenuClick>
|
|
/// <RadzenMenuItem Text="File">
|
|
/// <RadzenMenuItem Text="New" Value="new" Icon="add" />
|
|
/// <RadzenMenuItem Text="Open" Value="open" Icon="folder_open" />
|
|
/// <RadzenMenuItem Text="Save" Value="save" Icon="save" />
|
|
/// </RadzenMenuItem>
|
|
/// </RadzenMenu>
|
|
/// </code>
|
|
/// </example>
|
|
public partial class RadzenMenu : RadzenComponentWithChildren
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets whether the menu should automatically collapse to a hamburger menu on small screens.
|
|
/// When enabled, displays a toggle button that expands/collapses the menu on mobile devices.
|
|
/// </summary>
|
|
/// <value><c>true</c> to enable responsive behavior with hamburger menu; <c>false</c> for always-horizontal menu. Default is <c>true</c>.</value>
|
|
[Parameter]
|
|
public bool Responsive { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the interaction mode for opening submenus.
|
|
/// When true, submenus open on click. When false, submenus open on hover (desktop) and click (touch devices).
|
|
/// </summary>
|
|
/// <value><c>true</c> to open on click; <c>false</c> to open on hover. Default is <c>true</c>.</value>
|
|
[Parameter]
|
|
public bool ClickToOpen { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether nested submenus should fly out horizontally to the side instead of expanding vertically inline.
|
|
/// When enabled, 2nd level and deeper submenus appear as cascading flyout menus positioned to the right of their parent item.
|
|
/// </summary>
|
|
/// <value><c>true</c> to enable flyout submenus; <c>false</c> for default accordion-style nesting. Default is <c>false</c>.</value>
|
|
[Parameter]
|
|
public bool Flyout { get; set; }
|
|
|
|
private bool IsOpen { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
protected override string GetComponentCssClass() => ClassList.Create("rz-menu")
|
|
.Add("rz-menu-open", Responsive && IsOpen)
|
|
.Add("rz-menu-closed", Responsive && !IsOpen)
|
|
.Add("rz-menu-flyout", Flyout)
|
|
.ToString();
|
|
|
|
IJSObjectReference? _jsRef;
|
|
bool _clickToOpenChanged;
|
|
|
|
/// <inheritdoc />
|
|
public override async Task SetParametersAsync(ParameterView parameters)
|
|
{
|
|
if (parameters.DidParameterChange(nameof(ClickToOpen), ClickToOpen))
|
|
{
|
|
_clickToOpenChanged = true;
|
|
}
|
|
|
|
await base.SetParametersAsync(parameters);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
|
|
if ((firstRender || _clickToOpenChanged) && Visible && JSRuntime != null)
|
|
{
|
|
_clickToOpenChanged = false;
|
|
|
|
if (_jsRef != null)
|
|
{
|
|
await _jsRef.InvokeVoidAsync("dispose");
|
|
await _jsRef.DisposeAsync();
|
|
}
|
|
|
|
_jsRef = await JSRuntime.InvokeAsync<IJSObjectReference>(
|
|
"Radzen.createMenu", Element, ClickToOpen);
|
|
}
|
|
}
|
|
|
|
void OnToggle()
|
|
{
|
|
IsOpen = !IsOpen;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the click callback.
|
|
/// </summary>
|
|
/// <value>The click callback.</value>
|
|
[Parameter]
|
|
public EventCallback<MenuItemEventArgs> Click { get; set; }
|
|
|
|
private string? ariaLabel;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the menu aria label text.
|
|
/// </summary>
|
|
/// <value>The menu aria label text.</value>
|
|
[Parameter]
|
|
public string AriaLabel { get => ariaLabel ?? Localize(nameof(RadzenStrings.Menu_AriaLabel)); set => ariaLabel = value; }
|
|
|
|
[Inject]
|
|
NavigationManager? NavigationManager { get; set; }
|
|
|
|
bool subMenuOpen;
|
|
internal int focusedIndex = -1;
|
|
bool preventKeyPress = true;
|
|
bool stopKeydownPropagation;
|
|
async Task OnKeyPress(KeyboardEventArgs args)
|
|
{
|
|
var key = args.Code != null ? args.Code : args.Key;
|
|
|
|
if (currentItems.Count == 0)
|
|
{
|
|
currentItems = items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
}
|
|
|
|
if (key == "ArrowUp" || key == "ArrowDown")
|
|
{
|
|
preventKeyPress = true;
|
|
stopKeydownPropagation = true;
|
|
|
|
if (subMenuOpen)
|
|
{
|
|
focusedIndex = Math.Clamp(focusedIndex + (key == "ArrowUp" ? -1 : 1), 0, currentItems.Count - 1);
|
|
}
|
|
else
|
|
{
|
|
if (key == "ArrowDown" && currentItems.Count > 0)
|
|
{
|
|
focusedIndex = Math.Clamp(focusedIndex, 0, currentItems.Count - 1);
|
|
|
|
var item = currentItems[focusedIndex];
|
|
|
|
if (item.items.Count > 0)
|
|
{
|
|
currentItems = item.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = -1;
|
|
subMenuOpen = true;
|
|
await item.Open();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (key == "ArrowLeft" || key == "ArrowRight")
|
|
{
|
|
preventKeyPress = true;
|
|
stopKeydownPropagation = true;
|
|
|
|
// Flyout mode: ArrowRight opens nested submenu, ArrowLeft closes it
|
|
if (Flyout && subMenuOpen)
|
|
{
|
|
if (key == "ArrowRight" && focusedIndex >= 0 && focusedIndex < currentItems.Count)
|
|
{
|
|
var item = currentItems[focusedIndex];
|
|
if (item.items.Count > 0)
|
|
{
|
|
currentItems = item.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = 0;
|
|
subMenuOpen = true;
|
|
await item.Open();
|
|
return;
|
|
}
|
|
}
|
|
else if (key == "ArrowLeft")
|
|
{
|
|
var firstItem = currentItems.FirstOrDefault();
|
|
var parentItem = firstItem?.ParentItem;
|
|
if (parentItem?.ParentItem != null)
|
|
{
|
|
currentItems = parentItem.ParentItem.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = currentItems.IndexOf(parentItem);
|
|
subMenuOpen = true;
|
|
await parentItem.Close();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool shouldOpenNextMenu = false;
|
|
if (subMenuOpen)
|
|
{
|
|
var firstItem = currentItems.FirstOrDefault();
|
|
var parentItem = firstItem?.ParentItem;
|
|
if (parentItem != null && parentItem.Parent != null)
|
|
{
|
|
currentItems = parentItem.Parent.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = currentItems.IndexOf(parentItem);
|
|
subMenuOpen = false;
|
|
await parentItem.Close();
|
|
shouldOpenNextMenu = true;
|
|
}
|
|
}
|
|
|
|
focusedIndex = Math.Clamp(focusedIndex + (key == "ArrowLeft" ? -1 : 1), 0, currentItems.Count - 1);
|
|
|
|
if (shouldOpenNextMenu)
|
|
{
|
|
shouldOpenNextMenu = false;
|
|
|
|
var item = currentItems[focusedIndex];
|
|
|
|
if (item.items.Count > 0)
|
|
{
|
|
currentItems = item.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = -1;
|
|
subMenuOpen = true;
|
|
await item.Toggle();
|
|
}
|
|
}
|
|
}
|
|
else if (key == "Space" || key == "Enter")
|
|
{
|
|
preventKeyPress = true;
|
|
stopKeydownPropagation = true;
|
|
|
|
if (focusedIndex >= 0 && focusedIndex < currentItems.Count)
|
|
{
|
|
var item = currentItems[focusedIndex];
|
|
|
|
if (item.items.Count > 0)
|
|
{
|
|
currentItems = item.items.Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = -1;
|
|
subMenuOpen = true;
|
|
await item.Toggle();
|
|
}
|
|
else
|
|
{
|
|
if (item.Path != null)
|
|
{
|
|
NavigationManager?.NavigateTo(item.Path);
|
|
}
|
|
else
|
|
{
|
|
await item.OnClick(new MouseEventArgs());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (key == "Escape")
|
|
{
|
|
preventKeyPress = true;
|
|
stopKeydownPropagation = true;
|
|
|
|
if (currentItems.Any(i => i.ParentItem != null))
|
|
{
|
|
var firstItem = currentItems.FirstOrDefault();
|
|
var parentItem = firstItem?.ParentItem;
|
|
if (parentItem != null)
|
|
{
|
|
currentItems = (parentItem.ParentItem != null ? parentItem.ParentItem.items : parentItem.Parent?.items ?? new List<RadzenMenuItem>()).Where(i => i.Visible && !i.Disabled).ToList();
|
|
focusedIndex = currentItems.IndexOf(parentItem);
|
|
subMenuOpen = false;
|
|
await parentItem.Close();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
preventKeyPress = false;
|
|
stopKeydownPropagation = false;
|
|
}
|
|
}
|
|
|
|
internal bool IsFocused(RadzenMenuItem item)
|
|
{
|
|
return focusedIndex != -1 && currentItems.IndexOf(item) == focusedIndex;
|
|
}
|
|
|
|
List<RadzenMenuItem> currentItems = new();
|
|
|
|
internal List<RadzenMenuItem> items = new List<RadzenMenuItem>();
|
|
|
|
/// <summary>
|
|
/// Adds the item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
public void AddItem(RadzenMenuItem item)
|
|
{
|
|
if (items.IndexOf(item) == -1)
|
|
{
|
|
items.Add(item);
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private string? toggleAriaLabel;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the add button aria-label attribute.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string ToggleAriaLabel { get => toggleAriaLabel ?? Localize(nameof(RadzenStrings.Menu_ToggleAriaLabel)); set => toggleAriaLabel = value; }
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnInitialized()
|
|
{
|
|
if (NavigationManager != null)
|
|
{
|
|
NavigationManager.LocationChanged += OnLocationChanged;
|
|
}
|
|
}
|
|
|
|
private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
|
{
|
|
IsOpen = false;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Dispose()
|
|
{
|
|
base.Dispose();
|
|
if (NavigationManager != null)
|
|
{
|
|
NavigationManager.LocationChanged -= OnLocationChanged;
|
|
}
|
|
_jsRef?.InvokeVoidAsync("dispose");
|
|
_jsRef?.DisposeAsync();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
void OnFocus()
|
|
{
|
|
focusedIndex = focusedIndex == -1 ? 0 : focusedIndex;
|
|
}
|
|
}
|
|
} |