Skip to content

Commit a55c4bd

Browse files
authored
NavigateTo should not scroll to the top in internal navigation to same url (#60190)
Condition the scroll action with the check if internal navigation is not happening for the same path. `isForSamePath` compares ignoring queries.
1 parent c43eb9a commit a55c4bd

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

src/Components/Web.JS/src/Services/NavigationManager.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import '@microsoft/dotnet-js-interop';
55
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
66
import { EventDelegator } from '../Rendering/Events/EventDelegator';
7-
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
7+
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
88
import { WebRendererId } from '../Rendering/WebRendererId';
99
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
1010

@@ -169,7 +169,9 @@ async function performInternalNavigation(absoluteInternalHref: string, intercept
169169
// position, so reset it.
170170
// To avoid ugly flickering effects, we don't want to change the scroll position until
171171
// we render the new page. As a best approximation, wait until the next batch.
172-
resetScrollAfterNextBatch();
172+
if (!isForSamePath(absoluteInternalHref, location.href)) {
173+
resetScrollAfterNextBatch();
174+
}
173175

174176
saveToBrowserHistory(absoluteInternalHref, replace, state);
175177

src/Components/test/E2ETest/Tests/RoutingTest.cs

+37
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,43 @@ public void CanNavigateProgrammaticallyWithStateReplaceHistoryEntry()
510510
Assert.Equal(typeof(TestRouter).FullName, testSelector.SelectedOption.GetDomProperty("value"));
511511
}
512512

513+
[Fact]
514+
public void NavigationToSamePathDoesNotScrollToTheTop()
515+
{
516+
// This test checks if the navigation to same path or path with query appeneded,
517+
// keeps the scroll in the position from before navigation
518+
// but moves it when we navigate to a fragment
519+
SetUrlViaPushState("/");
520+
521+
var app = Browser.MountTestComponent<TestRouter>();
522+
var testSelector = Browser.WaitUntilTestSelectorReady();
523+
524+
app.FindElement(By.LinkText("Programmatic navigation cases")).Click();
525+
Browser.True(() => Browser.Url.EndsWith("/ProgrammaticNavigationCases", StringComparison.Ordinal));
526+
Browser.Contains("programmatic navigation", () => app.FindElement(By.Id("test-info")).Text);
527+
528+
var jsExecutor = (IJavaScriptExecutor)Browser;
529+
var maxScrollPosition = (long)jsExecutor.ExecuteScript("return document.documentElement.scrollHeight - window.innerHeight;");
530+
// scroll max up to find the position of fragment
531+
BrowserScrollY = 0;
532+
var fragmentScrollPosition = (long)jsExecutor.ExecuteScript("return document.getElementById('fragment').getBoundingClientRect().top + window.scrollY;");
533+
534+
// scroll maximally down
535+
BrowserScrollY = maxScrollPosition;
536+
537+
app.FindElement(By.Id("do-self-navigate")).Click();
538+
var scrollPosition = BrowserScrollY;
539+
Assert.True(scrollPosition == maxScrollPosition, "Expected to stay scrolled down.");
540+
541+
app.FindElement(By.Id("do-self-navigate-with-query")).Click();
542+
scrollPosition = BrowserScrollY;
543+
Assert.True(scrollPosition == maxScrollPosition, "Expected to stay scrolled down.");
544+
545+
app.FindElement(By.Id("do-self-navigate-to-fragment")).Click();
546+
scrollPosition = BrowserScrollY;
547+
Assert.True(scrollPosition == fragmentScrollPosition, "Expected to scroll to the fragment.");
548+
}
549+
513550
[Fact]
514551
public void CanNavigateProgrammaticallyValidateNoReplaceHistoryEntry()
515552
{

src/Components/test/testassets/BasicTestApp/RouterTest/ProgrammaticNavigationCases.razor

+59
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33

44
<div id="test-info">This page has test cases for programmatic navigation.</div>
55

6+
<style>
7+
.sticky-top {
8+
position: -webkit-sticky;
9+
position: sticky;
10+
top: 0;
11+
z-index: 1020;
12+
background-color: white;
13+
}
14+
</style>
15+
16+
<div class="sticky-top">
17+
<button id="do-self-navigate" @onclick="SelfNavigate">Navigate to Self</button>
18+
<button id="do-self-navigate-with-query" @onclick="SelfNavigateWithQuery">Navigate to Self With Query</button>
19+
<button id="do-self-navigate-to-fragment" @onclick="SelfNavigateWithFragment">Navigate to Self with Fragment</button>
20+
</div>
21+
622
<button id="do-other-navigation" @onclick="@(() => NavigationManager.NavigateTo("Other", new NavigationOptions()))">
723
Programmatic navigation (NavigationOptions overload)
824
</button>
@@ -34,3 +50,46 @@
3450
<button id="do-other-navigation-state-replacehistoryentry" @onclick="@(() => NavigationManager.NavigateTo("Other", new NavigationOptions { HistoryEntryState = "state", ReplaceHistoryEntry = true }))">
3551
Programmatic navigation (NavigationOptions overload) with replace and state
3652
</button>
53+
54+
<div>
55+
@foreach (var i in Enumerable.Range(0, 200))
56+
{
57+
<p>before fragment: @i</p>
58+
}
59+
60+
<div id="fragment">
61+
Middle section
62+
</div>
63+
64+
@foreach (var i in Enumerable.Range(0, 200))
65+
{
66+
<p>after fragment: @i</p>
67+
}
68+
</div>
69+
70+
@code
71+
{
72+
private void SelfNavigate()
73+
{
74+
NavigationManager.NavigateTo(NavigationManager.Uri.ToString(), false);
75+
}
76+
77+
private void SelfNavigateWithQuery()
78+
{
79+
var newUri = new UriBuilder(NavigationManager.Uri)
80+
{
81+
Query = $"random={Random.Shared.Next()}"
82+
};
83+
NavigationManager.NavigateTo(newUri.ToString(), false);
84+
}
85+
86+
private void SelfNavigateWithFragment()
87+
{
88+
var newUri = new UriBuilder(NavigationManager.Uri)
89+
{
90+
Fragment = "fragment"
91+
};
92+
NavigationManager.NavigateTo(newUri.ToString(), false);
93+
}
94+
95+
}

0 commit comments

Comments
 (0)