Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/Components/Web/src/Routing/NavLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public class NavLink : ComponentBase, IDisposable
[Parameter]
public string? ActiveClass { get; set; }

/// <summary>
/// Gets or sets the CSS class name applied to the NavLink when the
/// current route does not match the NavLink href. When <c>null</c>
/// (the default), no extra class is added in the inactive state.
/// </summary>
[Parameter]
public string? InactiveClass { get; set; }

/// <summary>
/// Gets or sets a collection of additional attributes that will be added to the generated
/// <c>a</c> element.
Expand Down Expand Up @@ -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)
Expand Down
91 changes: 91 additions & 0 deletions src/Components/Web/test/Routing/NavLinkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object?> RenderNavLinkAndGetAttributeAsync(
string baseUri, string currentUri, string href, bool relativeToCurrentUri, string attributeName)
{
Expand All @@ -67,6 +127,37 @@ public async Task NavLink_WithRelativeToCurrentUri_PreservesActiveClassLogic()
return batch.ReferenceFrames.FirstOrDefault(f => f.AttributeName == attributeName).AttributeValue;
}

private async Task<object?> 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<string, object> { ["href"] = href };
if (additionalClass is not null)
{
additionalAttributes["class"] = additionalClass;
}

var parametersDict = new Dictionary<string, object?>
{
[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);
Expand Down
Loading