Menu blocks in Layout Builder get malformed aria-labelledby attribute

Created on 23 September 2019, about 5 years ago
Updated 23 December 2023, 12 months ago

Problem/Motivation

The aria-labelledby attribute is not being set correctly for menu blocks that are placed in Layout Builder. This problem could affect other blocks.

Steps to reproduce:
1. Set up a standard install.
2. Enable the Layout Builder module.
3. Enable Layout Builder for a content type, Basic Page or Article, it doesn't matter which.
4. Place a menu block in the layout for that content type. Give the menu block a label.
5. Create a node of that content type and view it.
6. Verify that the menu is appearing on the node's page with its label.
7. Inspect the HTML for the menu block.

Expected result:
The NAV element surrounding the menu should have an aria-labelledby attribute like "unique-block-id-menu" and the block heading should have an id attribute that is the same.

Actual result:
The NAV's aria-labelledby attribute and the heading's id are "-menu".

I've traced the problem back to https://git.drupalcode.org/project/drupal/blob/8.8.x/core/modules/block/... of the block.module file, part of template_preprocess_block(). $variables['attributes']['role'] is NULL when the block is placed with Layout Builder. So that IF statement is skipped. core/modules/system/templates/block--system-menu-block.html.twig concatenates "-menu" to an empty value and that's what gets printed.

I didn't have time to try and figure out where that role attribute is supposed to be coming from, so I couldn't debug it any further.

This is specifically affecting our menus when placed with Layout Builder, but it would impact any block that is relying on the aria-labelledby attribute from template_preprocess_block(). That's why I marked this issue as being for the block.module component.

Technically, the aria-labelledby attribute and id are the same, so that isn't specifically an accessibility problem. But if you try to place more than one menu on the same page with Layout Builder, then you have duplicate IDs. That's what made us aware of the problem.

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Block 

Last updated 2 days ago

Created by

🇺🇸United States dcam

