diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor
new file mode 100644
index 0000000000..dd2092acea
--- /dev/null
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor
@@ -0,0 +1,37 @@
+@using MudBlazor
+
+
+
+
+ @ChildContent
+
+
+
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs
new file mode 100644
index 0000000000..ff0ff5efc9
--- /dev/null
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Localization.Resources.AbpUi;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Localization;
+using MudBlazor;
+
+namespace Volo.Abp.MudBlazorUI.Components;
+
+///
+/// A for actions that open a dialog. Use it with .
+///
+public partial class AbpMudActionMenu : ComponentBase
+{
+ protected MudMenu? _menu;
+
+ [Inject]
+ protected IStringLocalizer UiLocalizer { get; set; } = default!;
+
+ [Parameter]
+ public string? Icon { get; set; }
+
+ [Parameter]
+ public Color IconColor { get; set; } = Color.Inherit;
+
+ [Parameter]
+ public string? StartIcon { get; set; }
+
+ [Parameter]
+ public string? EndIcon { get; set; }
+
+ [Parameter]
+ public string? Label { get; set; }
+
+ ///
+ /// The accessible name of an activator that has no . Defaults to "Actions".
+ ///
+ [Parameter]
+ public string? AriaLabel { get; set; }
+
+ [Parameter]
+ public Color Color { get; set; } = Color.Default;
+
+ [Parameter]
+ public Variant Variant { get; set; } = Variant.Text;
+
+ [Parameter]
+ public Size Size { get; set; } = Size.Medium;
+
+ [Parameter]
+ public bool Dense { get; set; }
+
+ [Parameter]
+ public bool FullWidth { get; set; }
+
+ [Parameter]
+ public int? MaxHeight { get; set; }
+
+ [Parameter]
+ public Origin? AnchorOrigin { get; set; }
+
+ [Parameter]
+ public Origin TransformOrigin { get; set; } = Origin.TopLeft;
+
+ [Parameter]
+ public MouseEvent ActivationEvent { get; set; } = MouseEvent.LeftClick;
+
+ [Parameter]
+ public bool PositionAtCursor { get; set; }
+
+ [Parameter]
+ public bool PopoverFixed { get; set; }
+
+ [Parameter]
+ public DropdownWidth RelativeWidth { get; set; } = DropdownWidth.Ignore;
+
+ [Parameter]
+ public bool LockScroll { get; set; }
+
+ [Parameter]
+ public bool Ripple { get; set; } = true;
+
+ [Parameter]
+ public bool DropShadow { get; set; } = true;
+
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ [Parameter]
+ public string? Class { get; set; }
+
+ [Parameter]
+ public string? Style { get; set; }
+
+ [Parameter]
+ public string? ListClass { get; set; }
+
+ [Parameter]
+ public string? PopoverClass { get; set; }
+
+ ///
+ /// Never null, because MudBlazor reads it without a null check while rendering.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public Dictionary UserAttributes { get; set; } = new();
+
+ [Parameter]
+ public RenderFragment? ActivatorContent { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected virtual string? GetAriaLabel()
+ {
+ if (!AriaLabel.IsNullOrEmpty())
+ {
+ return AriaLabel;
+ }
+
+ // A label is already the accessible name, so overriding it would hide the visible text.
+ return Label.IsNullOrEmpty() ? UiLocalizer["Actions"].Value : null;
+ }
+
+ ///
+ /// Closes the whole menu hierarchy. MudBlazor restores the focus to the activator while closing,
+ /// so calls this before its handler opens a dialog.
+ ///
+ public virtual async Task CloseAsync()
+ {
+ if (_menu != null)
+ {
+ await _menu.CloseAllMenusAsync();
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor
new file mode 100644
index 0000000000..53b2064860
--- /dev/null
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor
@@ -0,0 +1,15 @@
+@using MudBlazor
+
+
+ @ChildContent
+
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs
new file mode 100644
index 0000000000..48299b84a2
--- /dev/null
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+using MudBlazor;
+
+namespace Volo.Abp.MudBlazorUI.Components;
+
+///
+/// A menu item that closes its menu before running , so a dialog opened by the
+/// handler keeps the focus. Works in an and in a plain .
+///
+public partial class AbpMudActionMenuItem : ComponentBase
+{
+ [CascadingParameter]
+ protected AbpMudActionMenu? ParentMenu { get; set; }
+
+ [CascadingParameter]
+ protected MudMenu? ParentMudMenu { get; set; }
+
+ ///
+ /// Whether this item can close the menu itself. When it cannot, MudBlazor keeps closing it.
+ ///
+ protected virtual bool ControlsMenu => ParentMenu != null || ParentMudMenu != null;
+
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ [Parameter]
+ public string? Icon { get; set; }
+
+ [Parameter]
+ public Color IconColor { get; set; } = Color.Inherit;
+
+ [Parameter]
+ public string? Label { get; set; }
+
+ [Parameter]
+ public string? Href { get; set; }
+
+ [Parameter]
+ public string? Target { get; set; }
+
+ [Parameter]
+ public bool ForceLoad { get; set; }
+
+ [Parameter]
+ public string? Class { get; set; }
+
+ ///
+ /// Never null, because MudBlazor reads it without a null check while rendering.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public Dictionary UserAttributes { get; set; } = new();
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected virtual async Task OnClickHandlerAsync(MouseEventArgs args)
+ {
+ if (ParentMenu != null)
+ {
+ await ParentMenu.CloseAsync();
+ }
+ else if (ParentMudMenu != null)
+ {
+ await ParentMudMenu.CloseAllMenusAsync();
+ }
+
+ await OnClick.InvokeAsync(args);
+ }
+}
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor
index c1ae598b89..7436c570e9 100644
--- a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor
@@ -7,13 +7,13 @@
{
if (Primary == false)
{
-
+
@if (!string.IsNullOrEmpty(Icon))
{
}
@Text
-
+
}
}
else
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor
index e60654fdf1..379cb192fc 100644
--- a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor
+++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor
@@ -4,15 +4,16 @@
@if (Type == MudActionType.Dropdown || DisabledOrNoActions())
{
-
+
@ChildContent
-
+
}
else
{
diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor
index 17062b39a6..efd2c3980a 100644
--- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor
+++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor
@@ -18,10 +18,11 @@
{
- @foreach (var group in Groups)
- {
-
-
+ @{
+ @* The features come from the definitions, so there is no known input to put
+ AutoFocus on. The first panel gets a container that takes the focus. *@
+ RenderFragment renderGroup = group =>
+ @
@group.DisplayName
@foreach (var feature in group.Features)
{
@@ -68,7 +69,24 @@
}
}
-
+ ;
+ }
+ @for (var groupIndex = 0; groupIndex < Groups.Count; groupIndex++)
+ {
+ var group = Groups[groupIndex];
+ @* MudTabs renders the panel content after the loop is over. *@
+ var isFirstGroup = groupIndex == 0;
+
+ @if (isFirstGroup)
+ {
+
+ @renderGroup(group)
+
+ }
+ else
+ {
+ @renderGroup(group)
+ }
}
diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs
index c913b05f3c..2b0c50a103 100644
--- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs
+++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
+using Microsoft.JSInterop;
using Microsoft.Extensions.Options;
using MudBlazor;
using Volo.Abp.AspNetCore.Components.Messages;
@@ -44,6 +45,14 @@ public partial class FeatureManagementModal
protected Dictionary SelectionStringValues = new();
+ protected const int MaxFocusRenderCount = 5;
+
+ protected ElementReference _firstGroupContainer;
+
+ protected bool _shouldFocusFirstGroup;
+
+ protected int _focusRenderCount;
+
public virtual async Task OpenAsync(string providerName, string? providerKey = null, string? providerKeyDisplayName = null)
{
try
@@ -89,6 +98,10 @@ public partial class FeatureManagementModal
}
_isVisible = true;
+ // The previous reference points to an element that is gone by now.
+ _firstGroupContainer = default;
+ _shouldFocusFirstGroup = Groups.Any();
+ _focusRenderCount = 0;
await InvokeAsync(StateHasChanged);
}
catch (Exception ex)
@@ -97,6 +110,49 @@ public partial class FeatureManagementModal
}
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (!_shouldFocusFirstGroup)
+ {
+ return;
+ }
+
+ if (!_isVisible)
+ {
+ _shouldFocusFirstGroup = false;
+ return;
+ }
+
+ // The dialog provider renders the content in a later batch, so rendering again is what brings
+ // this method back once the container is bound.
+ if (_firstGroupContainer.Id.IsNullOrEmpty())
+ {
+ if (_focusRenderCount++ < MaxFocusRenderCount)
+ {
+ await InvokeAsync(StateHasChanged);
+ }
+ else
+ {
+ _shouldFocusFirstGroup = false;
+ }
+
+ return;
+ }
+
+ _shouldFocusFirstGroup = false;
+
+ try
+ {
+ await _firstGroupContainer.MudFocusFirstAsync();
+ }
+ catch (JSException)
+ {
+ // The dialog was closed before the focus call reached the element.
+ }
+ }
+
public virtual Task CloseModal()
{
_isVisible = false;
diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor
index 8333db25ac..ebaf4536d5 100644
--- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor
+++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor
@@ -16,6 +16,7 @@
UserAttributes="@(new Dictionary { { "aria-label", L["Search"].Value } })"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
+ AutoFocus="true"
Immediate="true" />
diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor
index be05954983..7f01a7bcec 100644
--- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor
+++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor
@@ -24,14 +24,14 @@
-
-
+
+
@L["Edit"]
-
-
+
+
@L["Delete"]
-
-
+
+
diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor
index 614bc218c7..aa44b2cc7f 100644
--- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor
+++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor
@@ -2,3 +2,4 @@
@using Volo.Abp.AspNetCore.Components.Web
@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout
@using MudBlazor
+@using Volo.Abp.MudBlazorUI.Components