Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
virtual Microsoft.AspNetCore.Components.Routing.NavLink.ShouldMatch(string! currentUriAbsolute) -> bool
27 changes: 25 additions & 2 deletions src/Components/Web/src/Routing/NavLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace Microsoft.AspNetCore.Components.Routing;
/// </summary>
public class NavLink : ComponentBase, IDisposable
{
private const string DisableMatchAllIgnoresLeftUriPartSwitchKey = "Microsoft.AspNetCore.Components.Routing.NavLink.DisableMatchAllIgnoresLeftUriPart";
private readonly bool _disableMatchAllIgnoresLeftUriPart = AppContext.TryGetSwitch(DisableMatchAllIgnoresLeftUriPartSwitchKey, out var switchValue) && switchValue;

private const string DefaultActiveClass = "active";

private bool _isActive;
Expand Down Expand Up @@ -106,7 +109,12 @@ private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
}
}

private bool ShouldMatch(string currentUriAbsolute)
/// <summary>
/// Determines whether the current URI should match the link.
/// </summary>
/// <param name="currentUriAbsolute">The absolute URI of the current location.</param>
/// <returns>True if the link should be highlighted as active; otherwise, false.</returns>
protected virtual bool ShouldMatch(string currentUriAbsolute)
{
if (_hrefAbsolute == null)
{
Expand All @@ -124,9 +132,24 @@ private bool ShouldMatch(string currentUriAbsolute)
return true;
}

if (_disableMatchAllIgnoresLeftUriPart)
{
return false;
}

string uriWithoutQueryAndFragment = GetUriIgnoreQueryAndFragment(currentUriAbsolute);
if (Match == NavLinkMatch.All
&& EqualsHrefExactlyOrIfTrailingSlashAdded(uriWithoutQueryAndFragment))
{
return true;
}

return false;
}

private static string GetUriIgnoreQueryAndFragment(string uri) =>
new Uri(uri).GetLeftPart(UriPartial.Path);

private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute)
{
Debug.Assert(_hrefAbsolute != null);
Expand Down Expand Up @@ -199,7 +222,7 @@ private static bool IsStrictlyPrefixWithSeparator(string value, string prefix)

private static bool IsUnreservedCharacter(char c)
{
// Checks whether it is an unreserved character according to
// Checks whether it is an unreserved character according to
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
// Those are characters that are allowed in a URI but do not have a reserved
// purpose (e.g. they do not separate the components of the URI)
Expand Down
48 changes: 42 additions & 6 deletions src/Components/test/E2ETest/Tests/RoutingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ public void CanFollowLinkToOtherPageWithQueryString()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other with query")).Click();
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with query");
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)", "Other with query");
}

[Fact]
Expand All @@ -310,7 +310,10 @@ public void CanFollowLinkToDefaultPageWithQueryString()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with query")).Click();
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with query");
AssertHighlightedLinks(
"Default (matches all)",
"Default with base-relative URL (matches all)",
"Default with query");
}

[Fact]
Expand All @@ -321,7 +324,11 @@ public void CanFollowLinkToDefaultPageWithQueryString_NoTrailingSlash()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with query, no trailing slash")).Click();
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with query, no trailing slash");
AssertHighlightedLinks(
"Default (matches all)",
"Default with base-relative URL (matches all)",
"Default, no trailing slash (matches all)",
"Default with query, no trailing slash");
}

[Fact]
Expand All @@ -332,7 +339,7 @@ public void CanFollowLinkToOtherPageWithHash()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other with hash")).Click();
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with hash");
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)", "Other with hash");
}

[Fact]
Expand All @@ -343,7 +350,10 @@ public void CanFollowLinkToDefaultPageWithHash()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with hash")).Click();
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with hash");
AssertHighlightedLinks(
"Default (matches all)",
"Default with base-relative URL (matches all)",
"Default with hash");
}

[Fact]
Expand All @@ -354,7 +364,11 @@ public void CanFollowLinkToDefaultPageWithHash_NoTrailingSlash()
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with hash, no trailing slash")).Click();
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with hash, no trailing slash");
AssertHighlightedLinks(
"Default (matches all)",
"Default with base-relative URL (matches all)",
"Default, no trailing slash (matches all)",
"Default with hash, no trailing slash");
}

[Fact]
Expand Down Expand Up @@ -383,6 +397,28 @@ public void CanFollowLinkDefinedInOpenShadowRoot()
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
}

[Fact]
public void CanOverrideNavLinkToNotIgnoreFragment()
{
SetUrlViaPushState("/layout-overridden/for-hash");

var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Override layout with hash, no trailing slash")).Click();
Browser.Equal("This is the page with overridden layout.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Override layout with hash, no trailing slash");
}

[Fact]
public void CanOverrideNavLinkToNotIgnoreQuery()
{
SetUrlViaPushState("/layout-overridden");

var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Override layout with query, no trailing slash")).Click();
Browser.Equal("This is the page with overridden layout.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Override layout with query, no trailing slash");
}

[Fact]
public void CanGoBackFromNotAComponent()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page "/layout-overridden"
@page "/layout-overridden/for-hash"
@layout RouterTestLayoutNavLinksOverridden
<div id="test-info">This is the page with overridden layout.</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@using Microsoft.AspNetCore.Components.Routing
<style type="text/css">
a.active {
background-color: yellow;
font-weight: bold;
}
</style>
<ul>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden/" Match=NavLinkMatch.All>Override layout (matches all)</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden" Match=NavLinkMatch.All>Override layout, no trailing slash (matches all)</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden/?abc=123">Override layout with query</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden?abc=123">Override layout with query, no trailing slash</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden/#blah">Override layout with hash</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden#blah">Override layout with hash, no trailing slash</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden/Default.html">Override layout with extension</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/layout-overridden/Other">Override Other</NavLinkNotIgnoreQueryOrFragmentString></li>
<li><NavLinkNotIgnoreQueryOrFragmentString href="/subdir/Other" Match=NavLinkMatch.All>Override Other with base-relative URL (matches all)</NavLinkNotIgnoreQueryOrFragmentString></li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

public class NavLinkNotIgnoreQueryOrFragmentString : NavLink
{
string hrefAbsolute;
NavigationManager _navigationManager;

public NavLinkNotIgnoreQueryOrFragmentString(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}

protected override void OnInitialized()
{
string href = "";
if (AdditionalAttributes != null && AdditionalAttributes.TryGetValue("href", out var obj))
{
href = Convert.ToString(obj, CultureInfo.InvariantCulture) ?? "";
}
hrefAbsolute = _navigationManager.ToAbsoluteUri(href).AbsoluteUri;
base.OnInitialized();
}
protected override bool ShouldMatch(string currentUriAbsolute)
{
bool baseMatch = base.ShouldMatch(currentUriAbsolute);
if (!baseMatch || string.IsNullOrEmpty(hrefAbsolute) || Match != NavLinkMatch.All)
{
return baseMatch;
}

if (NormalizeUri(hrefAbsolute) == NormalizeUri(currentUriAbsolute))
{
return true;
}
return false;
}

private static string NormalizeUri(string uri) =>
uri.EndsWith('/') ? uri.TrimEnd('/') : uri;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@using Microsoft.AspNetCore.Components
@inherits LayoutComponentBase

@Body

<BasicTestApp.RouterTest.LinksOverridden />
Loading