Render aria-invalid if the state is invalid (#23131)

* Render aria-invalid if the state is invalid

Co-authored-by: Pranav K <prkrishn@hotmail.com>
This commit is contained in:
Artak 2020-06-21 21:43:29 -07:00 committed by GitHub
parent 8541bf6c98
commit adbedd2cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 138 additions and 1 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.Forms
/// </summary>
protected InputBase()
{
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
_validationStateChangedHandler = OnValidateStateChanged;
}
/// <summary>
@ -216,10 +217,68 @@ namespace Microsoft.AspNetCore.Components.Forms
$"{nameof(Forms.EditContext)} dynamically.");
}
SetAdditionalAttributesIfValidationFailed();
// For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc.
return base.SetParametersAsync(ParameterView.Empty);
}
private void OnValidateStateChanged(object sender, ValidationStateChangedEventArgs eventArgs)
{
SetAdditionalAttributesIfValidationFailed();
StateHasChanged();
}
private void SetAdditionalAttributesIfValidationFailed()
{
if (EditContext.GetValidationMessages(FieldIdentifier).Any())
{
if (AdditionalAttributes != null && AdditionalAttributes.ContainsKey("aria-invalid"))
{
// Do not overwrite the attribute value
return;
}
if (ConvertToDictionary(AdditionalAttributes, out var additionalAttributes))
{
AdditionalAttributes = additionalAttributes;
}
// To make the `Input` components accessible by default
// we will automatically render the `aria-invalid` attribute when the validation fails
additionalAttributes["aria-invalid"] = true;
}
}
/// <summary>
/// Returns a dictionary with the same values as the specified <paramref name="source"/>.
/// </summary>
/// <returns>true, if a new dictrionary with copied values was created. false - otherwise.</returns>
private bool ConvertToDictionary(IReadOnlyDictionary<string, object> source, out Dictionary<string, object> result)
{
bool newDictionaryCreated = true;
if (source == null)
{
result = new Dictionary<string, object>();
}
else if (source is Dictionary<string, object> currentDictionary)
{
result = currentDictionary;
newDictionaryCreated = false;
}
else
{
result = new Dictionary<string, object>();
foreach (var item in source)
{
result.Add(item.Key, item.Value);
}
}
return newDictionaryCreated;
}
protected virtual void Dispose(bool disposing)
{
}

View File

@ -362,6 +362,7 @@ namespace Microsoft.AspNetCore.Components.Forms
var inputComponentId = componentFrame1.ComponentId;
var component = (TestInputComponent<string>)componentFrame1.Component;
Assert.Equal("valid", component.CssClass);
Assert.Null(component.AdditionalAttributes);
// Act: update the field state in the EditContext and notify
var messageStore = new ValidationMessageStore(rootComponent.EditContext);
@ -372,6 +373,8 @@ namespace Microsoft.AspNetCore.Components.Forms
var batch2 = renderer.Batches.Skip(1).Single();
Assert.Equal(inputComponentId, batch2.DiffsByComponentId.Keys.Single());
Assert.Equal("invalid", component.CssClass);
Assert.NotNull(component.AdditionalAttributes);
Assert.True(component.AdditionalAttributes.ContainsKey("aria-invalid"));
}
[Fact]
@ -400,6 +403,73 @@ namespace Microsoft.AspNetCore.Components.Forms
Assert.Empty(renderer.Batches.Skip(1));
}
[Fact]
public async Task AriaAttributeIsRenderedWhenTheValidationStateIsInvalidOnFirstRender()
{
// Arrange// Arrange
var model = new TestModel();
var invalidContext = new EditContext(model);
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
{
EditContext = invalidContext,
ValueExpression = () => model.StringProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
var messageStore = new ValidationMessageStore(invalidContext);
messageStore.Add(fieldIdentifier, "Test error message");
var renderer = new TestRenderer();
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
await renderer.RenderRootComponentAsync(rootComponentId);
// Initally, it rendered one batch and is valid
var batch1 = renderer.Batches.Single();
var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
var inputComponentId = componentFrame1.ComponentId;
var component = (TestInputComponent<string>)componentFrame1.Component;
Assert.Equal("invalid", component.CssClass);
Assert.NotNull(component.AdditionalAttributes);
Assert.Equal(1, component.AdditionalAttributes.Count);
Assert.True((bool)component.AdditionalAttributes["aria-invalid"]);
}
[Fact]
public async Task UserSpecifiedAriaValueIsNotChangedIfInvalid()
{
// Arrange// Arrange
var model = new TestModel();
var invalidContext = new EditContext(model);
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
{
EditContext = invalidContext,
ValueExpression = () => model.StringProperty
};
rootComponent.AdditionalAttributes = new Dictionary<string, object>();
rootComponent.AdditionalAttributes["aria-invalid"] = "userSpecifiedValue";
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
var messageStore = new ValidationMessageStore(invalidContext);
messageStore.Add(fieldIdentifier, "Test error message");
var renderer = new TestRenderer();
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
await renderer.RenderRootComponentAsync(rootComponentId);
// Initally, it rendered one batch and is valid
var batch1 = renderer.Batches.Single();
var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
var inputComponentId = componentFrame1.ComponentId;
var component = (TestInputComponent<string>)componentFrame1.Component;
Assert.Equal("invalid", component.CssClass);
Assert.NotNull(component.AdditionalAttributes);
Assert.Equal(1, component.AdditionalAttributes.Count);
Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
}
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
=> batch.ReferenceFrames
.Where(f => f.FrameType == RenderTreeFrameType.Component)

View File

@ -94,16 +94,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal("valid", () => nameInput.GetAttribute("class"));
nameInput.SendKeys("Bert\t");
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
EnsureAttributeRendering(nameInput, "aria-invalid", false);
// Can become invalid
nameInput.SendKeys("01234567890123456789\t");
Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
EnsureAttributeRendering(nameInput, "aria-invalid");
Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
// Can become valid
nameInput.Clear();
nameInput.SendKeys("Bert\t");
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
EnsureAttributeRendering(nameInput, "aria-invalid", false);
Browser.Empty(messagesAccessor);
}
@ -491,5 +494,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
+ $"elem.value = {JsonSerializer.Serialize(invalidValue, TestJsonSerializerOptionsProvider.Options)};"
+ "elem.dispatchEvent(new KeyboardEvent('change'));");
}
private void EnsureAttributeRendering(IWebElement element, string attributeName, bool shouldBeRendered = true)
{
Browser.Equal(shouldBeRendered, () => element.GetAttribute(attributeName) != null);
}
}
}