microsoft / microsoft/fluentui-blazor
fix: JSException disposing PreviouslyFocusedElement when Custom splash screen auto-closes (SplashScreen demo)
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.8k
- Forks
- 483
- Avg merge
- 14h 41m
- Merged PRs (30d)
- 68
Description
🐛 Bug Report
Opening the Custom splash screen demo on the docs site throws an unhandled Microsoft.JSInterop.JSException (JS object instance with Id 'NNN' does not exist. It may have been disposed.) when the splash screen auto-closes. The crash originates in FluentDialogProvider.ReturnFocusAsync, which invokes focusElement on a PreviouslyFocusedElement IJSObjectReference that has already been disposed.
💻 Repro or Code Sample
Live repro on the documentation site:
- Go to https://www.fluentui-blazor.net/SplashScreen
- In the Custom splash screen section, click Open splash screen.
- Wait for the splash screen to run (~5–7 seconds) and auto-close.
- Open the browser dev-tools console — an unhandled exception is logged (twice).
The demo is driven by DialogSplashScreenCustom + CustomSplashScreen:
// DialogSplashScreenCustom.razor.cs
private async Task OpenSplashCustomAsync()
{
DialogParameters<SplashScreenContent> parameters = new()
{
Content = new SplashScreenContent()
{
Title = "Water drinking 101",
LoadingText = "Filling the re-useable bottles...",
Message = (MarkupString)"Don't drink <strong>too</strong> much water!",
Logo = "_content/FluentUI.Demo.Shared/images/Splash_Corporation_logo.png",
DisplayTime = 7000
},
Width = "500px",
Height = "300px",
};
_dialog = await DialogService.ShowSplashScreenAsync<CustomSplashScreen>(parameters);
for (var i = 0; i < 5; i++)
{
await Task.Delay(1000);
parameters.Content.LoadingText = $"Filling the re-useable bottles... {i + 1}";
await DialogService.UpdateDialogAsync(_dialog.Id, parameters); // updates dialog 5x
}
DialogResult result = await _dialog.Result;
await HandleCustomSplashAsync(result);
}
// CustomSplashScreen.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Delay(Content.DisplayTime); // 7000ms
await Dialog.CloseAsync(); // <-- self-close triggers the crash
}
}
🤔 Expected Behavior
The custom splash screen closes cleanly after its DisplayTime and returns focus to the trigger element without throwing.
😯 Current Behavior
When the dialog self-closes, FluentDialog.CloseAsync() calls DialogContext.DialogContainer.ReturnFocusAsync(Instance.PreviouslyFocusedElement), which invokes focusElement on an IJSObjectReference that has already been disposed. Two unhandled exceptions are logged in succession (JS object Id 264 and 265):
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: JS object instance with Id '264' does not exist. It may have been disposed.
Microsoft.JSInterop.JSException: JS object instance with Id '264' does not exist. It may have been disposed.
at Microsoft.JSInterop.JSRuntime.InvokeAsync[IJSVoidResult](...)
at Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeVoidAsync(IJSObjectReference , String , Object[] )
at Microsoft.FluentUI.AspNetCore.Components.FluentDialogProvider.ReturnFocusAsync(IJSObjectReference element)
at Microsoft.FluentUI.AspNetCore.Components.FluentDialog.CloseAsync(DialogResult dialogResult)
at Microsoft.FluentUI.AspNetCore.Components.FluentDialog.CloseAsync()
at FluentUI.Demo.Shared.Pages.SplashScreen.Examples.CustomSplashScreen.OnAfterRenderAsync(Boolean firstRender)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task , ComponentState )
A second identical exception follows for JS object Id 265.
💁 Possible Solution
The relevant source paths:
FluentDialog.razor.cs(CloseAsync) returns focus to the captured element:if (DialogContext is not null && Instance.PreviouslyFocusedElement is not null) { await Task.Delay(50); await DialogContext.DialogContainer.ReturnFocusAsync(Instance.PreviouslyFocusedElement); }FluentDialogProvider.razor.cs(ReturnFocusAsync) invokes JS then disposes the reference:internal async Task ReturnFocusAsync(IJSObjectReference element) { if (_module is not null) { await _module.InvokeVoidAsync("focusElement", element); // throws if element already disposed } await element.DisposeAsync(); }
By the time the splash screen self-closes, PreviouslyFocusedElement is a stale IJSObjectReference that has already been disposed — the repeated UpdateDialogAsync re-render cycle appears to leave the reference in a disposed state. Suggestions:
- Guard
ReturnFocusAsyncagainst a disposed/stale reference, e.g. wrap thefocusElementinterop in atry/catchforJSException/JSDisconnectedException/ObjectDisposedExceptionso a failed focus-return never surfaces as an unhandled render exception. - Ensure
PreviouslyFocusedElementis not disposed (or is re-captured) acrossUpdateDialogAsynccalls, and null it out after it is disposed to avoid a double-invoke/double-dispose (the two consecutive errors suggest the reference is being used after disposal more than once).
Happy to contribute a fix if the maintainers agree on the preferred approach.
🔦 Context
The Custom splash screen documentation example crashes on the public docs site, which is a poor first impression for anyone evaluating the splash screen feature. The same pattern (a self-closing dialog combined with UpdateDialogAsync) can surface in real apps.
🌍 Your Environment
- OS & Device: Reproduced on the public docs site (any desktop OS/browser)
- Browser: Google Chrome (dev-tools console)
- .NET and Fluent UI Blazor library Version: Blazor WebAssembly; JSInterop
Version=10.0.0.0(.NET 10) per the stack trace. Vulnerable code confirmed present ondevatv4.14.3. - URL: https://www.fluentui-blazor.net/SplashScreen
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with FluentDialog.razor.cs CloseAsync and FluentDialogProvider.razor.cs ReturnFocusAsync, then reproduce the Custom splash screen flow at /SplashScreen with repeated UpdateDialogAsync calls. Trace the PreviouslyFocusedElement lifetime and verify that auto-closing returns focus without an unhandled JSException or repeated use of a disposed reference.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, javascript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100