From e5eb5aaaf30e07d0887732001a3bd2583f95e826 Mon Sep 17 00:00:00 2001 From: KitKeen Date: Sun, 19 Apr 2026 12:02:25 +0300 Subject: [PATCH] Add InactiveClass parameter to NavLink Adds an optional InactiveClass parameter to that mirrors ActiveClass: when the link's href does not match the current URI, the supplied class is appended to any user-provided class. The default is null, so existing usage is unaffected. Fixes #37765 --- .../Web/src/PublicAPI.Unshipped.txt | 2 + src/Components/Web/src/Routing/NavLink.cs | 11 ++- .../Web/test/Routing/NavLinkTest.cs | 91 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 3e6daf94f47d..e0acde65c88d 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -7,6 +7,8 @@ Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Exclude.get -> string? Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Exclude.set -> void Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Include.get -> string? Microsoft.AspNetCore.Components.Web.EnvironmentBoundary.Include.set -> void +Microsoft.AspNetCore.Components.Routing.NavLink.InactiveClass.get -> string? +Microsoft.AspNetCore.Components.Routing.NavLink.InactiveClass.set -> void Microsoft.AspNetCore.Components.Routing.NavLink.RelativeToCurrentUri.get -> bool Microsoft.AspNetCore.Components.Routing.NavLink.RelativeToCurrentUri.set -> void *REMOVED*Microsoft.AspNetCore.Components.Forms.RemoteBrowserFileStreamOptions diff --git a/src/Components/Web/src/Routing/NavLink.cs b/src/Components/Web/src/Routing/NavLink.cs index ef45761f1099..0d92db203173 100644 --- a/src/Components/Web/src/Routing/NavLink.cs +++ b/src/Components/Web/src/Routing/NavLink.cs @@ -29,6 +29,14 @@ public class NavLink : ComponentBase, IDisposable [Parameter] public string? ActiveClass { get; set; } + /// + /// Gets or sets the CSS class name applied to the NavLink when the + /// current route does not match the NavLink href. When null + /// (the default), no extra class is added in the inactive state. + /// + [Parameter] + public string? InactiveClass { get; set; } + /// /// Gets or sets a collection of additional attributes that will be added to the generated /// a element. @@ -110,7 +118,8 @@ public void Dispose() private void UpdateCssClass() { - CssClass = _isActive ? CombineWithSpace(_class, ActiveClass ?? DefaultActiveClass) : _class; + var stateClass = _isActive ? (ActiveClass ?? DefaultActiveClass) : InactiveClass; + CssClass = stateClass is null ? _class : CombineWithSpace(_class, stateClass); } private void OnLocationChanged(object? sender, LocationChangedEventArgs args) diff --git a/src/Components/Web/test/Routing/NavLinkTest.cs b/src/Components/Web/test/Routing/NavLinkTest.cs index a967274717b3..81e66a35306e 100644 --- a/src/Components/Web/test/Routing/NavLinkTest.cs +++ b/src/Components/Web/test/Routing/NavLinkTest.cs @@ -45,6 +45,66 @@ public async Task NavLink_WithRelativeToCurrentUri_PreservesActiveClassLogic() Assert.Equal("active", classValue); } + [Fact] + public async Task NavLink_Active_UsesActiveClassAndIgnoresInactiveClass() + { + var classValue = await RenderNavLinkAndGetClassAsync( + currentUri: "https://example.com/page", + href: "https://example.com/page", + additionalClass: null, + inactiveClass: "text-gray"); + + Assert.Equal("active", classValue); + } + + [Fact] + public async Task NavLink_Inactive_AppliesInactiveClassWhenSet() + { + var classValue = await RenderNavLinkAndGetClassAsync( + currentUri: "https://example.com/other", + href: "https://example.com/page", + additionalClass: null, + inactiveClass: "text-gray"); + + Assert.Equal("text-gray", classValue); + } + + [Fact] + public async Task NavLink_Inactive_OmitsInactiveClassWhenNotSet() + { + var classValue = await RenderNavLinkAndGetClassAsync( + currentUri: "https://example.com/other", + href: "https://example.com/page", + additionalClass: "bg-dark-gray", + inactiveClass: null); + + Assert.Equal("bg-dark-gray", classValue); + } + + [Fact] + public async Task NavLink_Inactive_CombinesUserClassWithInactiveClass() + { + var classValue = await RenderNavLinkAndGetClassAsync( + currentUri: "https://example.com/other", + href: "https://example.com/page", + additionalClass: "bg-dark-gray", + inactiveClass: "text-gray"); + + Assert.Equal("bg-dark-gray text-gray", classValue); + } + + [Fact] + public async Task NavLink_Active_CombinesUserClassWithActiveClass_WhenInactiveClassAlsoSet() + { + var classValue = await RenderNavLinkAndGetClassAsync( + currentUri: "https://example.com/page", + href: "https://example.com/page", + additionalClass: "bg-dark-gray", + inactiveClass: "text-gray"); + + Assert.Equal("bg-dark-gray active", classValue); + } + private async Task RenderNavLinkAndGetAttributeAsync( string baseUri, string currentUri, string href, bool relativeToCurrentUri, string attributeName) { @@ -67,6 +127,37 @@ public async Task NavLink_WithRelativeToCurrentUri_PreservesActiveClassLogic() return batch.ReferenceFrames.FirstOrDefault(f => f.AttributeName == attributeName).AttributeValue; } + private async Task RenderNavLinkAndGetClassAsync( + string currentUri, string href, string? additionalClass, string? inactiveClass) + { + var navigationManager = new TestNavigationManager(); + navigationManager.Initialize("https://example.com/", currentUri); + + var renderer = new TestRenderer(); + var component = new NavLink { NavigationManager = navigationManager }; + var componentId = renderer.AssignRootComponentId(component); + + var additionalAttributes = new Dictionary { ["href"] = href }; + if (additionalClass is not null) + { + additionalAttributes["class"] = additionalClass; + } + + var parametersDict = new Dictionary + { + [nameof(NavLink.AdditionalAttributes)] = additionalAttributes, + }; + if (inactiveClass is not null) + { + parametersDict[nameof(NavLink.InactiveClass)] = inactiveClass; + } + + await renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(parametersDict)); + + var batch = renderer.Batches.Single(); + return batch.ReferenceFrames.FirstOrDefault(f => f.AttributeName == "class").AttributeValue; + } + private class TestNavigationManager : NavigationManager { public new void Initialize(string baseUri, string uri) => base.Initialize(baseUri, uri);