| File: Routing\FocusOnNavigate.cs | Web Access |
| Project: src\aspnetcore\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web) |
// 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.Rendering; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Routing; /// <summary> /// After navigating from one page to another, sets focus to an element /// matching a CSS selector. This can be used to build an accessible /// navigation system compatible with screen readers. /// </summary> public class FocusOnNavigate : ComponentBase { private const string CustomElementName = "blazor-focus-on-navigate"; private Type? _lastNavigatedPageType = typeof(NonMatchingType); private bool _focusAfterRender; [Inject] private IJSRuntime JSRuntime { get; set; } = default!; /// <summary> /// Gets or sets the route data. This can be obtained from an enclosing /// <see cref="Router"/> component. /// </summary> [Parameter] public RouteData? RouteData { get; set; } // TODO: [EditorRequired] /// <summary> /// Gets or sets a CSS selector describing the element to be focused after /// navigation between pages. /// </summary> [Parameter] public string? Selector { get; set; } // TODO: [EditorRequired] /// <inheritdoc /> protected override void OnParametersSet() { if (RouteData is null) { throw new InvalidOperationException($"{nameof(FocusOnNavigate)} requires a non-null value for the parameter '{nameof(RouteData)}'."); } if (string.IsNullOrWhiteSpace(Selector)) { throw new InvalidOperationException($"{nameof(FocusOnNavigate)} requires a nonempty value for the parameter '{nameof(Selector)}'."); } // We focus whenever the page type changes, including to or from 'null' if (RouteData!.PageType != _lastNavigatedPageType) { _lastNavigatedPageType = RouteData!.PageType; _focusAfterRender = true; } } /// <inheritdoc/> protected override void BuildRenderTree(RenderTreeBuilder builder) { if (AssignedRenderMode is not null) { // When interactivity is enabled, functionality is handled via JS interop. // We don't need to render anything to the page in that case. // In non-interactive scenarios, a custom element is rendered so that // JS logic can find it and focus the element matching the specified // selector. return; } builder.OpenElement(0, CustomElementName); builder.AddAttribute(1, "selector", Selector); builder.CloseElement(); } /// <inheritdoc /> protected override async Task OnAfterRenderAsync(bool firstRender) { if (_focusAfterRender) { _focusAfterRender = false; await JSRuntime.InvokeVoidAsync(DomWrapperInterop.FocusBySelector, Selector); } } // On the first render, we always want to consider the page type changed, even if it's null. // So we need some other non-null type to compare with it. private sealed class NonMatchingType { } }