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 9 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
2 changes: 2 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash.ScrollToLocationHash(string! locationAbsolute) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Sections.SectionContent
Microsoft.AspNetCore.Components.Sections.SectionContent.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
Microsoft.AspNetCore.Components.Sections.SectionContent.ChildContent.set -> void
Expand Down
17 changes: 17 additions & 0 deletions src/Components/Components/src/Routing/IScrollToLocationHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Routing;

/// <summary>
/// Contract to setup scroll to location hash.
/// </summary>
public interface IScrollToLocationHash
{
/// <summary>
/// Scrolls to location hash on the client.
/// </summary>
/// <param name="locationAbsolute">Absolute URL of location</param>
/// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
Task ScrollToLocationHash(string locationAbsolute);
}
11 changes: 9 additions & 2 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary

[Inject] private INavigationInterception NavigationInterception { get; set; }

[Inject] private IScrollToLocationHash ScrollToLocationHash { get; set; }

[Inject] private ILoggerFactory LoggerFactory { get; set; }

/// <summary>
Expand Down Expand Up @@ -277,14 +279,19 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args)
}

Task IHandleAfterRender.OnAfterRenderAsync()
{
return EnableNavigationInterceptionThenScrollToLocationHash();
}

private async Task EnableNavigationInterceptionThenScrollToLocationHash()
{
if (!_navigationInterceptionEnabled)
{
_navigationInterceptionEnabled = true;
return NavigationInterception.EnableNavigationInterceptionAsync();
await NavigationInterception.EnableNavigationInterceptionAsync();
}

return Task.CompletedTask;
await ScrollToLocationHash.ScrollToLocationHash(_locationAbsolute);
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}

private static partial class Log
Expand Down
11 changes: 11 additions & 0 deletions src/Components/Components/test/Routing/RouterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public RouterTest()
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddSingleton<NavigationManager>(_navigationManager);
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
var serviceProvider = services.BuildServiceProvider();

_renderer = new TestRenderer(serviceProvider);
Expand Down Expand Up @@ -220,6 +221,16 @@ public Task EnableNavigationInterceptionAsync()
}
}

internal sealed class TestScrollToLocationHash : IScrollToLocationHash
{
public static readonly TestScrollToLocationHash Instance = new TestScrollToLocationHash();
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved

public Task ScrollToLocationHash(string locationAbsolute)
{
return Task.CompletedTask;
}
}

[Route("feb")]
public class FebComponent : ComponentBase { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
services.TryAddScoped<IScrollToLocationHash, UnsupportedScrollToLocationHash>();
services.TryAddScoped<ComponentStatePersistenceManager>();
services.TryAddScoped<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
services.TryAddScoped<IErrorBoundaryLogger, PrerenderingErrorBoundaryLogger>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Routing;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class UnsupportedScrollToLocationHash : IScrollToLocationHash
{
public Task ScrollToLocationHash(string locationAbsolute)
{
throw new InvalidOperationException("Scroll to location hash calls cannot be issued during server-side static rendering, because the page has not yet loaded in the browser.");
}
}
2 changes: 2 additions & 0 deletions src/Components/Server/src/Circuits/CircuitFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(

var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
var scrollToLocationHash = (RemoteScrollToLocationHash)scope.ServiceProvider.GetRequiredService<IScrollToLocationHash>();
if (client.Connected)
{
navigationManager.AttachJsRuntime(jsRuntime);
navigationManager.Initialize(baseUri, uri);

navigationInterception.AttachJSRuntime(jsRuntime);
scrollToLocationHash.AttachJSRuntime(jsRuntime);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Routing;
using Microsoft.JSInterop;
using Interop = Microsoft.AspNetCore.Components.Web.BrowserNavigationManagerInterop;

namespace Microsoft.AspNetCore.Components.Server.Circuits;

internal sealed class RemoteScrollToLocationHash : IScrollToLocationHash
{
private IJSRuntime _jsRuntime;

public void AttachJSRuntime(IJSRuntime jsRuntime)
{
if (HasAttachedJSRuntime)
{
throw new InvalidOperationException("JSRuntime has already been initialized.");
}

_jsRuntime = jsRuntime;
}

public bool HasAttachedJSRuntime => _jsRuntime != null;

public async Task ScrollToLocationHash(string locationAbsolute)
{
if (!HasAttachedJSRuntime)
{
// We should generally never get here in the ordinary case. Router will only call this API once pre-rendering is complete.
// This would guard any unusual usage of this API.
throw new InvalidOperationException("Navigation commands can not be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering or while the client is disconnected.");
}

var hashIndex = locationAbsolute.IndexOf("#", StringComparison.CurrentCultureIgnoreCase);
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved

if (hashIndex > -1 && locationAbsolute.Length > hashIndex + 1)
{
var elementId = locationAbsolute[(hashIndex + 1)..];

await _jsRuntime.InvokeVoidAsync(Interop.ScrollToElement, elementId).AsTask();
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
services.AddScoped<NavigationManager, RemoteNavigationManager>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IScrollToLocationHash, RemoteScrollToLocationHash>();
services.TryAddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();

services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ internal static class BrowserNavigationManagerInterop
public const string NavigateTo = Prefix + "navigateTo";

public const string SetHasLocationChangingListeners = Prefix + "setHasLocationChangingListeners";

public const string ScrollToElement = Prefix + "scrollToElement";
}
10 changes: 10 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,15 @@ function focus(element: HTMLOrSVGElement, preventScroll: boolean): void {
}
}

function focusOnNavigate(selector: string): void {
let hash = location.hash;
let element : HTMLElement | null;
element = hash.length > 1 ? document.getElementById(hash.slice(1)) : null;
if (!element) {
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 Down
23 changes: 21 additions & 2 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const internalFunctions = {
navigateTo: navigateToFromDotNet,
getBaseURI: (): string => document.baseURI,
getLocationHref: (): string => location.href,
scrollToElement
};

function listenForNavigationEvents(
Expand All @@ -53,6 +54,14 @@ function setHasLocationChangingListeners(hasListeners: boolean) {
hasLocationChangingEventListeners = hasListeners;
}

function scrollToElement(id : string) : void {
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
var element = document.getElementById(id);
if (element)
{
element.scrollIntoView();
}
}

export function attachToEventDelegator(eventDelegator: EventDelegator): void {
// We need to respond to clicks on <a> elements *after* the EventDelegator has finished
// running its simulated bubbling process so that we can respect any preventDefault requests.
Expand All @@ -76,8 +85,18 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
const anchorTarget = findAnchorTarget(event);

if (anchorTarget && canProcessAnchor(anchorTarget)) {
const href = anchorTarget.getAttribute('href')!;
const absoluteHref = toAbsoluteUri(href);
const anchorHref = anchorTarget.getAttribute('href')!;

if (anchorHref.startsWith('#') && anchorHref.length > 1) {
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
event.preventDefault();
const hash = location.hash;
const urlInBrowser = hash.length > 1 ? location.href.replace(hash, anchorHref) : location.href + anchorHref;
window.history.pushState({}, "", urlInBrowser);
scrollToElement(anchorHref.slice(1));
return;
}

const absoluteHref = toAbsoluteUri(anchorHref);

if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
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
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ internal void InitializeDefaultServices()
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<IScrollToLocationHash>(WebAssemblyScrollToLocationHash.Instance);
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
Services.AddSingleton<ComponentStatePersistenceManager>();
Services.AddSingleton<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal interface IInternalJSImportMethods

void NavigationManager_EnableNavigationInterception();

void NavigationManager_ScrollToElement(string id);

string NavigationManager_GetLocationHref();

string NavigationManager_GetBaseUri();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public string GetApplicationEnvironment()
public void NavigationManager_EnableNavigationInterception()
=> NavigationManager_EnableNavigationInterceptionCore();

public void NavigationManager_ScrollToElement(string id)
=> NavigationManager_ScrollToElementCore(id);

public string NavigationManager_GetLocationHref()
=> NavigationManager_GetLocationHrefCore();

Expand Down Expand Up @@ -64,6 +67,9 @@ public string RegisteredComponents_GetParameterValues(int id)
[JSImport(BrowserNavigationManagerInterop.EnableNavigationInterception, "blazor-internal")]
private static partial void NavigationManager_EnableNavigationInterceptionCore();

[JSImport(BrowserNavigationManagerInterop.ScrollToElement, "blazor-internal")]
private static partial void NavigationManager_ScrollToElementCore(string id);

[JSImport(BrowserNavigationManagerInterop.GetLocationHref, "blazor-internal")]
private static partial string NavigationManager_GetLocationHrefCore();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Routing;

namespace Microsoft.AspNetCore.Components.WebAssembly.Services;

internal sealed class WebAssemblyScrollToLocationHash : IScrollToLocationHash
{
public static readonly WebAssemblyScrollToLocationHash Instance = new WebAssemblyScrollToLocationHash();

public Task ScrollToLocationHash(string locationAbsolute)
{
var hashIndex = locationAbsolute.IndexOf("#", StringComparison.CurrentCultureIgnoreCase);

if (hashIndex > -1 && locationAbsolute.Length > hashIndex + 1)
{
var elementId = locationAbsolute[(hashIndex + 1)..];

InternalJSImportMethods.Instance.NavigationManager_ScrollToElement(elementId);
}

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public string GetPersistedState()

public void NavigationManager_EnableNavigationInterception() { }

public void NavigationManager_ScrollToElement(string id) { }

public string NavigationManager_GetBaseUri()
=> "https://www.example.com/awesome-part-that-will-be-truncated-in-tests";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic
services.AddLogging();
services.TryAddScoped<IJSRuntime, WebViewJSRuntime>();
services.TryAddScoped<INavigationInterception, WebViewNavigationInterception>();
services.TryAddScoped<IScrollToLocationHash, WebViewScrollToLocationHash>();
services.TryAddScoped<NavigationManager, WebViewNavigationManager>();
services.TryAddScoped<IErrorBoundaryLogger, WebViewErrorBoundaryLogger>();
return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Routing;

namespace Microsoft.AspNetCore.Components.WebView.Services;

internal sealed class WebViewScrollToLocationHash : IScrollToLocationHash
{
public Task ScrollToLocationHash(string locationAbsolute) => Task.CompletedTask;
}
Loading