6
votes

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?

3

3 Answers

8
votes

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.

2
votes

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.

0
votes

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>