Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
96e603b
Add support for tooltips for metabox controls
joedolson Jul 14, 2026
d877331
Update template.php
joedolson Jul 14, 2026
1498835
Update text so accessible name matches visible name
joedolson Jul 14, 2026
67a52d8
Merge remote-tracking branch 'upstream/trunk' into add-metabox-tooltips
joedolson Jul 15, 2026
679511c
Add HTML API parsing of button markup.
joedolson Jul 15, 2026
69bf5ff
Add styles to reduce highlighting of tooltips when on disabled buttons
joedolson Jul 15, 2026
87bbb5d
Need to process all buttons
joedolson Jul 15, 2026
1332e47
Using the HTML API changes the order of button attributes
joedolson Jul 15, 2026
a8214be
Merge branch 'trunk' into add-metabox-tooltips
joedolson Jul 19, 2026
59d377a
Merge branch 'trunk' into add-metabox-tooltips
joedolson Jul 19, 2026
235515b
Restore button handling
joedolson Jul 20, 2026
9dbb894
Remove color shift on disabled buttons
joedolson Jul 20, 2026
d75ff00
Try: add role=region & aria-label to widget, simplify labels
joedolson Jul 20, 2026
4b5bb12
Move default values to default args array
joedolson Jul 20, 2026
a31c87c
$id now undefined
joedolson Jul 20, 2026
11955b5
Don't override hover/focus when disabled.
joedolson Jul 20, 2026
7fd7bdb
Update template.php
joedolson Jul 20, 2026
1cce6b1
Add aria-describedby referencing widget container ID.
joedolson Jul 20, 2026
4bac23f
Remove popovertarget on tooltips
joedolson Jul 20, 2026
cb3f7a1
Further test updates
joedolson Jul 20, 2026
63afc12
Add test to check that non-control HTML is ignored
joedolson Jul 21, 2026
b29541e
Potential fix for pull request finding
joedolson Jul 21, 2026
10bd55f
Fix doc; better handling of icon and button default args
joedolson Jul 21, 2026
0c8b172
Ensure aria-label does not contain HTML
joedolson Jul 21, 2026
c06fe8a
Merge branch 'add-metabox-tooltips' of https://github.com/joedolson/w…
joedolson Jul 21, 2026
a55493d
Fix disabled selector
joedolson Jul 21, 2026
a9046a4
Clarify test
joedolson Jul 21, 2026
a5bb234
Properly enforce usage of correct elements.
joedolson Jul 21, 2026
50afc88
Add support for generating tooltips on links.
joedolson Jul 21, 2026
4a0ce29
Update wpGetTooltip.php
joedolson Jul 21, 2026
664ab87
Reset HTML processor, as the advanced pointer prevents the link from …
joedolson Jul 21, 2026
7b96a15
Ensure default button HTML is used if no valid HTML passed.
joedolson Jul 21, 2026
1f9d7f5
Move dashicon separator to template.
joedolson Jul 21, 2026
0ab435a
Move wp-tooltip.css to a global admin style; move wp-tooltip to a pos…
joedolson Jul 21, 2026
942e7e4
Improve docs for wp_get_toggletip() and pass default arg
joedolson Jul 21, 2026
779549b
Set aria-describedby to h2 heading, instead of container.
joedolson Jul 21, 2026
96a9289
Bah.
joedolson Jul 21, 2026
556c3dc
Reflect `$button` argument in docsl
joedolson Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/js/_enqueues/wp/wp-tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
let openTimeout;

