Skip to content

Commit 1c277e1

Browse files
committed
Admin: Admin notice accessibility improvements.
Capture all admin notices with message content generated via `wp_get_admin_notices()`. Wrap all captured notices inside an `aside` so notices are identified within a navigable landmark. Add information about the number of notices produced on a page in the `title` element of the page. Ensures users have awareness on page entry whether there are notices present, how many there are, and provides screen readers with a mechanism to locate them as a group. Inline notices are counted in the total, but not contained within the landmark. Developed in #13671. Props afercia, psykro, ryokuhi, raaaahman, alexstine, sergeybiryukov, oglekler, marybaum, swissspidy, davidbaumwald, sanket.parmar, sabernhardt, peterwilsoncc, williampatton, im3dabasia1, audrasjb, khokansardar, ugyensupport, joedolson. Fixes #50486. git-svn-id: https://develop.svn.wordpress.org/trunk@64282 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 4fa9283 commit 1c277e1

7 files changed

Lines changed: 292 additions & 79 deletions

File tree

‎src/js/_enqueues/admin/common.js‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,17 +1075,6 @@ $( function() {
10751075
});
10761076
}
10771077

1078-
/*
1079-
* The `.below-h2` class is here just for backward compatibility with plugins
1080-
* that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570.
1081-
* If '.wp-header-end' is found, append the notices after it otherwise
1082-
* after the first h1 or h2 heading found within the main content.
1083-
*/
1084-
if ( ! $headerEnd.length ) {
1085-
$headerEnd = $( '.wrap h1, .wrap h2' ).first();
1086-
}
1087-
$( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd );
1088-
10891078
/**
10901079
* Makes notices dismissible.
10911080
*
@@ -1525,6 +1514,32 @@ $( function() {
15251514
input.on('change', toggleUploadButton);
15261515
})();
15271516

1517+
/**
1518+
* Moves the admin notices into an `aside` element after the first `h1`.
1519+
* Prior to 7.2.0, admin notices were only moved after the first `h1`;
1520+
* The `aside` wrapper was added in 7.2.0.
1521+
*
1522+
* @since 7.2.0
1523+
*
1524+
* @return {void}
1525+
*/
1526+
function moveAdminNotices() {
1527+
/*
1528+
* The `.below-h2` class is here just for backward compatibility with plugins
1529+
* that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570.
1530+
* If '.wp-header-end' is found, append the notices after it otherwise
1531+
* after the first h1 or h2 heading found within the main content.
1532+
*/
1533+
if ( ! $headerEnd.length ) {
1534+
$headerEnd = $( '.wrap h1, .wrap h2' ).first();
1535+
}
1536+
const $notices = $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' );
1537+
if ( $notices.length ) {
1538+
const $wpAdminNotices = $( '<aside id="wp-admin-notices"></aside>' ).attr( 'aria-label', __( 'Admin Notices' ) ).insertAfter( $headerEnd );
1539+
$notices.appendTo( $wpAdminNotices );
1540+
}
1541+
}
1542+
15281543
/**
15291544
* Pins the menu while distraction-free writing is enabled.
15301545
*
@@ -2127,6 +2142,7 @@ $( function() {
21272142
});
21282143

21292144
window.wpResponsive.init();
2145+
moveAdminNotices();
21302146
setPinMenu();
21312147
setMenuState();
21322148
makeNoticesDismissible();

‎src/wp-admin/admin.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101

102102
require_once ABSPATH . 'wp-admin/includes/admin.php';
103103

104+
WP_Admin_Notice_Collector::init();
105+
104106
auth_redirect();
105107

106108
// Schedule Trash collection.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* Administration API: WP_Admin_Notice_Collector class
4+
*
5+
* @package WordPress
6+
* @subpackage Administration
7+
* @since x.x.x
8+
*/
9+
10+
/**
11+
* Capture admin notices generated in PHP during a request.
12+
*
13+
* Notices are captured via the {@see 'wp_admin_notice_markup'} filter.
14+
* a title placeholder is inserted into the admin page's `<title>` tag,
15+
* the admin page title is modified via the {@see 'admin_title'}
16+
* filter, the admin page is wrapped in an output buffer, and an
17+
* `ob_start()` callback replaces the placeholder with the final count once
18+
* the buffer is flushed.
19+
*
20+
* @since 7.2.0
21+
*/
22+
class WP_Admin_Notice_Collector {
23+
24+
/**
25+
* Placeholder inserted into the admin title, replaced with the
26+
* actual notice count once it is known.
27+
*
28+
* @since 7.2.0
29+
* @var string
30+
*/
31+
const TITLE_PLACEHOLDER = '%%wp_admin_notice_count%%';
32+
33+
/**
34+
* Notices captured during the current request.
35+
*
36+
* @since 7.2.0
37+
* @var array[]
38+
*/
39+
protected static $notices = array();
40+
41+
/**
42+
* Whether the collector's hooks have already been registered.
43+
*
44+
* @since 7.2.0
45+
* @var bool
46+
*/
47+
protected static $hooked = false;
48+
49+
/**
50+
* Registers the hooks used to collect notices and to annotate the admin title.
51+
*
52+
* @since 7.2.0
53+
*/
54+
public static function init() {
55+
if ( self::$hooked ) {
56+
return;
57+
}
58+
59+
self::$hooked = true;
60+
61+
add_filter( 'wp_admin_notice_markup', array( __CLASS__, 'capture_notice' ), 10, 3 );
62+
63+
// Ajax/REST admin requests don't render a <title>, so skip the buffer there.
64+
if ( is_admin() && ! wp_doing_ajax() ) {
65+
add_filter( 'admin_title', array( __CLASS__, 'add_title_placeholder' ), 10, 2 );
66+
ob_start( array( __CLASS__, 'inject_notice_count' ) );
67+
}
68+
}
69+
70+
/**
71+
* Captures a single admin notice without altering its markup.
72+
*
73+
* Hooked to the {@see 'wp_admin_notice_markup'} filter.
74+
*
75+
* @since 7.2.0
76+
*
77+
* @param string $markup The HTML markup for the admin notice.
78+
* @param string $message The message for the admin notice.
79+
* @param array $args The arguments for the admin notice.
80+
* @return string The unmodified markup, so the filter remains transparent.
81+
*/
82+
public static function capture_notice( $markup, $message, $args ) {
83+
if ( '' !== trim( wp_strip_all_tags( $message ) ) ) {
84+
self::$notices[ $args['notice_id'] ] = array(
85+
'markup' => $markup,
86+
'message' => $message,
87+
'args' => $args,
88+
);
89+
}
90+
91+
return $markup;
92+
}
93+
94+
/**
95+
* Returns all notices captured during the current request.
96+
*
97+
* @since 7.2.0
98+
*
99+
* @return array[] Array of captured notices. Each entry contains the
100+
* notice's 'markup', 'message', and 'args'.
101+
*/
102+
public static function get_notices() {
103+
return self::$notices;
104+
}
105+
106+
/**
107+
* Returns the number of notices captured during the current request.
108+
*
109+
* @since 7.2.0
110+
*
111+
* @return int Notice count.
112+
*/
113+
public static function get_notice_count() {
114+
return count( self::$notices );
115+
}
116+
117+
/**
118+
* Clears all previously captured notices.
119+
*
120+
* @since 7.2.0
121+
*/
122+
public static function reset() {
123+
self::$notices = array();
124+
}
125+
126+
/**
127+
* Prepends a title placeholder via the {@see 'admin_title'} filter.
128+
*
129+
* The real count isn't known yet at this point, since notices are usually
130+
* produced later in the page. inject_notice_count() resolves it.
131+
*
132+
* @since 7.2.0
133+
*
134+
* @param string $admin_title The page title, with extra context added.
135+
* @param string $title The original page title.
136+
* @return string The admin title with the placeholder prepended.
137+
*/
138+
public static function add_title_placeholder( $admin_title, $title ) {
139+
return self::TITLE_PLACEHOLDER . $admin_title;
140+
}
141+
142+
/**
143+
* Replaces the title placeholder with the final notice count.
144+
*
145+
* Used as an `ob_start()` callback so it runs once the full page - including
146+
* any notices printed after `<title>` - has been buffered.
147+
*
148+
* @since 7.2.0
149+
*
150+
* @param string $buffer The full buffered page output.
151+
* @return string The page output with the placeholder resolved.
152+
*/
153+
public static function inject_notice_count( $buffer ) {
154+
$count = self::get_notice_count();
155+
156+
if ( 0 === $count ) {
157+
return str_replace( self::TITLE_PLACEHOLDER, '', $buffer );
158+
}
159+
160+
/* translators: %d: Number of admin notices on the current page. */
161+
$prefix = sprintf( _n( '(%d notice) ', '(%d notices) ', $count ), $count );
162+
163+
return str_replace( self::TITLE_PLACEHOLDER, $prefix, $buffer );
164+
}
165+
}

