Problem/Motivation

Example stack trace:

Warning: Undefined array key "aria-describedby" in webform_process_options() (line 1093 of .../modules/contrib/webform/webform.module)
#0 .../core/includes/bootstrap.inc(164): _drupal_error_handler_real()
#1 .../modules/contrib/webform/webform.module(1093): _drupal_error_handler()
#2 [internal function]: webform_process_options()
#3 .../core/lib/Drupal/Core/Form/FormBuilder.php(1025): call_user_func_array()
#4 .../core/lib/Drupal/Core/Form/FormBuilder.php(1088): Drupal\Core\Form\FormBuilder->doBuildForm()
#5 .../core/lib/Drupal/Core/Form/FormBuilder.php(1088): Drupal\Core\Form\FormBuilder->doBuildForm()
#6 .../core/lib/Drupal/Core/Form/FormBuilder.php(579): Drupal\Core\Form\FormBuilder->doBuildForm()
#7 .../core/lib/Drupal/Core/Form/FormBuilder.php(325): Drupal\Core\Form\FormBuilder->processForm()
#8 .../core/lib/Drupal/Core/Entity/EntityFormBuilder.php(48): Drupal\Core\Form\FormBuilder->buildForm()
#9 .../modules/contrib/webform/src/Entity/Webform.php(1257): Drupal\Core\Entity\EntityFormBuilder->getForm()
#10 .../modules/contrib/webform/src/Element/Webform.php(112): Drupal\webform\Entity\Webform->getSubmissionForm()
#11 [internal function]: Drupal\webform\Element\Webform::preRenderWebformElement()

Steps to reproduce

  1. Go to admin/config/people/accounts
  2. Navigate to REGISTRATION AND CANCELLATION > When cancelling a user account
  3. Inspect the radio buttons. Seeing that aria-describedby="edit-user-cancel-method--description" which edit-user-cancel-method--description doesn't exists.

Those 3 radio buttons don't have any description. So I think they don't need to have aria-describedby attribute.
For example, in REGISTRATION AND CANCELLATION > Who can register accounts?, the radio buttons don't have aria-describedby attribute since they don't have description.
However, the description below (Users with the Select method ...) is a description of the fieldset wrapper.

Attached Screenshot

Proposed resolution

core/lib/Drupal/Core/Render/Element/Checkboxes.php and core/lib/Drupal/Core/Render/Element/Radios.php are responsible for copying the parent element's attributes to the children. Update both classes to remove the parent's aria-describedby from the copy before applying it to the children.

Remaining tasks

  • Review
  • Commit

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-2839344

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

Thew created an issue. See original summary.

mgifford’s picture

Version: 8.2.x-dev » 8.3.x-dev
StatusFileSize
new179.62 KB

Tested this in 8.3 & can repeat the problem. This is what the WAVE Toolbar sees:

screenshot with wave toolbar

Hard to argue with "the radio buttons don't have aria-describedby attribute since they don't have description."

I wonder if it would make sense to open up a new issue for this relationship:

<fieldset data-drupal-selector="edit-user-cancel-method" aria-describedby="edit-user-cancel-method--wrapper--description" id="edit-user-cancel-method--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper" aria-describedby="edit-user-cancel-method--wrapper--description">

<div id="edit-user-cancel-method--wrapper--description" class="description">Users with the <em class="placeholder">Select method for cancelling account</em> or <em class="placeholder">Administer users</em> <a href="/admin/people/permissions">permissions</a> can override this default method.</div>

Agreed that they should be semantically linked, especially since that DIV isn't inside the fieldset.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lstrange’s picture

Is there any status on fixing this?
Thanks...

andrewmacpherson’s picture

Title: Some aria-describedby refers to not existing element ID. » Broken aria-describedby IDREF in User registration/cancellation radio buttons.
Component: forms system » user.module
Issue tags: +Needs issue rescope

@lstrange - thanks for waking this issue up after 3 years :-)

The accessibility maintainers are aware of several other scenarios where an aria-* ID reference wasn't working. Some issues have already been fixed, other issues have work-in-progress.

This issue has a vague title (aria-* ID refs in general), but a very detailed description of a problem with the registration/cancellation radio options on the user admin form.

So for now, let's narrow the issue scope to the User module. Maybe it has a more general-purpose solution for all radio buttons; we'll see.

