Problem/Motivation

I would like the field errors to have the same structure as the description of the field - unique id and his id added to his corrsponding input `aria-describedby` attribut (look screenshot).

Proposed resolution

Add this attribute as it is added to the Webform description label.

Issue fork drupal-3083103

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Lus created an issue. See original summary.

mgifford’s picture

Can you provide a sample of code with this and some instructions to repeat this?

We'll also want to know it's a core issue and not a webform issue.

Thanks!

lus’s picture

Issue summary: View changes
StatusFileSize
new113.17 KB
lus’s picture

Issue summary: View changes
andrewmacpherson’s picture

Thanks for filing this @Lus. Adding IDs to error messages is a good idea. It already occurred to me that we can programmatically associate error messages to their inputs. I've been meaning to file a feature request for it myself but never got around to it. Lets flesh this out into a plan.

There are 3 ways of doing this:

  1. Use aria-describedby. This has wide support, but isn't always treated as important by assistive tech (depends on user settings in some cases). Since inputs might have an associated error message AND some help text, it's probably a good idea to get the IDs in order so the error message is announced first. That's because when aria-describedby has multiple IDREFs, they are concatenated into a single string. Putting the error message first should reduce the likelihood of it being skipped.
  2. Use aria-errormessage, introduced by the ARIA 1.1 recommendation. This doesn't have as wide support yet, but has the advantage of better semantics. So assistive tech (in theory) can treat error messages as things to be announced even if users don't want to hear descriptions.
  3. A belt-and-braces approach using both aria-describedby and aria-errormessage. The first points at the error text and the description, and the second just points at the error. I've read encouraging things about this approach as a short-term approach. It works very well, at the cost of some extra verbosity in browser/screenreader combinations which support both properties. Later when support for aria-errormessage is more widespread, we can change to keeping the description and error separated.

Bonus points if we can make this work regardless of whether inline form errors module is enables. Programmatic associations like this don't depend on DOM order. It will be great if screen reader users can get the error message even though it's far away at the top of the page.

andrewmacpherson’s picture

Title: Missing attribute - accessibility. » Programmatically associate error messages with inputs
Version: 8.6.x-dev » 8.8.x-dev
Component: inline_form_errors.module » forms system
Issue tags: -webform

Updating the vague title.

Also, this probably isn't specific to webform or inline form errors module. It'll probably be best to make it work at lower level in Drupal's Form API.

lus’s picture

StatusFileSize
new40.92 KB

Well, I meant in particular the inline form errors, since the drupal Error messages (at the top) are in fact a list of errors in a form of anchor links. So we would add an ID for every <li> ?

Only local images are allowed.

I have managed to add the ID to the aria-describedby but I'm stuck with the inline form error, since the module adds a container (with a class) and a <strong> around the error in the template and not in the module variable.
(Screenshot with inputs aria-describedby pointing on the inline form error added by me in the FormValidator)

template screenshot

So I wanted to make it this way:

/**
 * Populates form errors in the template.
 */
function _inline_form_errors_set_errors(&$variables) {
  $element = $variables['element'];
  if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
    $errors = $element['#errors'];
    $errors_attributes = [];

    if (!empty($element['#id'])) {
      $errors_attributes['id'] = $element['#id'] . '--errors';
      $errors_attributes['class'] = 'form-item--error-message';
    }

    $variables['errors'] = [];
    $variables['errors']['content'] = [
        '#type' => 'container',
        '#attributes' => new Attribute($errors_attributes),
        '#markup' => '<strong>' . $errors . '</strong>',
      ];

  }
}

Then delete the container and <strong> from the templates, unless theres is an other solution possible ?

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

andrewmacpherson’s picture

Well, I meant in particular the inline form errors, since the drupal Error messages (at the top) are in fact a list of errors in a form of anchor links. So we would add an ID for every <li> ?

You only get the list of anchor links when the Inline Form Errors module is enabled. That's not what I meant.

When Inline Form Errors module is disabled, you actually get the error messages at the top of the page.

So what I meant is, the error message can be associated by IDREF whether it is near the form element, or far away at the top of the page.

I'll give your code a closer look soon.

lus’s picture

StatusFileSize
new3.45 KB

Ye I have misunderstood your comment. Here is a patch that work at the lower level - in Drupal core FormErrorHandler. It adds to the form input fields an attribute aria-describedby and creates the list of messages but this time with an id on the <li> elements.

lus’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, 10: form-error-accessbility-3083103-10.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

lus’s picture

StatusFileSize
new1.83 KB
lus’s picture

StatusFileSize
new15.13 KB
lus’s picture

StatusFileSize
new2.88 KB
lus’s picture

StatusFileSize
new2.84 KB

I'm bad at adding patches ...

lus’s picture

Status: Needs work » Needs review
brunorios1’s picture

@Lus,
I got this error:

Error: Call to a member function renderPlain() on null in Drupal\Core\Form\FormErrorHandler->displayErrorMessages() (line 99 of /home/myuser/www/myproject/web/core/lib/Drupal/Core/Form/FormErrorHandler.php)

Thanks.

lus’s picture

Hi @brunorios1,
Could you please specify when you had this error, was it when you submitted a simple Drupal Web form? Was the Inline Form Errors module activated ?

brunorios1’s picture

Hi @Lus,
Yes, a Webform with Ajax enabled.
And yes, Inline Form Errors module is activated.

lus’s picture

@brunorios1 I made some manually tests but I can't see the same error, however the Drupal Tests are not passing by.

lus’s picture

StatusFileSize
new3.4 KB

Fix constructor error.

Status: Needs review » Needs work

The last submitted patch, 22: form-error-accessbility-3083103-17.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

lus’s picture

StatusFileSize
new3.63 KB

Re-test.

lus’s picture

andrewmacpherson’s picture

A contrib module is trying this too: A11Y: Form helpers.

It's worth studying, but it assumes Inline Form Errors module is enabled. This core issue should still seek to make the IDREFs work regardless of whether the Inline Form Errors module is enabled.

@Lus - Thanks for working on this. I see many more patches have been posted since I was last here. I haven't tested or studied them in depth, but will try to do that soon.

Do you know how to create an interdiff? Those help reviewers follow the progress of a patch as it evolves.

