Make WordPress Core

Opened 5 weeks ago

Last modified 4 weeks ago

#65793 new defect (bug)

Clean up the accessible name of menu and admin bar items with counters

Reported by: afercia Owned by:
Priority: normal Milestone: 7.2
Component: Administration Version:
Severity: normal Keywords: has-screenshots has-patch has-unit-tests
Cc: Focuses: accessibility

Description

In the admin menu and in the admin bar, some items report a count that is visually presented with a number within a circle. See attached scdeenshot.

While the visual presentation is similar, the actual accessible name (the text content) of these items is inconsistent and can't be operated with voice control.

  • The visible label doesn't match the actual text content.
  • The visually hidden text of the counters is inconsistent.

Some examples of the actual text content:

In the admin menu:

  • Updates 2
  • Comments 3 Comments in moderation
  • Themes 1
  • Plugins 1

In the admin bar:

  • 2 updates available
  • 3 Comments in moderation

While this has been already reported in broader tickets related to the menus, this ticket aims to improve specifically these links labels.

I'd like to propose:

Standardize the text content of all these links to the name of the item, e;g;:

  • Updates
  • Comments
  • Themes
  • Plugins

Move the information about pending updates, comments in moderation etx to a visually hidden element referenced by aria-describedby.

This way:

  • All the menu items accessible names would be simpliied and consistent.
  • Voice control users would be able to activate the links with a voice command, as the visibel text (excluding the counter) would match the accessible name.

Attachments (1)

01 example.png (13.3 KB ) - added by afercia 5 weeks ago.

Download all attachments as: .zip

Change History (8)

@afercia
5 weeks ago

This ticket was mentioned in Slack in #accessibility by joedolson. View the logs.


5 weeks ago

#2 @joedolson
5 weeks ago

  • Milestone Awaiting Review7.2

This ticket was mentioned in PR #12850 on WordPress/wordpress-develop by @sanket.parmar.


5 weeks ago
#3

  • Keywords has-patch has-unit-tests added

In the admin menu and toolbar, several items show a count in a circle (Updates, Comments, Themes, Plugins, Site Health). Today the count is exposed inconsistently in each item's accessible name — Updates/Themes/Plugins/Site Health leak the raw number (*"Updates 2"*), Comments folds descriptive text into the name (*"Comments 3 Comments in moderation"*), and the icon-only toolbar items use the count text as the whole name. The result: the accessible name doesn't match the visible label, so voice-control users can't activate the item by name, and announcements are inconsistent.

This standardizes them so each link's accessible name is just the item name (*"Updates"*, *"Comments"*, …) and the count is exposed as a description via aria-describedby, per the approach in the ticket.

How it works — for every counter: the visible bubble is aria-hidden="true"; the count text lives in a visually-hidden screen-reader-text element with a unique id; the link references it with aria-describedby.

  • Admin menu (menu.php): each counter title carries a marker description span; menu-header.php generically lifts its id onto the link (top-level *and* submenu anchors) and strips the span from the repeated wp-submenu-head so the id is never duplicated.
  • Toolbar (admin-bar.php): Comments/Updates get an explicit hidden name plus a described-by count span; class-wp-admin-bar.php allows aria-describedby as a node meta attribute.

JS-updated classes (comments-in-moderation-text, updates-available-text, pending-count, *-count) are preserved so live count updates keep working.

How to test

  1. As an admin, load the Dashboard.
  2. Inspect the menu links for Updates, Comments, Themes, Plugins, Tools → Site Health, and the toolbar Comments/Updates items.
  3. Confirm each link has aria-describedby → a hidden screen-reader-text span, the number span is aria-hidden="true", and each description id appears exactly once in the DOM.
  4. With a screen reader, confirm the link is announced as its name plus the count as a description.

Automated teststests/phpunit/tests/admin/wpMenuOutput.php (new, describedby wiring + duplicate-id regression) and additions to tests/phpunit/tests/adminbar.php (toolbar + meta whitelist).

