diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index b0b78fd52b890..24604a86cdff3 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1075,17 +1075,6 @@ $( function() { }); } - /* - * The `.below-h2` class is here just for backward compatibility with plugins - * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. - * If '.wp-header-end' is found, append the notices after it otherwise - * after the first h1 or h2 heading found within the main content. - */ - if ( ! $headerEnd.length ) { - $headerEnd = $( '.wrap h1, .wrap h2' ).first(); - } - $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd ); - /** * Makes notices dismissible. * @@ -1525,6 +1514,32 @@ $( function() { input.on('change', toggleUploadButton); })(); + /** + * Moves the admin notices into an `aside` element after the first `h1`. + * Prior to 7.2.0, admin notices were only moved after the first `h1`; + * The `aside` wrapper was added in 7.2.0. + * + * @since 7.2.0 + * + * @return {void} + */ + function moveAdminNotices() { + /* + * The `.below-h2` class is here just for backward compatibility with plugins + * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. + * If '.wp-header-end' is found, append the notices after it otherwise + * after the first h1 or h2 heading found within the main content. + */ + if ( ! $headerEnd.length ) { + $headerEnd = $( '.wrap h1, .wrap h2' ).first(); + } + const $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ); + if ( $notices.length ) { + const $wpAdminNotices = $( '' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd ); + $notices.appendTo( $wpAdminNotices ); + } + } + /** * Pins the menu while distraction-free writing is enabled. * @@ -2127,6 +2142,7 @@ $( function() { }); window.wpResponsive.init(); + moveAdminNotices(); setPinMenu(); setMenuState(); makeNoticesDismissible(); diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 2902c8de209ed..1c88e4a818d67 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -101,6 +101,8 @@ require_once ABSPATH . 'wp-admin/includes/admin.php'; +WP_Admin_Notice_Collector::init(); + auth_redirect(); // Schedule Trash collection. diff --git a/src/wp-includes/class-wp-admin-notice-collector.php b/src/wp-includes/class-wp-admin-notice-collector.php new file mode 100644 index 0000000000000..0d29cd9404d7a --- /dev/null +++ b/src/wp-includes/class-wp-admin-notice-collector.php @@ -0,0 +1,165 @@ +` tag, + * the admin page title is modified via the {@see 'admin_title'} + * filter, the admin page is wrapped in an output buffer, and an + * `ob_start()` callback replaces the placeholder with the final count once + * the buffer is flushed. + * + * @since 7.2.0 + */ +class WP_Admin_Notice_Collector { + + /** + * Placeholder inserted into the admin title, replaced with the + * actual notice count once it is known. + * + * @since 7.2.0 + * @var string + */ + const TITLE_PLACEHOLDER = '%%wp_admin_notice_count%%'; + + /** + * Notices captured during the current request. + * + * @since 7.2.0 + * @var array[] + */ + protected static $notices = array(); + + /** + * Whether the collector's hooks have already been registered. + * + * @since 7.2.0 + * @var bool + */ + protected static $hooked = false; + + /** + * Registers the hooks used to collect notices and to annotate the admin title. + * + * @since 7.2.0 + */ + public static function init() { + if ( self::$hooked ) { + return; + } + + self::$hooked = true; + + add_filter( 'wp_admin_notice_markup', array( __CLASS__, 'capture_notice' ), 10, 3 ); + + // Ajax/REST admin requests don't render a
' : ''; $wrap_closer = $wrap_with_p ? '
' : ''; @@ -9351,9 +9355,15 @@ function wp_get_admin_notice( $message, $args = array() ) { if ( is_string( $args['id'] ) ) { $trimmed_id = trim( $args['id'] ); + $html_builder->set_attribute( 'data-id', $unique_id ); if ( '' !== $trimmed_id ) { $html_builder->set_attribute( 'id', $trimmed_id ); + } else { + $html_builder->set_attribute( 'id', $unique_id ); } + } else { + $html_builder->set_attribute( 'data-id', $unique_id ); + $html_builder->set_attribute( 'id', $unique_id ); } if ( is_string( $args['type'] ) ) { diff --git a/src/wp-settings.php b/src/wp-settings.php index 1d55db83e2d85..8c9ea4a86be37 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -115,6 +115,7 @@ require ABSPATH . WPINC . '/formatting.php'; require ABSPATH . WPINC . '/meta.php'; require ABSPATH . WPINC . '/functions.php'; +require ABSPATH . WPINC . '/class-wp-admin-notice-collector.php'; require ABSPATH . WPINC . '/class-wp-meta-query.php'; require ABSPATH . WPINC . '/class-wp-matchesmapregex.php'; require ABSPATH . WPINC . '/class-wp.php'; diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php index 3fb5391dead3e..239c5a1b2d2e6 100644 --- a/tests/phpunit/tests/functions/wpAdminNotice.php +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -21,6 +21,11 @@ class Tests_Functions_WpAdminNotice extends WP_UnitTestCase { * @param string $expected The expected admin notice markup. */ public function test_should_output_admin_notice( $message, $args, $expected ) { + // Replace placeholder %%ID%% in expected string, required for test isolation. + $next_id = array_last( explode( '-', wp_unique_prefixed_id( 'wp-admin-notice-' ) ) ); + ++$next_id; + $expected = str_replace( '%%ID%%', $next_id, $expected ); + ob_start(); wp_admin_notice( $message, $args ); $actual = ob_get_clean(); @@ -38,7 +43,7 @@ public function data_should_output_admin_notice() { 'defaults' => array( 'message' => 'A notice with defaults.', 'args' => array(), - 'expected' => 'A notice with defaults.
A notice with defaults.
An "error" notice.
An "error" notice.
A "success" notice.
A "success" notice.
A "warning" notice.
A "warning" notice.
An "info" notice.
An "info" notice.
A type that already starts with "notice-".
A type that already starts with "notice-".
A dismissible notice.
A dismissible notice.
A notice with an ID.
A notice with an ID.
A warning notice with an ID.
A warning notice with an ID.
A notice with additional classes.
A notice with additional classes.
A warning notice with additional classes.
A warning notice with additional classes.
A dismissible warning notice with a type and additional classes.
A dismissible warning notice with a type and additional classes.
A notice with an unsafe type.
A notice with an unsafe type.
A notice with an unsafe ID.
A notice with an unsafe ID.
A notice with unsafe additional classes.
A notice with unsafe additional classes.
A notice with a type that is not a string.
A notice with a type that is not a string.
A notice with a type with only empty space.
A notice with a type with only empty space.
A notice with an ID that is not a string.
A notice with an ID that is not a string.
A notice with an ID with only empty space.
A notice with an ID with only empty space.
A notice with dismissible as a truthy value rather than (bool) true.
A notice with dismissible as a truthy value rather than (bool) true.
A notice with additional classes that are not an array.
A notice with additional classes that are not an array.
A notice with an additional attribute with a value.
A notice with an additional attribute with a value.
A notice with the hidden attribute.
A notice with the hidden attribute.
A notice with a boolean attribute without an associative key.
A notice with a boolean attribute without an associative key.
A notice with an additional attribute role.
A notice with an additional attribute role.
A notice with multiple additional attributes.
A notice with multiple additional attributes.
A notice with an additional attribute with an unsafe value.
A notice with an additional attribute with an unsafe value.
A notice with an additional attribute that is invalid.
A notice with an additional attribute that is invalid.
A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.
A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.
A notice with paragraph wrapping as a falsy value rather than (bool) false.
A notice with paragraph wrapping as a falsy value rather than (bool) false.
A type containing spaces.
A type containing spaces.
A notice with defaults.
A notice with defaults.
An "error" notice.
An "error" notice.
A "success" notice.
A "success" notice.
A "warning" notice.
A "warning" notice.
An "info" notice.
An "info" notice.
A type that already starts with "notice-".
A type that already starts with "notice-".
A dismissible notice.
A dismissible notice.
A notice with an ID.
A notice with an ID.
A warning notice with an ID.
A warning notice with an ID.
A notice with additional classes.
A notice with additional classes.
A warning notice with additional classes.
A warning notice with additional classes.
A dismissible warning notice with a type and additional classes.
A dismissible warning notice with a type and additional classes.
A notice with an unsafe type.
A notice with an unsafe type.
A notice with an unsafe ID.
A notice with an unsafe ID.
A notice with unsafe additional classes.
A notice with unsafe additional classes.
A notice with a type that is not a string.
A notice with a type that is not a string.
A notice with a type with only empty space.
A notice with a type with only empty space.
A notice with an ID that is not a string.
A notice with an ID that is not a string.
A notice with an ID with only empty space.
A notice with an ID with only empty space.
A notice with dismissible as a truthy value rather than (bool) true.
A notice with dismissible as a truthy value rather than (bool) true.
A notice with additional classes that are not an array.
A notice with additional classes that are not an array.
A notice with an additional attribute with a value.
A notice with an additional attribute with a value.
A notice with the hidden attribute.
A notice with the hidden attribute.
A notice with a boolean attribute without an associative key.
A notice with a boolean attribute without an associative key.
A notice with an additional attribute role.
A notice with an additional attribute role.
A notice with multiple additional attributes.
A notice with multiple additional attributes.
A notice with an additional attribute with an unsafe value.
A notice with an additional attribute with an unsafe value.
A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.
A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.
A notice with paragraph wrapping as a falsy value rather than (bool) false.
A notice with paragraph wrapping as a falsy value rather than (bool) false.
A type containing spaces.
A type containing spaces.