From 3550fb2fae9464847ca9912a221bc7080b3836b8 Mon Sep 17 00:00:00 2001 From: Juan Barahona Date: Thu, 13 Aug 2020 21:32:59 -0400 Subject: [PATCH] Expose "InputElement" on qualified classes --- .../EventCallbackFactoryBinderExtensions.cs | 4 +- src/Components/Web/src/Forms/InputCheckbox.cs | 9 ++ src/Components/Web/src/Forms/InputDate.cs | 9 ++ src/Components/Web/src/Forms/InputFile.cs | 14 +++ src/Components/Web/src/Forms/InputNumber.cs | 9 ++ src/Components/Web/src/Forms/InputRadio.cs | 1 - src/Components/Web/src/Forms/InputSelect.cs | 11 ++- src/Components/Web/src/Forms/InputText.cs | 9 ++ src/Components/Web/src/Forms/InputTextArea.cs | 9 ++ .../Web/src/PublicAPI.Unshipped.txt | 14 +++ .../Web/test/Forms/InputDateTest.cs | 18 ++++ .../Web/test/Forms/InputNumberTest.cs | 18 ++++ .../Web/test/Forms/InputSelectTest.cs | 19 +++++ .../Web/test/Forms/InputTextAreaTest.cs | 34 ++++++++ .../Web/test/Forms/InputTextTest.cs | 34 ++++++++ .../test/E2ETest/Tests/InputActionsTest.cs | 51 +++++++++++ .../FormsTest/InputFocusComponent.razor | 85 +++++++++++++++++++ .../test/testassets/BasicTestApp/Index.razor | 1 + 18 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 src/Components/Web/test/Forms/InputTextAreaTest.cs create mode 100644 src/Components/Web/test/Forms/InputTextTest.cs create mode 100644 src/Components/test/E2ETest/Tests/InputActionsTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/InputFocusComponent.razor diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs index b38e9ae0a835..0455bd558b33 100644 --- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs +++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs @@ -521,7 +521,7 @@ private static EventCallback CreateBinderCore( { Action callback = e => { - T value = default; + T? value = default; var converted = false; try { @@ -565,7 +565,7 @@ private static EventCallback CreateBinderCore( { Action callback = e => { - T value = default; + T? value = default; var converted = false; try { diff --git a/src/Components/Web/src/Forms/InputCheckbox.cs b/src/Components/Web/src/Forms/InputCheckbox.cs index f1eb030a355f..db372e25eb4a 100644 --- a/src/Components/Web/src/Forms/InputCheckbox.cs +++ b/src/Components/Web/src/Forms/InputCheckbox.cs @@ -21,6 +21,14 @@ namespace Microsoft.AspNetCore.Components.Forms /// public class InputCheckbox : InputBase { + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -30,6 +38,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(3, "class", CssClass); builder.AddAttribute(4, "checked", BindConverter.FormatValue(CurrentValue)); builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValue = __value, CurrentValue)); + builder.AddElementReferenceCapture(6, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 8ab564977f0c..5d25930f41e3 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -22,6 +22,14 @@ public class InputDate : InputBase /// [Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a date."; + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -31,6 +39,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(3, "class", CssClass); builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString)); builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); + builder.AddElementReferenceCapture(6, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/Forms/InputFile.cs b/src/Components/Web/src/Forms/InputFile.cs index c0e3659638bf..92b3f5162af3 100644 --- a/src/Components/Web/src/Forms/InputFile.cs +++ b/src/Components/Web/src/Forms/InputFile.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -41,6 +42,19 @@ public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable [Parameter(CaptureUnmatchedValues = true)] public IDictionary? AdditionalAttributes { get; set; } + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] + public ElementReference? Element + { + get => _inputFileElement; + protected set => _inputFileElement = value!.Value; + } + /// protected override void OnInitialized() { diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs index ed7593583008..bce974bb5efd 100644 --- a/src/Components/Web/src/Forms/InputNumber.cs +++ b/src/Components/Web/src/Forms/InputNumber.cs @@ -41,6 +41,14 @@ static InputNumber() /// [Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a number."; + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -51,6 +59,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(4, "class", CssClass); builder.AddAttribute(5, "value", BindConverter.FormatValue(CurrentValueAsString)); builder.AddAttribute(6, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); + builder.AddElementReferenceCapture(7, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/Forms/InputRadio.cs b/src/Components/Web/src/Forms/InputRadio.cs index f9091866ae3a..4399cd2955a2 100644 --- a/src/Components/Web/src/Forms/InputRadio.cs +++ b/src/Components/Web/src/Forms/InputRadio.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Components.Rendering; diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs index 1a77ff83726e..04fbe3a04f06 100644 --- a/src/Components/Web/src/Forms/InputSelect.cs +++ b/src/Components/Web/src/Forms/InputSelect.cs @@ -16,6 +16,14 @@ public class InputSelect : InputBase /// [Parameter] public RenderFragment? ChildContent { get; set; } + /// + /// Gets or sets the select . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -24,7 +32,8 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(2, "class", CssClass); builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString)); builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); - builder.AddContent(5, ChildContent); + builder.AddElementReferenceCapture(5, __selectReference => Element = __selectReference); + builder.AddContent(6, ChildContent); builder.CloseElement(); } diff --git a/src/Components/Web/src/Forms/InputText.cs b/src/Components/Web/src/Forms/InputText.cs index 17561eb942f8..71bd3a304a73 100644 --- a/src/Components/Web/src/Forms/InputText.cs +++ b/src/Components/Web/src/Forms/InputText.cs @@ -20,6 +20,14 @@ namespace Microsoft.AspNetCore.Components.Forms /// public class InputText : InputBase { + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -28,6 +36,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(2, "class", CssClass); builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue)); builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); + builder.AddElementReferenceCapture(5, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/Forms/InputTextArea.cs b/src/Components/Web/src/Forms/InputTextArea.cs index 80958270322b..ce8e711c164f 100644 --- a/src/Components/Web/src/Forms/InputTextArea.cs +++ b/src/Components/Web/src/Forms/InputTextArea.cs @@ -20,6 +20,14 @@ namespace Microsoft.AspNetCore.Components.Forms /// public class InputTextArea : InputBase { + /// + /// Gets or sets the associated . + /// + /// May be if accessed before the component is rendered. + /// + /// + [DisallowNull] public ElementReference? Element { get; protected set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -28,6 +36,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(2, "class", CssClass); builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue)); builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); + builder.AddElementReferenceCapture(5, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..37fe1e1859c6 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.AspNetCore.Components.Forms.InputCheckbox.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputCheckbox.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputDate.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputDate.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputFile.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputFile.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputNumber.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputNumber.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputSelect.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputSelect.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputText.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputText.Element.set -> void +Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.get -> Microsoft.AspNetCore.Components.ElementReference? +Microsoft.AspNetCore.Components.Forms.InputTextArea.Element.set -> void diff --git a/src/Components/Web/test/Forms/InputDateTest.cs b/src/Components/Web/test/Forms/InputDateTest.cs index 9c6f87b8321b..8ad6319ef761 100644 --- a/src/Components/Web/test/Forms/InputDateTest.cs +++ b/src/Components/Web/test/Forms/InputDateTest.cs @@ -36,6 +36,24 @@ public async Task ValidationErrorUsesDisplayAttributeName() Assert.Contains("The Date property field must be a date.", validationMessages); } + [Fact] + public async Task InputElementIsAssignedSuccessfully() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.DateProperty, + }; + + // Act + var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert + Assert.NotNull(inputSelectComponent.Element); + } + private class TestModel { public DateTime DateProperty { get; set; } diff --git a/src/Components/Web/test/Forms/InputNumberTest.cs b/src/Components/Web/test/Forms/InputNumberTest.cs index 6916f0e06e03..2273b7271283 100644 --- a/src/Components/Web/test/Forms/InputNumberTest.cs +++ b/src/Components/Web/test/Forms/InputNumberTest.cs @@ -35,6 +35,24 @@ public async Task ValidationErrorUsesDisplayAttributeName() Assert.Contains("The Some number field must be a number.", validationMessages); } + [Fact] + public async Task InputElementIsAssignedSuccessfully() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.SomeNumber, + }; + + // Act + var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert + Assert.NotNull(inputSelectComponent.Element); + } + private class TestModel { public int SomeNumber { get; set; } diff --git a/src/Components/Web/test/Forms/InputSelectTest.cs b/src/Components/Web/test/Forms/InputSelectTest.cs index 8945867fd0da..d16bca952da6 100644 --- a/src/Components/Web/test/Forms/InputSelectTest.cs +++ b/src/Components/Web/test/Forms/InputSelectTest.cs @@ -194,6 +194,25 @@ public async Task ValidationErrorUsesDisplayAttributeName() Assert.Contains("The Some number field is not valid.", validationMessages); } + [Fact] + public async Task InputElementIsAssignedSuccessfully() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent> + { + EditContext = new EditContext(model), + ValueExpression = () => model.NotNullableInt, + }; + + // Act + var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + var exception = Record.Exception(() => inputSelectComponent.Element); + + // Assert + Assert.NotNull(inputSelectComponent.Element); + } + enum TestEnum { One, diff --git a/src/Components/Web/test/Forms/InputTextAreaTest.cs b/src/Components/Web/test/Forms/InputTextAreaTest.cs new file mode 100644 index 000000000000..d9eceb370e01 --- /dev/null +++ b/src/Components/Web/test/Forms/InputTextAreaTest.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Forms +{ + public class InputTextAreaTest + { + [Fact] + public async Task InputElementIsAssignedSuccessfully() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.StringProperty, + }; + + // Act + var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert + Assert.NotNull(inputSelectComponent.Element); + } + + private class TestModel + { + public string StringProperty { get; set; } + } + } +} diff --git a/src/Components/Web/test/Forms/InputTextTest.cs b/src/Components/Web/test/Forms/InputTextTest.cs new file mode 100644 index 000000000000..f2b3dba669e7 --- /dev/null +++ b/src/Components/Web/test/Forms/InputTextTest.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Forms +{ + public class InputTextTest + { + [Fact] + public async Task InputElementIsAssignedSuccessfully() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.StringProperty, + }; + + // Act + var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent); + + // Assert + Assert.NotNull(inputSelectComponent.Element); + } + + private class TestModel + { + public string StringProperty { get; set; } + } + } +} diff --git a/src/Components/test/E2ETest/Tests/InputActionsTest.cs b/src/Components/test/E2ETest/Tests/InputActionsTest.cs new file mode 100644 index 000000000000..fc4a67b8db21 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/InputActionsTest.cs @@ -0,0 +1,51 @@ +// 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 BasicTestApp; +using BasicTestApp.FormsTest; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class InputFocusTest : ServerTestBase> + { + public InputFocusTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + // On WebAssembly, page reloads are expensive so skip if possible + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); + } + + protected virtual IWebElement MountInputActionsComponent() + => Browser.MountTestComponent(); + + [Fact] + public void InputElementsGetFocusedSuccessfully() + { + var appElement = MountInputActionsComponent(); + Browser.Exists(By.ClassName("input-group")); + var inputGroups = appElement.FindElements(By.ClassName("input-group")); + + foreach (var group in inputGroups) + { + var expected = group.FindElement(By.ClassName("input-control")); + var button = group.FindElement(By.TagName("button")); + button.Click(); + + Browser.Equal(expected, () => Browser.SwitchTo().ActiveElement()); + } + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/InputFocusComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/InputFocusComponent.razor new file mode 100644 index 000000000000..26f41f0cd45c --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/InputFocusComponent.razor @@ -0,0 +1,85 @@ +@using Microsoft.AspNetCore.Components.Forms + + +

+ Name: + +

+ +

+ Age (years): + +

+ +

+ Description: + +

+ +

+ Renewal date: + +

+ +

+ Ticket class: + + + + + + + @person.TicketClass + + +

+ +

+ Identifying photo: + + +

+ +

+ Accepts terms: + +

+ + +
+ +@code { + InputText inputTextReference; + InputNumber inputNumberReference; + InputTextArea inputTextAreaReference; + InputDate inputDateReference; + InputSelect inputSelectReference; + InputCheckbox inputCheckboxReference; + InputFile inputFile; + + Person person = new Person(); + EditContext editContext; + + protected override void OnInitialized() + { + editContext = new EditContext(person); + } + + class Person + { + public string Name { get; set; } + + public int AgeInYears { get; set; } + + public DateTime RenewalDate { get; set; } = DateTime.Now; + + public bool AcceptsTerms { get; set; } + + public string Description { get; set; } + + public TicketClass TicketClass { get; set; } + } + + enum TicketClass { Economy, Premium, First } +} + diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index b1d97155db54..e58c7ffc757d 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -33,6 +33,7 @@ +