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 );