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.
162 lines
3.3 KiB
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);
|
|
}
|
|
}
|