Skip to content

Commit adbedd2

Browse files
Render aria-invalid if the state is invalid (#23131)
* Render aria-invalid if the state is invalid Co-authored-by: Pranav K <[email protected]>
1 parent 8541bf6 commit adbedd2

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

src/Components/Web/src/Forms/InputBase.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Linq.Expressions;
78
using System.Threading.Tasks;
89

@@ -127,7 +128,7 @@ protected string CurrentValueAsString
127128
/// </summary>
128129
protected InputBase()
129130
{
130-
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
131+
_validationStateChangedHandler = OnValidateStateChanged;
131132
}
132133

133134
/// <summary>
@@ -216,10 +217,68 @@ public override Task SetParametersAsync(ParameterView parameters)
216217
$"{nameof(Forms.EditContext)} dynamically.");
217218
}
218219

220+
SetAdditionalAttributesIfValidationFailed();
221+
219222
// For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc.
220223
return base.SetParametersAsync(ParameterView.Empty);
221224
}
222225

226+
private void OnValidateStateChanged(object sender, ValidationStateChangedEventArgs eventArgs)
227+
{
228+
SetAdditionalAttributesIfValidationFailed();
229+
230+
StateHasChanged();
231+
}
232+
233+
private void SetAdditionalAttributesIfValidationFailed()
234+
{
235+
if (EditContext.GetValidationMessages(FieldIdentifier).Any())
236+
{
237+
if (AdditionalAttributes != null && AdditionalAttributes.ContainsKey("aria-invalid"))
238+
{
239+
// Do not overwrite the attribute value
240+
return;
241+
}
242+
243+
if (ConvertToDictionary(AdditionalAttributes, out var additionalAttributes))
244+
{
245+
AdditionalAttributes = additionalAttributes;
246+
}
247+
248+
// To make the `Input` components accessible by default
249+
// we will automatically render the `aria-invalid` attribute when the validation fails
250+
additionalAttributes["aria-invalid"] = true;
251+
}
252+
}
253+
254+
/// <summary>
255+
/// Returns a dictionary with the same values as the specified <paramref name="source"/>.
256+
/// </summary>
257+
/// <returns>true, if a new dictrionary with copied values was created. false - otherwise.</returns>
258+
private bool ConvertToDictionary(IReadOnlyDictionary<string, object> source, out Dictionary<string, object> result)
259+
{
260+
bool newDictionaryCreated = true;
261+
if (source == null)
262+
{
263+
result = new Dictionary<string, object>();
264+
}
265+
else if (source is Dictionary<string, object> currentDictionary)
266+
{
267+
result = currentDictionary;
268+
newDictionaryCreated = false;
269+
}
270+
else
271+
{
272+
result = new Dictionary<string, object>();
273+
foreach (var item in source)
274+
{
275+
result.Add(item.Key, item.Value);
276+
}
277+
}
278+
279+
return newDictionaryCreated;
280+
}
281+
223282
protected virtual void Dispose(bool disposing)
224283
{
225284
}

src/Components/Web/test/Forms/InputBaseTest.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ public async Task RespondsToValidationStateChangeNotifications()
362362
var inputComponentId = componentFrame1.ComponentId;
363363
var component = (TestInputComponent<string>)componentFrame1.Component;
364364
Assert.Equal("valid", component.CssClass);
365+
Assert.Null(component.AdditionalAttributes);
365366

366367
// Act: update the field state in the EditContext and notify
367368
var messageStore = new ValidationMessageStore(rootComponent.EditContext);
@@ -372,6 +373,8 @@ public async Task RespondsToValidationStateChangeNotifications()
372373
var batch2 = renderer.Batches.Skip(1).Single();
373374
Assert.Equal(inputComponentId, batch2.DiffsByComponentId.Keys.Single());
374375
Assert.Equal("invalid", component.CssClass);
376+
Assert.NotNull(component.AdditionalAttributes);
377+
Assert.True(component.AdditionalAttributes.ContainsKey("aria-invalid"));
375378
}
376379

377380
[Fact]
@@ -400,6 +403,73 @@ public async Task UnsubscribesFromValidationStateChangeNotifications()
400403
Assert.Empty(renderer.Batches.Skip(1));
401404
}
402405

