Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 110 additions & 0 deletions src/wp-admin/css/wp-tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* Accessible tooltip component. Markup from wp_get_tooltip(). */

.wp-tooltip {
display: inline-flex;
align-items: center;
vertical-align: middle;
}

/* Toggle and close buttons. */
.wp-tooltip .wp-tooltip__toggle,
.wp-tooltip .wp-tooltip__close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: 2px;
background: none;
color: #50575e;
cursor: pointer;
}

.wp-tooltip .wp-tooltip__toggle:hover,
.wp-tooltip .wp-tooltip__toggle:focus,
.wp-tooltip .wp-tooltip__close:hover,
.wp-tooltip .wp-tooltip__close:focus {
color: var(--wp-admin-theme-color, #2271b1);
}

.wp-tooltip .wp-tooltip__toggle:focus,
.wp-tooltip .wp-tooltip__close:focus {
outline: 2px solid transparent;
box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9);
}

.wp-tooltip .wp-tooltip__toggle .dashicons {
width: 20px;
height: 20px;
font-size: 20px;
}

.wp-tooltip .wp-tooltip__close .dashicons {
width: 18px;
height: 18px;
font-size: 18px;
}

/* Bubble. No position here, so it falls back to the UA's centered popover without anchoring. */
.wp-tooltip .wp-tooltip__bubble {
max-width: min(280px, calc(100vw - 32px));
margin: auto;
padding: 12px 16px;
overflow: visible;
border: 1px solid #c3c4c7;
border-radius: 4px;
background: #fff;
color: #1d2327;
font-size: 13px;
font-weight: 400;
line-height: 1.5;
text-align: left;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
}

.wp-tooltip .wp-tooltip__text {
display: block;
padding-inline-end: 24px;
}

.wp-tooltip .wp-tooltip__close {
position: absolute;
top: 6px;
inset-inline-end: 6px;
}

/* Anchor the bubble above its toggle, with a downward arrow. */
@supports (anchor-name: --a) {
.wp-tooltip .wp-tooltip__bubble {
position-area: top;
margin: 0 0 8px;
position-try-fallbacks: flip-block, flip-inline;
}

/* Arrow: gray border (::before) under a white fill (::after). */
.wp-tooltip .wp-tooltip__bubble::before,
.wp-tooltip .wp-tooltip__bubble::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
width: 0;
height: 0;
border: 8px solid transparent;
border-bottom: 0;
}

.wp-tooltip .wp-tooltip__bubble::before {
margin-left: -8px;
border-top-color: #c3c4c7;
}

.wp-tooltip .wp-tooltip__bubble::after {
margin-left: -7px;
border-width: 7px;
border-bottom: 0;
border-top-color: #fff;
}
}
71 changes: 71 additions & 0 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,77 @@ function get_search_form( $args = array() ) {
}
}

/**
* Retrieves the markup for an accessible tooltip.
*
* Returns a help button and its tooltip popover, linked with `aria-describedby`. Enqueue the
* `wp-tooltip` style where it is used; the login styles already do.
*
* @since 7.1.0
*
* @param string $content Plain-text tooltip content. An empty value returns an empty string.
* @param array $args {
* Optional. Arguments for building the tooltip.
*
* @type string $id Unique ID for the popover element. Default is a
* generated unique ID.
* @type string $label Accessible label for the toggle button.
* Default 'More information'.
* @type string $close_label Accessible label for the close button. Default 'Close'.
* @type string $icon Dashicons icon class for the toggle button.
* Default 'dashicons-editor-help'.
* @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.
*/
function wp_get_tooltip( $content, $args = array() ) {
$content = trim( (string) $content );

if ( '' === $content ) {
return '';
}

$defaults = array(
'id' => '',
'label' => __( 'More information' ),
'close_label' => __( 'Close' ),
'icon' => 'dashicons-editor-help',
'class' => '',
);

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

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

$classes = 'wp-tooltip';
if ( '' !== $args['class'] ) {
$classes .= ' ' . $args['class'];
}

$icon = '' !== $args['icon'] ? ' ' . $args['icon'] : '';

return sprintf(
'<span class="%1$s">' .
'<button type="button" class="wp-tooltip__toggle" popovertarget="%2$s" aria-describedby="%2$s-text" aria-label="%3$s">' .
'<span class="dashicons%4$s" aria-hidden="true"></span>' .
'</button>' .
'<span popover="auto" id="%2$s" class="wp-tooltip__bubble">' .
'<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>' .
'</button>' .
'</span>' .
'</span>',
esc_attr( $classes ),
esc_attr( $id ),
esc_attr( $args['label'] ),
esc_attr( $icon ),
esc_html( $content ),
esc_attr( $args['close_label'] )
);
}

