MudMenu: Fix JSInterop focus crash on WebView when closing/navigating#13317
Merged
danielchalmers merged 2 commits intoJun 19, 2026
Merged
Conversation
MudMenu threw `JSException: Unable to focus an invalid element` on MAUI BlazorWebView (iOS/Android) when the menu was closed or the page was navigated while the menu was open. This was a regression introduced in v8.14.0 by PR MudBlazor#11762, which added an `OnAfterRenderAsync` override that focuses `_menuWrapperRef` (and, via `FocusItemAsync`, menu items). The `_menuWrapperRef.Context is not null` / `elementRef.Context is not null` guards are not liveness checks: `ElementReference.Context` stays non-null after the underlying DOM node is detached, so the deferred focus interop runs against a missing element and Blazor's `domWrapper.focus` throws. It only reproduces on the WebView host because its native<->WebView message bridge is asynchronous, so the queued focus call can land after the close/navigation render batch removes the element; desktop browsers win that race. The same PR already guards `FocusActivatorAsync` with `try/catch (JSException)` but omitted equivalent handling on the two new calls. Fix: - Wrap the wrapper focus in `OnAfterRenderAsync` and the item focus in `FocusItemAsync` in `try/catch (JSException)`, matching the existing `FocusActivatorAsync`/`CloseMenuAsync` idiom (covers the rapid open/close path, which closes without disposing). - Add a `_disposed` guard set in `Dispose(bool)` and checked in `OnAfterRenderAsync` and `FocusItemAsync`, so the fire-and-forget focus calls scheduled by `TrackKeyboardInteraction` short-circuit after teardown instead of raising an unobserved exception (covers the navigate-while-open path). Adds regression tests that drive the focus interop to throw and assert the menu neither crashes on open nor propagates the exception, plus a test that `FocusItemAsync` is a no-op after disposal. Note: bUnit cannot reproduce the WebView timing race itself; the tests lock in the guard behavior. Final validation requires a real MAUI BlazorWebView device. Fixes MudBlazor#12184
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens MudMenu focus-related JS interop against WebView timing races (MAUI BlazorWebView) by preventing focus calls from crashing when the target element is already detached/disposed, and adds regression tests covering the failure modes reported in #12184.
Changes:
- Add a
_disposedflag and short-circuit focus paths (OnAfterRenderAsync,FocusItemAsync) after teardown. - Wrap wrapper-focus and item-focus JS interop calls in
try/catch (JSException)to swallow “Unable to focus an invalid element” when the DOM node is gone. - Add three
MenuTestsregressions that deterministically throw the underlying focus JS interop exception and assert it’s swallowed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/MudBlazor/Components/Menu/MudMenu.razor.cs |
Adds disposal guarding and JSException handling around focus interop to prevent WebView crashes on close/navigation. |
src/MudBlazor.UnitTests/Components/MenuTests.cs |
Adds regression tests to ensure focus interop exceptions don’t propagate and no focus interop runs after dispose. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses Copilot review feedback on MudBlazor#13317. A hovered menu holds a live CancellationTokenSource, so a second Dispose() (e.g. user code disposing a @ref before the renderer tears the component down) would call Cancel() on the already-disposed CTS and throw ObjectDisposedException. Return early when already disposed. Adds a regression test that hovers to create the CTS, then disposes twice and asserts no exception.
This was referenced Jun 27, 2026
This was referenced Jul 2, 2026
This was referenced Jul 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12184
Problem
MudMenuthrowsMicrosoft.JSInterop.JSException: Unable to focus an invalid elementon MAUI BlazorWebView (iOS/Android) when the menu is closed or the page is navigated while the menu is open. Reported repro paths: navigate while open, rapidly open/close an overflow menu, and keyboard submenu navigation. It only manifests on the WebView host — desktop browsers don't crash.Cause
A regression introduced in v8.14.0 by #11762, which added an
OnAfterRenderAsyncoverride that focuses_menuWrapperRef(and, viaFocusItemAsync, menu items). Before that PR,MudMenuhad no focus interop at all.The
…Context is not nullguards on those calls are not liveness checks —ElementReference.Contextstays non-null after the underlying DOM node is detached (TOCTOU). So the deferred focus interop runs against a missing element and Blazor'sdomWrapper.focusthrows. It only reproduces on the WebView host because its native↔WebView message bridge is asynchronous, so the queued focus call can land after the close/navigation render batch removes the element; desktop browsers win that race.Tellingly, the same PR already guards
FocusActivatorAsyncwithtry/catch (JSException)but omitted equivalent handling on the two new focus calls.Fix
OnAfterRenderAsyncand the item focus inFocusItemAsyncintry/catch (JSException), matching the existingFocusActivatorAsync/CloseMenuAsyncidiom in this file. (JSDisconnectedExceptionderives fromJSException, so circuit-disconnect is covered too.) This handles the rapid open/close path, which closes without disposing._disposedflag set inDispose(bool)and checked inOnAfterRenderAsyncandFocusItemAsync, so the fire-and-forget focus calls scheduled byTrackKeyboardInteractionshort-circuit after teardown instead of raising an unobserved exception. This handles the navigate-while-open path. Because every keyboard navigation path funnels throughFocusItemAsync, hardening that one method covers all of them.Tests
Added three regression tests in
MenuTeststhat driveBlazor._internal.domWrapper.focusto throw "Unable to focus an invalid element":OpenMenu_WhenFocusInteropThrows_DoesNotCrash— theOnAfterRenderAsyncwrapper-focus path (the exact reported stack frame).FocusItemAsync_WhenFocusInteropThrows_DoesNotPropagate— the keyboard focus funnel.FocusItemAsync_AfterDispose_DoesNotInvokeFocusInterop— the_disposedguard.Verified: with the fix reverted, exactly these 3 tests fail (93/96 pass); with the fix, all 96
MenuTestspass.Checklist: