Browse Source

Keep the action menu origins and the accessible name of labeled activators

pull/25949/head
maliming 4 days ago
parent
commit
d2f2572aff
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 15
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor
  2. 71
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs
  3. 2
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor
  4. 23
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs
  5. 31
      modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor
  6. 57
      modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs

15
framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor

@ -4,17 +4,30 @@
Icon="@Icon" Icon="@Icon"
IconColor="@IconColor" IconColor="@IconColor"
StartIcon="@StartIcon" StartIcon="@StartIcon"
EndIcon="@EndIcon"
Label="@Label" Label="@Label"
AriaLabel="@AriaLabel" AriaLabel="@GetAriaLabel()"
Color="@Color" Color="@Color"
Variant="@Variant" Variant="@Variant"
Size="@Size" Size="@Size"
Dense="@Dense" Dense="@Dense"
FullWidth="@FullWidth"
MaxHeight="@MaxHeight"
AnchorOrigin="@AnchorOrigin" AnchorOrigin="@AnchorOrigin"
TransformOrigin="@TransformOrigin" TransformOrigin="@TransformOrigin"
ActivationEvent="@ActivationEvent"
PositionAtCursor="@PositionAtCursor"
PopoverFixed="@PopoverFixed"
RelativeWidth="@RelativeWidth"
LockScroll="@LockScroll"
Ripple="@Ripple"
DropShadow="@DropShadow"
Disabled="@Disabled" Disabled="@Disabled"
Class="@Class" Class="@Class"
Style="@Style" Style="@Style"
ListClass="@ListClass"
PopoverClass="@PopoverClass"
UserAttributes="@UserAttributes"
ActivatorContent="@ActivatorContent"> ActivatorContent="@ActivatorContent">
<ChildContent> <ChildContent>
<CascadingValue TValue="AbpMudActionMenu" Value="@this" IsFixed="true"> <CascadingValue TValue="AbpMudActionMenu" Value="@this" IsFixed="true">