Live updates comments and jobs are added and updated live.
  • Accessibility

    It affects the ability of people with disabilities or special needs (such as blindness or color-blindness) to use Drupal.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    This looks great - nice and clean fix.

    Can we get a test-only patch so we can see red/green?

    Other than that, this is RTBC in my book

  • 🇺🇸United States smustgrave

    Test only patch

  • Status changed to RTBC almost 2 years ago
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Thanks, great work folks - this is RTBC in my book

  • Status changed to Needs work almost 2 years ago
  • 🇺🇸United States bnjmnm Ann Arbor, MI
    1. --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
      +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
      @@ -656,6 +656,7 @@ public function testPluginDependencies() {
           // Assert that the blocks are visible, and save the layout.
           $assert_session->pageTextContains('Powered by Drupal');
           $assert_session->pageTextContains('My Menu');
      +    $assert_session->elementExists('xpath', "//nav[contains(@aria-labelledby, 'block-system-menu-block-my-menu')]");
           $assert_session->elementExists('css', '.block.menu--my-menu');
           $page->pressButton('Save layout');

      Could this get an additional assertion that confirms #block-system-menu-block-my-menu exists, and has content suitable for a label?

    2. The issue summary and title make several references to aria-describedby. The solution impacts aria-labelledby. This appears to be the right approach, but the issue summary should be updated to be consistent with that.
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Updated the issue summary, +1 for more tests

  • Status changed to Needs review almost 2 years ago
  • 🇺🇸United States smustgrave

    Updated IS and added additional assertion.

  • 🇮🇳India rinku jacob 13 Kerala

    Hi @smustgrave , Tested your patch on drupal version 10.1.x. It was successfully applied and i got solution. Adding screenshots for the reference. Need RTBC+1

  • 🇮🇳India sonam.chaturvedi Pune

    Verified patch #38 on 10.1.x-dev. Patch applied cleanly.
    Test Result: The NAV element surrounding the menu should have an aria-labelledby attribute has correct and complete value.
    Screenshot available in #39

    RTBC +1

  • Status changed to RTBC over 1 year ago
  • 🇺🇸United States vleeayo1

    Tested in 10.1.x-dev. by adding 2 user account menus and was able to get the same result as #40.

  • 🇺🇸United States crasx Denver, CO

    I worked with vleeayo1 on this for their first contribution as part of the Discover Drupal program! Based on convos above this should be RTBC. Code and tests make sense to me

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,381 pass, 2 fail
  • 🇺🇸United States smustgrave

    Random failure

  • Status changed to Needs work over 1 year ago
  • 🇫🇮Finland lauriii Finland

    I think we should be using \Drupal\Component\Utility\Html::getUniqueId to make sure that each block gets a unique id even if the layout is rendered multiple times on a single page. I'm not sure that we need the str_replace call but we may as well keep it since it looks like colons would not be converted to a dash but just removed by \Drupal\Component\Utility\Html::getUniqueId.

  • First commit to issue fork.
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update over 1 year ago
    29,338 pass, 4 fail
  • @rpayanm opened merge request.
  • Open on Drupal.org →
    Environment: PHP 8.2 & MySQL 8
    last update about 1 year ago
    Not currently mergeable.
  • Status changed to Needs review about 1 year ago
  • 🇺🇸United States smustgrave

    Seems using Html::getUniqueId($block->getPluginId())) makes the ID block-system-menu-blockmy-menu vs block-system-menu-block-my-menu due to line 230 of Html.php

    But updated tests for using getUniqueId

  • last update about 1 year ago
    30,374 pass, 2 fail
  • last update about 1 year ago
    30,392 pass
  • 🇨🇦Canada mgifford Ottawa, Ontario

    This looks great. Look forward to seeing this bug fixed.

  • 🇺🇸United States smustgrave

    Addressed feedback.

  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    larowlan changed the visibility of the branch 3083181-10.1.x to hidden.

  • Status changed to RTBC about 1 year ago
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Everything looks good to me now. Thanks for the updates

  • last update about 1 year ago
    30,698 pass
  • last update about 1 year ago
    30,698 pass
  • last update about 1 year ago
    30,712 pass
  • last update about 1 year ago
    30,726 pass
  • last update about 1 year ago
    30,766 pass
  • 15:07
    11:03
    Running
  • last update about 1 year ago
    25,881 pass, 1,802 fail
  • Status changed to Needs review about 1 year ago
  • 🇬🇧United Kingdom alexpott 🇪🇺🌍

    Are we sure that the Html::getUniqueId() is necessary here - we're going to call that again in template_preprocess_block() on the #id

    Also I think it'd be great to work out the root cause so that we can add a warning or something if another system places a menu block in the same way as layout builder.

  • Status changed to Needs work about 1 year ago
  • 🇺🇸United States smustgrave

    Sounds like more work is needed.

  • 🇬🇧United Kingdom alexpott 🇪🇺🌍

    So the cause is the twig template - block--system-menu-block.html.twig

    {% set heading_id = attributes.id ~ '-menu'|clean_id %}
    <nav role="navigation" aria-labelledby="{{ heading_id }}"{{ attributes|without('role', 'aria-labelledby') }}>
      {# Label. If not displayed, we still provide it for screen readers. #}
      {% if not configuration.label_display %}
        {% set title_attributes = title_attributes.addClass('visually-hidden') %}
      {% endif %}
      {{ title_prefix }}
      <h2{{ title_attributes.setAttribute('id', heading_id) }}>{{ configuration.label }}</h2>
      {{ title_suffix }}
    

    This code pretty much requires attributes.id to be set. I think the fix there is correct - we should be adding an ID to all blocks placed by the layout builder.

    I was wondering the use-case of placing blocks without an ID. It's definitely not mandated by the system as we have code like:

      if (!empty($variables['elements']['#id'])) {
        $suggestions[] = 'block__' . $variables['elements']['#id'];
      }
    

    But this brings up something else interesting. We definitely should not be using getUniqueId() here as that results in different theme suggestions being applied. The ID we're adding here result in the following suggestions:

    • block__system
    • block__system_menu_block
    • block__system_menu_block__account
    • block__system-menu-blockaccount

    whereas the menu block placed by the block system results in:

    1. block__system
    2. block__system_menu_block
    3. block__system_menu_block__account
    4. block__olivero_account_menu

    So calling Html::cleanCssIdentifier() and Html::getUniqueId() is wrong. Because the mix of _ and - in theme suggestion seems very wrong. But also we need to do something to remove the : from the plugin ID. I guess we should be inspired by the code in block_theme_suggestions_block()...

      $parts = explode(':', $variables['elements']['#plugin_id']);
      $suggestion = 'block';
      while ($part = array_shift($parts)) {
        $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
      }
    

    We don't need to prefix the ID with block as that happens automatically but maybe something like:

    '#id' => 'layout_builder__' . str_replace(':', '__', $block->getPluginId());
    

    will work. But I'm very unsure about the 'layout_builder__' . part.

Production build 0.71.5 2024