microsoft / microsoft/fluentui-blazor

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

Open
#5,059 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

community:contribution community:good-first-issue v4
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:

  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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.