Menus: A11y: Set the parent and order using select fields.

Add `select` inputs to allow users to set the parent and position of items in the menu settings. Fixes a significant problem for screen reader users that makes updating menus extremely tedious, since the options for moving items do not explicitly set a position. This is also a significant improvement for all users manipulating large menus.

This could easily be considered an enhancement, but while it is a minor enhancement for most users, it is transformative for screen reader users in managing menus, moving that interface from nearly unusable to very manageable.

Props javad2000, audrasjb, juliemoynat, williamalexander, rcreators, milamj, joedolson. 
Fixes #43305.
Built from https://develop.svn.wordpress.org/trunk@59265


git-svn-id: http://core.svn.wordpress.org/trunk@58657 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
joedolson 2024-10-21 19:55:16 +00:00
parent bdea1930c7
commit 4b347a2e56
9 changed files with 288 additions and 56 deletions

View File

@ -828,22 +828,13 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
display: none; display: none;
} }
.menu-item-settings .description-thin, .description-group {
.menu-item-settings .description-wide { display: flex;
margin-left: 10px; column-gap: 10px;
float: right;
} }
.description-thin { .description-group > * {
width: calc(50% - 5px); flex-grow: 1;
}
.menu-item-settings .description-thin + .description-thin {
margin-left: 0;
}
.description-wide {
width: 100%;
} }
.menu-item-actions { .menu-item-actions {
@ -952,8 +943,7 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
} }
.menu-item-bar .menu-item-handle, .menu-item-bar .menu-item-handle,
.menu-item-settings, .menu-item-settings {
.description-wide {
width: auto; width: auto;
} }
@ -961,9 +951,8 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
padding: 10px; padding: 10px;
} }
.menu-item-settings .description-thin, .menu-item-settings .description-group {
.menu-item-settings .description-wide { display: block;
width: 100%;
} }
.menu-item-settings input { .menu-item-settings input {

File diff suppressed because one or more lines are too long

View File

@ -827,22 +827,13 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
display: none; display: none;
} }
.menu-item-settings .description-thin, .description-group {
.menu-item-settings .description-wide { display: flex;
margin-right: 10px; column-gap: 10px;
float: left;
} }
.description-thin { .description-group > * {
width: calc(50% - 5px); flex-grow: 1;
}
.menu-item-settings .description-thin + .description-thin {
margin-right: 0;
}
.description-wide {
width: 100%;
} }
.menu-item-actions { .menu-item-actions {
@ -951,8 +942,7 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
} }
.menu-item-bar .menu-item-handle, .menu-item-bar .menu-item-handle,
.menu-item-settings, .menu-item-settings {
.description-wide {
width: auto; width: auto;
} }
@ -960,9 +950,8 @@ body.menu-max-depth-11 { min-width: 1280px !important; }
padding: 10px; padding: 10px;
} }
.menu-item-settings .description-thin, .menu-item-settings .description-group {
.menu-item-settings .description-wide { display: block;
width: 100%;
} }
.menu-item-settings input { .menu-item-settings input {

File diff suppressed because one or more lines are too long

View File

@ -218,18 +218,20 @@ class Walker_Nav_Menu_Edit extends Walker_Nav_Menu {
<?php _e( 'Open link in a new tab' ); ?> <?php _e( 'Open link in a new tab' ); ?>
</label> </label>
</p> </p>
<p class="field-css-classes description description-thin"> <div class="description-group">
<label for="edit-menu-item-classes-<?php echo $item_id; ?>"> <p class="field-css-classes description description-thin">
<?php _e( 'CSS Classes (optional)' ); ?><br /> <label for="edit-menu-item-classes-<?php echo $item_id; ?>">
<input type="text" id="edit-menu-item-classes-<?php echo $item_id; ?>" class="widefat code edit-menu-item-classes" name="menu-item-classes[<?php echo $item_id; ?>]" value="<?php echo esc_attr( implode( ' ', $menu_item->classes ) ); ?>" /> <?php _e( 'CSS Classes (optional)' ); ?><br />
</label> <input type="text" id="edit-menu-item-classes-<?php echo $item_id; ?>" class="widefat code edit-menu-item-classes" name="menu-item-classes[<?php echo $item_id; ?>]" value="<?php echo esc_attr( implode( ' ', $menu_item->classes ) ); ?>" />
</p> </label>
<p class="field-xfn description description-thin"> </p>
<label for="edit-menu-item-xfn-<?php echo $item_id; ?>"> <p class="field-xfn description description-thin">
<?php _e( 'Link Relationship (XFN)' ); ?><br /> <label for="edit-menu-item-xfn-<?php echo $item_id; ?>">
<input type="text" id="edit-menu-item-xfn-<?php echo $item_id; ?>" class="widefat code edit-menu-item-xfn" name="menu-item-xfn[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->xfn ); ?>" /> <?php _e( 'Link Relationship (XFN)' ); ?><br />
</label> <input type="text" id="edit-menu-item-xfn-<?php echo $item_id; ?>" class="widefat code edit-menu-item-xfn" name="menu-item-xfn[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->xfn ); ?>" />
</p> </label>
</p>
</div>
<p class="field-description description description-wide"> <p class="field-description description description-wide">
<label for="edit-menu-item-description-<?php echo $item_id; ?>"> <label for="edit-menu-item-description-<?php echo $item_id; ?>">
<?php _e( 'Description' ); ?><br /> <?php _e( 'Description' ); ?><br />
@ -238,6 +240,31 @@ class Walker_Nav_Menu_Edit extends Walker_Nav_Menu {
</label> </label>
</p> </p>
<?php
/**
* Update parent and order of menu item using select inputs.
*
* @since 6.7.0
*/
?>
<div class="field-move-combo description-group">
<p class="description description-wide">
<label for="edit-menu-item-parent-<?php echo $item_id; ?>">
<?php _e( 'Menu Parent' ); ?>
</label>
<select class="edit-menu-item-parent widefat" id="edit-menu-item-parent-<?php echo $item_id; ?>" name="menu-item-parent[<?php echo $item_id; ?>]">
</select>
</p>
<p class="description description-wide">
<label for="edit-menu-item-order-<?php echo $item_id; ?>">
<?php _e( 'Menu Order' ); ?>
</label>
<select class="edit-menu-item-order widefat" id="edit-menu-item-order-<?php echo $item_id; ?>" name="menu-item-order[<?php echo $item_id; ?>]">
</select>
</p>
</div>
<?php <?php
/** /**
* Fires just before the move buttons of a nav menu item in the menu editor. * Fires just before the move buttons of a nav menu item in the menu editor.
@ -320,4 +347,4 @@ class Walker_Nav_Menu_Edit extends Walker_Nav_Menu {
<?php <?php
$output .= ob_get_clean(); $output .= ob_get_clean();
} }
} }

View File

@ -216,6 +216,8 @@
checkboxes.prop( 'checked', false ); checkboxes.prop( 'checked', false );
t.find( '.button-controls .select-all' ).prop( 'checked', false ); t.find( '.button-controls .select-all' ).prop( 'checked', false );
t.find( '.button-controls .spinner' ).removeClass( 'is-active' ); t.find( '.button-controls .spinner' ).removeClass( 'is-active' );
t.updateParentDropdown();
t.updateOrderDropdown();
}); });
}); });
}, },
@ -288,6 +290,105 @@
}); });
}); });
return this; return this;
},
updateParentDropdown : function() {
return this.each(function(){
var menuItems = $( '#menu-to-edit li' ),
parentDropdowns = $( '.edit-menu-item-parent' );
$.each( parentDropdowns, function() {
var parentDropdown = $( this ),
$html = '',
$selected = '',
currentItemID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val(),
currentparentID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val(),
currentItem = parentDropdown.closest( 'li.menu-item' ),
currentMenuItemChild = currentItem.childMenuItems(),
excludeMenuItem = [ currentItemID ];
if ( currentMenuItemChild.length > 0 ) {
$.each( currentMenuItemChild, function(){
var childItem = $(this),
childID = childItem.find( '.menu-item-data-db-id' ).val();
excludeMenuItem.push( childID );
});
}
if ( currentparentID == 0 ) {
$selected = 'selected';
}
$html += '<option ' + $selected + ' value="0">No Parent</option>';
$.each( menuItems, function() {
var menuItem = $(this),
$selected = '',
menuID = menuItem.find( '.menu-item-data-db-id' ).val(),
menuTitle = menuItem.find( '.edit-menu-item-title' ).val();
if ( ! excludeMenuItem.includes( menuID ) ) {
if ( currentparentID == menuID ) {
$selected = 'selected';
}
$html += '<option ' + $selected + ' value="' + menuID + '">' + menuTitle + '</option>';
}
});
parentDropdown.html( $html );
});
});
},
updateOrderDropdown : function() {
return this.each( function() {
var itemPosition,
orderDropdowns = $( '.edit-menu-item-order' );
$.each( orderDropdowns, function() {
var orderDropdown = $( this ),
menuItem = orderDropdown.closest( 'li.menu-item' ).first(),
depth = menuItem.menuItemDepth(),
isPrimaryMenuItem = ( 0 === depth ),
$html = '',
$selected = '';
if ( isPrimaryMenuItem ) {
var primaryItems = $( '.menu-item-depth-0' ),
totalMenuItems = primaryItems.length;
itemPosition = primaryItems.index( menuItem ) + 1;
for ( let i = 1; i < totalMenuItems + 1; i++ ) {
$selected = '';
if ( i == itemPosition ) {
$selected = 'selected';
}
$html += '<option ' + $selected + ' value="' + i + '">' + i + ' of ' + totalMenuItems + '</option>';
}
} else {
var parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
totalSubMenuItems = subItems.length;
itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1;
for ( let i = 1; i < totalSubMenuItems + 1; i++ ) {
$selected = '';
if ( i == itemPosition ) {
$selected = 'selected';
}
$html += '<option ' + $selected + ' value="' + i + '">' + i + ' of ' + totalSubMenuItems + '</option>';
}
}
orderDropdown.html( $html );
});
});
} }
}); });
}, },
@ -297,7 +398,6 @@
}, },
moveMenuItem : function( $this, dir ) { moveMenuItem : function( $this, dir ) {
var items, newItemPosition, newDepth, var items, newItemPosition, newDepth,
menuItems = $( '#menu-to-edit li' ), menuItems = $( '#menu-to-edit li' ),
menuItemsCount = menuItems.length, menuItemsCount = menuItems.length,
@ -400,6 +500,8 @@
api.registerChange(); api.registerChange();
api.refreshKeyboardAccessibility(); api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility(); api.refreshAdvancedAccessibility();
thisItem.updateParentDropdown();
thisItem.updateOrderDropdown();
if ( a11ySpeech ) { if ( a11ySpeech ) {
wp.a11y.speak( a11ySpeech ); wp.a11y.speak( a11ySpeech );
@ -431,6 +533,123 @@
api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), dir ); api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), dir );
} }
}); });
// Set menu parents data for all menu items.
menu.updateParentDropdown();
// Set menu order data for all menu items.
menu.updateOrderDropdown();
// Update menu item parent when value is changed.
menu.on( 'change', '.edit-menu-item-parent', function() {
api.changeMenuParent( $( this ) );
});
// Update menu item order when value is changed.
menu.on( 'change', '.edit-menu-item-order', function() {
api.changeMenuOrder( $( this ) );
});
},
/**
* changeMenuParent( [parentDropdown] )
*
* @since 6.7.0
*
* @param {object} parentDropdown select field
*/
changeMenuParent : function( parentDropdown ) {
var menuItemNewPosition,
menuItems = $( '#menu-to-edit li' ),
$this = $( parentDropdown ),
newParentID = $this.val(),
menuItem = $this.closest( 'li.menu-item' ).first(),
menuItemOldDepth = menuItem.menuItemDepth(),
menuItemChildren = menuItem.childMenuItems(),
menuItemNoChildren = parseInt( menuItem.childMenuItems().length, 10 ),
parentItem = $( '#menu-item-' + newParentID ),
parentItemDepth = parentItem.menuItemDepth(),
menuItemNewDepth = parseInt( parentItemDepth ) + 1;
if ( newParentID == 0 ) {
menuItemNewDepth = 0;
}
menuItem.find( '.menu-item-data-parent-id' ).val( newParentID );
menuItem.moveHorizontally( menuItemNewDepth, menuItemOldDepth );
if ( menuItemNoChildren > 0 ) {
menuItem = menuItem.add( menuItemChildren );
}
menuItem.detach();
menuItems = $( '#menu-to-edit li' );
var parentItemPosition = parseInt( parentItem.index(), 10 ),
parentItemNoChild = parseInt( parentItem.childMenuItems().length, 10 );
if ( parentItemNoChild > 0 ){
menuItemNewPosition = parentItemPosition + parentItemNoChild;
} else {
menuItemNewPosition = parentItemPosition;
}
if ( newParentID == 0 ) {
menuItemNewPosition = menuItems.length - 1;
}
menuItem.insertAfter( menuItems.eq( menuItemNewPosition ) ).updateParentMenuItemDBId().updateParentDropdown().updateOrderDropdown();
api.registerChange();
api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility();
$this.trigger( 'focus' );
wp.a11y.speak( menus.parentUpdated, 'polite' );
},
/**
* changeMenuOrder( [OrderDropdown] )
*
* @since 6.7.0
*
* @param {object} orderDropdown select field
*/
changeMenuOrder : function( orderDropdown ) {
var menuItems = $( '#menu-to-edit li' ),
$this = $( orderDropdown ),
newOrderID = parseInt( $this.val(), 10),
menuItem = $this.closest( 'li.menu-item' ).first(),
menuItemChildren = menuItem.childMenuItems(),
menuItemNoChildren = menuItemChildren.length,
menuItemCurrentPosition = parseInt( menuItem.index(), 10 ),
parentItemID = menuItem.find( '.menu-item-data-parent-id' ).val(),
subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemID + '"]' ),
currentItemAtPosition = $(subItems[newOrderID - 1]).closest( 'li.menu-item' );
if ( menuItemNoChildren > 0 ) {
menuItem = menuItem.add( menuItemChildren );
}
var currentItemNoChildren = currentItemAtPosition.childMenuItems().length,
currentItemPosition = parseInt( currentItemAtPosition.index(), 10 );
menuItems = $( '#menu-to-edit li' );
var menuItemNewPosition = currentItemPosition;
if(menuItemCurrentPosition > menuItemNewPosition){
menuItemNewPosition = currentItemPosition;
menuItem.detach().insertBefore( menuItems.eq( menuItemNewPosition ) ).updateOrderDropdown();
} else {
menuItemNewPosition = menuItemNewPosition + currentItemNoChildren;
menuItem.detach().insertAfter( menuItems.eq( menuItemNewPosition ) ).updateOrderDropdown();
}
api.registerChange();
api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility();
$this.trigger( 'focus' );
wp.a11y.speak( menus.orderUpdated, 'polite' );
}, },
/** /**
@ -737,6 +956,8 @@
api.refreshKeyboardAccessibility(); api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility(); api.refreshAdvancedAccessibility();
ui.item.updateParentDropdown();
ui.item.updateOrderDropdown();
api.refreshAdvancedAccessibilityOfItem( ui.item.find( 'a.item-edit' ) ); api.refreshAdvancedAccessibilityOfItem( ui.item.find( 'a.item-edit' ) );
}, },
change: function(e, ui) { change: function(e, ui) {
@ -988,6 +1209,8 @@
deletionSpeech = menus.itemsDeleted.replace( '%s', itemsPendingDeletion ); deletionSpeech = menus.itemsDeleted.replace( '%s', itemsPendingDeletion );
wp.a11y.speak( deletionSpeech, 'polite' ); wp.a11y.speak( deletionSpeech, 'polite' );
that.disableBulkSelection(); that.disableBulkSelection();
menus.updateParentDropdown();
menus.updateOrderDropdown();
} }
}); });
}, },
@ -1527,6 +1750,8 @@
} }
api.refreshAdvancedAccessibility(); api.refreshAdvancedAccessibility();
wp.a11y.speak( menus.itemRemoved ); wp.a11y.speak( menus.itemRemoved );
menus.updateParentDropdown();
menus.updateOrderDropdown();
}); });
}, },

File diff suppressed because one or more lines are too long

View File

@ -597,6 +597,8 @@ $nav_menus_l10n = array(
'movedTop' => __( 'Menu item moved to the top' ), 'movedTop' => __( 'Menu item moved to the top' ),
'movedLeft' => __( 'Menu item moved out of submenu' ), 'movedLeft' => __( 'Menu item moved out of submenu' ),
'movedRight' => __( 'Menu item is now a sub-item' ), 'movedRight' => __( 'Menu item is now a sub-item' ),
'parentUpdated' => __( 'Menu parent updated' ),
'orderUpdated' => __( 'Menu order updated' ),
); );
wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n ); wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );

View File

@ -16,7 +16,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '6.7-beta3-59264'; $wp_version = '6.7-beta3-59265';
/** /**
* 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.