diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 0fe5c62064b64..e3d13c8766868 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -170,6 +170,7 @@ function wp_dashboard_setup() { * * @since 2.7.0 * @since 5.6.0 The `$context` and `$priority` parameters were added. + * @since 7.2.0 The `$enqueue_callback` parameter was added. * * @global callable[] $wp_dashboard_control_callbacks * @@ -184,12 +185,29 @@ function wp_dashboard_setup() { * Accepts 'normal', 'side', 'column3', or 'column4'. Default 'normal'. * @param string $priority Optional. The priority within the context where the box should show. * Accepts 'high', 'core', 'default', or 'low'. Default 'core'. + * @param callable $enqueue_callback Optional. Function that enqueues the scripts and styles for the widget. + * Only called when the widget is not hidden via Screen Options, so assets + * for hidden widgets are not loaded. Default null. */ -function wp_add_dashboard_widget( $widget_id, $widget_name, $callback, $control_callback = null, $callback_args = null, $context = 'normal', $priority = 'core' ) { +function wp_add_dashboard_widget( $widget_id, $widget_name, $callback, $control_callback = null, $callback_args = null, $context = 'normal', $priority = 'core', $enqueue_callback = null ) { global $wp_dashboard_control_callbacks; $screen = get_current_screen(); + /* + * Only enqueue the widget's assets when the widget is actually visible. + * A widget hidden via Screen Options is removed from the DOM, so loading + * its scripts and styles wastes resources. Collapsed (closed) widgets are + * still present in the DOM, so they are intentionally treated as visible. + */ + if ( is_callable( $enqueue_callback ) && $screen instanceof WP_Screen ) { + $hidden_widgets = get_hidden_meta_boxes( $screen ); + + if ( ! in_array( $widget_id, $hidden_widgets, true ) ) { + call_user_func( $enqueue_callback ); + } + } + $private_callback_args = array( '__widget_basename' => $widget_name ); if ( is_null( $callback_args ) ) { diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index 4b9b8bc68034e..18149cbf8bfa6 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -495,6 +495,178 @@ public function test_wp_add_dashboard_widget() { remove_meta_box( 'dashboard2', 'dashboard', 'normal' ); } + /** + * Tests that the enqueue callback runs when the widget is visible. + * + * @ticket 55344 + * + * @covers ::wp_add_dashboard_widget + */ + public function test_wp_add_dashboard_widget_enqueue_callback_called_when_visible() { + set_current_screen( 'dashboard' ); + + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + $called = false; + + wp_add_dashboard_widget( + 'dashboard_enqueue_visible', + 'Visible', + '__return_false', + null, + null, + 'normal', + 'core', + function () use ( &$called ) { + $called = true; + } + ); + + $this->assertTrue( $called, 'The enqueue callback should run for a visible widget.' ); + + remove_meta_box( 'dashboard_enqueue_visible', 'dashboard', 'normal' ); + } + + /** + * Tests that the enqueue callback does not run when the widget is hidden + * via Screen Options. + * + * @ticket 55344 + * + * @covers ::wp_add_dashboard_widget + */ + public function test_wp_add_dashboard_widget_enqueue_callback_not_called_when_hidden() { + set_current_screen( 'dashboard' ); + + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + wp_set_current_user( self::$editor_id ); + update_user_option( get_current_user_id(), 'metaboxhidden_dashboard', array( 'dashboard_enqueue_hidden' ), true ); + + $called = false; + + wp_add_dashboard_widget( + 'dashboard_enqueue_hidden', + 'Hidden', + '__return_false', + null, + null, + 'normal', + 'core', + function () use ( &$called ) { + $called = true; + } + ); + + $this->assertFalse( $called, 'The enqueue callback should not run for a hidden widget.' ); + + remove_meta_box( 'dashboard_enqueue_hidden', 'dashboard', 'normal' ); + } + + /** + * Tests that the enqueue callback runs for a collapsed (closed) widget, + * because a collapsed widget is still present in the DOM. + * + * @ticket 55344 + * + * @covers ::wp_add_dashboard_widget + */ + public function test_wp_add_dashboard_widget_enqueue_callback_called_when_closed_but_not_hidden() { + set_current_screen( 'dashboard' ); + + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + wp_set_current_user( self::$editor_id ); + update_user_option( get_current_user_id(), 'closedpostboxes_dashboard', array( 'dashboard_enqueue_closed' ), true ); + + $called = false; + + wp_add_dashboard_widget( + 'dashboard_enqueue_closed', + 'Closed', + '__return_false', + null, + null, + 'normal', + 'core', + function () use ( &$called ) { + $called = true; + } + ); + + $this->assertTrue( $called, 'The enqueue callback should run for a collapsed (closed) widget.' ); + + remove_meta_box( 'dashboard_enqueue_closed', 'dashboard', 'normal' ); + } + + /** + * Tests that a null enqueue callback is accepted without error, + * preserving backward compatibility for existing callers. + * + * @ticket 55344 + * + * @covers ::wp_add_dashboard_widget + */ + public function test_wp_add_dashboard_widget_null_enqueue_callback_no_error() { + global $wp_meta_boxes; + + set_current_screen( 'dashboard' ); + + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + wp_add_dashboard_widget( 'dashboard_enqueue_null', 'Null', '__return_false', null, null, 'normal', 'core', null ); + + $this->assertArrayHasKey( 'dashboard_enqueue_null', $wp_meta_boxes['dashboard']['normal']['core'] ); + + remove_meta_box( 'dashboard_enqueue_null', 'dashboard', 'normal' ); + } + + /** + * Tests that the enqueue callback runs on a first visit, when no user + * preference for hidden meta boxes has been stored yet. + * + * @ticket 55344 + * + * @covers ::wp_add_dashboard_widget + */ + public function test_wp_add_dashboard_widget_enqueue_callback_called_when_no_user_pref() { + set_current_screen( 'dashboard' ); + + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + wp_set_current_user( self::$editor_id ); + delete_user_option( get_current_user_id(), 'metaboxhidden_dashboard', true ); + + $called = false; + + wp_add_dashboard_widget( + 'dashboard_enqueue_no_pref', + 'No Pref', + '__return_false', + null, + null, + 'normal', + 'core', + function () use ( &$called ) { + $called = true; + } + ); + + $this->assertTrue( $called, 'The enqueue callback should run when no hidden-widget preference is stored.' ); + + remove_meta_box( 'dashboard_enqueue_no_pref', 'dashboard', 'normal' ); + } + /** * Tests that get_post_states() handles a null value gracefully. *