amanire’s picture

I'm guessing that this is a widespread problem in Drupal form widgets with containers since I'm also running into this on the Content Moderation module "Save As" select list widget. The form-wrapper div element has a an aria-describedby value of edit-moderation-state-0--description but no corresponding ID exists on the form. This was confounding me since the description string "The moderation state of this piece of content." exists, but is never actually rendered on the node form.

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.

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.

jontynewman’s picture

StatusFileSize
new686 bytes

This may be a naive approach, but may be useful as an initial patch.

By removing the attribute from radios, the description associated with the fieldset may be relied upon instead.

jontynewman’s picture

StatusFileSize
new716 bytes

An amendment to the previous patch.

jontynewman’s picture

jacine’s picture

Title: Broken aria-describedby IDREF in User registration/cancellation radio buttons. » Broken aria-describedby IDREF in User registration/cancellation radios and checkboxes elements.
Status: Active » Needs work

This is an issue with all radios and checkboxes elements (and maybe other elements), and has to do with how attributes are applied to these composite elements and their children.

What happens now is that:

1. The code that connects any #description present, with aria-describedby does not account for the fact that these elements are "special" composite elements with children.
2. Attributes are simply pushed down to the children elements which results in the same attributes on BOTH the wrapper (fieldset) and the children (input type checkbox/radio).

This patch needs work because adding a #description to a radios or checkboxes and wanting to use it with aria-describedby is perfectly fine. The patch just removes that functionality (for radios) entirely, preventing the aria-describedby connection from being made, instead of addressing the root issue, which IMO is that there should be a formal separation of attributes for composite form elements like this.

$form['checkboxes_or_radios_element'] = [
  '#type' => 'checkboxes',
  '#title' => $this->t('Element label'),
  // Should only apply to the fieldset wrapper for checkboxes/radios.
  '#description' => $this->t('Some description'),
  // Should only apply to the fieldset wrapper, but gets pushed to children.
  '#attributes' = [
    'whatever' => 'attribute'
  ],
  '#options' => [
    // These options should NOT inherit #attributes, but they do, and at this point, aria-describedby from the parent is there.
    'option1' => $this->t('Option 1'),
  ],
];
jontynewman’s picture

Title: Broken aria-describedby IDREF in User registration/cancellation radios and checkboxes elements. » Broken aria-describedby in radios and checkboxes.
Status: Needs work » Needs review
StatusFileSize
new1.46 KB

Thanks for the feedback, Janice.

I believe I would agree with your suggestions entirely if the form API currently provided some mechanism for associating specific #descriptions with specific #options. Perhaps it would look something like this:

$form['checkboxes_or_radios_element'] = [
  '#type' => 'checkboxes',
  '#title' => $this->t('Element label'),
  '#description' => $this->t('Element description'),
  '#options' => [
    'option1' => [
      '#title' => $this->t('Option label'),
      // The following would perhaps generate an associated HTML element,
      // give it a suitable ID, and assign this ID to the aria-describedby attribute
      // of the option's input.
      '#description' => $this->t('Option description'),
    ],
  ],
];

However, perhaps this is beyond the scope of this issue, as this would seem to be feature enhancement of the form API, rather than a fix for the incorrect aria-describedby values.

This patch needs work because adding a #description to a radios or checkboxes and wanting to use it with aria-describedby is perfectly fine. The patch just removes that functionality (for radios) entirely...

The patch does not remove any existing functionality. The #description will still be generated with an ID which is associated with the aria-describedby attribute of the wrapper (such as the fieldset that contains the checkboxes). The patch solely seeks to remove the erroneous attributes.

I have amended the patch to also consider checkboxes, as you suggest.

Status: Needs review » Needs work

The last submitted patch, 17: 2839344.patch, failed testing. View results

jontynewman’s picture

Status: Needs work » Needs review

The testing now looks to have passed. It seems to have been a temporary issue in unrelated functional JavaScript tests.

jontynewman’s picture

Issue tags: -Needs issue rescope

I believe this issue to now be sufficiently re-scoped.

jontynewman’s picture

jontynewman’s picture

tanubansal’s picture

Tested Patch #17 on 9.1, its appearing fine
This can be moved to RTBC

jontynewman’s picture

