diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index 2a7daba0d2dc4..48a34c0a1bd28 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1859,6 +1859,7 @@ $( function() { var width = navigator.userAgent.indexOf('AppleWebKit/') > -1 ? $window.width() : window.innerWidth; if ( + $body.hasClass( 'metabox-reordering-disabled' ) || ( width <= 782 ) || ( 1 >= $sortables.find( '.ui-sortable-handle:visible' ).length && jQuery( '.columns-prefs-1 input' ).prop( 'checked' ) ) ) { diff --git a/src/js/_enqueues/admin/postbox.js b/src/js/_enqueues/admin/postbox.js index b6d1b6569dc84..4f7625a44d75d 100644 --- a/src/js/_enqueues/admin/postbox.js +++ b/src/js/_enqueues/admin/postbox.js @@ -104,6 +104,10 @@ postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ), firstOrLastPositionMessage; + if ( false === postboxes.reorderingEnabled ) { + return; + } + if ( 'dashboard_browser_nag' === postboxId ) { return; } @@ -344,6 +348,13 @@ postboxes.save_order( page ); } }); + + $( '.meta-box-reorder-tog' ).on( 'click.postboxes', function() { + var enabled = $( this ).prop( 'checked' ); + + postboxes.setReordering( enabled ); + postboxes.save_reordering_state( page, enabled ); + } ); }, /** @@ -419,6 +430,8 @@ } }); + this.setReordering( this.isReorderingEnabled() ); + if ( isMobile ) { $(document.body).on('orientationchange.postboxes', function(){ postboxes._pb_change(); }); this._pb_change(); @@ -437,6 +450,68 @@ }); }, + /** + * Checks whether meta box reordering is enabled. + * + * @since 7.1.0 + * + * @return {boolean} Whether meta box reordering is enabled. + */ + isReorderingEnabled: function() { + var $toggle = $( '#meta-box-reorder' ); + + if ( $toggle.length ) { + return $toggle.prop( 'checked' ); + } + + return ! $( document.body ).hasClass( 'metabox-reordering-disabled' ); + }, + + /** + * Enables or disables the meta box reordering UI. + * + * @since 7.1.0 + * + * @param {boolean} enabled Whether reordering should be enabled. + * @return {void} + */ + setReordering: function( enabled ) { + var $body = $( document.body ), + $orderButtons = $( '.postbox .handle-order-higher, .postbox .handle-order-lower' ); + + this.reorderingEnabled = !! enabled; + + $body + .toggleClass( 'metabox-reordering-enabled', this.reorderingEnabled ) + .toggleClass( 'metabox-reordering-disabled', ! this.reorderingEnabled ); + + if ( this.reorderingEnabled ) { + $orderButtons + .prop( 'disabled', false ) + .removeAttr( 'tabindex aria-hidden' ); + this.updateOrderButtonsProperties(); + } else { + $orderButtons + .prop( 'disabled', true ) + .attr( { + 'aria-hidden': 'true', + tabindex: '-1' + } ); + } + + if ( window.wpResponsive && window.wpResponsive.maybeDisableSortables ) { + window.wpResponsive.maybeDisableSortables(); + return; + } + + try { + $( '.meta-box-sortables' ) + .sortable( this.reorderingEnabled ? 'enable' : 'disable' ) + .find( '.ui-sortable-handle' ) + .toggleClass( 'is-non-sortable', ! this.reorderingEnabled ); + } catch ( e ) {} + }, + /** * Saves the state of the postboxes to the server. * @@ -476,6 +551,32 @@ ); }, + /** + * Saves the meta box reordering setting to the server. + * + * @since 7.1.0 + * + * @memberof postboxes + * + * @param {string} page The page we are currently on. + * @param {boolean} enabled Whether meta box reordering is enabled. + * @return {void} + */ + save_reordering_state : function( page, enabled ) { + $.post( + ajaxurl, + { + action: 'meta-box-reorder', + enabled: enabled ? 1 : 0, + screenoptionnonce: $( '#screenoptionnonce' ).val(), + page: page + }, + function() { + wp.a11y.speak( __( 'Screen Options updated.' ) ); + } + ); + }, + /** * Saves the order of the postboxes to the server. * diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 3ad60f95766e3..6fe90a95a150f 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -79,6 +79,7 @@ 'add-user', 'closed-postboxes', 'hidden-columns', + 'meta-box-reorder', 'update-welcome-panel', 'menu-get-metabox', 'wp-link-ajax', diff --git a/src/wp-admin/admin-header.php b/src/wp-admin/admin-header.php index e1e9ba0f6562b..358b41c246c79 100644 --- a/src/wp-admin/admin-header.php +++ b/src/wp-admin/admin-header.php @@ -214,6 +214,8 @@ $admin_body_class .= ' block-editor-page wp-embed-responsive'; } +$admin_body_class .= wp_is_meta_box_reordering_enabled( $current_screen ) ? ' metabox-reordering-enabled' : ' metabox-reordering-disabled'; + $admin_body_class .= ' wp-theme-' . sanitize_html_class( get_template() ); if ( is_child_theme() ) { $admin_body_class .= ' wp-child-theme-' . sanitize_html_class( get_stylesheet() ); diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 63e960b482c00..1e6fe24448dfb 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1947,6 +1947,26 @@ p.auto-update-status { line-height: 2.35; } +.metabox-prefs > p { + margin: 0 0 8px; +} + +.meta-box-reorder-prefs { + margin-top: 14px; +} + +.meta-box-reorder-prefs legend { + padding-bottom: 2px; +} + +.meta-box-reorder-prefs p { + margin: 0 0 4px; +} + +.meta-box-reorder-prefs label { + line-height: 2; +} + #number-of-columns { display: inline-block; vertical-align: middle; @@ -2223,6 +2243,12 @@ html.wp-toolbar { .postbox-header .handle-actions { flex-shrink: 0; + padding-right: 8px; +} + +.rtl .postbox-header .handle-actions { + padding-right: 0; + padding-left: 8px; } /* Post box order and toggle buttons. */ @@ -2244,6 +2270,30 @@ html.wp-toolbar { width: 1.62rem; } +.js.metabox-reordering-enabled .postbox .handle-order-higher, +.js.metabox-reordering-enabled .postbox .handle-order-lower { + opacity: 0; + pointer-events: none; + transform: translateY( -1px ); + transition: opacity 0.12s ease-in-out, transform 0.12s ease-in-out; +} + +.js.metabox-reordering-enabled .postbox-header:hover .handle-order-higher, +.js.metabox-reordering-enabled .postbox-header:hover .handle-order-lower, +.js.metabox-reordering-enabled .postbox-header:focus-within .handle-order-higher, +.js.metabox-reordering-enabled .postbox-header:focus-within .handle-order-lower, +.js.metabox-reordering-enabled .postbox .handle-order-higher:focus, +.js.metabox-reordering-enabled .postbox .handle-order-lower:focus { + opacity: 1; + pointer-events: auto; + transform: translateY( 0 ); +} + +.js.metabox-reordering-disabled .postbox .handle-order-higher, +.js.metabox-reordering-disabled .postbox .handle-order-lower { + display: none; +} + /* Post box order buttons in the block editor meta boxes area. */ .edit-post-meta-boxes-area .postbox .handle-order-higher, .edit-post-meta-boxes-area .postbox .handle-order-lower { @@ -3329,7 +3379,13 @@ img { } .postbox .handle-order-higher:focus, -.postbox .handle-order-lower:focus, +.postbox .handle-order-lower:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + border-radius: 4px; + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + .postbox .handlediv:focus { box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); border-radius: 50%; diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index ab73f828f7067..576f2255f7341 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -67,11 +67,10 @@ } #dashboard-widgets .postbox-container .empty-container { - outline: 2px dashed rgb(0, 0, 0, 0.15); - outline-offset: -2px; + box-sizing: border-box; + border: 1px dashed #c3c4c7; border-radius: 8px; height: 250px; - margin: 4px; } /* Only highlight drop zones when dragging. */ @@ -82,7 +81,7 @@ } .is-dragging-metaboxes #dashboard-widgets .postbox-container .empty-container { - background: rgb(0, 0, 0, 0.01); + background: rgb(var(--wp-admin-theme-color--rgb), 0.04); } #dashboard-widgets .postbox-container .empty-container:after { @@ -1385,6 +1384,19 @@ a.rsswidget { } } +.js.metabox-reordering-disabled #dashboard-widgets .postbox-container .empty-container { + display: none; + height: 0; + min-height: 0; + margin: 0; + outline: none; +} + +.js.metabox-reordering-disabled #dashboard-widgets .postbox-container .empty-container:after { + content: none; + display: none; +} + @media screen and (max-width: 870px) { /* @deprecated 5.9.0 -- Lists removed from welcome panel. */ .welcome-panel .welcome-panel-column li { diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..7e5bfeb9e1180 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -1856,6 +1856,30 @@ function wp_ajax_hidden_columns() { wp_die( 1 ); } +/** + * Handles the meta box reordering setting via AJAX. + * + * @since 7.1.0 + */ +function wp_ajax_meta_box_reorder() { + check_ajax_referer( 'screen-options-nonce', 'screenoptionnonce' ); + $page = $_POST['page'] ?? ''; + + if ( sanitize_key( $page ) !== $page ) { + wp_die( 0 ); + } + + $user = wp_get_current_user(); + if ( ! $user ) { + wp_die( -1 ); + } + + $enabled = isset( $_POST['enabled'] ) && '1' === (string) $_POST['enabled']; + update_user_meta( $user->ID, "metaboxreorder_$page", $enabled ? 'enabled' : 'disabled' ); + + wp_die( 1 ); +} + /** * Handles updating whether to display the welcome panel via AJAX. * diff --git a/src/wp-admin/includes/class-wp-screen.php b/src/wp-admin/includes/class-wp-screen.php index ab7dfef77f67c..8b4d5a3c28a5f 100644 --- a/src/wp-admin/includes/class-wp-screen.php +++ b/src/wp-admin/includes/class-wp-screen.php @@ -1023,7 +1023,7 @@ public function show_screen_options() { */ $this->_screen_settings = apply_filters( 'screen_settings', $this->_screen_settings, $this ); - if ( $this->_screen_settings || $this->_options ) { + if ( $this->_screen_settings || $this->_options || $this->show_meta_box_reorder_options() ) { $show_screen = true; } @@ -1081,6 +1081,7 @@ public function render_screen_options( $options = array() ) { $this->render_meta_boxes_preferences(); $this->render_list_table_columns_preferences(); $this->render_screen_layout(); + $this->render_meta_box_reorder_options(); $this->render_per_page_options(); $this->render_view_mode(); echo $this->_screen_settings; @@ -1120,8 +1121,8 @@ public function render_meta_boxes_preferences() {

