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:
parent
8541bf6c98
commit
adbedd2cfb
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue