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; /// /// Child content of the component. /// [Parameter] [Category(CategoryTypes.FocusTrap.Behavior)] public RenderFragment ChildContent { get; set; } /// /// If true, the focus will no longer loop inside the component. /// [Parameter] [Category(CategoryTypes.FocusTrap.Behavior)] public bool Disabled { get => _disabled; set { if (_disabled != value) { _disabled = value; _initialized = false; } } } /// /// 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. /// [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); } }