Browse Source

Merge pull request #25949 from abpframework/maliming/mudblazor-dialog-autofocus

Focus the first input of MudBlazor dialogs opened from action menus
pull/25964/head
Enis Necipoglu 2 days ago
committed by GitHub
parent
commit
b822e16169
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor
  2. 136
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenu.razor.cs
  3. 15
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor
  4. 75
      framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudActionMenuItem.razor.cs
  5. 4
      framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor
  6. 13
      framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor
  7. 28
      modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor
  8. 56
      modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs
  9. 1
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor
  10. 12
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor
  11. 1
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor

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

@ -0,0 +1,37 @@
@using MudBlazor
<MudMenu @ref="_menu"
Icon="@Icon"
IconColor="@IconColor"
StartIcon="@StartIcon"
EndIcon="@EndIcon"
Label="@Label"
AriaLabel="@GetAriaLabel()"
Color="@Color"
Variant="@Variant"
Size="@Size"
Dense="@Dense"
FullWidth="@FullWidth"
MaxHeight="@MaxHeight"
AnchorOrigin="@AnchorOrigin"
TransformOrigin="@TransformOrigin"
ActivationEvent="@ActivationEvent"
PositionAtCursor="@PositionAtCursor"
PopoverFixed="@PopoverFixed"
RelativeWidth="@RelativeWidth"
LockScroll="@LockScroll"
Ripple="@Ripple"
DropShadow="@DropShadow"
Disabled="@Disabled"
Class="@Class"
Style="@Style"
ListClass="@ListClass"
PopoverClass="@PopoverClass"
UserAttributes="@UserAttributes"
ActivatorContent="@ActivatorContent">
<ChildContent>
<CascadingValue TValue="AbpMudActionMenu" Value="@this" IsFixed="true">
@ChildContent
</CascadingValue>
</ChildContent>
</MudMenu>

136
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;
/// <summary>
/// A <see cref="MudMenu"/> for actions that open a dialog. Use it with <see cref="AbpMudActionMenuItem"/>.
/// </summary>
public partial class AbpMudActionMenu : ComponentBase
{
protected MudMenu? _menu;
[Inject]
protected IStringLocalizer<AbpUiResource> 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; }
/// <summary>
/// The accessible name of an activator that has no <see cref="Label"/>. Defaults to "Actions".
/// </summary>
[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; }
/// <summary>
/// Never null, because MudBlazor reads it without a null check while rendering.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object?> UserAttributes { get; set; } = new();
[Parameter]
public RenderFragment<MenuContext>? 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;
}
/// <summary>
/// Closes the whole menu hierarchy. MudBlazor restores the focus to the activator while closing,
/// so <see cref="AbpMudActionMenuItem"/> calls this before its handler opens a dialog.
/// </summary>
public virtual async Task CloseAsync()
{
if (_menu != null)
{
await _menu.CloseAllMenusAsync();
}
}
}

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

@ -0,0 +1,15 @@
@using MudBlazor
<MudMenuItem AutoClose="@(!ControlsMenu)"
OnClick="@OnClickHandlerAsync"
Disabled="@Disabled"
Icon="@Icon"
IconColor="@IconColor"
Label="@Label"
Href="@Href"
Target="@Target"
ForceLoad="@ForceLoad"
Class="@Class"
UserAttributes="@UserAttributes">
@ChildContent
</MudMenuItem>

75
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;
/// <summary>
/// A menu item that closes its menu before running <see cref="OnClick"/>, so a dialog opened by the
/// handler keeps the focus. Works in an <see cref="AbpMudActionMenu"/> and in a plain <see cref="MudMenu"/>.
/// </summary>
public partial class AbpMudActionMenuItem : ComponentBase
{
[CascadingParameter]
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]
public EventCallback<MouseEventArgs> 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; }
/// <summary>
/// Never null, because MudBlazor reads it without a null check while rendering.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object?> 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);
}
}

4
framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor

@ -7,13 +7,13 @@
{
if (Primary == false)
{
<MudMenuItem OnClick="@ActionClickedAsync" Disabled="@Disabled">
<AbpMudActionMenuItem OnClick="@ActionClickedAsync" Disabled="@Disabled">
@if (!string.IsNullOrEmpty(Icon))
{
<MudIcon Icon="@Icon" Class="me-2" Size="Size.Small" />
}
@Text
</MudMenuItem>
</AbpMudActionMenuItem>
}
}
else

13
framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor

@ -4,15 +4,16 @@
@if (Type == MudActionType.Dropdown || DisabledOrNoActions())
{
<MudMenu Icon="@Icons.Material.Filled.MoreVert"
AnchorOrigin="Origin.BottomRight"
TransformOrigin="Origin.TopRight"
Dense="true"
Disabled="@DisabledOrNoActions()">
<AbpMudActionMenu Icon="@Icons.Material.Filled.MoreVert"
AriaLabel="@ToggleText"
AnchorOrigin="Origin.BottomRight"
TransformOrigin="Origin.TopRight"
Dense="true"
Disabled="@DisabledOrNoActions()">
<CascadingValue TValue="IMudEntityActions" Value="@this" Name="ParentActions">
@ChildContent
</CascadingValue>
</MudMenu>
</AbpMudActionMenu>
}
else
{

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

@ -18,10 +18,11 @@
{
<MudTabs @bind-ActivePanelIndex="@_activeTabIndex" Variant="Variant.Pills" Position="Position.Left"
PanelClass="pa-4">
@foreach (var group in Groups)
{
<MudTabPanel Text="@group.DisplayName">
<MudStack Spacing="2">
@{
@* 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<FeatureGroupDto> renderGroup = group =>
@<MudStack Spacing="2">
<MudText Typo="Typo.h6">@group.DisplayName</MudText>
@foreach (var feature in group.Features)
{
@ -68,7 +69,24 @@
}
</div>
}
</MudStack>
</MudStack>;
}
@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;
<MudTabPanel Text="@group.DisplayName">
@if (isFirstGroup)
{
<div @ref="_firstGroupContainer" tabindex="-1" class="mud-width-full">
@renderGroup(group)
</div>
}
else
{
@renderGroup(group)
}
</MudTabPanel>
}
</MudTabs>

56
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<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)
{
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;

1
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor

@ -16,6 +16,7 @@
UserAttributes="@(new Dictionary<string, object> { { "aria-label", L["Search"].Value } })"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
AutoFocus="true"
Immediate="true" />
</MudItem>
<MudItem xs="12" sm="4" Class="d-flex align-items-center">

12
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor

@ -24,14 +24,14 @@
<Columns>
<TemplateColumn Title="@L["Actions"]" Sortable="false" Style="width: 150px;">
<CellTemplate>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem OnClick="@(() => OpenEditDialogAsync(context.Item))">
<AbpMudActionMenu Icon="@Icons.Material.Filled.MoreVert">
<AbpMudActionMenuItem OnClick="@(() => OpenEditDialogAsync(context.Item))">
@L["Edit"]
</MudMenuItem>
<MudMenuItem OnClick="@(() => DeleteResourcePermissionAsync(context.Item))">
</AbpMudActionMenuItem>
<AbpMudActionMenuItem OnClick="@(() => DeleteResourcePermissionAsync(context.Item))">
@L["Delete"]
</MudMenuItem>
</MudMenu>
</AbpMudActionMenuItem>
</AbpMudActionMenu>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.ProviderName" Title="@L["ResourcePermissionTarget"]" Sortable="false">

1
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

Loading…
Cancel
Save