Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hash routing to named element #47320

Merged
merged 37 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
be36fee
implemented scrolling to location hash
surayya-MS Mar 20, 2023
65133e0
BlazorServerApp test hash
surayya-MS Mar 20, 2023
176153a
Merge branch 'main' into hashNav
surayya-MS Mar 20, 2023
91d7c04
Revert "BlazorServerApp test hash"
surayya-MS Mar 20, 2023
67c44c7
remove spaces
surayya-MS Mar 20, 2023
59fc588
change Router OnAfterRenderAsync
surayya-MS Mar 20, 2023
dbdbbd8
fix
surayya-MS Mar 20, 2023
36cf879
small fix
surayya-MS Mar 20, 2023
e67eebc
implemented IScrollToLocationHash; added e2e tests
surayya-MS Mar 27, 2023
8763e5b
deleted IScrollToLocationHash; implemented scrolling on the client side
surayya-MS Mar 30, 2023
82e3726
small fix
surayya-MS Mar 30, 2023
dab0f04
small fix
surayya-MS Mar 30, 2023
ea8c152
fix focusOnNavigate
surayya-MS Mar 31, 2023
0fdf009
change timer to 5 seconds
surayya-MS Mar 31, 2023
a52fc5b
Update src/Components/Web.JS/src/DomWrapper.ts
surayya-MS Mar 31, 2023
4ccbce4
Update src/Components/Web.JS/src/DomWrapper.ts
surayya-MS Mar 31, 2023
58b7a55
Update src/Components/Web.JS/src/Rendering/Renderer.ts
surayya-MS Mar 31, 2023
e8363c3
Revert "Update src/Components/Web.JS/src/Rendering/Renderer.ts"
surayya-MS Apr 5, 2023
5f595e8
Revert "Update src/Components/Web.JS/src/DomWrapper.ts"
surayya-MS Apr 5, 2023
184d6eb
Revert "Update src/Components/Web.JS/src/DomWrapper.ts"
surayya-MS Apr 5, 2023
51d3371
Revert "change timer to 5 seconds"
surayya-MS Apr 5, 2023
cf5298b
Revert "fix focusOnNavigate"
surayya-MS Apr 5, 2023
d5c741d
Revert "small fix"
surayya-MS Apr 5, 2023
85bc518
Revert "small fix"
surayya-MS Apr 5, 2023
eecefec
Revert "deleted IScrollToLocationHash; implemented scrolling on the c…
surayya-MS Apr 5, 2023
c3bc18c
back to Router design;
surayya-MS Apr 5, 2023
90efca2
Merge branch 'main' into hashNav
surayya-MS Apr 5, 2023
1dcd3aa
Add pathbase support to Photino WebView so we can run the router E2E …
SteveSandersonMS Apr 5, 2023
1a30e2b
Merge branch 'hashNav' of https://github.com/surayya-MS/aspnetcore in…
surayya-MS Apr 6, 2023
2d93d36
Add pathbase support to Photino WebView so we can run the router E2E …
SteveSandersonMS Apr 5, 2023
2786e53
implemented WebViewScrollToLocationHash
SteveSandersonMS Apr 5, 2023
20ae2da
Merge branch 'hashNav' of https://github.com/surayya-MS/aspnetcore in…
surayya-MS Apr 6, 2023
4bb685e
small fix
surayya-MS Apr 6, 2023
bdaf535
include _index in history.push()
surayya-MS Apr 6, 2023
697d8be
Update src/Components/Server/src/Circuits/RemoteScrollToLocationHash.cs
surayya-MS Apr 6, 2023
2617145
1. Use StringComparison.Ordinal when searching for '#'
surayya-MS Apr 6, 2023
2923ba6
Merge branch 'hashNav' of https://github.com/surayya-MS/aspnetcore in…
surayya-MS Apr 6, 2023
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
27 changes: 27 additions & 0 deletions src/Components/Web.JS/src/DomWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '@microsoft/dotnet-js-interop';
export const domFunctions = {
focus,
focusBySelector,
focusOnNavigate
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
};

function focus(element: HTMLOrSVGElement, preventScroll: boolean): void {
Expand All @@ -22,6 +23,12 @@ function focus(element: HTMLOrSVGElement, preventScroll: boolean): void {
}
}

function focusOnNavigate(selector: string): void {
if ( !(location.hash.length > 1 && elementExists(location.hash.slice(1))) ) {
focusBySelector(selector);
}
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}

