From 337b3295fe0bb9c6012db71a01d5e5187d11a796 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 7 Jul 2020 12:58:10 +0000 Subject: [PATCH] Accessibility: Allow post boxes on the Dashboard and Classic Editor pages to be reordered by using the keyboard. So far, it has been possible to rearrange into a new order the post boxes (also known as "widgets" on the Dashboard and "meta boxes" on the Edit post page) only by using a pointing device, for example a mouse. This change adds new controls and functionality to allow the boxes to be rearranged also with the keyboard. Additionally, audible messages are sent to the admin ARIA live region to notify screen reader users of the reorder action result. Props joedolson, anevins, antpb, audrasjb, xkon, MarcoZ, karmatosed, afercia. Fixes #39074. git-svn-id: https://develop.svn.wordpress.org/trunk@48373 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/admin/postbox.js | 166 ++++++++++++++++++++++++- src/wp-admin/css/common.css | 90 ++++++++++---- src/wp-admin/css/dashboard.css | 1 + src/wp-admin/includes/ajax-actions.php | 2 +- src/wp-admin/includes/template.php | 43 +++++-- src/wp-includes/script-loader.php | 2 +- 6 files changed, 268 insertions(+), 36 deletions(-) diff --git a/src/js/_enqueues/admin/postbox.js b/src/js/_enqueues/admin/postbox.js index 1fdf0ebeb8..a309929d37 100644 --- a/src/js/_enqueues/admin/postbox.js +++ b/src/js/_enqueues/admin/postbox.js @@ -42,7 +42,7 @@ */ handle_click : function () { var $el = $( this ), - p = $el.parent( '.postbox' ), + p = $el.closest( '.postbox' ), id = p.attr( 'id' ), ariaExpandedValue; @@ -51,7 +51,6 @@ } p.toggleClass( 'closed' ); - ariaExpandedValue = ! p.hasClass( 'closed' ); if ( $el.hasClass( 'handlediv' ) ) { @@ -89,6 +88,143 @@ $document.trigger( 'postbox-toggled', p ); }, + /** + * Handles clicks on the move up/down buttons. + * + * @since 5.5.0 + * + * @return {void} + */ + handleOrder: function() { + var button = $( this ), + postbox = button.closest( '.postbox' ), + postboxId = postbox.attr( 'id' ), + postboxesWithinSortables = postbox.closest( '.meta-box-sortables' ).find( '.postbox:visible' ), + postboxesWithinSortablesCount = postboxesWithinSortables.length, + postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ), + firstOrLastPositionMessage; + + if ( 'dashboard_browser_nag' === postboxId ) { + return; + } + + // If on the first or last position, do nothing and send an audible message to screen reader users. + if ( 'true' === button.attr( 'aria-disabled' ) ) { + firstOrLastPositionMessage = button.hasClass( 'handle-order-higher' ) ? + __( 'The box is on the first position' ) : + __( 'The box is on the last position' ); + + wp.a11y.speak( firstOrLastPositionMessage ); + return; + } + + // Move a postbox up. + if ( button.hasClass( 'handle-order-higher' ) ) { + // If the box is first within a sortable area, move it to the previous sortable area. + if ( 0 === postboxWithinSortablesIndex ) { + postboxes.handleOrderBetweenSortables( 'previous', button, postbox ); + return; + } + + postbox.prevAll( '.postbox:visible' ).eq( 0 ).before( postbox ); + button.focus(); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + } + + // Move a postbox down. + if ( button.hasClass( 'handle-order-lower' ) ) { + // If the box is last within a sortable area, move it to the next sortable area. + if ( postboxWithinSortablesIndex + 1 === postboxesWithinSortablesCount ) { + postboxes.handleOrderBetweenSortables( 'next', button, postbox ); + return; + } + + postbox.nextAll( '.postbox:visible' ).eq( 0 ).after( postbox ); + button.focus(); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + } + + }, + + /** + * Moves postboxes between the sortables areas. + * + * @since 5.5.0 + * + * @param {string} position The "previous" or "next" sortables area. + * @param {object} button The jQuery object representing the button that was clicked. + * @param {object} postbox The jQuery object representing the postbox to be moved. + * + * @return {void} + */ + handleOrderBetweenSortables: function( position, button, postbox ) { + var closestSortablesId = button.closest( '.meta-box-sortables' ).attr( 'id' ), + sortablesIds = [], + sortablesIndex, + detachedPostbox; + + // Get the list of sortables within the page. + $( '.meta-box-sortables:visible' ).each( function() { + sortablesIds.push( $( this ).attr( 'id' ) ); + }); + + // Return if there's only one visible sortables area, e.g. in the block editor page. + if ( 1 === sortablesIds.length ) { + return; + } + + // Find the index of the current sortables area within all the sortable areas. + sortablesIndex = $.inArray( closestSortablesId, sortablesIds ); + // Detach the postbox to be moved. + detachedPostbox = postbox.detach(); + + // Move the detached postbox to its new position. + if ( 'previous' === position ) { + $( detachedPostbox ).appendTo( '#' + sortablesIds[ sortablesIndex - 1 ] ); + } + + if ( 'next' === position ) { + $( detachedPostbox ).prependTo( '#' + sortablesIds[ sortablesIndex + 1 ] ); + } + + postboxes._mark_area(); + button.focus(); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + }, + + /** + * Update the move buttons properties depending on the postbox position. + * + * @since 5.5.0 + * + * @return {void} + */ + updateOrderButtonsProperties: function() { + var firstSortablesId = $( '.meta-box-sortables:first' ).attr( 'id' ), + lastSortablesId = $( '.meta-box-sortables:last' ).attr( 'id' ), + firstPostbox = $( '.postbox:visible:first' ), + lastPostbox = $( '.postbox:visible:last' ), + firstPostboxSortablesId = firstPostbox.closest( '.meta-box-sortables' ).attr( 'id' ), + lastPostboxSortablesId = lastPostbox.closest( '.meta-box-sortables' ).attr( 'id' ); + + // Enable all buttons as a reset first. + $( '.handle-order-higher' ).attr( 'aria-disabled', 'false' ); + $( '.handle-order-lower' ).attr( 'aria-disabled', 'false' ); + + // Set an aria-disabled=true attribute on the first visible "move" buttons. + if ( firstSortablesId === firstPostboxSortablesId ) { + $( firstPostbox ).find( '.handle-order-higher' ).attr( 'aria-disabled', 'true' ); + } + + // Set an aria-disabled=true attribute on the last visible "move" buttons. + if ( lastSortablesId === lastPostboxSortablesId ) { + $( '.postbox:visible .handle-order-lower' ).last().attr( 'aria-disabled', 'true' ); + } + }, + /** * Adds event handlers to all postboxes and screen option on the current page. * @@ -103,13 +239,17 @@ * @return {void} */ add_postbox_toggles : function (page, args) { - var $handles = $( '.postbox .hndle, .postbox .handlediv' ); + var $handles = $( '.postbox .hndle, .postbox .handlediv' ), + $orderButtons = $( '.postbox .handle-order-higher, .postbox .handle-order-lower' ); this.page = page; this.init( page, args ); $handles.on( 'click.postboxes', this.handle_click ); + // Handle the order of the postboxes. + $orderButtons.on( 'click.postboxes', this.handleOrder ); + /** * @since 2.7.0 */ @@ -123,6 +263,8 @@ * Event handler for the postbox dismiss button. After clicking the button * the postbox will be hidden. * + * As of WordPress 5.5, this is only used for the browser update nag. + * * @since 3.2.0 * * @return {void} @@ -248,6 +390,8 @@ $el.sortable('cancel'); return; } + + postboxes.updateOrderButtonsProperties(); postboxes.save_order(page); }, receive: function(e,ui) { @@ -266,10 +410,14 @@ this._mark_area(); + // Update the "move" buttons properties. + this.updateOrderButtonsProperties(); + $document.on( 'postbox-toggled', this.updateOrderButtonsProperties ); + // Set the handle buttons `aria-expanded` attribute initial value on page load. $handleButtons.each( function () { var $el = $( this ); - $el.attr( 'aria-expanded', ! $el.parent( '.postbox' ).hasClass( 'closed' ) ); + $el.attr( 'aria-expanded', ! $el.closest( '.postbox' ).hasClass( 'closed' ) ); }); }, @@ -332,7 +480,15 @@ postVars[ 'order[' + this.id.split( '-' )[0] + ']' ] = $( this ).sortable( 'toArray' ).join( ',' ); } ); - $.post( ajaxurl, postVars ); + $.post( + ajaxurl, + postVars, + function( response ) { + if ( response.success ) { + wp.a11y.speak( __( 'The boxes order has been saved.' ) ); + } + } + ); }, /** diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 90cd988bc7..78e7c9b071 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -727,7 +727,6 @@ img.emoji { color: #23282d; } -.postbox .hndle, .stuffbox .hndle { border-bottom: 1px solid #ccd0d4; } @@ -1983,14 +1982,34 @@ html.wp-toolbar { cursor: auto; } +/* Configurable dashboard widgets "Configure" edit-box link. */ .hndle a { - font-size: 11px; + font-size: 12px; font-weight: 400; } +.postbox-header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #ccd0d4; +} + +.postbox-header .hndle { + flex-grow: 1; + /* Handle the alignment for the configurable dashboard widgets "Configure" edit-box link. */ + display: flex; + justify-content: space-between; + align-items: center; +} + +.postbox-header .handle-actions { + flex-shrink: 0; +} + +.postbox .handle-order-higher, +.postbox .handle-order-lower, .postbox .handlediv { - display: none; - float: right; width: 36px; height: 36px; margin: 0; @@ -2000,8 +2019,15 @@ html.wp-toolbar { cursor: pointer; } -.js .postbox .handlediv { - display: block; +.postbox .handle-order-higher, +.postbox .handle-order-lower { + color: #72777c; +} + +.postbox .handle-order-higher[aria-disabled="true"], +.postbox .handle-order-lower[aria-disabled="true"] { + cursor: default; + color: #a0a5aa; } .sortable-placeholder { @@ -2949,10 +2975,12 @@ img { } /* Metabox collapse arrow indicators */ -.sidebar-name .toggle-indicator:before, -.js .meta-box-sortables .postbox .toggle-indicator:before, -.bulk-action-notice .toggle-indicator:before, -.privacy-text-box .toggle-indicator:before { +.sidebar-name .toggle-indicator::before, +.meta-box-sortables .postbox .toggle-indicator::before, +.meta-box-sortables .postbox .order-higher-indicator::before, +.meta-box-sortables .postbox .order-lower-indicator::before, +.bulk-action-notice .toggle-indicator::before, +.privacy-text-box .toggle-indicator::before { content: "\f142"; display: inline-block; font: normal 20px/1 dashicons; @@ -2962,37 +2990,55 @@ img { text-decoration: none; } -.js .widgets-holder-wrap.closed .toggle-indicator:before, -.js .meta-box-sortables .postbox.closed .handlediv .toggle-indicator:before, -.bulk-action-notice .bulk-action-errors-collapsed .toggle-indicator:before, -.privacy-text-box.closed .toggle-indicator:before { +.js .widgets-holder-wrap.closed .toggle-indicator::before, +.meta-box-sortables .postbox.closed .handlediv .toggle-indicator::before, +.bulk-action-notice .bulk-action-errors-collapsed .toggle-indicator::before, +.privacy-text-box.closed .toggle-indicator::before { content: "\f140"; } -.js .postbox .handlediv .toggle-indicator:before { - margin-top: 4px; +.postbox .handle-order-higher .order-higher-indicator::before { + content: "\f343"; + color: inherit; +} + +.postbox .handle-order-lower .order-lower-indicator::before { + content: "\f347"; + color: inherit; +} + +.postbox .handle-order-higher .order-higher-indicator::before, +.postbox .handle-order-lower .order-lower-indicator::before, +.postbox .handlediv .toggle-indicator::before { width: 20px; border-radius: 50%; - text-indent: -1px; /* account for the dashicon alignment */ } -.rtl.js .postbox .handlediv .toggle-indicator:before { - text-indent: 1px; /* account for the dashicon alignment */ +.postbox .handlediv .toggle-indicator::before { + text-indent: -1px; /* account for the dashicon glyph uneven horizontal alignment */ } -.bulk-action-notice .toggle-indicator:before { +.rtl .postbox .handlediv .toggle-indicator::before { + text-indent: 1px; /* account for the dashicon glyph uneven horizontal alignment */ +} + +.bulk-action-notice .toggle-indicator::before { line-height: 16px; vertical-align: top; color: #72777c; } -.js .postbox .handlediv:focus { +.postbox .handle-order-higher:focus, +.postbox .handle-order-lower:focus, +.postbox .handlediv:focus { box-shadow: none; /* Only visible in Windows High Contrast mode */ outline: 1px solid transparent; } -.js .postbox .handlediv:focus .toggle-indicator:before { +.postbox .handle-order-higher:focus .order-higher-indicator::before, +.postbox .handle-order-lower:focus .order-lower-indicator::before, +.postbox .handlediv:focus .toggle-indicator::before { box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, 0.8); diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 91604459bb..f898143ec1 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -48,6 +48,7 @@ } #dashboard-widgets .meta-box-sortables { + display: flow-root; /* avoid margin collapsing between parent and first/last child elements */ /* Required min-height to make the jQuery UI Sortable drop zone work. */ min-height: 100px; margin: 0 8px 20px; diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 680a09ff32..2f426133f2 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -1927,7 +1927,7 @@ function wp_ajax_meta_box_order() { update_user_option( $user->ID, "screen_layout_$page", $page_columns, true ); } - wp_die( 1 ); + wp_send_json_success(); } /** diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 418b610c98..17a03a7389 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1314,6 +1314,16 @@ function do_meta_boxes( $screen, $context, $object ) { // get_hidden_meta_boxes() doesn't apply in the block editor. $hidden_class = ( ! $screen->is_block_editor() && in_array( $box['id'], $hidden, true ) ) ? ' hide-if-js' : ''; echo '
' . "\n"; + + echo '
'; + echo '