71
framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Localization.Resources.AbpUi;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using MudBlazor; using MudBlazor;
namespace Volo.Abp.MudBlazorUI.Components; namespace Volo.Abp.MudBlazorUI.Components;
@ -12,6 +16,9 @@ public partial class AbpMudActionMenu : ComponentBase
{ {
protected MudMenu? _menu; protected MudMenu? _menu;
[Inject]
protected IStringLocalizer<AbpUiResource> UiLocalizer { get; set; } = default!;
[Parameter] [Parameter]
public string? Icon { get; set; } public string? Icon { get; set; }
@ -21,9 +28,16 @@ public partial class AbpMudActionMenu : ComponentBase
[Parameter] [Parameter]
public string? StartIcon { get; set; } public string? StartIcon { get; set; }
[Parameter]
public string? EndIcon { get; set; }
[Parameter] [Parameter]
public string? Label { get; set; } public string? Label { get; set; }
/// <summary>
/// The accessible name of the activator. An activator without a <see cref="Label"/> falls back
/// to the localized "Actions" text.
/// </summary>
[Parameter] [Parameter]
public string? AriaLabel { get; set; } public string? AriaLabel { get; set; }
@ -39,12 +53,39 @@ public partial class AbpMudActionMenu : ComponentBase
[Parameter] [Parameter]
public bool Dense { get; set; } public bool Dense { get; set; }
[Parameter]
public bool FullWidth { get; set; }
[Parameter]
public int? MaxHeight { get; set; }
[Parameter] [Parameter]
public Origin? AnchorOrigin { get; set; } public Origin? AnchorOrigin { get; set; }
[Parameter] [Parameter]
public Origin TransformOrigin { get; set; } = Origin.TopLeft; 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] [Parameter]
public bool Disabled { get; set; } public bool Disabled { get; set; }
@ -54,21 +95,39 @@ public partial class AbpMudActionMenu : ComponentBase
[Parameter] [Parameter]
public string? Style { get; set; } public string? Style { get; set; }
[Parameter]
public string? ListClass { get; set; }
[Parameter]
public string? PopoverClass { get; set; }
/// <summary> /// <summary>
/// Replaces the default activator button. The menu is opened through the given /// Never null, because MudBlazor reads it without a null check while rendering.
/// <see cref="MenuContext"/>, same as <see cref="MudMenu.ActivatorContent"/>.
/// </summary> /// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object?> UserAttributes { get; set; } = new();
[Parameter] [Parameter]
public RenderFragment<MenuContext>? ActivatorContent { get; set; } public RenderFragment<MenuContext>? ActivatorContent { get; set; }
[Parameter] [Parameter]
public RenderFragment? ChildContent { get; set; } 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;
}
/// <summary> /// <summary>
/// Closes the menu and returns the focus to its activator. /// Closes the whole menu hierarchy. <see cref="AbpMudActionMenuItem"/> calls this before running
/// <see cref="AbpMudActionMenuItem"/> calls this before running its handler: MudBlazor restores /// its handler, because MudBlazor restores the focus to the activator while closing and that
/// the focus while closing the menu, and doing that after the handler opened a dialog would take /// would take the focus out of a dialog the handler opened.
/// the focus back out of that dialog.
/// </summary> /// </summary>
public virtual async Task CloseAsync() public virtual async Task CloseAsync()
{ {

2
framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor

@ -1,6 +1,6 @@
@using MudBlazor @using MudBlazor
<MudMenuItem AutoClose="@(ParentMenu == null)" <MudMenuItem AutoClose="@(!ControlsMenu)"
OnClick="@OnClickHandlerAsync" OnClick="@OnClickHandlerAsync"
Disabled="@Disabled" Disabled="@Disabled"
Icon="@Icon" Icon="@Icon"

23
framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs

@ -7,15 +7,23 @@ using MudBlazor;
namespace Volo.Abp.MudBlazorUI.Components; namespace Volo.Abp.MudBlazorUI.Components;
/// <summary> /// <summary>
/// An item of an <see cref="AbpMudActionMenu"/>. It closes the menu before running /// A menu item that closes its menu before running <see cref="OnClick"/>, so a dialog opened by
/// <see cref="OnClick"/>, so a dialog opened by the handler keeps the focus. /// the handler keeps the focus. Works inside an <see cref="AbpMudActionMenu"/> and inside a plain
/// Outside an <see cref="AbpMudActionMenu"/> it behaves like a plain <see cref="MudMenuItem"/>. /// <see cref="MudMenu"/>.
/// </summary> /// </summary>
public partial class AbpMudActionMenuItem : ComponentBase public partial class AbpMudActionMenuItem : ComponentBase
{ {
[CascadingParameter] [CascadingParameter]
protected AbpMudActionMenu? ParentMenu { get; set; } protected AbpMudActionMenu? ParentMenu { get; set; }
[CascadingParameter]
protected MudMenu? ParentMudMenu { get; set; }
/// <summary>
/// Whether this item can close the menu itself. When it cannot, MudBlazor keeps closing it.
/// </summary>
protected virtual bool ControlsMenu => ParentMenu != null || ParentMudMenu != null;
[Parameter] [Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; } public EventCallback<MouseEventArgs> OnClick { get; set; }
@ -43,8 +51,11 @@ public partial class AbpMudActionMenuItem : ComponentBase
[Parameter] [Parameter]
public string? Class { get; set; } public string? Class { get; set; }
/// <summary>
/// Never null, because MudBlazor reads it without a null check while rendering.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] [Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? UserAttributes { get; set; } public Dictionary<string, object?> UserAttributes { get; set; } = new();
[Parameter] [Parameter]
public RenderFragment? ChildContent { get; set; } public RenderFragment? ChildContent { get; set; }
@ -55,6 +66,10 @@ public partial class AbpMudActionMenuItem : ComponentBase
{ {
await ParentMenu.CloseAsync(); await ParentMenu.CloseAsync();
} }
else if (ParentMudMenu != null)
{
await ParentMudMenu.CloseAllMenusAsync();
}
await OnClick.InvokeAsync(args); await OnClick.InvokeAsync(args);
} }

31
modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor

@ -5,7 +5,7 @@
@using Volo.Abp.FeatureManagement.Localization @using Volo.Abp.FeatureManagement.Localization
@inherits AbpFeatureManagementComponentBase @inherits AbpFeatureManagementComponentBase
<MudDialog DefaultFocus="DefaultFocus.FirstChild" @bind-Visible="@_isVisible" Options="@(new DialogOptions { BackdropClick = false, MaxWidth = MaxWidth.Large, FullWidth = true, CloseOnEscapeKey = true })"> <MudDialog DefaultFocus="DefaultFocus.None" @bind-Visible="@_isVisible" Options="@(new DialogOptions { BackdropClick = false, MaxWidth = MaxWidth.Large, FullWidth = true, CloseOnEscapeKey = true })">
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6">@L["Features"]@ProviderKeyDisplayName</MudText> <MudText Typo="Typo.h6">@L["Features"]@ProviderKeyDisplayName</MudText>
</TitleContent> </TitleContent>
@ -18,10 +18,11 @@
{ {
<MudTabs KeepPanelsAlive="true" @bind-ActivePanelIndex="@_activeTabIndex" Variant="Variant.Pills" Position="Position.Left" <MudTabs KeepPanelsAlive="true" @bind-ActivePanelIndex="@_activeTabIndex" Variant="Variant.Pills" Position="Position.Left"
PanelClass="pa-4"> PanelClass="pa-4">
@foreach (var group in Groups) @{
{ @* The features come from the definitions, so there is no known input to put
<MudTabPanel Text="@group.DisplayName"> AutoFocus on. The first panel gets a container that takes the focus instead. *@
<MudStack Spacing="2"> RenderFragment<FeatureGroupDto> renderGroup = group =>
@<MudStack Spacing="2">
<MudText Typo="Typo.h6">@group.DisplayName</MudText> <MudText Typo="Typo.h6">@group.DisplayName</MudText>
@foreach (var feature in group.Features) @foreach (var feature in group.Features)
{ {
@ -68,7 +69,25 @@
} }
</div> </div>
} }
</MudStack> </MudStack>;
}
@for (var groupIndex = 0; groupIndex < Groups.Count; groupIndex++)
{
var group = Groups[groupIndex];
@* MudTabs renders the panel content later, when the loop variable is already past
the last index. *@
var isFirstGroup = groupIndex == 0;
<MudTabPanel Text="@group.DisplayName">
@if (isFirstGroup)
{
<div @ref="_firstGroupContainer" tabindex="-1" class="mud-width-full">
@renderGroup(group)
</div>
}
else
{
@renderGroup(group)
}
</MudTabPanel> </MudTabPanel>
} }
</MudTabs> </MudTabs>

57
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 System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.JSInterop;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MudBlazor; using MudBlazor;
using Volo.Abp.AspNetCore.Components.Messages; using Volo.Abp.AspNetCore.Components.Messages;
@ -44,6 +45,14 @@ public partial class FeatureManagementModal
protected Dictionary<string, string> SelectionStringValues = new(); protected Dictionary<string, string> 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) public virtual async Task OpenAsync(string providerName, string? providerKey = null, string? providerKeyDisplayName = null)
{ {
try try
@ -89,6 +98,11 @@ public partial class FeatureManagementModal
} }
_isVisible = true; _isVisible = true;
// The previous reference points to an element that is gone, and it would be taken for a
// bound container on the next render.
_firstGroupContainer = default;
_shouldFocusFirstGroup = Groups.Any();
_focusRenderCount = 0;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
catch (Exception ex) catch (Exception ex)
@ -97,6 +111,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 the container is not bound yet
// on the render that makes the dialog visible. Rendering again brings this method back.
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() public virtual Task CloseModal()
{ {
_isVisible = false; _isVisible = false;

Loading…
Cancel
Save