Open questions — exact wording of the new hidden strings; and the menu Updates/Themes/Plugins descriptions aren't yet refreshed by the AJAX count updater (Comments and toolbar are), so a follow-up would keep the described count from going stale.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Root cause analysis, locating the affected menu/toolbar rendering paths, initial patch and test implementation; final implementation, accessibility markup review, and correctness of the capability guards reviewed and edited by me.

#4 @sanket.parmar
5 weeks ago

I put up a proof-of-concept PR for this: https://github.com/WordPress/wordpress-develop/pull/12850

It follows the approach from the description — standardize every counter (Updates, Comments, Themes, Plugins, Site Health, plus the toolbar Comments/Updates) so the link's accessible name is just the item name and the count is moved to a hidden element referenced by aria-describedby.

Mechanism (consistent across all of them):

  • The visible count bubble gets aria-hidden="true".
  • The count text lives in a visually-hidden screen-reader-text element with a unique id.
  • The link references that element with aria-describedby.

Implementation notes / gotchas I ran into:

  • In the admin menu the count text is part of the menu title string, but aria-describedby has to land on the <a>, which is rendered generically in menu-header.php. So the title carries a marker description span and menu-header.php lifts its id onto the link — this covers both top-level and submenu anchors.
  • menu-header.php repeats the top-level title in the wp-submenu-head element, which duplicated the description id. The PR strips the description span from that repeated copy so each id stays unique.
  • The toolbar items are icon-only, so removing the count from the name would leave them unnamed — the PR adds an explicit hidden name (e.g. "Comments") in addition to the described-by count.
  • WP_Admin_Bar only renders a fixed allowlist of anchor attributes, so aria-describedby was added to that allowlist.
  • Classes that JS live-updates (comments-in-moderation-text, updates-available-text, pending-count, *-count) are preserved so dynamic count updates keep working.

Two things I'd like input on before polishing:

  1. Wording of the new hidden strings (e.g. %s plugin updates available, %s critical issues) — happy to match whatever the a11y team prefers.
  2. For the menu Updates/Themes/Plugins items, the visible number is refreshed by the AJAX update checker but the new described text is not (Comments and the toolbar items already update, since they keep their existing JS-target classes). Should the JS be extended to also update the description in this ticket, or is that better as a follow-up?

@afercia commented on PR #12850:


5 weeks ago
#5

I asked Copilot to review this PR. I see potential problems both in the PR _and_ in the Copilot review.

  • I'm concerned about the regex approach. I doesn't seem solid to me and that's up to Claude, used for the initial implementation of this PR.
  • Copilot completely ignored this potential problem.
  • Instead, it repeatedly warned about the element referenced by aria-describedby being hidden from assistive technology by the means of aria-hidden. That is plain wrong. The content of elements referenced by aria-describedby is exposed in the accessibility tree even if the element is aria-hidden and even if it's totally hidden with display: none.

So it appears that, at least in this PR, usage of AI is not only misleading but also unnecessarily time consuming.

@sanket.parmar commented on PR #12850:


5 weeks ago
#6

Thanks @afercia — you're right on both points.

On the Copilot flags: agreed, they're incorrect. An element referenced by aria-describedby is included in the description computation even when it's aria-hidden (or display:none) — per the Accessible Name and Description Computation, a hidden node that's directly referenced isn't skipped. The aria-hidden on the description span is deliberate: it sits inside the link, so aria-hidden keeps its text out of the link's accessible *name* while aria-describedby still exposes it as the *description*. So there's nothing to action there.

On the regex: agreed, that's the fragile part. I've reworked menu-header.php so the description id is passed through explicitly (a count_description entry on the menu item) rather than parsed out of the title HTML — the association no longer depends on attribute order or a plugin's markup shape, and the submenu-head dedup regex is gone too.

Separately, @irozum flagged a real regression I've now fixed in the same push: with the visible bubble aria-hidden, the Updates/Themes/Plugins menu descriptions would go stale after an AJAX update — wp.updates.refreshCount() now keeps them in sync (and clears them at zero).

@afercia commented on PR #12850:


4 weeks ago
#7

Let's see if Copilot gets it right now. I'm curious.

Note: See TracTickets for help on using tickets.