I skimmed the latest patch from #24....

  1. @@ -40,10 +68,47 @@ public function handleFormErrors(array &$form, FormStateInterface $form_state) {
    
    +          '#wrapper_attributes' => [

    I like the idea of using #wrapper_attributes, because that's flexible enough for any wrapper.

  2. @@ -40,10 +68,47 @@ public function handleFormErrors(array &$form, FormStateInterface $form_state) {
    
    +            'id' => $id . '--status-message'

    Nitpick: we are talking about errors, so maybe the ID suffix would be better as '--error-message'. It would make the markup easier to understand.

  3. @@ -162,6 +227,11 @@ protected function setElementErrorsFromFormState(array &$form, FormStateInterfac
    
    +      $elements['#attributes']['aria-describedby'] = $elements['#id'] . '--status-message';

    This looks fragile. Remember that the aria-describedby attribute accepts an ID reference list. In other words, it can point to multiple IDs. So it should be treated as an array of values, and we are adding an additional IDREF.

    A majority of our inputs will already have another IDREF pointing to the description. See Drupal\Core\Form\FormBuilder::doBuildForm() for this. We don't want to over-write the description IDREF.

    What we're aiming for is something like this: aria-describedby="edit-foo--error-message edit-foo--description". The intended effect is that screen readers will announce the error message AND still announce the extra help description for the input. The error message is the most important, so that comes first. We'll need something like this:

    array_unshift($elements['#attributes']['aria-describedby'], $elements['#id'] . '--error-message';

    A snag is that the existing logic in Drupal\Core\Form\FormBuilder::doBuildForm() also makes this mistake, and assumes there will only be a single IDREF value. I'll file a separate issue about that, because it's a wider problem than error messages IDs.

neelam_wadhwani’s picture

Assigned: Unassigned » neelam_wadhwani
neelam_wadhwani’s picture

Assigned: neelam_wadhwani » Unassigned
StatusFileSize
new636 bytes
new3.67 KB

I have updated patch.
Kindly review patch.

neelam_wadhwani’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 29: 3083103-29.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

aleevas’s picture

Status: Needs work » Needs review
StatusFileSize
new3.48 KB
new3.65 KB

Trying to fix latest patch

Status: Needs review » Needs work

The last submitted patch, 32: 3083103-32.patch, failed testing. View results

vsujeetkumar’s picture

Status: Needs work » Needs review
StatusFileSize
new5.94 KB

Fix more test cases, Please review.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

tim bozeman’s picture

One hard thing is going to be multi-value fields. Errors on sub-items like street name or telephone number are set on the address or telephone parent elements. I don't think putting the aria-describedby on the parent elements is going to be useful for screen readers and it's hard to discern which child element the error applies to.

andrewmacpherson’s picture

Yes, there are certainly going to be special cases for some kinds of complex Form API elements. Address, image fields, various date/time fields, and perhaps Form API '#type' => '#checkboxes'. There is already some special-case code for some of these to manage labels and descriptions.

"Multi-value" fields are different from these.

andrewmacpherson’s picture

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Liam Morland made their first commit to this issue’s fork.

liam morland’s picture

I have created an issue fork. I started with the patch in #34. I made the new id values both end in --error-message (one was still --status-message). I made it use aria-errormessage instead of aria-describedby. I have it set aria-invalid where aria-errormessage is set (this was already being set elsewhere later, but it makes sense to ensure that both are set together).

In testing from Webform, it still doesn't set the --error-message in the id of the error. This will probably require a change to form-element.html.twig.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

joachim’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
    @@ -14,6 +15,34 @@ class FormErrorHandler implements FormErrorHandlerInterface {
    +    // Instantiates this form class.
    ...
    +      // Load the service required to construct this class.
    

    These comments about DI aren't necessary.

  2. +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
    @@ -39,10 +68,34 @@ public function handleFormErrors(array &$form, FormStateInterface $form_state) {
    -    // Loop through all form errors and set an error message.
    

    We've lost this comment about setting an error message.

ankithashetty’s picture

Status: Needs work » Needs review
StatusFileSize
new5.8 KB
new4.1 KB

Re-rolled the patch in #34, addressed the changes specified in #43 and added a missing use Symfony\Component\DependencyInjection\ContainerInterface; statement in core/lib/Drupal/Core/Form/FormErrorHandler.php file.

Thank you!

joachim’s picture

Status: Needs review » Reviewed & tested by the community

Thanks!

liam morland’s picture

Updated merge request with changes in #44.

tostinni’s picture

I'm reviewing this issue (both path #44 and MR1063) and I see correctly the new aria-describedby or aria-errormessage but they link to non-existent ID.
The error message only has this content:

<div class="form-item--error-message">
    <strong>This field is mandatory.</strong>
</div>

Is there something that needs to be set up ?

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

THere's been loads of activity since the rtbc in #45 so this issue can't really be RTBC as this activity has not been reviewed.

Plus I've just added some more comments to the code.

The issue summary could do with an update to outline the problem and how it is being fixed.

The MR also seems light on test coverage - I would expect some test coverage of the new HTML that is being output.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sukanya.ramakrishnan’s picture

Thanks for the patch, but i am unable to see the id for the error message after applying this patch (aria-described-by attribute is added correctly though to the element that didnt validate correctly). Am I missing anything here?

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mgifford’s picture

Issue tags: +wcag331

Tagging for 3.3.1.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

neclimdul made their first commit to this issue’s fork.

neclimdul’s picture

Rebased and address the feedback from alex.

Unfortunately this doesn't work. It was really broken and the ID attachement didn't seem to work at all. I fixed that but it only works for simple elements. When trying to attach to groups like radios or checkboxes, the ID in displayErrorMessages is the collection, but the ID in setElementErrorsFromFormState is the ID of the specific input and these don't match.

For example testing on umami search,

edit-type--error-message

but the element is

edit-type-article

referencing

edit-type-article--error-message

Definitely needs a lot of tests.

neclimdul’s picture

got some tests in place that identify some of the pending issues. not passing because we've got some hard problems to solves still.

liam morland’s picture

I have rebased both merge requests.

fromme made their first commit to this issue’s fork.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

kentr made their first commit to this issue’s fork.

kentr’s picture

I rebased !1063.

What's the difference between the two MRs? It looks like they've both been kept up-to-date.

kentr’s picture

minoroffense made their first commit to this issue’s fork.

minoroffense’s picture

Updated 7080 to fix the merge conflict in the test

And as for the differences between the branches looks like 7080 takes care of template changes and other items required for the new attributes to work. The other has the base changes but not template changes.

joshahubbers made their first commit to this issue’s fork.

joshahubbers’s picture

In 1063 i added the aria-errormessage in RenderElementBase too, because I wasn't getting it in de form output else.

joarferme’s picture

The patch works, but it shows two duplicate error messages in my theme.

oily changed the visibility of the branch 3083103-programmatically-associate-error to hidden.

oily changed the visibility of the branch 11.x to hidden.

oily’s picture

Hid 2 of 3 of visible branches. The one I have left visible (active one) contains the most changed files. So I assume it built on the other MR which I have hidden. The '11.x' MR contained no changes so seems like it should definitely be hidden.

oily’s picture

Seem to have fixed one PHPSTAN/ PHPCS error. There is still one PHPSTAN error:

https://git.drupalcode.org/issue/drupal-3083103/-/jobs/9376769

sasanikolic made their first commit to this issue’s fork.

sasanikolic’s picture

Status: Needs work » Needs review

I just created MR from branch 3083103-improve-error-accessibility against 3083103-programmatically-associate-error-11 with improvements to the current patch. The changes address three areas:

  1. Error wrappers: The templates were using hard-coded <div> wrappers — fieldset and details had no class at all, and the id was built directly from element['#id']in Twig rather than passed from preprocess. Now FormPreprocess passes error_id as a template variable, all three templates use create_attribute() with consistent form-item--error-message class, and errors are no longer blindly suppressed.
  2. Error placement: fieldset and details placed errors before children, while form-element placed them after. Standardized all three to render errors after children, before the description.
  3. Accessibility: aria-errormessage alone is not sufficient — VoiceOver+Safari and TalkBack don't support it. Added aria-describedby as a fallback (with deduplication to avoid duplicates when both FormErrorHandler and RenderElementBase set it). Also strips aria-errormessage and aria-invalid from child radios/checkboxes to prevent invalid attributes on individual inputs. Introduced #error_id property so elements can override the default ID convention. Fixed displayErrorMessages() to use a proper render array (#type container) instead of raw #prefix/#suffix HTML.
  4. Also fixed the unit test — the renderer mock was set up for renderPlain but the code calls renderInIsolation, and the Prophecy renderer wasn't initialized in setUp().
rkoller’s picture

thank you! could you please also create a MR for the feature branch (3083103-improve-error-accessibility) you've created for your changes so it could become testable?

smustgrave’s picture

Is this not what inline form errors does?

kentr’s picture

Status: Needs review » Postponed (maintainer needs more info)

@smustgrave

I tested IFE for this yesterday.

My observation was that IFE doesn't currently link invalid fields to error messages with aria-describedby, though it was supposed to.

Use ARIA described-by to semantically associate the error message with the field in a machine-readable, assistive technology-friendly way.

I saw that the MR here touches IFE tests, so am just about to test it. Postponing until there's more data.

kentr’s picture

Status: Postponed (maintainer needs more info) » Needs work
Issue tags: +Needs rebase

Here's what I got for required errors with a hacked version of the form_test module's #required test form.

Test cases

Note: I merged main into the MR for these tests (locally only; did not push to gitlab).

  1. On main, without Inline Form Errors
  2. With MR, without Inline Form Errors
  3. On main, with Inline Form Errors
  4. With MR, with Inline Form Errors

Result

So far, without Inline Form Errors, the MR appears to work as expected.

I still observed that Inline Form Errors doesn't do this currently, and AFAICT the MR fixes it. See the result details for the markup.

Discussion

Questions for the accessibility experts:

Do we want to use aria-errormessage instead of aria-describedby?

In #accessibility Slack yesterday, the recommendation was to use aria-describedby:

Yes, aria-errormessage has more support than it used to but there are still significant gaps. Prepending error messages to the referenced aria-describedby is still the best option.

Adrian Roselli says this about aria-errormessage as of Aug 15, 2023:

  • The message associated via aria-errormessage paired with a live region is generally not exposed onblur;
  • The message associated via aria-errormessage is generally not exposed when navigating through fields;
  • The virtual cursor is the only certain way to encounter a message associated using aria-errormessage.

If we do go with aria-describedby, is it sufficient to prepend the error message id to aria-describedby instead of prepending the error message itself to the description text?

Either way, the pipeline is failing. This also could use a rebase to bring in #1797438: HTML5 validation is preventing form submit and not fully accessible for easier manual testing.

With regard to Inline Form Errors, this might be blocked on #3557245: Use $element['#attributes]['id'] instead of $element['#id'] to create a link anchor. Not sure yet.

Result details

1. On main, without Inline Form Errors

Page-top errors markup
<div data-drupal-messages="">
  <div role="contentinfo" aria-label="Error message">
    <div role="alert">
      <h2 class="visually-hidden">Error message</h2>
      Enter a name.
    </div>
  </div>
</div>
Field markup
<div
  class="js-form-item form-item form-type-textfield js-form-type-textfield form-item-textfield js-form-item-textfield">
  <label for="edit-textfield" class="js-form-required form-required">Name</label>
  <input data-drupal-selector="edit-textfield" aria-describedby="edit-textfield--description" type="text"
    id="edit-textfield" name="textfield" value="" size="60" maxlength="128" class="form-text required error"
    required="required" aria-invalid="true">

  <div id="edit-textfield--description" class="description">
    Description for name field
  </div>
</div>

2. With MR, without Inline Form Errors

Page-top errors markup
<div data-drupal-messages="">
  <div role="contentinfo" aria-label="Error message">
    <div role="alert">
      <h2 class="visually-hidden">Error message</h2>
      <div id="edit-textfield--error-message">Enter a name.</div>
    </div>
  </div>
</div>
Field markup
<div
  class="js-form-item form-item form-type-textfield js-form-type-textfield form-item-textfield js-form-item-textfield">
  <label for="edit-textfield" class="js-form-required form-required">Name</label>
  <input data-drupal-selector="edit-textfield" aria-describedby="edit-textfield--description"
    aria-errormessage="edit-textfield--error-message" aria-invalid="true" type="text" id="edit-textfield"
    name="textfield" value="" size="60" maxlength="128" class="form-text required error"
    required="required">

  <div id="edit-textfield--description" class="description">
    Description for name field
  </div>
</div>

3. On main, with Inline Form Errors

Page-top errors block
<div data-drupal-messages="">
  <div role="contentinfo" aria-label="Error message">
    <div role="alert">
      <h2 class="visually-hidden">Error message</h2>
      1 error has been found:
      <ul class="item-list__comma-list">
        <li><a href="#edit-textfield">Name</a></li>
      </ul>
    </div>
  </div>
</div>
Field markup
<div
  class="js-form-item form-item form-type-textfield js-form-type-textfield form-item-textfield js-form-item-textfield form-item--error">
  <label for="edit-textfield" class="js-form-required form-required">Name</label>
  <input data-drupal-selector="edit-textfield" aria-describedby="edit-textfield--description" type="text"
    id="edit-textfield" name="textfield" value="" size="60" maxlength="128" class="form-text required error"
    required="required" aria-invalid="true">

  <div class="form-item--error-message">
    Enter a name.
  </div>
  <div id="edit-textfield--description" class="description">
    Description for name field
  </div>
</div>

4. With MR, with Inline Form Errors

Page-top errors markup
<div data-drupal-messages="">
  <div role="contentinfo" aria-label="Error message">
    <div role="alert">
      <h2 class="visually-hidden">Error message</h2>
      1 error has been found: <ul class="item-list__comma-list">
        <li><a href="#edit-textfield">Name</a></li>
      </ul>
    </div>
  </div>
</div>
Field markup
<div
  class="js-form-item form-item form-type-textfield js-form-type-textfield form-item-textfield js-form-item-textfield form-item--error">
  <label for="edit-textfield" class="js-form-required form-required">Name</label>
  <input data-drupal-selector="edit-textfield" aria-describedby="edit-textfield--description"
    aria-errormessage="edit-textfield--error-message" aria-invalid="true" type="text" id="edit-textfield"
    name="textfield" value="" size="60" maxlength="128" class="form-text required error"
    required="required">

  <div id="edit-textfield--error-message" class="form-item--error-message">
    Enter a name.
  </div>
  <div id="edit-textfield--description" class="description">
    Description for name field
  </div>
</div>

kentr changed the visibility of the branch 3083103-improve-error-accessibility to hidden.

smustgrave’s picture

I still think the fix should be in the inline_form_error module, especially since the test coverage is located there. Shouldn't add test coverage for a module we aren't fixing :)

Also by putting there we could do away with the service and template updates probably, or least change it to an attributes array so the module could dynamically add the ID without much twig change, which need a CR.