Tooltip: ensure tooltip stays if focus moves from anchor into content

The tooltip has behaviour to ensure that when the focus
moves from the anchor to the tooltip that the tooltip does not hide,
however it only worked if the focus was the immediate child of the
tooltip. In our case (see attached bug) the focus was moving into a link
deeply nested within the contents of the tooltip.

This CL updates the code to make use of the blur target & relatedTarget
properties to detect this case and not hide the tooltip.

Fixed: 406543379
Change-Id: I7ad19fbde17e275663465e0bb86f8b2ea8ba768e
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6402534
Commit-Queue: Alina Varkki <alinavarkki@chromium.org>
Auto-Submit: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Alina Varkki <alinavarkki@chromium.org>
This commit is contained in:
Jack Franklin
2025-03-27 08:37:19 -07:00
committed by Devtools-frontend LUCI CQ
parent c14787393e
commit eb541d6436
2 changed files with 33 additions and 0 deletions
@@ -199,6 +199,28 @@ describe('Tooltip', () => {
assert.isFalse(container.querySelector('devtools-tooltip')?.open);
});
it('should not hide the tooltip if focus moves from the anchor into deep DOM within the tooltip', async () => {
const container = renderTooltip({variant: 'rich', attribute: 'aria-details'});
const anchor = container.querySelector('button');
assert.exists(anchor);
const tooltip = container.querySelector('devtools-tooltip');
assert.exists(tooltip);
// Make some nested DOM for this; this test exists because of a bug where
// the tooltip only stayed open if the focused element was an immediate
// child, so for this test we make a nested DOM structure and test on that.
tooltip.innerHTML = '<div><span><p class="deep-nested">nested</p></span></div>';
anchor.dispatchEvent(new FocusEvent('focus'));
await checkForPendingActivity();
assert.isTrue(tooltip.open);
const richContents = container.querySelector('devtools-tooltip')?.querySelector('p.deep-nested');
assert.exists(richContents);
anchor.dispatchEvent(new FocusEvent('blur', {relatedTarget: richContents}));
await checkForPendingActivity();
assert.isTrue(tooltip.open); // tooltip should still be open
});
it('automatically sets and updates jslog', () => {
const container = renderTooltip({jslogContext: 'context'});
const tooltip = container.querySelector('devtools-tooltip');
@@ -160,6 +160,17 @@ export class Tooltip extends HTMLElement {
if (this.#timeout) {
window.clearTimeout(this.#timeout);
}
// If the event is a blur event, then:
// 1. event.currentTarget = the element that got blurred
// 2. event.relatedTarget = the element that gained focus
// https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget
// If the blurred element (1) was our anchor, and the newly focused element
// (2) is within the tooltip, we do not want to hide the tooltip.
if (event && this.variant === 'rich' && event.target === this.#anchor && event.relatedTarget instanceof Node &&
this.contains(event.relatedTarget)) {
return;
}
// Don't hide a rich tooltip when hovering over the tooltip itself.
if (event && this.variant === 'rich' &&
(event.relatedTarget === this || (event.relatedTarget as Element)?.parentElement === this)) {