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
57 changes: 57 additions & 0 deletions src/js/_enqueues/wp/wp-tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,60 @@
trigger.addEventListener( 'blur', hideTooltip );
});
})();

/**
* Add focus and hover support for the toggle tip's hint in `wp_get_toggletip()`.
*
* A toggle tip has no visible text label, so a hover/focus hint exposes the
* toggle button's accessible name to sighted users. The hint is suppressed while
* the toggle tip dialog itself is open, so the two never overlap.
* This script can be made obsolete when support is available for Interest Invokers.
*/
(() => {

const toggletips = document.querySelectorAll( '.wp-is-toggletip' );
let openTimeout;

toggletips.forEach( function( toggletip ) {
let trigger = toggletip.querySelector( 'button.wp-tooltip__toggle' );
let hint = toggletip.querySelector( '.wp-tooltip__hint' );
let dialog = toggletip.querySelector( 'dialog.wp-tooltip__bubble' );

if ( ! trigger || ! hint ) {
return;
}

// Show Hint Function (with delay to prevent flickering).
const showHint = () => {
clearTimeout( openTimeout );
openTimeout = setTimeout( () => {
// Don't show the hint over an open toggle tip dialog.
if ( dialog && dialog.matches( ':popover-open' ) ) {
return;
}
// Only show if it's not already open.
if ( ! hint.matches( ':popover-open' ) ) {
// pass the triggering element so implicit position anchors work.
hint.showPopover( { source: trigger } );
}
}, 300 );
};
// Hide Hint Function.
const hideHint = () => {
clearTimeout( openTimeout );
if ( hint.matches( ':popover-open' ) ) {
hint.hidePopover();
}
};

// Bind Hover and Focus Events.
trigger.addEventListener( 'mouseenter', showHint );
trigger.addEventListener( 'focus', showHint );

trigger.addEventListener( 'mouseleave', hideHint );
trigger.addEventListener( 'blur', hideHint );

// Hide the hint as soon as the toggle tip is opened.
trigger.addEventListener( 'click', hideHint );
});
})();
16 changes: 9 additions & 7 deletions src/wp-admin/css/wp-tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,16 @@
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
}

.wp-tooltip.wp-is-tooltip .wp-tooltip__bubble {
.wp-tooltip.wp-is-tooltip .wp-tooltip__bubble,
.wp-tooltip .wp-tooltip__hint {
background: #000;
color: #f0f0f0;
text-align: center;
padding: 4px 8px;
line-height: 1.4;
}

.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__text {
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble:not(.wp-tooltip__hint) .wp-tooltip__text {
display: block;
padding-inline-end: 24px;
}
Expand All @@ -91,13 +92,14 @@
position-try-fallbacks: flip-block, flip-inline;
}

.wp-tooltip.wp-is-tooltip .wp-tooltip__bubble {
.wp-tooltip.wp-is-tooltip .wp-tooltip__bubble,
.wp-tooltip .wp-tooltip__hint {
margin: 0;
}

/* Arrow: gray border (::before) under a white fill (::after). */
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble::before,
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble::after {
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble:not(.wp-tooltip__hint)::before,
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble:not(.wp-tooltip__hint)::after {
content: "";
position: absolute;
top: 100%;
Expand All @@ -108,12 +110,12 @@
border-bottom: 0;
}

.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble::before {
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble:not(.wp-tooltip__hint)::before {
margin-left: -8px;
border-top-color: #c3c4c7;
}

.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble::after {
.wp-tooltip:not(.wp-is-tooltip) .wp-tooltip__bubble:not(.wp-tooltip__hint)::after {
margin-left: -7px;
border-width: 7px;
border-bottom: 0;
Expand Down
4 changes: 4 additions & 0 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
'<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="hint" id="%2$s-hint" class="wp-tooltip__bubble wp-tooltip__hint" aria-hidden="true">' .
'<span class="wp-tooltip__text">%7$s</span>' .
'</span>' .
'<dialog popover="auto" id="%2$s" class="wp-tooltip__bubble" 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">' .
Expand All @@ -525,6 +528,7 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
esc_attr( $icon ),
esc_html( $content ),
esc_attr( $args['close_label'] ),
esc_html( $args['label'] ),
);
}

Expand Down
56 changes: 56 additions & 0 deletions tests/phpunit/tests/general/wpGetTooltip.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public function test_wp_get_tooltip_returns_accessible_markup() {
$this->assertStringNotContainsString( 'class="wp-tooltip__close"', $tooltip );
$this->assertStringNotContainsString( 'popovertargetaction="hide"', $tooltip );

// A plain tooltip has no separate hint element.
$this->assertStringNotContainsString( 'wp-tooltip__hint', $tooltip );

$toggletip = wp_get_toggletip(
'Helpful text.',
array(
Expand All @@ -51,6 +54,59 @@ public function test_wp_get_tooltip_returns_accessible_markup() {
$this->assertStringContainsString( 'popovertargetaction="hide"', $toggletip );
}

/**
* Tests that a toggle tip exposes its accessible name through a hint tooltip.
*
* With no visible text label, the toggle button's accessible name is only
* available to assistive technologies via `aria-label`. A hover/focus hint
* tooltip exposes that same name to sighted users, while being hidden from
* assistive technologies to avoid a duplicate announcement.
*
* @ticket 65633
*/
public function test_wp_get_toggletip_includes_accessible_name_hint() {
$toggletip = wp_get_toggletip(
'Helpful text.',
array(
'id' => 'my-tip',
'label' => 'About this field',
)
);

// The hint is a hint popover that duplicates the toggle button's label.
$this->assertStringContainsString(
'<span popover="hint" id="my-tip-hint" class="wp-tooltip__bubble wp-tooltip__hint" aria-hidden="true">',
$toggletip
);
$this->assertStringContainsString(
'<span class="wp-tooltip__text">About this field</span>',
$toggletip
);

// The toggle button still exposes the accessible name to assistive technologies.
$this->assertStringContainsString( 'aria-label="About this field"', $toggletip );
}

/**
* Tests that the toggle tip hint escapes its content.
*
* @ticket 65633
*/
public function test_wp_get_toggletip_hint_escapes_label() {
$toggletip = wp_get_toggletip(
'Helpful text.',
array(
'label' => '<script>alert(1)</script>',
)
);

$this->assertStringContainsString(
'<span class="wp-tooltip__text">&lt;script&gt;',
$toggletip
);
$this->assertStringNotContainsString( '<span class="wp-tooltip__text"><script>', $toggletip );
}

/**
* Tests that content is escaped.
*
Expand Down
Loading