Menus: Accessibility: Improve screen reader text for edit button.

Change the edit menu item toggle to communicate more context about the item to be edited. Make edit text consistent between Customizer menu editor and admin menu editor.

The menu position is conveyed only visually, using indentation, because there are no organizational semantics in either editor. This change helps provide screen reader users with consistent contextual information about the order, position, and parent of the current item.

Props joedolson, rcreators, afercia, mohonchandra.
Fixes #60673, See #60672.
Built from https://develop.svn.wordpress.org/trunk@58306


git-svn-id: http://core.svn.wordpress.org/trunk@57763 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
joedolson 2024-06-03 20:35:13 +00:00
parent 5e65c40921
commit fb7ad212ca
8 changed files with 128 additions and 31 deletions

View File

@ -2,7 +2,7 @@
* @output wp-admin/js/customize-nav-menus.js * @output wp-admin/js/customize-nav-menus.js
*/ */
/* global _wpCustomizeNavMenusSettings, wpNavMenu, console */ /* global menus, _wpCustomizeNavMenusSettings, wpNavMenu, console */
( function( api, wp, $ ) { ( function( api, wp, $ ) {
'use strict'; 'use strict';
@ -1132,6 +1132,8 @@
$( '#menu-to-edit' ).removeAttr( 'id' ); $( '#menu-to-edit' ).removeAttr( 'id' );
wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' ); wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
api.Menus.MenuItemControl.prototype.initAccessibility();
_.each( api.section( section.id ).controls(), function( control ) { _.each( api.section( section.id ).controls(), function( control ) {
if ( 'nav_menu_item' === control.params.type ) { if ( 'nav_menu_item' === control.params.type ) {
control.actuallyEmbed(); control.actuallyEmbed();
@ -1574,6 +1576,80 @@
}; };
}, },
/**
* Set up the initial state of the screen reader accessibility information for menu items.
*
* @since 6.6.0
*/
initAccessibility: function() {
var control = this,
menu = $( '#menu-to-edit' );
// Refresh the accessibility when the user comes close to the item in any way.
menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility', '.menu-item', function(){
control.refreshAdvancedAccessibilityOfItem( $( this ).find( 'button.item-edit' ) );
} );
// We have to update on click as well because we might hover first, change the item, and then click.
menu.on( 'click', 'button.item-edit', function() {
control.refreshAdvancedAccessibilityOfItem( $( this ) );
} );
},
/**
* refreshAdvancedAccessibilityOfItem( [itemToRefresh] )
*
* Refreshes advanced accessibility buttons for one menu item.
* Shows or hides buttons based on the location of the menu item.
*
* @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed
*
* @since 6.6.0
*/
refreshAdvancedAccessibilityOfItem: function( itemToRefresh ) {
// Only refresh accessibility when necessary.
if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) {
return;
}
var primaryItems, itemPosition, title,
parentItem, parentItemId, parentItemName, subItems, totalSubItems,
$this = $( itemToRefresh ),
menuItem = $this.closest( 'li.menu-item' ).first(),
depth = menuItem.menuItemDepth(),
isPrimaryMenuItem = ( 0 === depth ),
itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
menuItemType = $this.closest( '.menu-item-handle' ).find( '.item-type' ).text(),
totalMenuItems = $( '#menu-to-edit li' ).length;
if ( isPrimaryMenuItem ) {
primaryItems = $( '.menu-item-depth-0' ),
itemPosition = primaryItems.index( menuItem ) + 1,
totalMenuItems = primaryItems.length,
// String together help text for primary menu items.
title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalMenuItems );
} else {
parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
parentItemName = parentItem.find( '.menu-item-title' ).text(),
subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
totalSubItems = subItems.length,
itemPosition = $( subItems.parents( '.menu-item' ).get().reverse() ).index( menuItem ) + 1;
// String together help text for sub menu items.
if ( depth < 2 ) {
title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName );
} else {
title = menus.subMenuMoreDepthFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName ).replace( '%6$d', depth );
}
}
$this.find( '.screen-reader-text' ).text( title );
// Mark this item's accessibility as refreshed.
$this.data( 'needs_accessibility_refresh', false );
},
/** /**
* Override the embed() method to do nothing, * Override the embed() method to do nothing,
* so that the control isn't embedded on load, * so that the control isn't embedded on load,
@ -1607,6 +1683,9 @@
} }
control.renderContent(); control.renderContent();
control.deferred.embedded.resolve(); // This triggers control.ready(). control.deferred.embedded.resolve(); // This triggers control.ready().
// Mark all menu items as unprocessed.
$( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
}, },
/** /**
@ -1679,22 +1758,15 @@
control.moveDown(); control.moveDown();
} else if ( isMoveLeft ) { } else if ( isMoveLeft ) {
control.moveLeft(); control.moveLeft();
if ( 1 === control.params.depth ) {
control.container.find( '.is-submenu' ).hide();
} else {
control.container.find( '.is-submenu' ).show();
}
} else if ( isMoveRight ) { } else if ( isMoveRight ) {
control.moveRight(); control.moveRight();
control.params.depth += 1; control.params.depth += 1;
if ( 0 === control.params.depth ) {
control.container.find( '.is-submenu' ).hide();
} else {
control.container.find( '.is-submenu' ).show();
}
} }
moveBtn.focus(); // Re-focus after the container was moved. moveBtn.focus(); // Re-focus after the container was moved.
// Mark all menu items as unprocessed.
$( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
} ); } );
}, },
@ -2726,6 +2798,9 @@
menuItemControl.setting.set( setting ); menuItemControl.setting.set( setting );
}); });
// Mark all menu items as unprocessed.
$( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
}); });
}); });

File diff suppressed because one or more lines are too long

View File

@ -449,12 +449,13 @@
} }
var thisLink, thisLinkText, primaryItems, itemPosition, title, var thisLink, thisLinkText, primaryItems, itemPosition, title,
parentItem, parentItemId, parentItemName, subItems, parentItem, parentItemId, parentItemName, subItems, totalSubItems,
$this = $( itemToRefresh ), $this = $( itemToRefresh ),
menuItem = $this.closest( 'li.menu-item' ).first(), menuItem = $this.closest( 'li.menu-item' ).first(),
depth = menuItem.menuItemDepth(), depth = menuItem.menuItemDepth(),
isPrimaryMenuItem = ( 0 === depth ), isPrimaryMenuItem = ( 0 === depth ),
itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(), itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
menuItemType = $this.closest( '.menu-item-handle' ).find( '.item-controls' ).find( '.item-type' ).text(),
position = parseInt( menuItem.index(), 10 ), position = parseInt( menuItem.index(), 10 ),
prevItemDepth = ( isPrimaryMenuItem ) ? depth : parseInt( depth - 1, 10 ), prevItemDepth = ( isPrimaryMenuItem ) ? depth : parseInt( depth - 1, 10 ),
prevItemNameLeft = menuItem.prevAll('.menu-item-depth-' + prevItemDepth).first().find( '.menu-item-title' ).text(), prevItemNameLeft = menuItem.prevAll('.menu-item-depth-' + prevItemDepth).first().find( '.menu-item-title' ).text(),
@ -503,18 +504,22 @@
primaryItems = $( '.menu-item-depth-0' ), primaryItems = $( '.menu-item-depth-0' ),
itemPosition = primaryItems.index( menuItem ) + 1, itemPosition = primaryItems.index( menuItem ) + 1,
totalMenuItems = primaryItems.length, totalMenuItems = primaryItems.length,
// String together help text for primary menu items. // String together help text for primary menu items.
title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$d', totalMenuItems ); title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalMenuItems );
} else { } else {
parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(), parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(), parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
parentItemName = parentItem.find( '.menu-item-title' ).text(), parentItemName = parentItem.find( '.menu-item-title' ).text(),
subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ), subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
totalSubItems = subItems.length,
itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1; itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1;
// String together help text for sub menu items. // String together help text for sub menu items.
title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$s', parentItemName ); if ( depth < 2 ) {
title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName );
} else {
title = menus.subMenuMoreDepthFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName ).replace( '%6$d', depth );
}
} }
$this.attr( 'aria-label', title ); $this.attr( 'aria-label', title );
@ -732,6 +737,7 @@
api.refreshKeyboardAccessibility(); api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility(); api.refreshAdvancedAccessibility();
api.refreshAdvancedAccessibilityOfItem( ui.item.find( 'a.item-edit' ) );
}, },
change: function(e, ui) { change: function(e, ui) {
// Make sure the placeholder is inside the menu. // Make sure the placeholder is inside the menu.

File diff suppressed because one or more lines are too long

View File

@ -580,10 +580,12 @@ $nav_menus_l10n = array(
'under' => __( 'Under %s' ), 'under' => __( 'Under %s' ),
/* translators: %s: Previous item name. */ /* translators: %s: Previous item name. */
'outFrom' => __( 'Out from under %s' ), 'outFrom' => __( 'Out from under %s' ),
/* translators: 1: Item name, 2: Item position, 3: Total number of items. */ /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items. */
'menuFocus' => __( '%1$s. Menu item %2$d of %3$d.' ), 'menuFocus' => __( 'Edit %1$s (%2$s, %3$d of %4$d)' ),
/* translators: 1: Item name, 2: Item position, 3: Parent item name. */ /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent. */
'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ), 'subMenuFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s)' ),
/* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent, 6: Item depth. */
'subMenuMoreDepthFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s, level %6$d)' ),
/* translators: %s: Item name. */ /* translators: %s: Item name. */
'menuItemDeletion' => __( 'item %s' ), 'menuItemDeletion' => __( 'item %s' ),
/* translators: %s: Item name. */ /* translators: %s: Item name. */

