Skip to content

error-message rule should exclude elements referenced by aria-controls #394

@koddsson

Description

@koddsson

Description

The error-message rule flags elements that are clearly interactive UI widgets (disclosure panels, popup menus) as potential unassociated error messages, because it only excludes elements by tag name (structuralSelector) or ARIA widget roles (widgetRoles).

Elements that are referenced by aria-controls on a button/trigger should also be excluded, since they are interactive widget targets — not error message containers.

Example

A disclosure button that toggles a filter panel:

<form>
  <button type="button"
          aria-expanded="false"
          aria-controls="dropdown-menu-filters">
    Filters
  </button>

  <div id="dropdown-menu-filters"
       aria-labelledby="dropdown-button-filters">
    <ul>
      <li>
        <input type="checkbox" id="filter-1" />
        <label for="filter-1">Option 1</label>
      </li>
      <li>
        <input type="checkbox" id="filter-2" />
        <label for="filter-2">Option 2</label>
      </li>
    </ul>
  </div>
</form>

The div#dropdown-menu-filters gets flagged by the error-message rule because:

  1. It has an id
  2. It has text content
  3. It's inside a <form>
  4. It doesn't match structuralSelector or have a widget role
  5. It isn't referenced by aria-describedby or aria-errormessage

But it IS referenced by aria-controls on the button, which clearly indicates it's an interactive widget target.

Suggested fix

In findCandidateErrorElements, also collect IDs referenced by aria-controls and exclude those elements:

function collectControlledIds(container: Element): Set<string> {
  const ids = new Set<string>();
  const allElements = querySelectorAll("[aria-controls]", container);
  for (const el of allElements) {
    const value = el.getAttribute("aria-controls");
    if (value) {
      for (const token of value.split(/\s+/)) {
        if (token) ids.add(token);
      }
    }
  }
  return ids;
}

Then in the main function, exclude candidates whose ID appears in the controlled set.

Related

This is a follow-up to #389 which added widget role exclusions. This addresses the remaining case where the element doesn't have a widget role but is clearly an interactive target.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions