Skip to content

Commit

Permalink
Blazor native focus trap (#389)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrissainty authored Mar 8, 2022
1 parent 97f2b35 commit 072afe4
Show file tree
Hide file tree
Showing 19 changed files with 118 additions and 56 deletions.
Binary file removed .DS_Store
Binary file not shown.
7 changes: 0 additions & 7 deletions Directory.Build.props

This file was deleted.

Binary file removed samples/.DS_Store
Binary file not shown.
Binary file removed samples/BlazorServer/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion samples/BlazorServer/Pages/LongRunningTask.razor
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

loading.Close(ModalResult.Ok("Closed with OK result"));
var result = await loading.Result;
if (result.DataType == typeof(string))
if (result.Data is not null && result.DataType == typeof(string))
_result = result.Data.ToString()!;

StateHasChanged();
Expand Down
Binary file removed samples/BlazorServer/wwwroot/.DS_Store
Binary file not shown.
Binary file removed samples/BlazorWebAssembly/.DS_Store
Binary file not shown.
4 changes: 2 additions & 2 deletions samples/BlazorWebAssembly/BlazorWebAssembly.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/BlazorWebAssembly/Pages/LongRunningTask.razor
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

loading.Close(ModalResult.Ok("Closed with OK result"));
var result = await loading.Result;
if (result.DataType == typeof(string))
if (result.Data is not null && result.DataType == typeof(string))
_result = result.Data.ToString()!;

StateHasChanged();
Expand Down
2 changes: 1 addition & 1 deletion samples/BlazorWebAssembly/Pages/ReturnDataFromModal.razor
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
var result = await messageForm.Result;

if (!result.Cancelled)
_message = result.Data.ToString() ?? string.Empty;
_message = result.Data?.ToString() ?? string.Empty;
}

}
Binary file removed samples/BlazorWebAssembly/wwwroot/.DS_Store
Binary file not shown.
Binary file removed samples/BlazorWebAssembly/wwwroot/css/.DS_Store
Binary file not shown.
6 changes: 3 additions & 3 deletions src/Blazored.Modal/Blazored.Modal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.JSInterop.WebAssembly" Version="6.0.2" />
<PackageReference Include="Microsoft.JSInterop.WebAssembly" Version="6.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 4 additions & 12 deletions src/Blazored.Modal/BlazoredModal.razor
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
private readonly Collection<ModalReference> _modals = new();
private readonly ModalOptions _globalModalOptions = new();

internal event Action? OnModalClosed;

protected override void OnInitialized()
{
if (CascadedModalService == null)
Expand Down Expand Up @@ -61,25 +63,14 @@
{
// Gracefully close the modal
await modal.ModalInstanceRef.CloseAsync(result);
OnModalClosed?.Invoke();
}
else
{
await DismissInstance(modal, result);
}
}

internal void CloseInstance(Guid id)
{
var reference = GetModalReference(id);
CloseInstance(reference, ModalResult.Ok());
}

internal void CancelInstance(Guid id)
{
var reference = GetModalReference(id);
CloseInstance(reference, ModalResult.Cancel());
}

internal Task DismissInstance(Guid id, ModalResult result)
{
var reference = GetModalReference(id);
Expand All @@ -93,6 +84,7 @@
modal.Dismiss(result);
_modals.Remove(modal);
await InvokeAsync(StateHasChanged);
OnModalClosed?.Invoke();
}
}

Expand Down
42 changes: 23 additions & 19 deletions src/Blazored.Modal/BlazoredModalInstance.razor
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,31 @@ else

<div class="blazored-modal-container @Position" @ref="_modalReference">

<div id="_@($"{Id.ToString("N")}-overlay")" class="blazored-modal-overlay
@OverlayCustomClass @OverlayAnimationClass" @onclick="HandleBackgroundClick"></div>
<div id="_@($"{Id.ToString("N")}-overlay")"
class="blazored-modal-overlay @OverlayCustomClass @OverlayAnimationClass"
@onclick="HandleBackgroundClick"></div>

<div id="_@Id.ToString("N")" class="@Class" role="dialog" aria-modal="true" >
@if (!HideHeader)
{
<div class="blazored-modal-header">
<h3 class="blazored-modal-title">@Title</h3>
@if (!HideCloseButton)
{
<button type="button" class="blazored-modal-close" aria-label="close" @onclick="CancelAsync" @attributes="@_closeBtnAttributes">
<span>&times;</span>
</button>
}
<FocusTrap @ref="_focusTrap" IsActive="ActivateFocusTrap">
<div id="_@Id.ToString("N")" class="@Class" role="dialog" aria-modal="true" >
@if (!HideHeader)
{
<div class="blazored-modal-header">
<h3 class="blazored-modal-title">@Title</h3>
@if (!HideCloseButton)
{
<button type="button" class="blazored-modal-close" aria-label="close" @onclick="CancelAsync" @attributes="@_closeBtnAttributes">
<span>&times;</span>
</button>
}
</div>
}
<div class="blazored-modal-content">
<CascadingValue Value="this">
@Content
</CascadingValue>
</div>
}
<div class="blazored-modal-content">
<CascadingValue Value="this">
@Content
</CascadingValue>
</div>
</div>
</FocusTrap>

