Skip to content
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
18 changes: 17 additions & 1 deletion Documentation/Blazorise.Docs/Resources/docs-api-index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generatedUtc": "2026-04-23T08:55:14.4845835Z",
"generatedUtc": "2026-04-29T14:35:34.9231734Z",
"components": [
{
"type": "global::Blazorise.Abbreviation",
Expand Down Expand Up @@ -63490,6 +63490,14 @@
"summary": "Represents a collection of options for message buttons. Each option is of type <strong>MessageOptionsChoice</strong>.",
"isBlazoriseEnum": false
},
{
"name": "CloseOnEscape",
"type": "bool?",
"typeName": "bool?",
"defaultValue": "null",
"summary": "If defined the message dialog will be closed when the user presses the Escape key. Confirmation dialogs will treat Escape as a cancel action.",
"isBlazoriseEnum": false
},
{
"name": "ConfirmButtonClass",
"type": "string",
Expand Down Expand Up @@ -63733,6 +63741,14 @@
"summary": "Determines whether to apply clearfix to manage floating children.",
"isBlazoriseEnum": false
},
{
"name": "CloseOnEscape",
"type": "bool",
"typeName": "bool",
"defaultValue": "false",
"summary": "If true, the message dialog will be closed when the user presses the Escape key. Confirmation dialogs will treat Escape as a cancel action.",
"isBlazoriseEnum": false
},
{
"name": "Display",
"type": "global::Blazorise.IFluentDisplay",
Expand Down
6 changes: 1 addition & 5 deletions Source/Blazorise/Components/FocusTrap/FocusTrap.razor
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
@namespace Blazorise
@inherits BaseFocusableContainerComponent
<CascadingValue Value="@this" IsFixed>
<div @ref="@ElementRef" id="@ElementId" class="@ClassNames" style="@StyleNames" @onkeydown="@OnKeyPressedHandler" @onkeyup="@OnKeyPressedHandler" tabindex="-1" @attributes="@Attributes">
<div tabindex="@FocusableTabIndex" @ref="@startSecondRef" @onfocus="@OnFocusEndHandler"></div>
<div tabindex="@FocusableTabIndex" @ref="@startFirstRef" @onfocus="@OnFocusEndHandler"></div>
<div @ref="@ElementRef" id="@ElementId" class="@ClassNames" style="@StyleNames" tabindex="-1" @attributes="@Attributes">
@ChildContent
<div tabindex="@FocusableTabIndex" @ref="@endFirstRef" @onfocus="@OnFocusStartHandler"></div>
<div tabindex="@FocusableTabIndex" @ref="@endSecondRef" @onfocus="@OnFocusStartHandler"></div>
</div>
</CascadingValue>
96 changes: 47 additions & 49 deletions Source/Blazorise/Components/FocusTrap/FocusTrap.razor.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#region Using directives
using System.Threading.Tasks;
using Blazorise.Modules;
using Blazorise.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
#endregion

namespace Blazorise;
Expand All @@ -14,17 +14,7 @@ public partial class FocusTrap : BaseFocusableContainerComponent
{
#region Members

private ElementReference startFirstRef;

private ElementReference startSecondRef;

private ElementReference endFirstRef;

private ElementReference endSecondRef;

private bool shiftTabPressed;

private bool shouldRender = true;
private bool jsInitialized;

#endregion

Expand All @@ -33,9 +23,16 @@ public partial class FocusTrap : BaseFocusableContainerComponent
/// <inheritdoc/>
public override async Task SetParametersAsync( ParameterView parameters )
{
if ( Rendered && parameters.TryGetValue<bool>( nameof( Active ), out var activeParam ) && Active != activeParam && activeParam )
if ( Rendered && parameters.TryGetValue<bool>( nameof( Active ), out var activeParam ) && Active != activeParam )
{
ExecuteAfterRender( SetFocus );
if ( activeParam )
{
ExecuteAfterRender( InitializeAndSetFocus );
}
else
{
ExecuteAfterRender( DestroyFocusTrap );
}
}

await base.SetParametersAsync( parameters );
Expand All @@ -46,12 +43,23 @@ protected override Task OnAfterRenderAsync( bool firstRender )
{
if ( firstRender && Active )
{
ExecuteAfterRender( SetFocus );
ExecuteAfterRender( InitializeAndSetFocus );
}

return base.OnAfterRenderAsync( firstRender );
}

/// <inheritdoc/>
protected override async ValueTask DisposeAsync( bool disposing )
{
if ( disposing && Rendered )
{
await DestroyFocusTrap();
}

await base.DisposeAsync( disposing );
}

/// <inheritdoc/>
protected override void BuildClasses( ClassBuilder builder )
{
Expand All @@ -69,72 +77,62 @@ public async Task SetFocus()
if ( Rendered )
{
if ( HasFocusableComponents )
{
await HandleFocusableComponent();
}
else
await startFirstRef.FocusAsync();
{
await JSFocusTrapModule.Focus( ElementRef, ElementId );
}
}
}

/// <summary>
/// Handles the focus start event.
/// Initializes the focus trap and sets focus to the first focusable child.
/// </summary>
/// <param name="args">Supplies information about a focus event that is being raised.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
protected virtual async Task OnFocusStartHandler( FocusEventArgs args )
private async Task InitializeAndSetFocus()
{
if ( !shiftTabPressed )
{
await startFirstRef.FocusAsync();
}
await InitializeFocusTrap();
await SetFocus();
}

/// <summary>
/// Handles the focus end event.
/// Initializes the JavaScript focus trap handler.
/// </summary>
/// <param name="args">Supplies information about a focus event that is being raised.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
protected virtual async Task OnFocusEndHandler( FocusEventArgs args )
private async Task InitializeFocusTrap()
{
if ( shiftTabPressed )
if ( !jsInitialized )
{
await endFirstRef.FocusAsync();
jsInitialized = true;

await JSFocusTrapModule.Initialize( ElementRef, ElementId );
}
}

/// <summary>
/// Handles the keyboard events.
/// Destroys the JavaScript focus trap handler.
/// </summary>
/// <param name="args">Supplies information about a keyboard event that is being raised.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
protected virtual void OnKeyPressedHandler( KeyboardEventArgs args )
private async Task DestroyFocusTrap()
{
shouldRender = false;

if ( args.Key == "Tab" )
if ( jsInitialized )
{
shiftTabPressed = args.ShiftKey;
}
}

/// <inheritdoc/>
protected override bool ShouldRender()
{
if ( shouldRender )
return true;

shouldRender = true;
jsInitialized = false;

return false;
await JSFocusTrapModule.Destroy( ElementRef, ElementId );
}
}

#endregion

#region Properties

/// <summary>
/// Gets the focusable element tab index.
/// Specifies the <see cref="IJSFocusTrapModule"/> instance.
/// </summary>
protected int FocusableTabIndex => Active ? 0 : -1;
[Inject] protected IJSFocusTrapModule JSFocusTrapModule { get; set; }

/// <summary>
/// If true the TAB focus will be activated.
Expand All @@ -147,4 +145,4 @@ protected override bool ShouldRender()
[Parameter] public RenderFragment ChildContent { get; set; }

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,7 @@ protected Task OnCancelClicked()
return InvokeAsync( async () =>
{
await ModalRef.Hide();

if ( IsConfirmation && Callback is not null && !Callback.Task.IsCompleted )
{
await InvokeAsync( () => Callback.SetResult( false ) );
}

await Canceled.InvokeAsync();
await NotifyCanceled();
} );
}

Expand Down Expand Up @@ -129,12 +123,38 @@ protected Task OnChoiceClicked( MessageOptionsChoice choice )
/// Handles the <see cref="Modal"/> closing event.
/// </summary>
/// <param name="eventArgs">Provides the data for the modal closing event.</param>
protected virtual Task OnModalClosing( ModalClosingEventArgs eventArgs )
protected virtual async Task OnModalClosing( ModalClosingEventArgs eventArgs )
{
var isEscapeClosing = eventArgs.CloseReason == CloseReason.EscapeClosing;
var isFocusLostClosing = eventArgs.CloseReason == CloseReason.FocusLostClosing;

if ( isEscapeClosing && ( Options?.CloseOnEscape ?? CloseOnEscape ) )
{
await NotifyCanceled();

return;
}

eventArgs.Cancel = ( Options?.BackgroundCancel ?? BackgroundCancel )
&& ( eventArgs.CloseReason == CloseReason.EscapeClosing || eventArgs.CloseReason == CloseReason.FocusLostClosing );
&& ( isEscapeClosing || isFocusLostClosing );
}

return Task.CompletedTask;
/// <summary>
/// Notifies that the message dialog was canceled.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
private async Task NotifyCanceled()
{
if ( IsConfirmation && Callback is not null && !Callback.Task.IsCompleted )
{
await InvokeAsync( () => Callback.SetResult( false ) );
}
else if ( IsChoice && Callback is not null && !Callback.Task.IsCompleted )
{
await InvokeAsync( () => Callback.SetResult( null ) );
}

await Canceled.InvokeAsync();
}

#endregion
Expand Down Expand Up @@ -370,5 +390,11 @@ protected virtual IEnumerable<MessageOptionsChoice> Choices
/// </summary>
[Parameter] public bool BackgroundCancel { get; set; } = true;

/// <summary>
/// If true, the message dialog will be closed when the user presses the Escape key.
/// Confirmation dialogs will treat Escape as a cancel action.
/// </summary>
[Parameter] public bool CloseOnEscape { get; set; }

#endregion
}
1 change: 1 addition & 0 deletions Source/Blazorise/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public static IServiceCollection AddBlazorise( this IServiceCollection serviceCo
public static IDictionary<Type, Type> JSModuleMap => new Dictionary<Type, Type>
{
{ typeof( IJSUtilitiesModule ), typeof( JSUtilitiesModule ) },
{ typeof( IJSFocusTrapModule ), typeof( JSFocusTrapModule ) },
{ typeof( IJSButtonModule ), typeof( JSButtonModule ) },
{ typeof( IJSClosableModule ), typeof( JSClosableModule ) },
{ typeof( IJSBreakpointModule ), typeof( JSBreakpointModule ) },
Expand Down
26 changes: 26 additions & 0 deletions Source/Blazorise/Interfaces/Modules/IJSFocusTrapModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace Blazorise.Modules;

/// <summary>
/// Contracts for the focus trap JS module.
/// </summary>
public interface IJSFocusTrapModule : IBaseJSModule, IJSDestroyableModule
{
/// <summary>
/// Initializes the focus trap.
/// </summary>
/// <param name="elementRef">Reference to the rendered element.</param>
/// <param name="elementId">ID of the rendered element.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask Initialize( ElementReference elementRef, string elementId );

/// <summary>
/// Focuses the first focusable element inside the focus trap.
/// </summary>
/// <param name="elementRef">Reference to the rendered element.</param>
/// <param name="elementId">ID of the rendered element.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask Focus( ElementReference elementRef, string elementId );
}
6 changes: 6 additions & 0 deletions Source/Blazorise/Models/MessageOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ public class MessageOptions
/// </summary>
public bool? BackgroundCancel { get; set; }

/// <summary>
/// If defined the message dialog will be closed when the user presses the Escape key.
/// Confirmation dialogs will treat Escape as a cancel action.
/// </summary>
public bool? CloseOnEscape { get; set; }

/// <summary>
/// Creates the default message options.
/// </summary>
Expand Down
51 changes: 51 additions & 0 deletions Source/Blazorise/Modules/JSFocusTrapModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#region Using directives
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
#endregion

namespace Blazorise.Modules;

/// <summary>
/// Default implementation of the focus trap JS module.
/// </summary>
public class JSFocusTrapModule : BaseJSModule, IJSFocusTrapModule
{
#region Constructors

/// <summary>
/// Default module constructor.
/// </summary>
/// <param name="jsRuntime">JavaScript runtime instance.</param>
/// <param name="versionProvider">Version provider.</param>
/// <param name="options">Blazorise options.</param>
public JSFocusTrapModule( IJSRuntime jsRuntime, IVersionProvider versionProvider, BlazoriseOptions options )
: base( jsRuntime, versionProvider, options )
{
}

#endregion

#region Methods

/// <inheritdoc/>
public virtual ValueTask Initialize( ElementReference elementRef, string elementId )
=> InvokeSafeVoidAsync( "initialize", elementRef, elementId );

/// <inheritdoc/>
public virtual ValueTask Destroy( ElementReference elementRef, string elementId )
=> InvokeSafeVoidAsync( "destroy", elementRef, elementId );

/// <inheritdoc/>
public virtual ValueTask Focus( ElementReference elementRef, string elementId )
=> InvokeSafeVoidAsync( "focus", elementRef, elementId );

#endregion

#region Properties

/// <inheritdoc/>
public override string ModuleFileName => $"./_content/Blazorise/focusTrap.js?v={VersionProvider.Version}";

#endregion
}
Loading
Loading