Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
return dialog focus after close
  • Loading branch information
adamint committed Aug 29, 2025
commit 596aeeaf65e51c7e7fef8818263ec9bbdcd42f58
8 changes: 8 additions & 0 deletions src/Core/Components/Dialog/FluentDialog.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ public async Task CloseAsync(DialogResult dialogResult)
{
await Instance.Parameters.OnDialogResult.InvokeAsync(dialogResult);
}

if (DialogContext is not null && Instance.PreviouslyFocusedElement is not null)
{
// Dialog does not close instantly, wait a little while to ensure that it has closed
// before trying to set focus. If dialog is not closed, focus cannot be set.
await Task.Delay(50);
await DialogContext.DialogContainer.ReturnFocusAsync(Instance.PreviouslyFocusedElement);
}
}
else
{
Expand Down
77 changes: 72 additions & 5 deletions src/Core/Components/Dialog/FluentDialogProvider.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,31 @@

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.FluentUI.AspNetCore.Components.Extensions;
using Microsoft.JSInterop;

namespace Microsoft.FluentUI.AspNetCore.Components;

public partial class FluentDialogProvider : IDisposable
public partial class FluentDialogProvider : IAsyncDisposable
{
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/js/dialog-utils.js";

private readonly InternalDialogContext _internalDialogContext;
private readonly RenderFragment _renderDialogs;
private IJSObjectReference? _module;

[Inject]
private IDialogService DialogService { get; set; } = default!;

[Inject]
private NavigationManager NavigationManager { get; set; } = default!;

[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;

[Inject]
private LibraryConfiguration LibraryConfiguration { get; set; } = default!;

/// <summary>
/// Constructs an instance of <see cref="FluentToastProvider"/>.
/// </summary>
Expand All @@ -40,24 +51,55 @@ protected override void OnInitialized()
DialogService.OnDialogCloseRequested += DismissInstance;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration));
}
}

private void ShowDialog(IDialogReference dialogReference, Type? dialogComponent, DialogParameters parameters, object content)
{
if (_module is null)
{
throw new InvalidOperationException("JS module is not loaded.");
}

DialogInstance dialog = new(dialogComponent, parameters, content);
dialogReference.Instance = dialog;

InvokeAsync(async () =>
{
var previouslyFocusedElement = await _module.InvokeAsync<IJSObjectReference>("getActiveElement");
dialog.PreviouslyFocusedElement = previouslyFocusedElement;
});

_internalDialogContext.References.Add(dialogReference);
InvokeAsync(StateHasChanged);
}

private async Task<IDialogReference> ShowDialogAsync(IDialogReference dialogReference, Type? dialogComponent, DialogParameters parameters, object content)
{
return await Task.Run(() =>
if (_module is null)
{
throw new InvalidOperationException("JS module is not loaded.");
}

var previouslyFocusedElementTask = _module.InvokeAsync<IJSObjectReference>("getActiveElement");

return await Task.Run(async () =>
{
DialogInstance dialog = new(dialogComponent, parameters, content);
dialogReference.Instance = dialog;

_internalDialogContext.References.Add(dialogReference);
InvokeAsync(StateHasChanged);
await InvokeAsync(StateHasChanged);

Task.Run(async () =>
{
var previouslyFocusedElement = await previouslyFocusedElementTask;
dialog.PreviouslyFocusedElement = previouslyFocusedElement;
});

return dialogReference;
});
Expand Down Expand Up @@ -130,6 +172,17 @@ internal void DismissInstance(string id, DialogResult result)
}
}

internal async Task ReturnFocusAsync(IJSObjectReference element)
{
if (_module is null)
{
throw new InvalidOperationException("JS module is not loaded.");
}

await _module.InvokeVoidAsync("focusElement", element);
await element.DisposeAsync();
}

internal IDialogReference? GetDialogReference(string id)
{
return _internalDialogContext.References.SingleOrDefault(x => x.Id == id);
Expand Down Expand Up @@ -183,11 +236,25 @@ private async Task OnDismissAsync(DialogEventArgs args)
}
}

public void Dispose()
public async ValueTask DisposeAsync()
{
if (NavigationManager != null)
{
NavigationManager.LocationChanged -= LocationChanged;
}

try
{
if (_module is not null)
{
await _module.DisposeAsync();
}
}
catch (Exception ex) when (ex is JSDisconnectedException ||
ex is OperationCanceledException)
{
// The JSRuntime side may routinely be gone already if the reason we're disposing is that
// the client disconnected. This is not an error.
}
}
}
4 changes: 4 additions & 0 deletions src/Core/Components/Dialog/Services/DialogInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using Microsoft.JSInterop;

namespace Microsoft.FluentUI.AspNetCore.Components;

public sealed class DialogInstance
Expand All @@ -20,6 +22,8 @@ public DialogInstance(Type? type, DialogParameters parameters, object content)

public object Content { get; internal set; } = default!;

public IJSObjectReference? PreviouslyFocusedElement { get; set; }

public DialogParameters Parameters { get; internal set; }

internal Dictionary<string, object>? GetParameterDictionary()
Expand Down
7 changes: 7 additions & 0 deletions src/Core/wwwroot/js/dialog-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getActiveElement() {
return document.activeElement
}

export function focusElement(element) {
element.focus();
}