Skip to content

fix: JSException disposing PreviouslyFocusedElement when Custom splash screen auto-closes (SplashScreen demo) #5059

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:

  1. Go to https://www.fluentui-blazor.net/SplashScreen
  2. In the Custom splash screen section, click Open splash screen.
  3. Wait for the splash screen to run (~5–7 seconds) and auto-close.
  4. 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 ReturnFocusAsync against a disposed/stale reference, e.g. wrap the focusElement interop in a try/catch for JSException/JSDisconnectedException/ObjectDisposedException so a failed focus-return never surfaces as an unhandled render exception.
  • Ensure PreviouslyFocusedElement is not disposed (or is re-captured) across UpdateDialogAsync calls, 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 on dev at v4.14.3.
  • URL: https://www.fluentui-blazor.net/SplashScreen

Metadata

Metadata

Assignees

No one assigned

    Labels

    community:contributionIssue will/can be addressed by community contributionv4

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions