ElementReference FocusAsync API (#23316)

* Added working focus extension method.

* Added return value documentation for FocusAsync().

* Removed IJSRuntime argument from FocusAsync().

* Removed ElementReference.JSRuntime in favor of IServiceProvider.

* Updated Web.JS release binaries.

* Implemented ElementReferenceContext.

* Made ElementReferenceContext a non-abstract property in Renderer.

* Made ElementReference.Context explicitly nullable.

* Removed useless IServiceProvider dependency in RemoteJSRuntime.

* Updated Microsoft.AspNetCore.Components reference assemblies.

* Improved documentation and limited public API.
This commit is contained in:
Mackinnon Buck 2020-06-26 09:19:50 -07:00 committed by GitHub
parent 7c0f02a04d
commit 36c6c2c2a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 246 additions and 23 deletions

View File

@ -125,8 +125,14 @@ namespace Microsoft.AspNetCore.Components
private readonly object _dummy;
private readonly int _dummyPrimitive;
public ElementReference(string id) { throw null; }
public ElementReference(string id, Microsoft.AspNetCore.Components.ElementReferenceContext? context) { throw null; }
public Microsoft.AspNetCore.Components.ElementReferenceContext? Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string Id { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public abstract partial class ElementReferenceContext
{
protected ElementReferenceContext() { }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct EventCallback
{
@ -468,6 +474,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
protected internal Microsoft.AspNetCore.Components.ElementReferenceContext? ElementReferenceContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } }
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }

View File

@ -125,8 +125,14 @@ namespace Microsoft.AspNetCore.Components
private readonly object _dummy;
private readonly int _dummyPrimitive;
public ElementReference(string id) { throw null; }
public ElementReference(string id, Microsoft.AspNetCore.Components.ElementReferenceContext? context) { throw null; }
public Microsoft.AspNetCore.Components.ElementReferenceContext? Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string Id { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public abstract partial class ElementReferenceContext
{
protected ElementReferenceContext() { }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct EventCallback
{
@ -467,6 +473,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
protected internal Microsoft.AspNetCore.Components.ElementReferenceContext? ElementReferenceContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] protected set { } }
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }

View File

@ -23,13 +23,32 @@ namespace Microsoft.AspNetCore.Components
/// </remarks>
public string Id { get; }
public ElementReference(string id)
/// <summary>
/// Gets the <see cref="ElementReferenceContext"/> instance.
/// </summary>
public ElementReferenceContext? Context { get; }
/// <summary>
/// Instantiates a new <see cref="ElementReference" />.
/// </summary>
/// <param name="id">A unique identifier for this <see cref="ElementReference"/>.</param>
/// <param name="context">The nullable <see cref="ElementReferenceContext"/> instance.</param>
public ElementReference(string id, ElementReferenceContext? context)
{
Id = id;
Context = context;
}
internal static ElementReference CreateWithUniqueId()
=> new ElementReference(CreateUniqueId());
/// <summary>
/// Instantiates a new <see cref="ElementReference"/>.
/// </summary>
/// <param name="id">A unique identifier for this <see cref="ElementReference"/>.</param>
public ElementReference(string id) : this(id, null)
{
}
internal static ElementReference CreateWithUniqueId(ElementReferenceContext? context)
=> new ElementReference(CreateUniqueId(), context);
private static string CreateUniqueId()
{

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Context for an <see cref="ElementReference"/>.
/// </summary>
public abstract class ElementReferenceContext
{
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>

View File

@ -917,7 +917,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
private static void InitializeNewElementReferenceCaptureFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
{
var newElementReference = ElementReference.CreateWithUniqueId();
var newElementReference = ElementReference.CreateWithUniqueId(diffContext.Renderer.ElementReferenceContext);
newFrame = newFrame.WithElementReferenceCaptureId(newElementReference.Id);
newFrame.ElementReferenceCaptureAction(newElementReference);
}

View File

@ -76,6 +76,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// </summary>
public abstract Dispatcher Dispatcher { get; }
/// <summary>
/// Gets or sets the <see cref="Components.ElementReferenceContext"/> associated with this <see cref="Renderer"/>,
/// if it exists.
/// </summary>
protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; }
/// <summary>
/// Constructs a new component of the specified type.
/// </summary>

View File

@ -64,7 +64,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_loggerFactory,
_options,
client,
_loggerFactory.CreateLogger<RemoteRenderer>());
_loggerFactory.CreateLogger<RemoteRenderer>(),
jsRuntime.ElementReferenceContext);
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)

View File

@ -17,12 +17,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private readonly ILogger<RemoteJSRuntime> _logger;
private CircuitClientProxy _clientProxy;
public ElementReferenceContext ElementReferenceContext { get; }
public RemoteJSRuntime(IOptions<CircuitOptions> options, ILogger<RemoteJSRuntime> logger)
{
_options = options.Value;
_logger = logger;
DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout;
JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter());
ElementReferenceContext = new WebElementReferenceContext(this);
JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext));
}
internal void Initialize(CircuitClientProxy clientProxy)

View File

@ -37,12 +37,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
ILoggerFactory loggerFactory,
CircuitOptions options,
CircuitClientProxy client,
ILogger logger)
ILogger logger,
ElementReferenceContext? elementReferenceContext)
: base(serviceProvider, loggerFactory)
{
_client = client;
_options = options;
_logger = logger;
ElementReferenceContext = elementReferenceContext;
}
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();

View File

@ -258,7 +258,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, IClientProxy client)
: base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), new CircuitClientProxy(client, "connection"), NullLogger.Instance)
: base(
serviceProvider,
NullLoggerFactory.Instance,
new CircuitOptions(),
new CircuitClientProxy(client, "connection"),
NullLogger.Instance,
null)
{
}

View File

@ -437,7 +437,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, CircuitClientProxy client, ILogger logger)
: base(serviceProvider, loggerFactory, options, client, logger)
: base(serviceProvider, loggerFactory, options, client, logger, null)
{
}

View File

@ -38,7 +38,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
NullLoggerFactory.Instance,
new CircuitOptions(),
clientProxy,
NullLogger.Instance);
NullLogger.Instance,
null);
}
handlers = handlers ?? Array.Empty<CircuitHandler>();

View File

@ -1,22 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class ElementReferenceJsonConverterTest
{
private readonly ElementReferenceJsonConverter Converter = new ElementReferenceJsonConverter();
private readonly ElementReferenceContext ElementReferenceContext;
private readonly ElementReferenceJsonConverter Converter;
public ElementReferenceJsonConverterTest()
{
ElementReferenceContext = Mock.Of<ElementReferenceContext>();
Converter = new ElementReferenceJsonConverter(ElementReferenceContext);
}
[Fact]
public void Serializing_Works()
{
// Arrange
var elementReference = ElementReference.CreateWithUniqueId();
var elementReference = ElementReference.CreateWithUniqueId(ElementReferenceContext);
var expected = $"{{\"__internalId\":\"{elementReference.Id}\"}}";
var memoryStream = new MemoryStream();
var writer = new Utf8JsonWriter(memoryStream);
@ -34,7 +43,7 @@ namespace Microsoft.AspNetCore.Components
public void Deserializing_Works()
{
// Arrange
var id = ElementReference.CreateWithUniqueId().Id;
var id = ElementReference.CreateWithUniqueId(ElementReferenceContext).Id;
var json = $"{{\"__internalId\":\"{id}\"}}";
var bytes = Encoding.UTF8.GetBytes(json);
var reader = new Utf8JsonReader(bytes);
@ -51,7 +60,7 @@ namespace Microsoft.AspNetCore.Components
public void Deserializing_WithFormatting_Works()
{
// Arrange
var id = ElementReference.CreateWithUniqueId().Id;
var id = ElementReference.CreateWithUniqueId(ElementReferenceContext).Id;
var json =
@$"{{
""__internalId"": ""{id}""

View File

@ -11,6 +11,13 @@ namespace Microsoft.AspNetCore.Components
{
private static readonly JsonEncodedText IdProperty = JsonEncodedText.Encode("__internalId");
private readonly ElementReferenceContext _elementReferenceContext;
public ElementReferenceJsonConverter(ElementReferenceContext elementReferenceContext)
{
_elementReferenceContext = elementReferenceContext;
}
public override ElementReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string id = null;
@ -39,7 +46,7 @@ namespace Microsoft.AspNetCore.Components
throw new JsonException("__internalId is required.");
}
return new ElementReference(id);
return new ElementReference(id, _elementReferenceContext);
}
public override void Write(Utf8JsonWriter writer, ElementReference value, JsonSerializerOptions options)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
import '@microsoft/dotnet-js-interop';
export const domFunctions = {
focus,
};
function focus(element: HTMLElement): void {
if (element instanceof HTMLElement) {
element.focus();
} else {
throw new Error('Unable to focus an invalid element.');
}
}

View File

@ -1,5 +1,6 @@
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
import { attachRootComponentToElement } from './Rendering/Renderer';
import { domFunctions } from './DomWrapper';
// Make the following APIs available in global scope for invocation from JS
window['Blazor'] = {
@ -8,5 +9,6 @@ window['Blazor'] = {
_internal: {
attachRootComponentToElement,
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
},
};

View File

@ -14,6 +14,14 @@ namespace Microsoft.AspNetCore.Components
public string? Type { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string? ValueAttribute { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public static partial class ElementReferenceExtensions
{
public static System.Threading.Tasks.ValueTask FocusAsync(this Microsoft.AspNetCore.Components.ElementReference elementReference) { throw null; }
}
public partial class WebElementReferenceContext : Microsoft.AspNetCore.Components.ElementReferenceContext
{
public WebElementReferenceContext(Microsoft.JSInterop.IJSRuntime jsRuntime) { }
}
}
namespace Microsoft.AspNetCore.Components.Forms
{

View File

@ -14,6 +14,14 @@ namespace Microsoft.AspNetCore.Components
public string? Type { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public string? ValueAttribute { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
public static partial class ElementReferenceExtensions
{
public static System.Threading.Tasks.ValueTask FocusAsync(this Microsoft.AspNetCore.Components.ElementReference elementReference) { throw null; }
}
public partial class WebElementReferenceContext : Microsoft.AspNetCore.Components.ElementReferenceContext
{
public WebElementReferenceContext(Microsoft.JSInterop.IJSRuntime jsRuntime) { }
}
}
namespace Microsoft.AspNetCore.Components.Forms
{

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components
{
internal static class DomWrapperInterop
{
private const string Prefix = "Blazor._internal.domWrapper.";
public const string Focus = Prefix + "focus";
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components
{
public static class ElementReferenceExtensions
{
/// <summary>
/// Gives focus to an element given its <see cref="ElementReference"/>.
/// </summary>
/// <param name="elementReference">A reference to the element to focus.</param>
/// <returns>The <see cref="ValueTask"/> representing the asynchronous focus operation.</returns>
public static ValueTask FocusAsync(this ElementReference elementReference)
{
var jsRuntime = elementReference.GetJSRuntime();
if (jsRuntime == null)
{
throw new InvalidOperationException("No JavaScript runtime found.");
}
return jsRuntime.InvokeVoidAsync(DomWrapperInterop.Focus, elementReference);
}
internal static IJSRuntime GetJSRuntime(this ElementReference elementReference)
{
if (!(elementReference.Context is WebElementReferenceContext context))
{
throw new InvalidOperationException("ElementReference has not been configured correctly.");
}
return context.JSRuntime;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components
{
public class WebElementReferenceContext : ElementReferenceContext
{
internal IJSRuntime JSRuntime { get; }
public WebElementReferenceContext(IJSRuntime jsRuntime)
{
JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
}
}
}

View File

@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
var parameters = ParameterView.FromDictionary(new Dictionary<string, object>
{
[_action] = RemoteAuthenticationActions.LogInCallback,
[_onLogInSucceded] = new EventCallbackFactory().Create< RemoteAuthenticationState>(
[_onLogInSucceded] = new EventCallbackFactory().Create<RemoteAuthenticationState>(
remoteAuthenticator,
(state) => loggingSucceededCalled = true),
});

View File

@ -34,6 +34,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
// The WebAssembly renderer registers and unregisters itself with the static registry
_webAssemblyRendererId = RendererRegistry.Add(this);
_logger = loggerFactory.CreateLogger<WebAssemblyRenderer>();
ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext;
}
public override Dispatcher Dispatcher => NullDispatcher.Instance;

View File

@ -10,9 +10,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
{
internal static readonly DefaultWebAssemblyJSRuntime Instance = new DefaultWebAssemblyJSRuntime();
public ElementReferenceContext ElementReferenceContext { get; }
private DefaultWebAssemblyJSRuntime()
{
JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter());
ElementReferenceContext = new WebElementReferenceContext(this);
JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext));
}
#pragma warning disable IDE0051 // Remove unused private members. Invoked via Mono's JS interop mechanism (invoke_method)

View File

@ -400,6 +400,26 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("Clicks: 2", () => inputElement.GetAttribute("value"));
}
[Fact]
public void CanUseFocusExtensionToFocusElement()
{
var appElement = Browser.MountTestComponent<ElementFocusComponent>();
var buttonElement = appElement.FindElement(By.Id("focus-button"));
// Make sure the input element isn't focused when the test begins; we don't want
// the test to pass just because the input started as the focused element
Browser.NotEqual("focus-input", getFocusedElementId);
// Click the button whose callback focuses the input element
buttonElement.Click();
// Verify that the input element is focused
Browser.Equal("focus-input", getFocusedElementId);
// A local helper that gets the ID of the focused element.
string getFocusedElementId() => Browser.SwitchTo().ActiveElement().GetAttribute("id");
}
[Fact]
public void CanCaptureReferencesToDynamicallyAddedElements()
{

View File

@ -0,0 +1,13 @@
@using Microsoft.JSInterop
<button id="focus-button" @onclick="FocusInput">Click to focus!</button>
<input id="focus-input" @ref="inputReference" />
@code {
private ElementReference inputReference;
private async Task FocusInput()
{
await inputReference.FocusAsync();
}
}

View File

@ -20,6 +20,7 @@
<option value="BasicTestApp.DataDashComponent">data-* attribute rendering</option>
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
<option value="BasicTestApp.ElementFocusComponent">Element focus component</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ErrorComponent">Error throwing</option>
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>

View File

@ -23,6 +23,9 @@ namespace Microsoft.AspNetCore.E2ETesting
public static void Equal<T>(this IWebDriver driver, T expected, Func<T> actual)
=> WaitAssertCore(driver, () => Assert.Equal(expected, actual()));
public static void NotEqual<T>(this IWebDriver driver, T expected, Func<T> actual)
=> WaitAssertCore(driver, () => Assert.NotEqual(expected, actual()));
public static void True(this IWebDriver driver, Func<bool> actual)
=> WaitAssertCore(driver, () => Assert.True(actual()));