From daa21ca73aa61320912c5b15f30f14bd3436d1e9 Mon Sep 17 00:00:00 2001
From: Daria Tiurina
Date: Wed, 27 Aug 2025 11:55:10 +0200
Subject: [PATCH 1/5] Fix fetch request in enanced navigation
---
.../src/Services/NavigationEnhancement.ts | 6 +++-
.../Web.JS/src/Services/NavigationUtils.ts | 11 +++++++
.../EnhancedNavigationTest.cs | 29 +++++++++++++++++++
.../EnhancedNav/PageForScrollingToHash.razor | 5 ++++
4 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index 9dcd7540d866..d6cda4b30389 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
import { synchronizeDomContent } from '../Rendering/DomMerging/DomSync';
-import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, isSamePageWithHash, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage } from './NavigationUtils';
+import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, isSamePageWithHash, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage, isHashOnlyChange } from './NavigationUtils';
import { resetScrollAfterNextBatch, resetScrollIfNeeded } from '../Rendering/Renderer';
/*
@@ -120,6 +120,10 @@ function onPopState(state: PopStateEvent) {
return;
}
+ if (isHashOnlyChange(currentContentUrl, location.href)){
+ return;
+ }
+
// load the new page
performEnhancedPageLoad(location.href, /* interceptedLink */ false);
}
diff --git a/src/Components/Web.JS/src/Services/NavigationUtils.ts b/src/Components/Web.JS/src/Services/NavigationUtils.ts
index bc58636c39c6..c142d555c54d 100644
--- a/src/Components/Web.JS/src/Services/NavigationUtils.ts
+++ b/src/Components/Web.JS/src/Services/NavigationUtils.ts
@@ -52,6 +52,17 @@ export function isSamePageWithHash(absoluteHref: string): boolean {
return url.hash !== '' && location.origin === url.origin && location.pathname === url.pathname && location.search === url.search;
}
+export function isHashOnlyChange(oldUrl: string, newUrl: string): boolean {
+ try {
+ const a = new URL(oldUrl);
+ const b = new URL(newUrl);
+ return a.origin === b.origin && a.pathname === b.pathname
+ && a.search === b.search && a.hash !== b.hash;
+ } catch {
+ return false;
+ }
+}
+
export function isForSamePath(url1: string, url2: string) {
// We are going to use the scheme, host, port and path to determine if the two URLs are compatible.
// We do not account for the query string as we want to allow for the query string to change.
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
index 49ca00e06915..b0b453d6e1db 100644
--- a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
+++ b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
@@ -10,6 +10,7 @@
using Microsoft.AspNetCore.InternalTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.BiDi.Communication;
+using OpenQA.Selenium.DevTools;
using OpenQA.Selenium.Support.Extensions;
using TestServer;
using Xunit.Abstractions;
@@ -195,6 +196,34 @@ public void CanScrollToHashWithoutPerformingFullNavigation()
.EndsWith("scroll-to-hash", StringComparison.Ordinal));
}
+ [Fact]
+ public void NonEnhancedNavCanScrollToHashWithoutFetchingPage()
+ {
+ Navigate($"{ServerPathBase}/nav/scroll-to-hash");
+ Browser.Equal("Scroll to hash", () => Browser.Exists(By.TagName("h1")).Text);
+
+ var javascript = (IJavaScriptExecutor)Browser;
+ javascript.ExecuteScript(@"
+ window.testFetchCalls = [];
+ const originalFetch = window.fetch;
+ window.fetch = function(...args) {
+ window.testFetchCalls.push(args[0]);
+ return originalFetch.apply(this, args);
+ };");
+
+ Browser.Exists(By.Id("scroll-anchor-enhance-nav-false")).Click();
+ Browser.True(() => Browser.GetScrollY() > 500);
+ Browser.True(() => Browser
+ .Exists(By.Id("uri-on-page-load-enhance-nav-false"))
+ .GetDomAttribute("data-value")
+ .EndsWith("scroll-to-hash", StringComparison.Ordinal));
+
+ var fetchCalls = javascript.ExecuteScript("return window.testFetchCalls;") as IEnumerable
+
+ Scroll via anchor
+
+
+
spacer
@if (showContent)
From a1fb2212c2be7ac6ae7acad67de37e566bad5cfe Mon Sep 17 00:00:00 2001
From: Daria Tiurina
Date: Wed, 27 Aug 2025 13:04:06 +0200
Subject: [PATCH 2/5] Fix typo
---
src/Components/Web.JS/src/Services/NavigationEnhancement.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index d6cda4b30389..0252079fb856 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -120,7 +120,7 @@ function onPopState(state: PopStateEvent) {
return;
}
- if (isHashOnlyChange(currentContentUrl, location.href)){
+ if (isHashOnlyChange(currentContentUrl, location.href)) {
return;
}
From 558cd83df15aab5e1048b02e0a4eb0a1241ef65f Mon Sep 17 00:00:00 2001
From: Daria Tiurina
Date: Wed, 27 Aug 2025 17:20:01 +0200
Subject: [PATCH 3/5] Feedback
---
.../src/Services/NavigationEnhancement.ts | 6 +--
.../Web.JS/src/Services/NavigationManager.ts | 4 +-
.../Web.JS/src/Services/NavigationUtils.ts | 9 +----
.../EnhancedNavigationTest.cs | 38 +++++++++++--------
.../EnhancedNav/PageForScrollingToHash.razor | 16 ++++++--
5 files changed, 41 insertions(+), 32 deletions(-)
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index 0252079fb856..d62c7b4e6c2e 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
import { synchronizeDomContent } from '../Rendering/DomMerging/DomSync';
-import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, isSamePageWithHash, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage, isHashOnlyChange } from './NavigationUtils';
+import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage, isSameUrlWithDifferentHash } from './NavigationUtils';
import { resetScrollAfterNextBatch, resetScrollIfNeeded } from '../Rendering/Renderer';
/*
@@ -99,7 +99,7 @@ function onDocumentClick(event: MouseEvent) {
handleClickForNavigationInterception(event, absoluteInternalHref => {
const originalLocation = location.href;
- const shouldScrollToHash = isSamePageWithHash(absoluteInternalHref);
+ const shouldScrollToHash = isSameUrlWithDifferentHash(originalLocation, absoluteInternalHref);
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
if (shouldScrollToHash) {
@@ -120,7 +120,7 @@ function onPopState(state: PopStateEvent) {
return;
}
- if (isHashOnlyChange(currentContentUrl, location.href)) {
+ if (isSameUrlWithDifferentHash(currentContentUrl, location.href)) {
return;
}
diff --git a/src/Components/Web.JS/src/Services/NavigationManager.ts b/src/Components/Web.JS/src/Services/NavigationManager.ts
index b3352b399f55..a45ae454b004 100644
--- a/src/Components/Web.JS/src/Services/NavigationManager.ts
+++ b/src/Components/Web.JS/src/Services/NavigationManager.ts
@@ -4,7 +4,7 @@
import '@microsoft/dotnet-js-interop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
import { EventDelegator } from '../Rendering/Events/EventDelegator';
-import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
+import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSameUrlWithDifferentHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
import { WebRendererId } from '../Rendering/WebRendererId';
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
@@ -150,7 +150,7 @@ function performExternalNavigation(uri: string, replace: boolean) {
async function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean, state: string | undefined = undefined, skipLocationChangingCallback = false) {
ignorePendingNavigation();
- if (isSamePageWithHash(absoluteInternalHref)) {
+ if (isSameUrlWithDifferentHash(location.href, absoluteInternalHref)) {
saveToBrowserHistory(absoluteInternalHref, replace, state);
performScrollToElementOnTheSamePage(absoluteInternalHref);
return;
diff --git a/src/Components/Web.JS/src/Services/NavigationUtils.ts b/src/Components/Web.JS/src/Services/NavigationUtils.ts
index c142d555c54d..613db7791f9e 100644
--- a/src/Components/Web.JS/src/Services/NavigationUtils.ts
+++ b/src/Components/Web.JS/src/Services/NavigationUtils.ts
@@ -47,17 +47,12 @@ export function isWithinBaseUriSpace(href: string) {
&& (nextChar === '' || nextChar === '/' || nextChar === '?' || nextChar === '#');
}
-export function isSamePageWithHash(absoluteHref: string): boolean {
- const url = new URL(absoluteHref);
- return url.hash !== '' && location.origin === url.origin && location.pathname === url.pathname && location.search === url.search;
-}
-
-export function isHashOnlyChange(oldUrl: string, newUrl: string): boolean {
+export function isSameUrlWithDifferentHash(oldUrl: string, newUrl: string): boolean {
try {
const a = new URL(oldUrl);
const b = new URL(newUrl);
return a.origin === b.origin && a.pathname === b.pathname
- && a.search === b.search && a.hash !== b.hash;
+ && a.search === b.search && b.hash !== '';
} catch {
return false;
}
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
index b0b453d6e1db..c11a30786c2e 100644
--- a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
+++ b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
@@ -197,31 +197,37 @@ public void CanScrollToHashWithoutPerformingFullNavigation()
}
[Fact]
- public void NonEnhancedNavCanScrollToHashWithoutFetchingPage()
+ public void NonEnhancedNavCanScrollToHashWithoutFetchingPageAnchor()
{
Navigate($"{ServerPathBase}/nav/scroll-to-hash");
- Browser.Equal("Scroll to hash", () => Browser.Exists(By.TagName("h1")).Text);
-
- var javascript = (IJavaScriptExecutor)Browser;
- javascript.ExecuteScript(@"
- window.testFetchCalls = [];
- const originalFetch = window.fetch;
- window.fetch = function(...args) {
- window.testFetchCalls.push(args[0]);
- return originalFetch.apply(this, args);
- };");
+ var originalTextElem = Browser.Exists(By.CssSelector("#anchor #text"));
+ Browser.Equal("Text", () => originalTextElem.Text);
- Browser.Exists(By.Id("scroll-anchor-enhance-nav-false")).Click();
+ Browser.Exists(By.CssSelector("#anchor #scroll-anchor")).Click();
Browser.True(() => Browser.GetScrollY() > 500);
Browser.True(() => Browser
- .Exists(By.Id("uri-on-page-load-enhance-nav-false"))
+ .Exists(By.CssSelector("#anchor #uri-on-page-load"))
.GetDomAttribute("data-value")
.EndsWith("scroll-to-hash", StringComparison.Ordinal));
- var fetchCalls = javascript.ExecuteScript("return window.testFetchCalls;") as IEnumerable;
- var relevantCalls = fetchCalls?.Where(call => call.ToString().Contains("scroll-to-hash")) ?? Enumerable.Empty();
+ Browser.Equal("Text", () => originalTextElem.Text);
+ }
+
+ [Fact]
+ public void NonEnhancedNavCanScrollToHashWithoutFetchingPageNavLink()
+ {
+ Navigate($"{ServerPathBase}/nav/scroll-to-hash");
+ var originalTextElem = Browser.Exists(By.CssSelector("#navlink #text"));
+ Browser.Equal("Text", () => originalTextElem.Text);
+
+ Browser.Exists(By.CssSelector("#navlink #scroll-anchor")).Click();
+ Browser.True(() => Browser.GetScrollY() > 500);
+ Browser.True(() => Browser
+ .Exists(By.CssSelector("#navlink #uri-on-page-load"))
+ .GetDomAttribute("data-value")
+ .EndsWith("scroll-to-hash", StringComparison.Ordinal));
- Assert.Empty(relevantCalls);
+ Browser.Equal("Text", () => originalTextElem.Text);
}
[Theory]
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageForScrollingToHash.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageForScrollingToHash.razor
index 5255cfdd04ed..22597ce37be3 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageForScrollingToHash.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageForScrollingToHash.razor
@@ -1,6 +1,7 @@
@page "/nav/scroll-to-hash"
@attribute [StreamRendering]
@inject NavigationManager NavigationManager
+@using Microsoft.AspNetCore.Components.Forms
Page for scrolling to hash
@@ -13,10 +14,17 @@
-
- Scroll via anchor
-
-
+
+
+
+
Scroll via NavLink
+
+
Text
+
spacer
From c365e7d53d941d2874b2e2f83e723f6e01a4b501 Mon Sep 17 00:00:00 2001
From: Daria Tiurina
Date: Fri, 29 Aug 2025 11:34:09 +0200
Subject: [PATCH 4/5] Fix
---
src/Components/Web.JS/src/Services/NavigationEnhancement.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index d62c7b4e6c2e..aa0a3665a56f 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -120,7 +120,8 @@ function onPopState(state: PopStateEvent) {
return;
}
- if (isSameUrlWithDifferentHash(currentContentUrl, location.href)) {
+ if (state.state == null && isSameUrlWithDifferentHash(currentContentUrl, location.href)) {
+ currentContentUrl = location.href;
return;
}
From 84466b251176f9eb917a87aff6930248f94efa36 Mon Sep 17 00:00:00 2001
From: Daria Tiurina
Date: Fri, 29 Aug 2025 12:25:11 +0200
Subject: [PATCH 5/5] Feedback
---
.../Web.JS/src/Services/NavigationEnhancement.ts | 6 +++---
.../Web.JS/src/Services/NavigationManager.ts | 4 ++--
.../Web.JS/src/Services/NavigationUtils.ts | 14 +++++---------
3 files changed, 10 insertions(+), 14 deletions(-)
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index aa0a3665a56f..c39a00bd0337 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
import { synchronizeDomContent } from '../Rendering/DomMerging/DomSync';
-import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage, isSameUrlWithDifferentHash } from './NavigationUtils';
+import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isForSamePath, notifyEnhancedNavigationListeners, performScrollToElementOnTheSamePage, isSamePageWithHash } from './NavigationUtils';
import { resetScrollAfterNextBatch, resetScrollIfNeeded } from '../Rendering/Renderer';
/*
@@ -99,7 +99,7 @@ function onDocumentClick(event: MouseEvent) {
handleClickForNavigationInterception(event, absoluteInternalHref => {
const originalLocation = location.href;
- const shouldScrollToHash = isSameUrlWithDifferentHash(originalLocation, absoluteInternalHref);
+ const shouldScrollToHash = isSamePageWithHash(originalLocation, absoluteInternalHref);
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
if (shouldScrollToHash) {
@@ -120,7 +120,7 @@ function onPopState(state: PopStateEvent) {
return;
}
- if (state.state == null && isSameUrlWithDifferentHash(currentContentUrl, location.href)) {
+ if (state.state == null && isSamePageWithHash(currentContentUrl, location.href)) {
currentContentUrl = location.href;
return;
}
diff --git a/src/Components/Web.JS/src/Services/NavigationManager.ts b/src/Components/Web.JS/src/Services/NavigationManager.ts
index a45ae454b004..8e2de809505a 100644
--- a/src/Components/Web.JS/src/Services/NavigationManager.ts
+++ b/src/Components/Web.JS/src/Services/NavigationManager.ts
@@ -4,7 +4,7 @@
import '@microsoft/dotnet-js-interop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
import { EventDelegator } from '../Rendering/Events/EventDelegator';
-import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSameUrlWithDifferentHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
+import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
import { WebRendererId } from '../Rendering/WebRendererId';
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
@@ -150,7 +150,7 @@ function performExternalNavigation(uri: string, replace: boolean) {
async function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean, replace: boolean, state: string | undefined = undefined, skipLocationChangingCallback = false) {
ignorePendingNavigation();
- if (isSameUrlWithDifferentHash(location.href, absoluteInternalHref)) {
+ if (isSamePageWithHash(location.href, absoluteInternalHref)) {
saveToBrowserHistory(absoluteInternalHref, replace, state);
performScrollToElementOnTheSamePage(absoluteInternalHref);
return;
diff --git a/src/Components/Web.JS/src/Services/NavigationUtils.ts b/src/Components/Web.JS/src/Services/NavigationUtils.ts
index 613db7791f9e..9976eafc898c 100644
--- a/src/Components/Web.JS/src/Services/NavigationUtils.ts
+++ b/src/Components/Web.JS/src/Services/NavigationUtils.ts
@@ -47,15 +47,11 @@ export function isWithinBaseUriSpace(href: string) {
&& (nextChar === '' || nextChar === '/' || nextChar === '?' || nextChar === '#');
}
-export function isSameUrlWithDifferentHash(oldUrl: string, newUrl: string): boolean {
- try {
- const a = new URL(oldUrl);
- const b = new URL(newUrl);
- return a.origin === b.origin && a.pathname === b.pathname
- && a.search === b.search && b.hash !== '';
- } catch {
- return false;
- }
+export function isSamePageWithHash(oldUrl: string, newUrl: string): boolean {
+ const a = new URL(oldUrl);
+ const b = new URL(newUrl);
+ return a.origin === b.origin && a.pathname === b.pathname
+ && a.search === b.search && b.hash !== '';
}
export function isForSamePath(url1: string, url2: string) {