Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazor native focus trap #389

Merged
merged 10 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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