406+
[Fact]
407+
public async Task AriaAttributeIsRenderedWhenTheValidationStateIsInvalidOnFirstRender()
408+
{
409+
// Arrange// Arrange
410+
var model = new TestModel();
411+
var invalidContext = new EditContext(model);
412+
413+
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
414+
{
415+
EditContext = invalidContext,
416+
ValueExpression = () => model.StringProperty
417+
};
418+
419+
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
420+
var messageStore = new ValidationMessageStore(invalidContext);
421+
messageStore.Add(fieldIdentifier, "Test error message");
422+
423+
var renderer = new TestRenderer();
424+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
425+
await renderer.RenderRootComponentAsync(rootComponentId);
426+
427+
428+
// Initally, it rendered one batch and is valid
429+
var batch1 = renderer.Batches.Single();
430+
var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
431+
var inputComponentId = componentFrame1.ComponentId;
432+
var component = (TestInputComponent<string>)componentFrame1.Component;
433+
Assert.Equal("invalid", component.CssClass);
434+
Assert.NotNull(component.AdditionalAttributes);
435+
Assert.Equal(1, component.AdditionalAttributes.Count);
436+
Assert.True((bool)component.AdditionalAttributes["aria-invalid"]);
437+
}
438+
439+
[Fact]
440+
public async Task UserSpecifiedAriaValueIsNotChangedIfInvalid()
441+
{
442+
// Arrange// Arrange
443+
var model = new TestModel();
444+
var invalidContext = new EditContext(model);
445+
446+
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
447+
{
448+
EditContext = invalidContext,
449+
ValueExpression = () => model.StringProperty
450+
};
451+
rootComponent.AdditionalAttributes = new Dictionary<string, object>();
452+
rootComponent.AdditionalAttributes["aria-invalid"] = "userSpecifiedValue";
453+
454+
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
455+
var messageStore = new ValidationMessageStore(invalidContext);
456+
messageStore.Add(fieldIdentifier, "Test error message");
457+
458+
var renderer = new TestRenderer();
459+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
460+
await renderer.RenderRootComponentAsync(rootComponentId);
461+
462+
// Initally, it rendered one batch and is valid
463+
var batch1 = renderer.Batches.Single();
464+
var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
465+
var inputComponentId = componentFrame1.ComponentId;
466+
var component = (TestInputComponent<string>)componentFrame1.Component;
467+
Assert.Equal("invalid", component.CssClass);
468+
Assert.NotNull(component.AdditionalAttributes);
469+
Assert.Equal(1, component.AdditionalAttributes.Count);
470+
Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
471+
}
472+
403473
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
404474
=> batch.ReferenceFrames
405475
.Where(f => f.FrameType == RenderTreeFrameType.Component)

src/Components/test/E2ETest/Tests/FormsTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,19 @@ public void InputTextInteractsWithEditContext()
9494
Browser.Equal("valid", () => nameInput.GetAttribute("class"));
9595
nameInput.SendKeys("Bert\t");
9696
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
97+
EnsureAttributeRendering(nameInput, "aria-invalid", false);
9798

9899
// Can become invalid
99100
nameInput.SendKeys("01234567890123456789\t");
100101
Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
102+
EnsureAttributeRendering(nameInput, "aria-invalid");
101103
Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
102104

103105
// Can become valid
104106
nameInput.Clear();
105107
nameInput.SendKeys("Bert\t");
106108
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
109+
EnsureAttributeRendering(nameInput, "aria-invalid", false);
107110
Browser.Empty(messagesAccessor);
108111
}
109112

@@ -491,5 +494,10 @@ private void ApplyInvalidInputDateValue(string cssSelector, string invalidValue)
491494
+ $"elem.value = {JsonSerializer.Serialize(invalidValue, TestJsonSerializerOptionsProvider.Options)};"
492495
+ "elem.dispatchEvent(new KeyboardEvent('change'));");
493496
}
497+
498+
private void EnsureAttributeRendering(IWebElement element, string attributeName, bool shouldBeRendered = true)
499+
{
500+
Browser.Equal(shouldBeRendered, () => element.GetAttribute(attributeName) != null);
501+
}
494502
}
495503
}

0 commit comments

Comments
 (0)