View File

@ -562,10 +562,12 @@ final class WP_Customize_Nav_Menus {
'under' => __( 'Under %s' ), 'under' => __( 'Under %s' ),
/* translators: %s: Previous item name. */ /* translators: %s: Previous item name. */
'outFrom' => __( 'Out from under %s' ), 'outFrom' => __( 'Out from under %s' ),
/* translators: 1: Item name, 2: Item position, 3: Total number of items. */ /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items. */
'menuFocus' => __( '%1$s. Menu item %2$d of %3$d.' ), 'menuFocus' => __( 'Edit %1$s (%2$s, %3$d of %4$d)' ),
/* translators: 1: Item name, 2: Item position, 3: Parent item name. */ /* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent. */
'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ), 'subMenuFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s)' ),
/* translators: 1: Item name, 2: Item type, 3: Item index, 4: Total items, 5: Item parent, 6: Item depth. */
'subMenuMoreDepthFocus' => __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s, level %6$d)' ),
); );
wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n ); wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
} }

View File

@ -77,10 +77,22 @@ class WP_Customize_Nav_Menu_Item_Control extends WP_Customize_Control {
</span> </span>
<span class="item-controls"> <span class="item-controls">
<button type="button" class="button-link item-edit" aria-expanded="false"><span class="screen-reader-text"> <button type="button" class="button-link item-edit" aria-expanded="false"><span class="screen-reader-text">
<# if ( 0 === data.depth ) { #>
<?php <?php
/* translators: 1: Title of a menu item, 2: Type of a menu item. */ /* translators: 1: Title of a menu item, 2: Type of a menu item. 3: Item index, 4: Total items. */
printf( __( 'Edit menu item: %1$s (%2$s)' ), '{{ data.title || data.original_title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.item_type_label }}' ); printf( __( 'Edit %1$s (%2$s, %3$d of %4$d)' ), '{{ data.title || data.original_title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.item_type_label }}', '', '' );
?> ?>
<# } else if ( 1 === data.depth ) { #>
<?php
/* translators: 1: Title of a menu item, 2: Type of a menu item, 3, Item index, 4, Total items, 5: Item parent. */
printf( __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s)' ), '{{ data.title || data.original_title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.item_type_label }}', '', '', '' );
?>
<# } else { #>
<?php
/* translators: 1: Title of a menu item, 2: Type of a menu item, 3, Item index, 4, Total items, 5: Item parent, 6: Item depth. */
printf( __( 'Edit %1$s (%2$s, sub-item %3$d of %4$d under %5$s, level %6$s)' ), '{{ data.title || data.original_title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.item_type_label }}', '', '', '', '{{data.depth}}' );
?>
<# } #>
</span><span class="toggle-indicator" aria-hidden="true"></span></button> </span><span class="toggle-indicator" aria-hidden="true"></span></button>
<button type="button" class="button-link item-delete submitdelete deletion"><span class="screen-reader-text"> <button type="button" class="button-link item-delete submitdelete deletion"><span class="screen-reader-text">
<?php <?php

View File

@ -16,7 +16,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '6.6-alpha-58305'; $wp_version = '6.6-alpha-58306';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.