/**
* Displays the Log In/Out link.
*
Expand Down
3 changes: 2 additions & 1 deletion src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ function wp_default_styles( $styles ) {
$suffix = SCRIPT_DEBUG ? '' : '.min';

// Admin CSS.
$styles->add( 'wp-tooltip', "/wp-admin/css/wp-tooltip$suffix.css", array( 'dashicons' ) );
$styles->add( 'common', "/wp-admin/css/common$suffix.css" );
$styles->add( 'forms', "/wp-admin/css/forms$suffix.css" );
$styles->add( 'admin-menu', "/wp-admin/css/admin-menu$suffix.css" );
Expand All @@ -1636,7 +1637,7 @@ function wp_default_styles( $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' ) );

$styles->add( 'login', "/wp-admin/css/login$suffix.css", array( 'dashicons', 'buttons', 'forms', 'l10n', 'wp-base-styles' ) );
$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' ) );
$styles->add( 'wp-color-picker', "/wp-admin/css/color-picker$suffix.css" );
$styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'imgareaselect' ) );
Expand Down
29 changes: 28 additions & 1 deletion src/wp-login.php
Original file line number Diff line number Diff line change
Expand Up @@ -1541,7 +1541,34 @@ function wp_login_viewport_meta() {
do_action( 'login_form' );

?>
<p class="forgetmenot"><input name="rememberme" type="checkbox" id="rememberme" value="forever" <?php checked( $rememberme ); ?> /> <label for="rememberme"><?php esc_html_e( 'Remember Me' ); ?></label></p>
<?php
/**
* Filters the help text shown in the "Remember Me" tooltip on the login form.
*
* Returning an empty string removes the tooltip toggle from the form.
*
* @since 7.1.0
*
* @param string $rememberme_help_text The tooltip help text.
*/
$rememberme_help_text = apply_filters(
'login_remember_me_help_text',
__( 'Selecting "Remember Me" reduces the number of times you&#8217;ll be asked to log in using this device. To keep your account secure, use this option only on your personal devices.' )
);
?>
<p class="forgetmenot">
<input name="rememberme" type="checkbox" id="rememberme" value="forever" <?php checked( $rememberme ); ?> />
<label for="rememberme"><?php esc_html_e( 'Remember Me' ); ?></label>
<?php
echo wp_get_tooltip(
$rememberme_help_text,
array(
'id' => 'rememberme-help',
'label' => __( 'More information about &#8220;Remember Me&#8221;' ),
)
);
?>
</p>
<p class="submit">
<input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Log In' ); ?>" />
<?php
Expand Down
123 changes: 123 additions & 0 deletions tests/phpunit/tests/general/wpGetTooltip.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php
/**
* Test wp_get_tooltip().
*
* @group general
* @group template
* @group tooltip
*
* @covers ::wp_get_tooltip
*/
class Tests_General_wpGetTooltip extends WP_UnitTestCase {

/**
* Tests that an empty content value returns an empty string.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_returns_empty_string_without_content() {
$this->assertSame( '', wp_get_tooltip( '' ) );
$this->assertSame( '', wp_get_tooltip( ' ' ) );
}

/**
* Tests that the markup contains the expected accessible structure.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_returns_accessible_markup() {
$html = 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"', $html );
$this->assertStringContainsString( 'popovertarget="my-tip"', $html );
$this->assertStringContainsString( 'aria-describedby="my-tip-text"', $html );

// The bubble is a popover holding a text-only described element.
$this->assertStringContainsString( '<span popover="auto" id="my-tip" class="wp-tooltip__bubble">', $html );
$this->assertStringContainsString( '<span id="my-tip-text" class="wp-tooltip__text">Helpful text.</span>', $html );

// A native close button that hides the popover.
$this->assertStringContainsString( 'class="wp-tooltip__close"', $html );
$this->assertStringContainsString( 'popovertargetaction="hide"', $html );
}

/**
* Tests that disallowed roles and attributes are omitted.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_omits_disallowed_attributes() {
$html = wp_get_tooltip( 'Helpful text.' );

$this->assertStringNotContainsString( 'role="tooltip"', $html );
$this->assertStringNotContainsString( 'aria-haspopup', $html );
$this->assertStringNotContainsString( 'aria-live', $html );
$this->assertStringNotContainsString( 'title=', $html );
}

/**
* Tests that content is escaped.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_escapes_content() {
$html = wp_get_tooltip( '<script>alert(1)</script>' );

$this->assertStringNotContainsString( '<script>', $html );
$this->assertStringContainsString( '&lt;script&gt;', $html );
}

/**
* Tests that the accessible labels are output and escaped in attributes.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_outputs_labels() {
$html = wp_get_tooltip(
'Helpful text.',
array(
'label' => 'About this field',
'close_label' => 'Dismiss',
)
);

$this->assertStringContainsString( 'aria-label="About this field"', $html );
$this->assertStringContainsString( 'aria-label="Dismiss"', $html );
}

/**
* Tests that a custom icon class and wrapper class are applied.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_applies_icon_and_class() {
$html = wp_get_tooltip(
'Helpful text.',
array(
'icon' => 'dashicons-info',
'class' => 'my-wrap',
)
);

$this->assertStringContainsString( 'class="wp-tooltip my-wrap"', $html );
$this->assertStringContainsString( 'class="dashicons dashicons-info"', $html );
}

/**
* Tests that a generated ID is used when none is supplied, and that the
* describedby target matches the bubble ID.
*
* @ticket 55343
*/
public function test_wp_get_tooltip_generates_unique_id() {
$html = wp_get_tooltip( 'Helpful text.' );

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

$id = $matches[1];
$this->assertStringContainsString( 'popovertarget="' . $id . '"', $html );
$this->assertStringContainsString( 'aria-describedby="' . $id . '-text"', $html );
$this->assertStringContainsString( 'id="' . $id . '-text"', $html );
}
}
Loading