Verified Commit 0c13b6a3 authored by Mike Herchel's avatar Mike Herchel
Browse files

fix: #3587678 Ensure status message ARIA landmarks are unique

By: mgifford
By: jurgenhaas
By: dharizza
By: longwave
parent e79fa321
Loading
Loading
Loading
Loading
Loading
+2 −2
Changes for core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -105,11 +105,11 @@ public static function cases(?ContainerInterface $container = NULL, ?AccountInte
          'method' => 'replaceWith',
          // cspell:disable-next-line
          'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args%5B0%5D&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]',
          'data' => '<div data-drupal-messages>' . "\n" . ' <div role="contentinfo" aria-label="Status message">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n" . '</div>' . "\n",
          'data' => '<div data-drupal-messages>' . "\n" . ' <div role="status" aria-label="Status message">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n" . '</div>' . "\n",
          'settings' => NULL,
        ],
      ];
      $status_messages->embeddedHtmlResponse = '<div data-drupal-messages-fallback class="hidden"></div><div data-drupal-messages>' . "\n" . '  <div role="contentinfo" aria-label="Status message">' . "\n" . '              <h2 class="visually-hidden">Status message</h2>' . "\n" . '              Hello from BigPipe!' . "\n" . '          </div>' . "\n" . '</div>' . "\n";
      $status_messages->embeddedHtmlResponse = '<div data-drupal-messages-fallback class="hidden"></div><div data-drupal-messages>' . "\n" . '  <div role="status" aria-label="Status message">' . "\n" . '          <h2 class="visually-hidden">Status message</h2>' . "\n" . '              Hello from BigPipe!' . "\n" . '      </div>' . "\n" . '</div>' . "\n";
    }

    // 2. Real-world example of HTML attribute value placeholder: form action.
+8 −9
Changes for core/modules/system/templates/status-messages.html.twig: 8 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -9,8 +9,13 @@
 * Sighted users see a colored box. See https://www.w3.org/TR/WCAG-TECHS/H69.html
 * for info.
 *
 * Add an ARIA label to the contentinfo area so that assistive technology
 * user agents will better describe this landmark.
 * Each message block exposes an ARIA live region role so assistive technology
 * user agents announce it appropriately: "alert" for errors and warnings
 * (assertive) and "status" for other message types (polite). This matches the
 * role assignment used by Drupal.theme.message() for messages added by
 * JavaScript. The "contentinfo" landmark role is reserved for the page footer
 * and is intentionally not used here, which avoids duplicate landmark and
 * non-unique landmark accessibility violations.
 *
 * Available variables:
 * - message_list: List of messages to be displayed, grouped by type.
@@ -23,10 +28,7 @@
#}
<div data-drupal-messages>
{% for type, messages in message_list %}
  <div role="contentinfo" aria-label="{{ status_headings[type] }}"{{ attributes|without('role', 'aria-label') }}>
    {% if type == 'error' %}
      <div role="alert">
    {% endif %}
  <div role="{{ type in ['error', 'warning'] ? 'alert' : 'status' }}" aria-label="{{ status_headings[type] }}"{{ attributes|without('role', 'aria-label') }}>
    {% if status_headings[type] %}
      <h2 class="visually-hidden">{{ status_headings[type] }}</h2>
    {% endif %}
@@ -39,9 +41,6 @@
    {% else %}
      {{ messages|first }}
    {% endif %}
    {% if type == 'error' %}
      </div>
    {% endif %}
  </div>
{% endfor %}
</div>
+6 −6
Changes for core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php: 6 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -76,21 +76,21 @@ public function testModulesListFormStatusMessage(): void {
    // Enable a module that does not define permissions.
    $edit = ['modules[layout_discovery][enable]' => 'layout_discovery'];
    $this->submitForm($edit, 'Install');
    $this->assertSession()->elementTextContains('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]", 'Module Layout Discovery has been installed.');
    $this->assertSession()->elementNotExists('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]//a");
    $this->assertSession()->elementTextContains('xpath', "//div[@role='status' and h2[text()='Status message']]", 'Module Layout Discovery has been installed.');
    $this->assertSession()->elementNotExists('xpath', "//div[@role='status' and h2[text()='Status message']]//a");

    // Enable a module that defines permissions.
    $edit = ['modules[filter][enable]' => 'filter'];
    $this->submitForm($edit, 'Install');
    $this->assertSession()->elementTextContains('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]", 'Module Filter has been installed.');
    $this->assertSession()->elementExists('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]//a[contains(@href, '/admin/people/permissions/module/filter')]");
    $this->assertSession()->elementTextContains('xpath', "//div[@role='status' and h2[text()='Status message']]", 'Module Filter has been installed.');
    $this->assertSession()->elementExists('xpath', "//div[@role='status' and h2[text()='Status message']]//a[contains(@href, '/admin/people/permissions/module/filter')]");

    // Enable a module that has dependencies and both define permissions.
    $edit = ['modules[content_moderation][enable]' => 'content_moderation'];
    $this->submitForm($edit, 'Install');
    $this->submitForm([], 'Continue');
    $this->assertSession()->elementTextContains('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]", '2 modules have been installed: Content Moderation, Workflows.');
    $this->assertSession()->elementExists('xpath', "//div[@role='contentinfo' and h2[text()='Status message']]//a[contains(@href, '/admin/people/permissions/module/content_moderation%2Cworkflows')]");
    $this->assertSession()->elementTextContains('xpath', "//div[@role='status' and h2[text()='Status message']]", '2 modules have been installed: Content Moderation, Workflows.');
    $this->assertSession()->elementExists('xpath', "//div[@role='status' and h2[text()='Status message']]//a[contains(@href, '/admin/people/permissions/module/content_moderation%2Cworkflows')]");
  }

  /**
+1 −1
Changes for core/modules/system/tests/src/Functional/Render/PlaceholderMessageTest.php: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ class PlaceholderMessageTest extends BrowserTestBase {
   * Tests rendering of message placeholder.
   */
  public function testMessagePlaceholder(): void {
    $messages_markup = '<div role="contentinfo" aria-label="Status message"';
    $messages_markup = '<div role="status" aria-label="Status message"';

    $test_routes = [
      // Messages placeholder rendered first.
+1 −7
Changes for core/modules/system/tests/themes/test_messages/templates/status-messages.html.twig: 1 added line, 7 removed lines.
Original line number Diff line number Diff line
@@ -13,10 +13,7 @@
      'messages--' ~ type,
    ]
  %}
  <div role="contentinfo" aria-label="{{ status_headings[type] }}"{{ attributes.addClass(classes)|without('role', 'aria-label') }}>
    {% if type == 'error' %}
      <div role="alert">
    {% endif %}
  <div role="{{ type in ['error', 'warning'] ? 'alert' : 'status' }}" aria-label="{{ status_headings[type] }}"{{ attributes.addClass(classes)|without('role', 'aria-label') }}>
    {% if status_headings[type] %}
      <h2 class="visually-hidden">{{ status_headings[type] }}</h2>
    {% endif %}
@@ -29,9 +26,6 @@
    {% else %}
      {{ messages|first }}
    {% endif %}
    {% if type == 'error' %}
      </div>
    {% endif %}
  </div>
  {# Remove type specific classes. #}
  {% set attributes = attributes.removeClass(classes) %}
Loading