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?
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.