'; + if ( 'dashboard_php_nag' === $box['id'] ) { + echo ''; + echo '' . __( 'Warning:' ) . ' '; + } + echo "{$box['title']}"; + echo "

\n"; + if ( 'dashboard_browser_nag' !== $box['id'] ) { $widget_title = $box['title']; @@ -1323,6 +1333,28 @@ function do_meta_boxes( $screen, $context, $object ) { unset( $box['args']['__widget_basename'] ); } + echo '
'; + + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + + echo '
'; } - echo '

'; - if ( 'dashboard_php_nag' === $box['id'] ) { - echo ''; - echo '' . __( 'Warning:' ) . ' '; - } - echo "{$box['title']}"; - echo "

\n"; + echo '
'; + echo '
' . "\n"; if ( WP_DEBUG && ! $block_compatible && 'edit' === $screen->parent_base && ! $screen->is_block_editor() && ! isset( $_GET['meta-box-loader'] ) ) { diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index a09879f1fe..825f48f754 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1208,7 +1208,7 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'xfn', "/wp-admin/js/xfn$suffix.js", array( 'jquery' ), false, 1 ); - $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable' ), false, 1 ); + $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'postbox' ); $scripts->add( 'tags-box', "/wp-admin/js/tags-box$suffix.js", array( 'jquery', 'tags-suggest' ), false, 1 );