You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/FocusTrap/FocusTrap.razor.cs

162 lines
3.3 KiB

using Connected.Annotations;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace Connected.Components;
public partial class FocusTrap : IDisposable
{
protected string Classname =>
new CssBuilder("outline-none")
.AddClass(Class)
.Build();
protected ElementReference _firstBumper;
protected ElementReference _lastBumper;
protected ElementReference _fallback;
protected ElementReference _root;
private bool _shiftDown;
private bool _disabled;
private bool _initialized;
/// <summary>
/// Child content of the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// If true, the focus will no longer loop inside the component.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public bool Disabled
{
get => _disabled;
set
{
if (_disabled != value)
{
_disabled = value;
_initialized = false;
}
}
}
/// <summary>
/// Defines on which element to set the focus when the component is created or enabled.
/// When DefaultFocus.Element is used, the focus will be set to the FocusTrap itself, so the user will have to press TAB key once to focus the first tabbable element.
/// </summary>
[Parameter]
[Category(CategoryTypes.FocusTrap.Behavior)]
public DefaultFocus DefaultFocus { get; set; } = DefaultFocus.FirstChild;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
await SaveFocusAsync();
if (!_initialized)
await InitializeFocusAsync();
}
private Task OnBottomFocusAsync(FocusEventArgs args)
{
return FocusLastAsync();
}
private Task OnBumperFocusAsync(FocusEventArgs args)
{
return _shiftDown ? FocusLastAsync() : FocusFirstAsync();
}
private Task OnRootFocusAsync(FocusEventArgs args)
{
return FocusFallbackAsync();
}
private void OnRootKeyDown(KeyboardEventArgs args)
{
HandleKeyEvent(args);
}
private void OnRootKeyUp(KeyboardEventArgs args)
{
HandleKeyEvent(args);
}
private Task OnTopFocusAsync(FocusEventArgs args)
{
return FocusFirstAsync();
}
private Task InitializeFocusAsync()
{
_initialized = true;
if (!_disabled)
{
switch (DefaultFocus)
{
case DefaultFocus.Element: return FocusFallbackAsync();
case DefaultFocus.FirstChild: return FocusFirstAsync();
case DefaultFocus.LastChild: return FocusLastAsync();
}
}
return Task.CompletedTask;
}
private Task FocusFallbackAsync()
{
return _fallback.FocusAsync().AsTask();
}
private Task FocusFirstAsync()
{
return _root.FocusFirstAsync(2, 4).AsTask();
}
private Task FocusLastAsync()
{
return _root.FocusLastAsync(2, 4).AsTask();
}
private void HandleKeyEvent(KeyboardEventArgs args)
{
_shouldRender = false;
if (args.Key == "Tab")
_shiftDown = args.ShiftKey;
}
private Task RestoreFocusAsync()
{
return _root.RestoreFocusAsync().AsTask();
}
private Task SaveFocusAsync()
{
return _root.SaveFocusAsync().AsTask();
}
bool _shouldRender = true;
protected override bool ShouldRender()
{
if (_shouldRender)
return true;
_shouldRender = true; // auto-reset _shouldRender to true
return false;
}
public void Dispose()
{
if (!_disabled)
RestoreFocusAsync().AndForget(TaskOption.Safe);
}
}