‎src/wp-includes/functions.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9342,6 +9342,10 @@ function wp_get_admin_notice( $message, $args = array() ) {
93429342
*/
93439343
$args = apply_filters( 'wp_admin_notice_args', $args, $message );
93449344

9345+
// Unique ID is intended for internal use, and is not a filterable argument.
9346+
$unique_id = wp_unique_prefixed_id( 'wp-admin-notice-' );
9347+
$args['notice_id'] = $unique_id;
9348+
93459349
$wrap_with_p = false !== $args['paragraph_wrap'];
93469350
$wrap_opener = $wrap_with_p ? '<p>' : '';
93479351
$wrap_closer = $wrap_with_p ? '</p>' : '';
@@ -9351,9 +9355,15 @@ function wp_get_admin_notice( $message, $args = array() ) {
93519355
if ( is_string( $args['id'] ) ) {
93529356
$trimmed_id = trim( $args['id'] );
93539357

9358+
$html_builder->set_attribute( 'data-id', $unique_id );
93549359
if ( '' !== $trimmed_id ) {
93559360
$html_builder->set_attribute( 'id', $trimmed_id );
9361+
} else {
9362+
$html_builder->set_attribute( 'id', $unique_id );
93569363
}
9364+
} else {
9365+
$html_builder->set_attribute( 'data-id', $unique_id );
9366+
$html_builder->set_attribute( 'id', $unique_id );
93579367
}
93589368

93599369
if ( is_string( $args['type'] ) ) {

‎src/wp-settings.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
require ABSPATH . WPINC . '/formatting.php';
116116
require ABSPATH . WPINC . '/meta.php';
117117
require ABSPATH . WPINC . '/functions.php';
118+
require ABSPATH . WPINC . '/class-wp-admin-notice-collector.php';
118119
require ABSPATH . WPINC . '/class-wp-meta-query.php';
119120
require ABSPATH . WPINC . '/class-wp-matchesmapregex.php';
120121
require ABSPATH . WPINC . '/class-wp.php';

0 commit comments

Comments
 (0)