function focusBySelector(selector: string): void {
const element = document.querySelector(selector) as HTMLElement;
if (element) {
Expand All @@ -35,3 +42,23 @@ function focusBySelector(selector: string): void {
element.focus();
}
}

function elementExists(identifier : string) : boolean {
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
let element : HTMLElement | null = null;

element = document.getElementById(identifier);

if (!element) {

let elements = document.getElementsByName(identifier);
if (elements.length > 0) {
element = elements[0];
}
}

if (element) {
return true;
}

return false;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}
43 changes: 42 additions & 1 deletion src/Components/Web.JS/src/Rendering/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface BrowserRendererRegistry {
}
const browserRenderers: BrowserRendererRegistry = {};
let shouldResetScrollAfterNextBatch = false;
let elementToScrollTo : string | null = null;
let scrollToElementTimeout : NodeJS.Timeout;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved

export function attachRootComponentToLogicalElement(browserRendererId: number, logicalElement: LogicalElement, componentId: number, appendContent: boolean): void {
let browserRenderer = browserRenderers[browserRendererId];
Expand Down Expand Up @@ -88,7 +90,46 @@ export function renderBatch(browserRendererId: number, batch: RenderBatch): void
browserRenderer.disposeEventHandler(eventHandlerId);
}

resetScrollIfNeeded();
if (elementToScrollTo && scrollToElement(elementToScrollTo)) {
elementToScrollTo = null;
clearTimeout(scrollToElementTimeout);

// We found the element on the page and we don't want to scroll to the top of the page on the next batch
shouldResetScrollAfterNextBatch = false;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
} else {
resetScrollIfNeeded();
}
}

export function scrollToElement(identifier : string) : boolean {
let element : HTMLElement | null = null;

element = document.getElementById(identifier);

if (!element) {
let elements = document.getElementsByName(identifier);
if (elements.length > 0) {
element = elements[0];
}
}
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved

if (element) {
element.scrollIntoView();
return true;
}

return false;
}

export function setTimeoutToScrollToElement(identifier : string) : void {
if (scrollToElementTimeout) {
clearTimeout(scrollToElementTimeout);
}
elementToScrollTo = identifier;

scrollToElementTimeout = setTimeout( () => {
elementToScrollTo = null;
}, 5000);
}

export function resetScrollAfterNextBatch(): void {
Expand Down
41 changes: 38 additions & 3 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

import '@microsoft/dotnet-js-interop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
import { resetScrollAfterNextBatch, scrollToElement, setTimeoutToScrollToElement } from '../Rendering/Renderer';
import { EventDelegator } from '../Rendering/Events/EventDelegator';

let hasEnabledNavigationInterception = false;
Expand Down Expand Up @@ -76,8 +76,18 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
const anchorTarget = findAnchorTarget(event);

if (anchorTarget && canProcessAnchor(anchorTarget)) {
const href = anchorTarget.getAttribute('href')!;
const absoluteHref = toAbsoluteUri(href);
let anchorHref = anchorTarget.getAttribute('href')!;
if (anchorHref.startsWith('#')) {
anchorHref = location.hash.length > 1 ? location.href.replace(location.hash, anchorHref) : location.href + anchorHref;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}

const absoluteHref = toAbsoluteUri(anchorHref);

if (isSamePageWithHash(absoluteHref)) {
event.preventDefault();
performScrollToElementOnTheSamePage(absoluteHref);
return;
}

if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
Expand All @@ -87,6 +97,24 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
});
}

function isSamePageWithHash(absoluteHref : string) : boolean {
const hashIndex = absoluteHref.indexOf('#');
// Excluding case hash="#"
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
return hashIndex > -1 && absoluteHref.length > hashIndex + 1 &&
location.href.replace(location.hash, '') === absoluteHref.substring(0, hashIndex);
}

function performScrollToElementOnTheSamePage(absoluteHref : string) {
const hashIndex = absoluteHref.indexOf('#');
var hash = absoluteHref.substring(hashIndex);

const urlInBrowser = location.hash.length > 1 ? location.href.replace(location.hash, hash) : location.href + hash;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
history.pushState({}, "", urlInBrowser);

var identifier = hash.slice(1);
scrollToElement(identifier);
}

