diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index ef4b47e66093e..02007e1d8f6ab 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -387,6 +387,23 @@ $dashboardNavMenuUpdateCount.remove(); } + /* + * Keep the menu item's hidden count description in sync. The visible + * count bubble is aria-hidden, so this description is what assistive + * technologies announce via aria-describedby. + */ + if ( settings.totals.counts.total > 0 ) { + $( '#wp-menu-updates-count-description' ).text( + sprintf( + /* translators: %s: Number of updates available. */ + _n( '%s update available', '%s updates available', settings.totals.counts.total ), + settings.totals.counts.total + ) + ); + } else { + $( '#wp-menu-updates-count-description' ).text( '' ); + } + // Update the "Plugins" menu item. $pluginsNavMenuUpdateCount.each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins ); @@ -397,6 +414,19 @@ $pluginsNavMenuUpdateCount.remove(); } + // Keep the menu item's hidden count description in sync. See above. + if ( settings.totals.counts.plugins > 0 ) { + $( '#wp-menu-plugins-count-description' ).text( + sprintf( + /* translators: %s: Number of available plugin updates. */ + _n( '%s plugin update available', '%s plugin updates available', settings.totals.counts.plugins ), + settings.totals.counts.plugins + ) + ); + } else { + $( '#wp-menu-plugins-count-description' ).text( '' ); + } + // Update the "Appearance" menu item. $appearanceNavMenuUpdateCount.each( function( index, element ) { element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes ); @@ -407,6 +437,19 @@ $appearanceNavMenuUpdateCount.remove(); } + // Keep the menu item's hidden count description in sync. See above. + if ( settings.totals.counts.themes > 0 ) { + $( '#wp-menu-themes-count-description' ).text( + sprintf( + /* translators: %s: Number of available theme updates. */ + _n( '%s theme update available', '%s theme updates available', settings.totals.counts.themes ), + settings.totals.counts.themes + ) + ); + } else { + $( '#wp-menu-themes-count-description' ).text( '' ); + } + // Update list table filter navigation. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { itemCount = settings.totals.counts.plugins; diff --git a/src/wp-admin/menu-header.php b/src/wp-admin/menu-header.php index 5cced0ceaed96..a639e9e1824ac 100644 --- a/src/wp-admin/menu-header.php +++ b/src/wp-admin/menu-header.php @@ -75,6 +75,7 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { $first = true; // 0 = menu_title, 1 = capability, 2 = menu_slug, 3 = page_title, 4 = classes, 5 = hookname, 6 = icon_url. + // Optional 'count_description' = array( 'id' => string, 'html' => string ) for a counter's hidden description. foreach ( $menu as $key => $item ) { $admin_is_parent = false; $class = array(); @@ -145,6 +146,23 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { $title = wptexturize( $item[0] ); + /* + * When a menu item carries a hidden count description (e.g. pending + * updates or comments awaiting moderation), associate it with the link + * via aria-describedby and render it inside the link. This keeps the + * count out of the link's accessible name so voice control users can + * operate it by its visible label, while the count is still announced by + * assistive technologies. The id and markup are supplied explicitly by + * the menu item's 'count_description' entry rather than parsed from the + * title, so the association does not depend on the title's HTML shape. + */ + $describedby = ''; + $count_description = ''; + if ( ! empty( $item['count_description']['id'] ) ) { + $describedby = ' aria-describedby="' . esc_attr( $item['count_description']['id'] ) . '"'; + $count_description = $item['count_description']['html']; + } + // Hide separators from screen readers. if ( $is_separator ) { $aria_hidden = ' aria-hidden="true"'; @@ -170,9 +188,9 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { && ! file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) ) { $admin_is_parent = true; - echo ""; + echo ""; } else { - echo "\n\t"; + echo "\n\t"; } } elseif ( ! empty( $item[2] ) && current_user_can( $item[1] ) ) { $menu_hook = get_plugin_page_hook( $item[2], 'admin.php' ); @@ -189,19 +207,26 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { && ! file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) ) { $admin_is_parent = true; - echo "\n\t"; + echo "\n\t"; } else { - echo "\n\t"; + echo "\n\t"; } } if ( ! empty( $submenu_items ) ) { echo "\n\t
'; diff --git a/src/wp-admin/menu.php b/src/wp-admin/menu.php index 2a3430ebc3df8..45c345340737d 100644 --- a/src/wp-admin/menu.php +++ b/src/wp-admin/menu.php @@ -49,20 +49,36 @@ $capability = 'update_languages'; } + $updates_count = sprintf( + '', + $update_data['counts']['total'], + number_format_i18n( $update_data['counts']['total'] ) + ); + + $updates_text = sprintf( + /* translators: Hidden accessibility text. %s: Number of updates available. */ + _n( '%s update available', '%s updates available', $update_data['counts']['total'] ), + number_format_i18n( $update_data['counts']['total'] ) + ); + + $updates_description = ''; + $submenu['index.php'][10] = array( sprintf( /* translators: %s: Number of pending updates. */ __( 'Updates %s' ), - sprintf( - '%s', - $update_data['counts']['total'], - number_format_i18n( $update_data['counts']['total'] ) - ) + $updates_count ), $capability, 'update-core.php', ); + // Associate the hidden count description with the link. See _wp_menu_output(). + $submenu['index.php'][10]['count_description'] = array( + 'id' => 'wp-menu-updates-count-description', + 'html' => $updates_description, + ); + unset( $capability ); } @@ -103,8 +119,11 @@ $awaiting_moderation_text = sprintf( _n( '%s Comment in moderation', '%s Comments in moderation', $awaiting_moderation ), $awaiting_moderation_i18n ); $menu[25] = array( - /* translators: %s: Number of comments. */ - sprintf( __( 'Comments %s' ), '' . $awaiting_moderation_text . '' ), + sprintf( + /* translators: %s: Number of comments. */ + __( 'Comments %s' ), + '' + ), 'edit_posts', 'edit-comments.php', '', @@ -113,6 +132,12 @@ 'dashicons-admin-comments', ); + // Associate the hidden count description with the link. See _wp_menu_output(). + $menu[25]['count_description'] = array( + 'id' => 'wp-menu-comments-count-description', + 'html' => '', + ); + unset( $awaiting_moderation ); } @@ -208,22 +233,39 @@ $menu[60] = array( __( 'Appearance' ), $appearance_capability, 'themes.php', '', 'menu-top menu-icon-appearance', 'menu-appearance', 'dashicons-admin-appearance' ); -$count = ''; +$count = ''; +$description = ''; if ( ! is_multisite() && current_user_can( 'update_themes' ) ) { if ( ! isset( $update_data ) ) { $update_data = wp_get_update_data(); } $count = sprintf( - '%s', + '', $update_data['counts']['themes'], number_format_i18n( $update_data['counts']['themes'] ) ); + + $themes_text = sprintf( + /* translators: Hidden accessibility text. %s: Number of available theme updates. */ + _n( '%s theme update available', '%s theme updates available', $update_data['counts']['themes'] ), + number_format_i18n( $update_data['counts']['themes'] ) + ); + + $description = ''; } /* translators: %s: Number of available theme updates. */ $submenu['themes.php'][5] = array( sprintf( __( 'Themes %s' ), $count ), $appearance_capability, 'themes.php' ); + // Associate the hidden count description with the link. See _wp_menu_output(). +if ( '' !== $description ) { + $submenu['themes.php'][5]['count_description'] = array( + 'id' => 'wp-menu-themes-count-description', + 'html' => $description, + ); +} + if ( wp_is_block_theme() ) { $submenu['themes.php'][6] = array( _x( 'Editor', 'site editor menu item' ), 'edit_theme_options', 'site-editor.php' ); } else { @@ -307,21 +349,38 @@ function _add_plugin_file_editor_to_tools() { ); } -$count = ''; +$count = ''; +$description = ''; if ( ! is_multisite() && current_user_can( 'update_plugins' ) ) { if ( ! isset( $update_data ) ) { $update_data = wp_get_update_data(); } $count = sprintf( - '%s', + '', $update_data['counts']['plugins'], number_format_i18n( $update_data['counts']['plugins'] ) ); + + $plugins_text = sprintf( + /* translators: Hidden accessibility text. %s: Number of available plugin updates. */ + _n( '%s plugin update available', '%s plugin updates available', $update_data['counts']['plugins'] ), + number_format_i18n( $update_data['counts']['plugins'] ) + ); + + $description = ''; } /* translators: %s: Number of available plugin updates. */ $menu[65] = array( sprintf( __( 'Plugins %s' ), $count ), 'activate_plugins', 'plugins.php', '', 'menu-top menu-icon-plugins', 'menu-plugins', 'dashicons-admin-plugins' ); +// Associate the hidden count description with the link. See _wp_menu_output(). +if ( '' !== $description ) { + $menu[65]['count_description'] = array( + 'id' => 'wp-menu-plugins-count-description', + 'html' => $description, + ); +} + $submenu['plugins.php'][5] = array( __( 'Installed Plugins' ), 'activate_plugins', 'plugins.php' ); if ( ! is_multisite() ) { @@ -362,7 +421,8 @@ function _add_plugin_file_editor_to_tools() { } } -$site_health_count = ''; +$site_health_count = ''; +$site_health_description = ''; if ( ! is_multisite() && current_user_can( 'view_site_health_checks' ) ) { $get_issues = get_transient( 'health-check-site-status-result' ); @@ -381,10 +441,18 @@ function _add_plugin_file_editor_to_tools() { } $site_health_count = sprintf( - '', + '', $issue_counts['critical'], number_format_i18n( $issue_counts['critical'] ) ); + + $site_health_text = sprintf( + /* translators: Hidden accessibility text. %s: Number of critical Site Health checks. */ + _n( '%s critical issue', '%s critical issues', $issue_counts['critical'] ), + number_format_i18n( $issue_counts['critical'] ) + ); + + $site_health_description = ''; } $menu[75] = array( __( 'Tools' ), 'edit_posts', 'tools.php', '', 'menu-top menu-icon-tools', 'menu-tools', 'dashicons-admin-tools' ); @@ -393,6 +461,14 @@ function _add_plugin_file_editor_to_tools() { $submenu['tools.php'][15] = array( __( 'Export' ), 'export', 'export.php' ); /* translators: %s: Number of critical Site Health checks. */ $submenu['tools.php'][20] = array( sprintf( __( 'Site Health %s' ), $site_health_count ), 'view_site_health_checks', 'site-health.php' ); + + // Associate the hidden count description with the link. See _wp_menu_output(). +if ( '' !== $site_health_description ) { + $submenu['tools.php'][20]['count_description'] = array( + 'id' => 'wp-menu-site-health-count-description', + 'html' => $site_health_description, + ); +} $submenu['tools.php'][25] = array( __( 'Export Personal Data' ), 'export_others_personal_data', 'export-personal-data.php' ); $submenu['tools.php'][30] = array( __( 'Erase Personal Data' ), 'erase_others_personal_data', 'erase-personal-data.php' ); if ( is_multisite() && ! is_main_site() && '1' !== get_site()->deleted ) { diff --git a/src/wp-includes/admin-bar.php b/src/wp-includes/admin-bar.php index 99bb122ec4c36..926ee6fd9c78a 100644 --- a/src/wp-includes/admin-bar.php +++ b/src/wp-includes/admin-bar.php @@ -1134,13 +1134,17 @@ function wp_admin_bar_comments_menu( $wp_admin_bar ) { $icon = ''; $title = ''; - $title .= '' . $awaiting_text . ''; + $title .= '' . __( 'Comments' ) . ''; + $title .= ''; $wp_admin_bar->add_node( array( 'id' => 'comments', 'title' => $icon . $title, 'href' => admin_url( 'edit-comments.php' ), + 'meta' => array( + 'aria-describedby' => 'wp-admin-bar-comments-count-description', + ), ) ); } @@ -1249,13 +1253,17 @@ function wp_admin_bar_updates_menu( $wp_admin_bar ) { $icon = ''; $title = ''; - $title .= '' . $updates_text . ''; + $title .= '' . __( 'Updates' ) . ''; + $title .= ''; $wp_admin_bar->add_node( array( 'id' => 'updates', 'title' => $icon . $title, 'href' => network_admin_url( 'update-core.php' ), + 'meta' => array( + 'aria-describedby' => 'wp-admin-bar-updates-count-description', + ), ) ); } diff --git a/src/wp-includes/class-wp-admin-bar.php b/src/wp-includes/class-wp-admin-bar.php index 0cf2cad3fcf24..5fa9ede5eb242 100644 --- a/src/wp-includes/class-wp-admin-bar.php +++ b/src/wp-includes/class-wp-admin-bar.php @@ -118,7 +118,8 @@ public function remove_menu( $id ) { * @type string $href Optional. Link for the item. * @type bool $group Optional. Whether or not the node is a group. Default false. * @type array $meta Meta data including the following keys: 'html', 'class', 'rel', 'lang', 'dir', - * 'onclick', 'target', 'title', 'tabindex', 'menu_title'. Default empty. + * 'onclick', 'target', 'title', 'tabindex', 'menu_title', 'aria-describedby'. + * Default empty. * } */ public function add_node( $args ) { @@ -577,10 +578,10 @@ final protected function _render_item( $node ) { echo "