Status: Needs review » Reviewed & tested by the community

Many thanks, tanubansal.

Moving to RTBC as suggested.

andrewmacpherson’s picture

+  public static function isComposite(array $element) {
+    return isset($element['#type']) && in_array($element['#type'], ['radios', 'checkboxes']);
+  }

This doesn't look very extensible. I'd expect this to be some kind of property/method on the class which defines the form element type, so that any element type might be a composite element. There are various places where this might be done, but I'm not sure which would be best.

  • A new method on Drupal\Core\Render\Element\ElementInterface or Drupal\Core\Render\Element\FormElementInterface. So then you might have Drupal\Core\Render\Element\Checkboxes::isCompositeElement().
  • A new CompositeFormElementInterface, so you can use a instanceof CompositeFormElementInterface test.

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.

lauriii’s picture

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

Besides #25, this also needs tests.

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.

codebymikey’s picture

This issue also affects the Details form element, these accessibility issues are more apparent through the use of the Styleguide module.

https://www.drupal.org/project/styleguide

I think the details element would require a change to the twig template to include the appropriate wrapper and details attributes just like the fieldset template does. Or just simply omitted as well.

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.

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.

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.

kalpaitch’s picture

Following on from @codebymikey's point #29 I've also identified this happens to the 'container' element too, (albeit with the caveat that the container element does not support the #description attribute):

$form['element'] = [
      '#type' => 'container',
      '#description' => 'A description added to the container.'
];

This will result in a container wrapper being rendered with an incorrect 'aria-describedby' attribute.

Following on from @andrewmacpherson's comment #25, I agree there needs to be a more extensible way to declare a form element as a composite form element. But there are also needs to be some consideration for other render elements which get the 'aria-describedby' attribute added.

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

liam morland’s picture

Title: Broken aria-describedby in radios and checkboxes. » Broken aria-describedby in radios and checkboxes

I have made a merge request with patch 17 rebased onto 11.x.

solideogloria’s picture

Issue summary: View changes

Added stack trace

solideogloria’s picture

Issue summary: View changes

Fixed merge conflict.

solideogloria’s picture

I agree with #25, that we should use something more extensible. However, I don't think either of the suggestions is enough information for making it work. The issue is in the FormBuilder class, and at that point, you don't have instances of ElementInterface, you have a render array. So you can't do an instanceof test so easily.

It might be easier to add an attribute #is_composite to the render array?

Or if I'm wrong, then I would say it's probably better to have CompositeFormElementInterface, because there is also a CompositeFormElementTrait. So you could even put isComposite in the trait, and it would be added to any contrib/custom classes using the trait as well.

jldust’s picture

I agree that this needs to be more extensive, I'm seeing these broken references with selection options as well.

dcam’s picture

For anyone who needs a workaround, you can implement this in a custom theme or module:

/**
 * Implements hook_element_info_alter().
 */
function my_theme_element_info_alter(array &$info) {
  $info['checkboxes']['#process'][] = '_my_theme_process_options_element';
  $info['radios']['#process'][] = '_my_theme_process_options_element';
}

/**
 * Processes checkboxes and radios elements.
 *
 * Checkboxes and radios have an accessibility bug where the child option
 * elements inherit an empty aria-describedby attribute from the parent.
 * Because the child elements are fully rendered before the parent, this can't
 * be handled in preprocess functions.
 *
 * @param array $element
 *   An options form element.
 *
 * @return array
 *   The processed options form element.
 *
 * @see https://www.drupal.org/project/drupal/issues/2839344
 */
function _my_theme_process_options_element(array $element) {
  foreach ($element as $key => $value) {
    if (substr((string) $key, 0, 1) == '#') {
      continue;
    }
    if (!isset($value['#description']) && isset($value['#attributes']['aria-describedby'])) {
      unset($value['#attributes']['aria-describedby']);
      $element[$key] = $value;
    }
  }
  return $element;
}

The same thing can be accomplished by adding a process function to individual checkbox and radio buttons. I tested it. Doing that means you don't need to loop through all the keys in the parent element to find its children. But my preference was to target the checkboxes and radios since they're the ones that have the problem.

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

dcam’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

I disagree with the approach in MR 7117 and from reading through comments it was contentious to begin with.

I spent the last couple of evenings studying the checkboxes and radios form elements, trying to figure out what parts of the system were setting wrappers and IDs and descriptions. I wanted to make absolutely certain that I'd found the source of these attributes so that it could be solved there.

It's the Checkboxes and Radios form element classes that do it. They both copy the parent element's attributes to the children, including the aria-describedby. This choice is made unilaterally by those classes. The rest of the form system, e.g. FormBuilder, is just doing what it's supposed to do. The element classes are also managing the child descriptions or rather the lack of them. So if the element classes are setting the attributes, then they should be in charge of unsetting any irrelevant attributes. I opened MR 12287 with this strategy in mind. If this idea is accepted, then MR 7117 should be closed.

I can sympathize with any desire to not duplicate code, but I will note that there is already a lot of duplicated code between the two elements. I probably wouldn't bother. If someone wants that to happen, then I assert that we should put off eliminating duplication for a follow-up.

dcam’s picture

Component: user.module » forms system
Issue tags: +Needs issue summary update

I'll update the IS when a decision is made.

smustgrave’s picture

Status: Needs review » Needs work

If there are 2 MRs to choose from usually helps to still update the issue summary but in the proposed solution mention there are 2 solutions

Option 1 solves it by xyz
Option 2 solves it by abc

dcam’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update

If there are 2 MRs to choose from usually helps to still update the issue summary but in the proposed solution mention there are 2 solutions

Fair enough. It's done.

dcam’s picture

Issue summary: View changes

Tweaked the Option 2 text.

dcam’s picture

mgifford’s picture

Thanks Liam. I'll see if I can get someone to renew it.

mgifford’s picture

smustgrave’s picture

Issue tags: +Needs tests

not a sub-maintainer but let me see if I can't help this along. Of the 2 options it seems like MR 7117 would be the most flexible correct? To add a new composite type I mean.

solideogloria’s picture

@smustgrave What do you think about having an interface for composite elements? The check should probably be extensible, so that contrib modules can add composite elements and have them checked by extending the interface or having an attribute or something. See comment #40. Then you could check if the element extends the interface or if it has an attribute.

smustgrave’s picture

I can't speak as the form maintainer but if that made it easier to add a new item too I would be a +1

dcam’s picture

Of the 2 options it seems like MR 7117 would be the most flexible correct?

Let me be blunt: MR 7117 is a workaround for a problem caused by irresponsible copying of parent element attributes to the children. MR 12287 fixes the problem at its source. I feel strongly that it's the way to go, so much that I've considered unilaterally closing 7117 so this issue would progress. But I left it open to be fair to the others who have contributed here. By the way, 12287 has a test.

If there is interest in having the isComposite() function or doing something similar with interfaces, then that can go into a follow-up issue as a feature request. Let's fix this bug properly instead.

solideogloria’s picture

I do agree that MR 7117 is not the way to go. It's not flexible at all, as it won't work for anything added/changed by contrib.

in_array($element['#type'], ['radios', 'checkboxes']);

This is a hardcoded list of types. Not flexible.

smustgrave’s picture

So we want to close the 7117 then and go from there?

dcam’s picture

That's what I would do.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests

Alright lets go that route
Rebase the MR

And if needed open a follow up

dcam changed the visibility of the branch 2839344-broken-aria-describedby-in to hidden.

dcam’s picture

Issue summary: View changes
Issue tags: -Needs subsystem maintainer review
dcam’s picture

Status: Needs work » Needs review

Rebased and updated the IS.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs Review Queue Initiative
StatusFileSize
new136.77 KB
new154.78 KB
new63.99 KB

Before

before

After

after

I also checked Require email verification when a visitor creates an account which should have aria-described

should-work

Ran the test-only job here https://git.drupalcode.org/issue/drupal-2839344/-/jobs/6981702 which shows the coverage

Believe this one is good to go.

  • catch committed 77d0af67 on 11.3.x
    fix: #2839344 Broken aria-describedby in radios and checkboxes
    
    By: Thew...

  • catch committed 2e855eca on 11.x
    fix: #2839344 Broken aria-describedby in radios and checkboxes
    
    By: Thew...
catch’s picture

Version: 11.x-dev » 11.3.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 11.x and cherry-picked to 11.3.x, thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

dcam’s picture

I can't wait to remove the workaround from my theme!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.