popovers.forEach( function( popover ) {
const trigger = /** @type {HTMLButtonElement|null} */ ( popover.querySelector( 'button.wp-tooltip__toggle' ) );
const trigger = /** @type {HTMLButtonElement|HTMLAnchorElement|null} */ ( popover.querySelector( '.wp-tooltip__toggle' ) );
const panel = /** @type {HTMLSpanElement|null} */ ( popover.querySelector( 'span.wp-tooltip__bubble' ) );

if ( ! trigger || ! panel ) {
return;
}
Expand Down
7 changes: 3 additions & 4 deletions src/wp-admin/css/wp-tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
.wp-tooltip {
display: inline-flex;
align-items: center;
vertical-align: middle;
}

/* Toggle and close buttons. */
Expand All @@ -22,14 +21,14 @@
cursor: pointer;
}

.wp-tooltip .wp-tooltip__toggle:hover,
.wp-tooltip .wp-tooltip__toggle:focus,
.wp-tooltip:not(:has(button[aria-disabled="true"],button[disabled])) .wp-tooltip__toggle:hover,
.wp-tooltip:not(:has(button[aria-disabled="true"],button[disabled])) .wp-tooltip__toggle:focus,
.wp-tooltip .wp-tooltip__close:hover,
.wp-tooltip .wp-tooltip__close:focus {
color: var(--wp-admin-theme-color, #3858e9);
}

.wp-tooltip .wp-tooltip__toggle:focus,
.wp-tooltip:not(:has(button[aria-disabled="true"],button[disabled])) .wp-tooltip__toggle:focus,
.wp-tooltip .wp-tooltip__close:focus {
Comment thread
joedolson marked this conversation as resolved.
outline: 2px solid transparent;
box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9);
Expand Down
67 changes: 31 additions & 36 deletions src/wp-admin/includes/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -1372,10 +1372,10 @@ function do_meta_boxes( $screen, $context, $data_object ) {
++$i;
// 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 '<div id="' . $box['id'] . '" class="postbox ' . postbox_classes( $box['id'], $page ) . $hidden_class . '" ' . '>' . "\n";
echo '<div id="' . $box['id'] . '" class="postbox ' . postbox_classes( $box['id'], $page ) . $hidden_class . '" ' . ' role="region" aria-label="' . esc_attr( wp_strip_all_tags( $box['title'] ) ) . '">' . "\n";

echo '<div class="postbox-header">';
echo '<h2 class="hndle">';
echo '<h2 class="hndle" id="' . $box['id'] . '-title">';
if ( 'dashboard_php_nag' === $box['id'] ) {
echo '<span aria-hidden="true" class="dashicons dashicons-warning"></span>';
echo '<span class="screen-reader-text">' .
Expand All @@ -1397,40 +1397,35 @@ function do_meta_boxes( $screen, $context, $data_object ) {

echo '<div class="handle-actions hide-if-no-js">';

echo '<button type="button" class="handle-order-higher" aria-disabled="false" aria-describedby="' . $box['id'] . '-handle-order-higher-description">';
echo '<span class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'Move up' ) .
'</span>';
echo '<span class="order-higher-indicator" aria-hidden="true"></span>';
echo '</button>';
echo '<span class="hidden" id="' . $box['id'] . '-handle-order-higher-description">' . sprintf(
/* translators: %s: Meta box title. */
__( 'Move %s box up' ),
$widget_title
) . '</span>';

echo '<button type="button" class="handle-order-lower" aria-disabled="false" aria-describedby="' . $box['id'] . '-handle-order-lower-description">';
echo '<span class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'Move down' ) .
'</span>';
echo '<span class="order-lower-indicator" aria-hidden="true"></span>';
echo '</button>';
echo '<span class="hidden" id="' . $box['id'] . '-handle-order-lower-description">' . sprintf(
/* translators: %s: Meta box title. */
__( 'Move %s box down' ),
$widget_title
) . '</span>';

echo '<button type="button" class="handlediv" aria-expanded="true">';
echo '<span class="screen-reader-text">' . sprintf(
/* translators: %s: Hidden accessibility text. Meta box title. */
__( 'Show or hide panel: %s' ),
$widget_title
) . '</span>';
echo '<span class="toggle-indicator" aria-hidden="true"></span>';
echo '</button>';
$move_up_button = '<button type="button" class="handle-order-higher" aria-describedby="' . $box['id'] . '-title">
<span class="screen-reader-text">' . __( 'Move up' ) . '</span>
<span class="order-higher-indicator" aria-hidden="true"></span>
</button>';
$move_up_args = array(
'id' => $box['id'] . '-handle-order-higher-description',
'button' => $move_up_button,
);
echo wp_get_tooltip( __( 'Move up' ), $move_up_args );

$move_down_button = '<button type="button" class="handle-order-lower" aria-describedby="' . $box['id'] . '-title">
<span class="screen-reader-text">' . __( 'Move down' ) . '</span>
<span class="order-lower-indicator" aria-hidden="true"></span>
</button>';
$move_down_args = array(
'id' => $box['id'] . '-handle-order-lower-description',
'button' => $move_down_button,
);
echo wp_get_tooltip( __( 'Move down' ), $move_down_args );

$show_hide_button = '<button type="button" class="handlediv" aria-expanded="true" aria-describedby="' . $box['id'] . '-title">
<span class="screen-reader-text">' . __( 'Show or hide panel' ) . '</span>
<span class="toggle-indicator" aria-hidden="true"></span>
</button>';
$show_hide_args = array(
'id' => $box['id'] . '-handlediv',
'button' => $show_hide_button,
);
echo wp_get_tooltip( __( 'Show or hide panel' ), $show_hide_args );

echo '</div>';
}
Expand Down
73 changes: 51 additions & 22 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ function get_search_form( $args = array() ) {
/**
* Retrieves the markup for an accessible tooltip.
*
* Returns a button with an accessible name popover.
* Returns a button with an accessible name popover hint.
*
* @since 7.1.0
*
Expand All @@ -383,6 +383,8 @@ function get_search_form( $args = array() ) {
*
* @type string $id Unique ID for the popover element. Default is a
* generated unique ID.
* @type string $button Existing `button` or `a` markup. Used instead of generated button.
* Default standard button HTML.
* @type string $label Not used for tooltips.
* @type string $close_label Not used for tooltips.
* @type string $icon Dashicons icon class for the toggle button.
Expand All @@ -399,12 +401,9 @@ function wp_get_tooltip( $content, $args = array() ) {
}

/**
* Retrieves the markup for an accessible tooltip or toggletip.
* Retrieves the markup for an accessible toggle tip.
*
* Returns a button and either a hover/focus triggered tooltip popover or an action
* triggered toggle tip. Enqueue the `wp-tooltip` style and script where it is used.
* Tooltips are used to show the accessible name of a control.
* Toggletips are be used for longer supporting text explaining context.
* Returns a button and an action triggered toggle tip with `$content`.
*
Comment thread
joedolson marked this conversation as resolved.
* @since 7.1.0
*
Expand All @@ -414,6 +413,8 @@ function wp_get_tooltip( $content, $args = array() ) {
*
* @type string $id Unique ID for the popover element. Default is a
* generated unique ID.
* @type string $button Existing `button` markup. Used instead of generated button.
* Default standard button HTML.
* @type string $label Accessible label for the toggle button.
* Default 'Help', matching the default icon.
* Ignored for tooltips.
Expand All @@ -424,9 +425,9 @@ function wp_get_tooltip( $content, $args = array() ) {
* @type string $class Additional class(es) for the wrapping element.
* Default empty.
* }
* @return string Tooltip HTML markup, or an empty string when no content is provided.
* @return string Toggletip HTML markup, or an empty string when no content is provided.
*/
function wp_get_toggletip( $content, $args ) {
function wp_get_toggletip( $content, $args = array() ) {
$args['type'] = 'toggletip';
return wp_get_tooltip_helper( $content, $args );
}
Expand All @@ -447,6 +448,8 @@ function wp_get_toggletip( $content, $args ) {
*
* @type string $id Unique ID for the popover element. Default is a
* generated unique ID.
* @type string $button Existing `button` or `a` markup. Used instead of generated button.
* Default standard button HTML.
* @type string $label Accessible label for the toggle button.
* Default 'Help', matching the default icon.
* Ignored for tooltips.
Expand All @@ -469,7 +472,8 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
}

$defaults = array(
'id' => '',
'id' => wp_unique_id( 'wp-tooltip-' ),
'button' => '<button type="button" aria-label="%3$s"><span class="dashicons %4$s" aria-hidden="true"></span></button>',
'label' => __( 'Help' ),
'close_label' => __( 'Close' ),
'icon' => 'dashicons-editor-help',
Expand All @@ -479,14 +483,43 @@ function wp_get_tooltip_helper( $content, $args = array() ) {

$args = wp_parse_args( $args, $defaults );

$id = '' !== $args['id'] ? $args['id'] : wp_unique_id( 'wp-tooltip-' );

$classes = ( 'tooltip' === $args['type'] ) ? 'wp-tooltip wp-is-tooltip' : 'wp-tooltip wp-is-toggletip';
Comment thread
joedolson marked this conversation as resolved.
if ( '' !== $args['class'] ) {
$classes .= ' ' . $args['class'];
}

$icon = '' !== $args['icon'] ? ' ' . $args['icon'] : '';
$icon = ( $args['icon'] ) ? trim( $args['icon'] ) : $defaults['icon'];
$id = ( $args['id'] ) ? $args['id'] : $defaults['id'];
$button = ( $args['button'] ) ? $args['button'] : $defaults['button'];
$processed = false;
$processor = new WP_HTML_Tag_Processor( $button );
if ( true === $processor->next_tag( 'button' ) ) {
$processor->add_class( 'wp-tooltip__toggle' );
if ( 'tooltip' !== $args['type'] ) {
$processor->set_attribute( 'popovertarget', '%2$s' );
$processor->set_attribute( 'aria-haspopup', 'dialog' );
}
$button = $processor->get_updated_html();
$processed = true;
} else {
// Reset processor.
$processor = new WP_HTML_Tag_Processor( $button );
if ( true === $processor->next_tag( 'a' ) && 'tooltip' === $args['type'] ) {
$processor->add_class( 'wp-tooltip__toggle' );
$button = $processor->get_updated_html();
$processed = true;
}
}
if ( ! $processed ) {
// Button HTML passed was not valid.
$processor = new WP_HTML_Tag_Processor( $defaults['button'] );
$processor->add_class( 'wp-tooltip__toggle' );
if ( 'tooltip' !== $args['type'] ) {
$processor->set_attribute( 'popovertarget', '%2$s' );
$processor->set_attribute( 'aria-haspopup', 'dialog' );
}
$button = $processor->get_updated_html();
}

/*
* The markup only uses phrasing content so it is valid when nested
Expand All @@ -498,11 +531,9 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
// Tooltips are only used to visually display labels.
$label = wp_strip_all_tags( $content, true );
$markup = sprintf(
'<span class="%1$s">' .
'<button type="button" class="wp-tooltip__toggle" popovertarget="%2$s" aria-label="%3$s">' .
'<span class="dashicons%4$s" aria-hidden="true"></span>' .
'</button>' .
'<span popover="hint" id="%2$s" class="wp-tooltip__bubble" role="tooltip">' .
'<span class="%1$s">
' . $button . '
<span popover="hint" id="%2$s" class="wp-tooltip__bubble" role="tooltip">' .
'<span id="%2$s-text" class="wp-tooltip__text">%5$s</span>' .
'</span>' .
'</span>',
Expand All @@ -519,11 +550,9 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
* attributes reproduce the accessible name and focus handling of the native element.
*/
$markup = sprintf(
'<span class="%1$s">' .
'<button type="button" class="wp-tooltip__toggle" popovertarget="%2$s" aria-label="%3$s" aria-haspopup="dialog">' .
'<span class="dashicons%4$s" aria-hidden="true"></span>' .
'</button>' .
'<span popover="auto" id="%2$s" class="wp-tooltip__bubble" role="dialog" aria-label="%3$s" tabindex="-1" autofocus>' .
'<span class="%1$s">
' . $button . '
<span popover="auto" id="%2$s" class="wp-tooltip__bubble" role="dialog" aria-label="%3$s" tabindex="-1" autofocus>' .
'<span id="%2$s-text" class="wp-tooltip__text">%5$s</span>' .
'<button type="button" class="wp-tooltip__close" popovertarget="%2$s" popovertargetaction="hide" aria-label="%6$s">' .
'<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>' .
Expand Down
4 changes: 2 additions & 2 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,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', 'wp-a11y' ), false, 1 );
$scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable', 'wp-a11y', 'wp-tooltip' ), false, 1 );
$scripts->set_translations( 'postbox' );

$scripts->add( 'tags-box', "/wp-admin/js/tags-box$suffix.js", array( 'jquery', 'tags-suggest' ), false, 1 );
Expand Down Expand Up @@ -1638,7 +1638,7 @@ function wp_default_styles( $styles ) {
$styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) );
$styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" );

$styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'wp-base-styles' ) );
$styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'wp-base-styles', 'wp-tooltip' ) );

$styles->add( 'login', "/wp-admin/css/login$suffix.css", array( 'dashicons', 'buttons', 'forms', 'l10n', 'wp-base-styles', 'wp-tooltip' ) );
$styles->add( 'install', "/wp-admin/css/install$suffix.css", array( 'dashicons', 'buttons', 'forms', 'l10n', 'wp-base-styles' ) );
Expand Down
63 changes: 57 additions & 6 deletions tests/phpunit/tests/general/wpGetTooltip.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ public function test_wp_get_tooltip_returns_empty_string_without_content() {
public function test_wp_get_tooltip_returns_accessible_markup() {
$tooltip = wp_get_tooltip( 'Helpful text.', array( 'id' => 'my-tip' ) );

// Toggle is a button that controls the popover and describes it.
$this->assertStringContainsString( '<button type="button" class="wp-tooltip__toggle"', $tooltip );
$this->assertStringContainsString( 'popovertarget="my-tip"', $tooltip );
// Tooltip output should include a toggle button, but tooltips do not use a popover trigger.
$this->assertStringContainsString( '<button', $tooltip );
$this->assertStringContainsString( 'class="wp-tooltip__toggle"', $tooltip );
$this->assertStringContainsString( 'type="button"', $tooltip );
$this->assertStringNotContainsString( 'popovertarget="my-tip"', $tooltip );

// The bubble is a popover holding a text-only described element.
$this->assertStringContainsString( '<span popover="hint" id="my-tip" class="wp-tooltip__bubble" role="tooltip">', $tooltip );
Expand All @@ -47,6 +49,7 @@ public function test_wp_get_tooltip_returns_accessible_markup() {
)
);
// Ensure the toggle tip does contain a close button.
$this->assertStringContainsString( 'popovertarget="my-tip"', $toggletip );
$this->assertStringContainsString( 'class="wp-tooltip__close"', $toggletip );
$this->assertStringContainsString( 'popovertargetaction="hide"', $toggletip );
}
Expand All @@ -63,6 +66,50 @@ public function test_wp_get_tooltip_escapes_content() {
$this->assertStringContainsString( '&lt;script&gt;', $html );
}

/**
* Tests that custom markup that does not contain valid control markup is ignored.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_ignores_invalid_markup() {
$html = wp_get_tooltip(
'Helpful text.',
array(
'button' => '<div role="button" aria-expanded="false">Contains text</div>',
)
);

$this->assertStringNotContainsString( 'div role="button"', $html, 'String does not contain element from custom HTML.' );
$this->assertStringContainsString( '<button', $html, 'String does contain a button element.' );
}

/**
* Tests that custom button markup is retained, and has required attributes.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_has_custom_button_markup() {
$html = wp_get_tooltip(
'Helpful text.',
array(
'button' => '<button type="button" aria-expanded="false">Contains text</button>',
)
);

$this->assertStringContainsString( 'aria-expanded="false"', $html, 'String contains attribute not supported in function.' );
$this->assertStringNotContainsString( 'dashicons', $html, 'String does not contain content of default button.' );

$html2 = wp_get_tooltip(
'Helpful text.',
array(
'button' => '<a href="#">Contains text</a>',
)
);

$this->assertStringContainsString( '<a ', $html2, 'String contains anchor element.' );
$this->assertStringNotContainsString( '<button', $html2, 'String does not contain button element.' );
}

/**
* Tests that the accessible labels are output and escaped in attributes.
*
Expand Down Expand Up @@ -101,12 +148,16 @@ public function test_wp_get_tooltip_applies_icon_and_class() {

/**
* Tests that a generated ID is used when none is supplied, and that the
* describedby target matches the bubble ID.
* popovertarget matches the bubble ID.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_generates_unique_id() {
$html = wp_get_tooltip( 'Helpful text.' );
public function test_wp_get_toggletip_generates_unique_id() {
$args = array(
'label' => 'About this field',
'close_label' => 'Dismiss',
);
$html = wp_get_toggletip( 'Helpful text.', $args );

$this->assertSame( 1, preg_match( '/id="(wp-tooltip-\d+)"/', $html, $matches ) );

Expand Down
Loading