// For back-compat, we need to accept multiple overloads
export function navigateTo(uri: string, options: NavigationOptions): void;
export function navigateTo(uri: string, forceLoad: boolean): void;
Expand Down Expand Up @@ -152,6 +180,13 @@ async function performInternalNavigation(absoluteInternalHref: string, intercept
// we render the new page. As a best approximation, wait until the next batch.
resetScrollAfterNextBatch();

const hashIndex = absoluteInternalHref.indexOf('#');
// Excluding cases when hash="#"
if ( hashIndex > -1 && absoluteInternalHref.length > hashIndex + 1) {
var identifier = absoluteInternalHref.substring(hashIndex+1);
setTimeoutToScrollToElement(identifier);
}
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved

if (!replace) {
currentHistoryIndex++;
history.pushState({
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web/src/DomWrapperInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ internal static class DomWrapperInterop

public const string Focus = Prefix + "focus";

public const string FocusBySelector = Prefix + "focusBySelector";
public const string FocusOnNavigate = Prefix + "focusOnNavigate";
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion src/Components/Web/src/Routing/FocusOnNavigate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (_focusAfterRender)
{
_focusAfterRender = false;
await JSRuntime.InvokeVoidAsync(DomWrapperInterop.FocusBySelector, Selector);
await JSRuntime.InvokeVoidAsync(DomWrapperInterop.FocusOnNavigate, Selector);
}
}

Expand Down
52 changes: 51 additions & 1 deletion src/Components/test/E2ETest/Tests/RoutingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1546,9 +1546,59 @@ public void CanNavigateBetweenPagesWithQueryStrings()
AssertHighlightedLinks("With query parameters (none)", "With query parameters (passing string value)");
}

[Fact]
public void AnchorWithHrefStartingWithHash_ScrollsToElementOnTheSamePage()
{
SetUrlViaPushState("/");
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Long page with hash")).Click();

app.FindElement(By.Id("anchor-test1")).Click();

var currentWindowScrollY = BrowserScrollY;
var test1VerticalLocation = app.FindElement(By.Id("test1")).Location.Y;
var currentRelativeUrl = _serverFixture.RootUri.MakeRelativeUri(new Uri(Browser.Url)).ToString();
Assert.Equal("subdir/LongPageWithHash#test1", currentRelativeUrl);
Assert.Equal(test1VerticalLocation, currentWindowScrollY);
}

[Fact]
public void AnchorWithHrefContainingHash_NavigatesToPageAndScrollsToElement()
{
SetUrlViaPushState("/");
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Long page with hash")).Click();

app.FindElement(By.Id("anchor-test2")).Click();

var currentWindowScrollY = BrowserScrollY;
var test2VerticalLocation = app.FindElement(By.Id("test2")).Location.Y;
var currentRelativeUrl = _serverFixture.RootUri.MakeRelativeUri(new Uri(Browser.Url)).ToString();
Assert.Equal("subdir/LongPageWithHash2#test2", currentRelativeUrl);
Assert.Equal(test2VerticalLocation, currentWindowScrollY);
}

[Fact]
public void AnchorWithHrefContainingHash_FocusOnNavigateDoesNotHappenAndScrollsToElement()
{
SetUrlViaPushState("/");
var app = Browser.MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Long page with hash")).Click();

app.FindElement(By.Id("anchor-test3")).Click();

var currentWindowScrollY = BrowserScrollY;
var test3VerticalLocation = app.FindElement(By.Id("test3")).Location.Y;
var focusOnNavigateSelectorVerticalLocation = app.FindElement(By.Id("test-info")).Location.Y;
var currentRelativeUrl = _serverFixture.RootUri.MakeRelativeUri(new Uri(Browser.Url)).ToString();
Assert.Equal("subdir/LongPageWithHash3#test3", currentRelativeUrl);
Assert.Equal(test3VerticalLocation, currentWindowScrollY);
Assert.NotEqual(focusOnNavigateSelectorVerticalLocation, currentWindowScrollY);
}

private long BrowserScrollY
{
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
get => Convert.ToInt64(((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY"), CultureInfo.CurrentCulture);
set => ((IJavaScriptExecutor)Browser).ExecuteScript($"window.scrollTo(0, {value})");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<li><a href="/subdir/images/blazor_logo_1000x.png" download>Download Me</a></li>
<li><NavLink>Null href never matches</NavLink></li>
<li><custom-link-with-shadow-root target-url="Other"></custom-link-with-shadow-root></li>
<li><NavLink href="/subdir/LongPageWithHash">Long page with hash</NavLink></li>
</ul>

<button id="do-navigation" @onclick=@(x => NavigationManager.NavigateTo("Other"))>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/LongPageWithHash"
@inject NavigationManager NavigationManager

<a id="anchor-test1" href="#test1">Go to test1 on this page</a>
<br/>
<a id="anchor-test2" href="/subdir/LongPageWithHash2#test2">Go to test2 on LongPageWithHash2 page</a>
<br />
<a id="anchor-test3" href="/subdir/LongPageWithHash3#test3">Go to test3 on LongPageWithHash3 page (contains focus on navigate selector)</a>

<div style="border: 2px dashed red; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>

<p id="test1">Test1</p>

<div style="border: 2px dashed red; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@page "/LongPageWithHash2"
@inject NavigationManager NavigationManager

<div style="border: 2px dashed blue; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>

<p id="test2">Test2</p>

<div style="border: 2px dashed blue; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/LongPageWithHash3"
@inject NavigationManager NavigationManager

<div style="border: 2px dashed green; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>

<p id="test3">Test3</p>

<div style="border: 2px dashed green; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>

<p id="test-info">Focus On Navigate Selector</p>

<div style="border: 2px dashed green; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@using Microsoft.AspNetCore.Components.Routing
@inject NavigationManager NavigationManager

<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="#test-info" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(RouterTestLayout)">
<div id="test-info">Oops, that component wasn't found!</div>
</LayoutView>
</NotFound>
</Router>


<a id="anchor-test1" href="#test1">Go to test1 on this page</a>
<br />
<a id="anchor-test2" href="/subdir/LongPageWithHash2#test2">Go to test2 on LongPageWithHash2 page</a>
<br />
<a id="anchor-test3" href="/subdir/LongPageWithHash3#test3">Go to test3 on LongPageWithHash3 page (contains focus on navigate selector)</a>

<div style="border: 2px dashed red; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>

<p id="test1">Test1</p>

<div style="border: 2px dashed red; margin: 1rem; padding: 1rem; height: 1500px;">
Scroll past me to find the links
</div>