From 3a91bf7b96d3e41d0e221e71ca36d00ddc6c9e7a Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 9 Sep 2025 18:36:26 +0200 Subject: [PATCH 1/3] Return focus to element when dialog is closed --- .../src/Components/Dialog/FluentDialog.ts | 10 +++++- .../Components/Dialog/FluentDialog.razor.cs | 33 +++++++++++++++---- .../Dialog/Services/DialogInstance.cs | 4 +++ .../Dialog/Services/DialogService.cs | 4 +++ .../Dialog/Services/IDialogInstance.cs | 7 ++++ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts index 4798f4b6a4..9dbcb5a865 100644 --- a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -4,9 +4,11 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { * Display the fluent-dialog with the given id * @param id The id of the fluent-dialog to display */ - export function Show(id: string): void { + export function Show(id: string): HTMLElement | null { + const previousElement = document.activeElement as HTMLElement; const dialog = document.getElementById(id) as any; dialog?.show(); + return previousElement; } /** @@ -17,4 +19,10 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { const dialog = document.getElementById(id) as any; dialog?.hide(); } + + export function FocusPreviousElement(element: HTMLElement): void { + if (element) { + element.focus(); + } + } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 18e5841ac0..b4f372b85c 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -12,7 +12,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// The dialog component is a window overlaid on either the primary window or another dialog window. -/// Windows under a modal dialog are inert. +/// Windows under a modal dialog are inert. /// public partial class FluentDialog : FluentComponentBase { @@ -76,11 +76,11 @@ public FluentDialog(LibraryConfiguration configuration) : base(configuration) public EventCallback OnStateChange { get; set; } /// - /// + /// /// /// /// - protected override Task OnAfterRenderAsync(bool firstRender) + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && LaunchedFromService) { @@ -90,10 +90,15 @@ protected override Task OnAfterRenderAsync(bool firstRender) instance.FluentDialog = this; } - return ShowAsync(); + var pfe = await ShowAsync(); + + if (instance is not null) + { + instance.PreviouslyFocusedElement = pfe; + } } - return Task.CompletedTask; + return; } /// @@ -145,9 +150,9 @@ private async Task RaiseOnStateChangeAsync(DialogEventArgs args /// Displays the dialog. /// [ExcludeFromCodeCoverage] - public async Task ShowAsync() + public async Task ShowAsync() { - await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); + return await JSRuntime.InvokeAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); } /// @@ -159,6 +164,20 @@ public async Task HideAsync() await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Hide", Id); } + /// + /// Set the focus back to the element that had focus before the dialog was opened. + /// + /// + [ExcludeFromCodeCoverage] + public async Task FocusPreviousElementAsync() + { + if (Instance?.PreviouslyFocusedElement is not null) + { + await Task.Delay(50); + await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.FocusPreviousElement", Instance.PreviouslyFocusedElement); + } + } + /// private bool LaunchedFromService => Instance is not null; diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 4fb222c617..2f9b1c39b7 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using Microsoft.FluentUI.AspNetCore.Components.Utilities; +using Microsoft.JSInterop; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -40,6 +41,9 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// public Task Result => ResultCompletion.Task; + /// " + public IJSObjectReference? PreviouslyFocusedElement { get; set; } + /// " public string Id { get; } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 8ed4116302..2640297028 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Components; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -47,6 +48,9 @@ public async Task CloseAsync(IDialogInstance dialog, DialogResult result) // Raise the DialogState.Closed event dialogInstance?.FluentDialog?.RaiseOnStateChangeAsync(dialog, DialogState.Closed); + + //Focus the previously focused element (if any) + dialogInstance?.FluentDialog?.FocusPreviousElementAsync(); } /// diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index c25a9fae98..b607961979 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -2,6 +2,8 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using Microsoft.JSInterop; + namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -14,6 +16,11 @@ public interface IDialogInstance /// internal Type ComponentType { get; } + /// + /// Holds a reference to the previously focused element (before the dialog was opened). + /// + IJSObjectReference? PreviouslyFocusedElement { get; set; } + /// /// Gets the unique identifier for the dialog. /// If this value is not set in the , a new identifier is generated. From 1bd2037e0ddadbaac3267327ee5581a6e65bc9e7 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 11 Sep 2025 11:52:02 +0200 Subject: [PATCH 2/3] Better solution for focussing previous eleent --- .../src/Components/Dialog/FluentDialog.ts | 36 +++++++++++++++---- src/Core.Scripts/src/FluentUICustomEvents.ts | 5 ++- .../Components/Dialog/FluentDialog.razor.cs | 30 +++------------- .../Dialog/FluentDialogProvider.razor.cs | 5 +-- .../Dialog/Services/DialogInstance.cs | 4 --- .../Dialog/Services/DialogService.cs | 15 ++++---- .../Dialog/Services/IDialogInstance.cs | 7 ---- 7 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts index 9dbcb5a865..9f5187c977 100644 --- a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -12,17 +12,41 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { } /** - * Hide the fluent-dialog with the given id - * @param id The id of the fluent-dialog to hide - */ + * Hide the fluent-dialog with the given id + * @param id The id of the fluent-dialog to hide + */ export function Hide(id: string): void { const dialog = document.getElementById(id) as any; dialog?.hide(); + FocusOnPreviousActiveElement(id); } - export function FocusPreviousElement(element: HTMLElement): void { - if (element) { - element.focus(); + /** + * Save the element that was active before the dialog was opened + * @param id + */ + export function DialogToggle_PreviousActiveElement(id: string, newState: string): void { + const dialog = document.getElementById(id) as any; + if (dialog) { + if (newState === 'open') { + dialog.previousActiveElement = document.activeElement; + } + else if (newState === 'closed') { + FocusOnPreviousActiveElement(id); + } + } + } + + /** + * Focus on the element that was active before the dialog was opened + * @param id + */ + export function FocusOnPreviousActiveElement(id: string): void { + const dialog = document.getElementById(id) as any; + if (dialog) { + setTimeout(() => { + dialog.previousActiveElement?.focus(); + }, 25); } } } diff --git a/src/Core.Scripts/src/FluentUICustomEvents.ts b/src/Core.Scripts/src/FluentUICustomEvents.ts index e9700c9e71..c3eb615c93 100644 --- a/src/Core.Scripts/src/FluentUICustomEvents.ts +++ b/src/Core.Scripts/src/FluentUICustomEvents.ts @@ -1,3 +1,5 @@ +import { Microsoft as FluentDialogFile } from "./Components/Dialog/FluentDialog"; + export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents { /** @@ -28,6 +30,7 @@ export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents { blazor.registerCustomEventType('dialogbeforetoggle', { browserEventName: 'beforetoggle', createEventArgs: (event: any) => { + FluentDialogFile.FluentUI.Blazor.Components.Dialog.DialogToggle_PreviousActiveElement(event.target.id, event.detail?.newState ?? event.newState); return { id: event.target.id, type: event.type, @@ -130,7 +133,7 @@ export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents { newState: event.detail?.newState ?? event.newState, }; } - }); + }); } // [^^^ Add your other custom events before this line ^^^] diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index b4f372b85c..d521e22de4 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -80,25 +80,17 @@ public FluentDialog(LibraryConfiguration configuration) : base(configuration) /// /// /// - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender && LaunchedFromService) { var instance = Instance as DialogInstance; - if (instance is not null) - { - instance.FluentDialog = this; - } + instance?.FluentDialog = this; - var pfe = await ShowAsync(); - - if (instance is not null) - { - instance.PreviouslyFocusedElement = pfe; - } + return ShowAsync(); } - return; + return Task.CompletedTask; } /// @@ -164,20 +156,6 @@ public async Task HideAsync() await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Hide", Id); } - /// - /// Set the focus back to the element that had focus before the dialog was opened. - /// - /// - [ExcludeFromCodeCoverage] - public async Task FocusPreviousElementAsync() - { - if (Instance?.PreviouslyFocusedElement is not null) - { - await Task.Delay(50); - await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.FocusPreviousElement", Instance.PreviouslyFocusedElement); - } - } - /// private bool LaunchedFromService => Instance is not null; diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index da6b526e6c..5fd08cedac 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -66,9 +66,6 @@ internal void UpdateId(string? id) { Id = id; - if (DialogService is not null) - { - DialogService.ProviderId = id; - } + DialogService?.ProviderId = id; } } diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 2f9b1c39b7..4fb222c617 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -3,7 +3,6 @@ // ------------------------------------------------------------------------ using Microsoft.FluentUI.AspNetCore.Components.Utilities; -using Microsoft.JSInterop; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -41,9 +40,6 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// public Task Result => ResultCompletion.Task; - /// " - public IJSObjectReference? PreviouslyFocusedElement { get; set; } - /// " public string Id { get; } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 2640297028..03c1cb55ae 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -3,8 +3,9 @@ // ------------------------------------------------------------------------ using System.Diagnostics.CodeAnalysis; - using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -14,6 +15,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public partial class DialogService : FluentServiceBase, IDialogService { private readonly IServiceProvider _serviceProvider; + private readonly IJSRuntime _jsRuntime; /// /// Initializes a new instance of the class. @@ -26,6 +28,7 @@ public partial class DialogService : FluentServiceBase, IDialog public DialogService(IServiceProvider serviceProvider, IFluentLocalizer? localizer) { _serviceProvider = serviceProvider; + _jsRuntime = serviceProvider.GetRequiredService(); Localizer = localizer ?? FluentLocalizerInternal.Default; } @@ -48,9 +51,6 @@ public async Task CloseAsync(IDialogInstance dialog, DialogResult result) // Raise the DialogState.Closed event dialogInstance?.FluentDialog?.RaiseOnStateChangeAsync(dialog, DialogState.Closed); - - //Focus the previously focused element (if any) - dialogInstance?.FluentDialog?.FocusPreviousElementAsync(); } /// @@ -108,11 +108,11 @@ public virtual async Task ShowDialogAsync([DynamicallyAccessedMemb /// /// /// - internal Task RemoveDialogFromProviderAsync(IDialogInstance? dialog) + internal async Task RemoveDialogFromProviderAsync(IDialogInstance? dialog) { if (dialog is null) { - return Task.CompletedTask; + return; } // Remove the HTML code from the DialogProvider @@ -121,6 +121,7 @@ internal Task RemoveDialogFromProviderAsync(IDialogInstance? dialog) throw new InvalidOperationException($"Failed to remove dialog from DialogProvider: the ID '{dialog.Id}' doesn't exist in the DialogServiceProvider."); } - return ServiceProvider.OnUpdatedAsync.Invoke(dialog); + await ServiceProvider.OnUpdatedAsync.Invoke(dialog); + await _jsRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.FocusOnPreviousActiveElement", dialog.Id); } } diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index b607961979..c25a9fae98 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -2,8 +2,6 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using Microsoft.JSInterop; - namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -16,11 +14,6 @@ public interface IDialogInstance /// internal Type ComponentType { get; } - /// - /// Holds a reference to the previously focused element (before the dialog was opened). - /// - IJSObjectReference? PreviouslyFocusedElement { get; set; } - /// /// Gets the unique identifier for the dialog. /// If this value is not set in the , a new identifier is generated. From d39766b04825ded2fad9d889fd09f15c92ef0e1d Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 11 Sep 2025 12:09:28 +0200 Subject: [PATCH 3/3] Undo more changes because of better approach --- .../src/Components/Dialog/FluentDialog.ts | 4 +--- src/Core/Components/Dialog/FluentDialog.razor.cs | 15 +++++++-------- .../Dialog/FluentDialogProvider.razor.cs | 5 ++++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts index 9f5187c977..dc5aca34ac 100644 --- a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -4,11 +4,9 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { * Display the fluent-dialog with the given id * @param id The id of the fluent-dialog to display */ - export function Show(id: string): HTMLElement | null { - const previousElement = document.activeElement as HTMLElement; + export function Show(id: string): void { const dialog = document.getElementById(id) as any; dialog?.show(); - return previousElement; } /** diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index d521e22de4..1ed1e3c958 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -75,17 +75,16 @@ public FluentDialog(LibraryConfiguration configuration) : base(configuration) [Parameter] public EventCallback OnStateChange { get; set; } - /// - /// - /// - /// - /// + /// protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender && LaunchedFromService) { var instance = Instance as DialogInstance; - instance?.FluentDialog = this; + if (instance is not null) + { + instance.FluentDialog = this; + } return ShowAsync(); } @@ -142,9 +141,9 @@ private async Task RaiseOnStateChangeAsync(DialogEventArgs args /// Displays the dialog. /// [ExcludeFromCodeCoverage] - public async Task ShowAsync() + public async Task ShowAsync() { - return await JSRuntime.InvokeAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); + await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); } /// diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index 5fd08cedac..da6b526e6c 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -66,6 +66,9 @@ internal void UpdateId(string? id) { Id = id; - DialogService?.ProviderId = id; + if (DialogService is not null) + { + DialogService.ProviderId = id; + } } }