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