How to allow screen readers to announce ASP.NET MVC 5 form validation messages when moving focus to the field in error?
Asked Answered
T

3

6

We have an ASP.NET MVC 5 web application. We do regular accessibility testing with JAWS in conjunction with Internet Explorer 11 (enterprise mandate) and Chrome. We keep running into problems with JAWS not reading off the validation messages associated with form fields when the user TABs into the field. We use FluentValidation and the standard HTML helpers to display form fields and validation messages (example below):

@Html.LabelFor(model => model.Email)
@Html.EditorFor(model => model.Email)
@Html.ValidationMessageFor(model => model.Email, null, new { role = "alert" })

A sample FluentValidation might query the database for the e-mail address in the form and show a message that "This e-mail has already been taken" which runs on the server side.

The resulting HTML sent back to the browser is:

<label for="Email">E-mail address:</label>

<input type="text" name="Email" id="Email" ...>

<span class="..." data-valmsg-for="Email" data-valmsg-replace="true" role="alert">
    This e-mail has already been taken
</span>

Nothing is associating the validation message with the form field. I always thought the MVC framework made this connection automatically, but apparently it doesn't.

According to WebAIM, we should utilize the aria-describedby attribute to associate form fields with inline validation errors, but to replumb the existing MVC5 framework to do that is quite the undertaking.

How can we get screen readers to announce inline validation messages when bringing focus to a form field generated by ASP.NET MVC5 without rewriting major HTML helpers?

Towery answered 14/8, 2018 at 14:51 Comment(0)
G
9

Without creating custom HtmlHelper extension methods to generate the aria-describedby attribute in the for control, and an associated id attribute in the error element, you will need to use javascript to add them.

Note that each error message placeholder (generated by @Html.ValidationMessageFor()) is associated with its form control by the data-valmsg-for="...." attribute.

Assuming you want to include the aria-describedby for all form controls with an associated error message (and within a <form> element), so that its available if client side errors are added via jquery.validate.js, then the script (jQuery) will be

$(function () {
    // Get the form controls
    var controls = $('form').find('input, textarea, select')
        .not(':hidden, :input[type=submit], :input[type=button], :input[type=reset]');
    $.each(controls, function (index, item) {
        // Get the name of the form control
        var name = $(this).attr('name'); 
        if (!name) {
            return true;
        }
        // Get the associated error element
        var errorElement = $('[data-valmsg-for="' + name + '"]');
        if (!errorElement) {
            return true;
        }
        // Generate an id attribute based on the name of the control
        var errorId = name + "-error"
        // Add attributes to the input and the error element
        $(this).attr('aria-describedby', errorId)
        errorElement.attr('id', errorId);
    });
});

If you not interested in client side validation errors, then you could just use var controls = $('.input-validation-error'); as the selector to get only form controls where a server side validation error has been added.

I would suggest including this script in an external (say) screenreadervalidation.js file and including it in your jquery or jqueryval bundle so that its included in all views that include forms for creating or editing data.

Gentle answered 22/9, 2018 at 8:30 Comment(6)
After playing around with things yesterday, this was basically the direction I started taking. I was actually building the aria-describedby on focus/focusin. I still need to test a JavaScript solution with JAWS.Towery
We have lots of form fields that are dynamically generated via JavaScript. While the basic algorithm is sound, the DOMContentLoaded event doesn't catch these new fields. And the jQuery validate plugin seems to run onblur sometimes too for certain fields. And I have JavaScript setting focus to new newly generated fields. Oh what a tangled web I've weaved...Towery
If you are dynamically adding new form controls, then you just need to add the relevant class and id attributes - but you did not note that in your question, and have not shown how you are adding them so impossible to include the code for that.Gentle
jQuery validation is 'lazy' and is triggered on .blur() and thereafter on '.keyup() (and I assume you are reparsing the $.validator when adding the new elements to the DOM? - refer Required field validations not working in JQuery Popup MVC 4)Gentle
While we are doing some funky things with adding elements dynamically, this answer put me on the right track. Well done, sir!Towery
The solution that worked for us was listening for the "blur" and "focus" events in the capturing phase, so jQuery didn't work because it only listens for events in the bubbling phase. But after responding to the event, the steps were exactly the same. Manually patch together the aria-describedby attribute with the validation messages.Towery
H
2

From a pure html perspective, the aria-describedby attribute is how you handle that.

<label for="Email">E-mail address:</label>
<input type="text" id="Email" aria-describedby="more_stuff">
<span id="more_stuff">
    This e-mail has already been taken
</span>

I'm not sure why that requires "replumbing" the framework or writing "major" helpers, but then I'm not an asp.net user. If the framework doesn't allow you to associate a description with the field, then the framework is deficient and a feature request should be submitted to the framework.

Hazel answered 14/8, 2018 at 20:12 Comment(0)
W
0

I use unobstrusive validation via the data-val-* attributes and create the spans that hold validation error messages so I can control placement. In doing so I can also assign them an ID easily and use aria-describedby. When an error occurs and the span is populated while the customer has focus on the input, then the reader finds the related span ID and reads the error that appears there. The key piece here is aria-describedby="NameOfCustomerError" matches id="NameOfCustomerError" where the error will appear (because it is the element with data-valmsg-for="NameOfCustomer" indicating it shows errors automatically for that input).

<input data-val="true"
   data-val-required="@($"{CustomerResource.Label_Name}: {SharedResource.Validation_General}")"
   data-val-minlength-min="4"
   data-val-minlength="@CustomerResource.Validation_NameOfCustomer"
   type="text" maxlength="100"
   name="NameOfCustomer" id="NameOfCustomer"
   class="form-control" aria-describedby="NameOfCustomerError"
   placeholder="@CustomerResource.Label_Name" />

<span role="alert" id="NameOfCustomerError" class="field-validation-valid text-danger" data-valmsg-for="NameOfCustomer" data-valmsg-replace="true"></span>
Wina answered 10/4, 2020 at 19:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.