- - + +

show_meta_box_reorder_options() ) { + return; + } + + ?> +
+ +

+ +
+ id ] ); + + /** + * Filters whether to show the meta box reordering option. + * + * @since 7.1.0 + * + * @param bool $show_reorder_option Whether to show the option. + * @param WP_Screen $screen WP_Screen object. + */ + return (bool) apply_filters( 'screen_options_show_meta_box_reorder', $show_reorder_option, $this ); + } + /** * Renders the list table columns preferences. * diff --git a/src/wp-admin/includes/screen.php b/src/wp-admin/includes/screen.php index 3388ced49a4aa..9ae7ed964166d 100644 --- a/src/wp-admin/includes/screen.php +++ b/src/wp-admin/includes/screen.php @@ -194,6 +194,40 @@ function get_hidden_meta_boxes( $screen ) { return apply_filters( 'hidden_meta_boxes', $hidden, $screen, $use_defaults ); } +/** + * Determines whether meta box reordering is enabled for a screen. + * + * @since 7.1.0 + * + * @param string|WP_Screen|null $screen Optional. Screen identifier or object. + * Default is the current screen. + * @return bool Whether meta box reordering is enabled. + */ +function wp_is_meta_box_reordering_enabled( $screen = null ) { + if ( null === $screen ) { + $screen = get_current_screen(); + } elseif ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + if ( ! $screen instanceof WP_Screen ) { + return true; + } + + $setting = get_user_option( "metaboxreorder_{$screen->id}" ); + $enabled = false === $setting ? true : 'disabled' !== $setting; + + /** + * Filters whether meta box reordering is enabled for a screen. + * + * @since 7.1.0 + * + * @param bool $enabled Whether meta box reordering is enabled. + * @param WP_Screen $screen WP_Screen object of the current screen. + */ + return (bool) apply_filters( 'meta_box_reordering_enabled', $enabled, $screen ); +} + /** * Register and configure an admin screen option * diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php index 2e86c29e67284..aeda34519a74d 100644 --- a/tests/phpunit/includes/testcase-ajax.php +++ b/tests/phpunit/includes/testcase-ajax.php @@ -66,6 +66,7 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { 'add-user', 'closed-postboxes', 'hidden-columns', + 'meta-box-reorder', 'update-welcome-panel', 'menu-get-metabox', 'wp-link-ajax', diff --git a/tests/phpunit/tests/admin/includesScreen.php b/tests/phpunit/tests/admin/includesScreen.php index 26186391f261d..5305b31da9e3c 100644 --- a/tests/phpunit/tests/admin/includesScreen.php +++ b/tests/phpunit/tests/admin/includesScreen.php @@ -260,6 +260,68 @@ public function test_taxonomy_with_special_suffix_as_hookname() { $this->assertFalse( $screen->is_block_editor ); } + public function test_meta_box_reordering_enabled_defaults_to_true() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $this->assertTrue( wp_is_meta_box_reordering_enabled( 'dashboard' ) ); + } + + public function test_meta_box_reordering_enabled_respects_user_option() { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + update_user_meta( $user_id, 'metaboxreorder_dashboard', 'disabled' ); + + $this->assertFalse( wp_is_meta_box_reordering_enabled( 'dashboard' ) ); + } + + public function test_meta_box_reordering_enabled_can_be_filtered() { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + update_user_meta( $user_id, 'metaboxreorder_dashboard', 'disabled' ); + + add_filter( 'meta_box_reordering_enabled', '__return_true' ); + + $this->assertTrue( wp_is_meta_box_reordering_enabled( 'dashboard' ) ); + + remove_filter( 'meta_box_reordering_enabled', '__return_true' ); + } + + public function test_meta_box_reordering_option_is_rendered_for_screens_with_meta_boxes() { + global $wp_meta_boxes; + + $old_wp_meta_boxes = $wp_meta_boxes; + $screen = convert_to_screen( 'dashboard' ); + + add_meta_box( 'testbox1', 'Test Metabox', '__return_false', $screen ); + + try { + ob_start(); + $screen->render_meta_box_reorder_options(); + $output = ob_get_clean(); + } finally { + $wp_meta_boxes = $old_wp_meta_boxes; + } + + $this->assertStringContainsString( 'id="meta-box-reorder"', $output ); + $this->assertStringContainsString( 'Reordering', $output ); + $this->assertStringContainsString( 'When enabled, boxes can be rearranged by dragging their headings or using the up and down controls.', $output ); + $this->assertStringContainsString( 'Enable box reordering', $output ); + } + + public function test_meta_box_reordering_option_display_can_be_filtered() { + $screen = convert_to_screen( 'dashboard' ); + + add_filter( 'screen_options_show_meta_box_reorder', '__return_true' ); + + ob_start(); + $screen->render_meta_box_reorder_options(); + $output = ob_get_clean(); + + remove_filter( 'screen_options_show_meta_box_reorder', '__return_true' ); + + $this->assertStringContainsString( 'id="meta-box-reorder"', $output ); + } + public function test_post_type_with_edit_prefix() { register_post_type( 'edit-some-thing' ); $screen = convert_to_screen( 'edit-some-thing' ); diff --git a/tests/phpunit/tests/ajax/wpAjaxMetaBoxReorder.php b/tests/phpunit/tests/ajax/wpAjaxMetaBoxReorder.php new file mode 100644 index 0000000000000..8e1c5c3223625 --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxMetaBoxReorder.php @@ -0,0 +1,56 @@ +_setRole( 'administrator' ); + + $_POST = array( + 'screenoptionnonce' => wp_create_nonce( 'screen-options-nonce' ), + 'page' => 'dashboard', + 'enabled' => $enabled, + ); + + try { + $this->_handleAjax( 'meta-box-reorder' ); + $this->fail( 'Expected exception: WPAjaxDieStopException' ); + } catch ( WPAjaxDieStopException $e ) { + unset( $e ); + } + + $this->assertSame( $expected, get_user_meta( get_current_user_id(), 'metaboxreorder_dashboard', true ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_meta_box_reordering_states() { + return array( + 'enabled' => array( 1, 'enabled' ), + 'enabled as string' => array( '1', 'enabled' ), + 'disabled' => array( 0, 'disabled' ), + 'disabled as string' => array( '0', 'disabled' ), + 'disabled when false' => array( 'false', 'disabled' ), + ); + } +}