From 1a48fe2b0a50cc5f5a9781a8cbd885a711b3adde Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 9 Jun 2026 12:42:00 +0800 Subject: [PATCH] Patch MudBlazor ripple cleanup for Blazor SSR / InteractiveAuto --- .../MauiBlazorMudBlazorScriptContributor.cs | 1 + .../BlazorServerMudBlazorScriptContributor.cs | 1 + ...orWebAssemblyMudBlazorScriptContributor.cs | 1 + .../wwwroot/abp-mud-ripple-patch.js | 63 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs index 767b0cc22e..e15c4e02ff 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs @@ -9,6 +9,7 @@ public class MauiBlazorMudBlazorScriptContributor : BundleContributor { context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.js"); context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs index ca0d9f5690..d9818564a9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs @@ -17,6 +17,7 @@ public class BlazorServerMudBlazorScriptContributor : BundleContributor } context.Files.AddIfNotContains("/_content/MudBlazor/MudBlazor.min.js"); context.Files.AddIfNotContains("/_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("/_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs index 82e6184c06..7949bf0d72 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs @@ -10,6 +10,7 @@ public class BlazorWebAssemblyMudBlazorScriptContributor : BundleContributor context.Files.AddIfNotContains("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.js"); context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); diff --git a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js new file mode 100644 index 0000000000..1a17a4420f --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js @@ -0,0 +1,63 @@ +(function () { + 'use strict'; + + // Workaround for MudBlazor 9.x ripple cleanup not always running in Blazor SSR / + // WebApp / InteractiveAuto render modes. mud's own Ripple.js relies on the + // pointerdown / pointerup pair to schedule `.mud-ripple-effect-expanding` removal + // (see Q/J/K in MudBlazor.min.js). When Blazor re-renders the click target before + // pointerup fires, mud loses its `_mudRipples` Map and the ripple span is never + // cleaned up - stale spans accumulate as gray splotches on tabs / nav-links / + // buttons, looking as if several items are "selected". + // + // Tracking upstream at https://github.com/MudBlazor/MudBlazor/issues/12128 (open + // since 2025-11, reproduces on 9.1+ with Blazor Server). Until that ships we + // observe DOM mutations and schedule the same fade-then-remove sequence mud's + // own Q() would. If mud's cleanup runs first the span is already gone and the + // setTimeout callbacks are no-ops. + + var CLEANUP_DELAY = 600; + var FADE_DELAY = 400; + + function scheduleCleanup(ripple) { + setTimeout(function () { + if (!ripple.parentNode) { + return; + } + if (!ripple.classList.contains('mud-ripple-effect-fading')) { + ripple.classList.add('mud-ripple-effect-fading'); + } + setTimeout(function () { + if (ripple.parentNode) { + ripple.remove(); + } + }, FADE_DELAY); + }, CLEANUP_DELAY); + } + + function startObserving() { + new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var added = mutations[i].addedNodes; + for (var j = 0; j < added.length; j++) { + var n = added[j]; + if (n.nodeType !== 1 || !n.classList) { + continue; + } + // mud appends the ripple span with className `mud-ripple-effect` + // first, and only adds `-expanding` on the next tick. Match by + // the base class so the MutationObserver `childList` callback + // catches the node on the very first mutation. + if (n.classList.contains('mud-ripple-effect')) { + scheduleCleanup(n); + } + } + } + }).observe(document.body, { subtree: true, childList: true }); + } + + if (document.body) { + startObserving(); + } else { + document.addEventListener('DOMContentLoaded', startObserving); + } +})();