</div>
}
26 changes: 21 additions & 5 deletions src/Blazored.Modal/BlazoredModalInstance.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Blazored.Modal;

public partial class BlazoredModalInstance
public partial class BlazoredModalInstance : IDisposable
{
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[CascadingParameter] private BlazoredModal Parent { get; set; } = default!;
Expand Down Expand Up @@ -38,6 +38,8 @@ private string AnimationDuration

[SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "This is assigned in Razor code and isn't currently picked up by the tooling.")]
private ElementReference _modalReference;
private FocusTrap _focusTrap = default!;
private bool _setFocus;

// Temporarily add a tabindex of -1 to the close button so it doesn't get selected as the first element by activateFocusTrap
private readonly Dictionary<string, object> _closeBtnAttributes = new() { { "tabindex", "-1" } };
Expand All @@ -49,13 +51,20 @@ protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// if (ActivateFocusTrap)
// await JsRuntime.InvokeVoidAsync("BlazoredModal.activateFocusTrap", _modalReference, Id);
_closeBtnAttributes.Clear();
StateHasChanged();
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_setFocus)
{
await _focusTrap.SetFocus();
_setFocus = false;
}
}

/// <summary>
/// Sets the title for the modal being displayed
/// </summary>
Expand Down Expand Up @@ -112,8 +121,12 @@ private void ConfigureInstance()
OverlayCustomClass = SetOverlayCustomClass();
ActivateFocusTrap = SetActivateFocusTrap();
OverlayAnimationClass = SetAnimationClass();
Parent.OnModalClosed += AttemptFocus;
}

private void AttemptFocus()
=> _setFocus = true;

private bool SetUseCustomLayout()
{
if (Options.UseCustomLayout.HasValue)
Expand Down Expand Up @@ -169,7 +182,7 @@ private string SetPosition()
if (!string.IsNullOrWhiteSpace(GlobalModalOptions.PositionCustomClass))
return GlobalModalOptions.PositionCustomClass;

throw new InvalidOperationException("Position set to Custom without a PositionCustomClass set.");
throw new InvalidOperationException("Position set to Custom without a PositionCustomClass set");

default:
return "blazored-modal-center";
Expand Down Expand Up @@ -270,7 +283,7 @@ private bool SetActivateFocusTrap()
if (GlobalModalOptions.ActivateFocusTrap.HasValue)
return GlobalModalOptions.ActivateFocusTrap.Value;

return true; // Default to true to match old behaviour
return true;
}

private async Task HandleBackgroundClick()
Expand All @@ -279,4 +292,7 @@ private async Task HandleBackgroundClick()

await CancelAsync();
}

void IDisposable.Dispose()
=> Parent.OnModalClosed -= AttemptFocus;
}
54 changes: 54 additions & 0 deletions src/Blazored.Modal/FocusTrap.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<div class="blazored-modal-focus-trap" @ref="_container" @onkeydown="HandleKeyPresses" @onkeyup="HandleKeyPresses">
<div tabindex="@(IsActive ? 0 : -1)" @ref="_startSecond" @onfocus="FocusEndAsync"></div>
<div tabindex="@(IsActive ? 0 : -1)" @ref="_startFirst" @onfocus="FocusEndAsync"></div>
@ChildContent
<div tabindex="@(IsActive ? 0 : -1)" @ref="_endFirst" @onfocus="FocusStartAsync"></div>
<div tabindex="@(IsActive ? 0 : -1)" @ref="_endSecond" @onfocus="FocusStartAsync"></div>
</div>

@code {
private ElementReference _container;
private ElementReference _startFirst;
private ElementReference _startSecond;
private ElementReference _endFirst;
private ElementReference _endSecond;
private bool _shiftPressed;

[Parameter] public RenderFragment ChildContent { get; set; } = default!;
[Parameter] public bool IsActive { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await _startFirst.FocusAsync();
}
}

internal async Task SetFocus()
=> await _startFirst.FocusAsync();

private async Task FocusStartAsync(FocusEventArgs args)
{
if (!_shiftPressed)
{
await _startFirst.FocusAsync();
}
}

private async Task FocusEndAsync(FocusEventArgs args)
{
if (_shiftPressed)
{
await _endFirst.FocusAsync();
}
}

private void HandleKeyPresses(KeyboardEventArgs args)
{
if (args.Key == "Tab")
{
_shiftPressed = args.ShiftKey;
}
}
}
6 changes: 5 additions & 1 deletion src/Blazored.Modal/wwwroot/blazored-modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
left: 0;
}

.blazored-modal-focus-trap {
z-index: 102;
}

.blazored-modal {
display: flex;
z-index: 102;
z-index: 103;
flex-direction: column;
background-color: #fff;
border-radius: 4px;
Expand Down
7 changes: 3 additions & 4 deletions tests/src/Blazored.Modal.Tests/Blazored.Modal.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="bunit.core" Version="1.3.42" />
<PackageReference Include="bunit.web" Version="1.3.42" />
<PackageReference Include="bunit.xunit" Version="1.0.0-preview-01" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="bunit.core" Version="1.6.4" />
<PackageReference Include="bunit.web" Version="1.6.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
Expand Down

0 comments on commit 072afe4

Please sign in to comment.