Skip to content

Commit cea485a

Browse files
Marcel PützDevtools-frontend LUCI CQ
authored andcommitted
Close tooltip automatically if anchor is removed
The css solution works when scrolling/moving the anchor outside of the viewport. The mutation observer is needed when the tooltip can't be nested as a child of the anchor and is removed from the DOM while the tooltip is still open. A use-case for this is the editing of a css selector, while the specificity tooltip is still visible, see http://crrev/c/6298060. Demo: https://screencast.googleplex.com/cast/NDgzNjA0OTc1MzczNTE2OHxlYzllMmU0MC0yMA Change-Id: I3092910abb8eb397cd2ba0fe0d01dd721ecfd4ff Bug: 399801002 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6308773 Reviewed-by: Philip Pfaffe <[email protected]> Reviewed-by: Danil Somsikov <[email protected]> Commit-Queue: Marcel Pütz <[email protected]>
1 parent 962f5d7 commit cea485a

File tree

4 files changed

+49
-4
lines changed

4 files changed

+49
-4
lines changed

front_end/ui/components/docs/tooltip/basic.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ Lit.render(
2525
<devtools-tooltip id="simple-tooltip">Simple content</devtools-tooltip>
2626
</div>
2727
<div style="position: relative; z-index: 0;">
28-
<span aria-details="rich-tooltip" style="position: absolute; left: 16px; top: 116px; border: 1px solid black;">
28+
<span
29+
aria-details="rich-tooltip"
30+
style="position: absolute; left: 16px; top: 116px; border: 1px solid black;"
31+
>
2932
Non-button click trigger
3033
</span>
3134
<devtools-tooltip id="rich-tooltip" variant="rich" use-click>
@@ -34,8 +37,13 @@ Lit.render(
3437
</devtools-tooltip>
3538
</div>
3639
<div>
37-
<button class="anchor" style="position: absolute; left: 16px; top: 216px;">
38-
Programmatic creation
40+
<button
41+
id="removable"
42+
@click=${() => document.getElementById('removable')?.remove()}
43+
class="anchor"
44+
style="position: absolute; left: 16px; top: 216px;"
45+
>
46+
Click to remove anchor
3947
</button>
4048
</div>
4149
`,
@@ -44,7 +52,7 @@ Lit.render(
4452
const anchor = container.querySelector('.anchor') as HTMLElement;
4553
const programmaticTooltip = new Tooltip({id: 'programatic', variant: 'rich', anchor});
4654
programmaticTooltip.append('Text content');
47-
anchor.appendChild(programmaticTooltip);
55+
anchor.insertAdjacentElement('afterend', programmaticTooltip);
4856

4957
// Make the buttons draggable, so that we can experiment with the position of the tooltip.
5058
container.querySelectorAll('button,span').forEach(anchor => draggable(anchor as HTMLElement));

front_end/ui/components/tooltip/Tooltip.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ describe('Tooltip', () => {
121121

122122
assert.strictEqual(anchor.style.anchorName, '--tooltip-id-anchor');
123123
});
124+
125+
it('should hide the tooltip if anchor is removed from DOM', async () => {
126+
const container = renderTooltip();
127+
128+
const button = container.querySelector('button');
129+
button?.dispatchEvent(new MouseEvent('mouseenter'));
130+
await checkForPendingActivity();
131+
button?.remove();
132+
await checkForPendingActivity();
133+
134+
assert.isFalse(container.querySelector('devtools-tooltip')?.open);
135+
});
124136
});
125137

126138
describe('closestAnchor', () => {

front_end/ui/components/tooltip/Tooltip.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class Tooltip extends HTMLElement {
3434
#anchor: HTMLElement|null = null;
3535
#timeout: number|null = null;
3636
#closing = false;
37+
#anchorObserver: MutationObserver|null = null;
3738

3839
get open(): boolean {
3940
return this.matches(':popover-open');
@@ -104,6 +105,7 @@ export class Tooltip extends HTMLElement {
104105

105106
disconnectedCallback(): void {
106107
this.#removeEventListeners();
108+
this.#anchorObserver?.disconnect();
107109
}
108110

109111
showTooltip = (): void => {
@@ -211,8 +213,30 @@ export class Tooltip extends HTMLElement {
211213
const anchorName = `--${id}-anchor`;
212214
anchor.style.anchorName = anchorName;
213215
this.style.positionAnchor = anchorName;
216+
this.#observeAnchorRemoval(anchor);
214217
this.#anchor = anchor;
215218
}
219+
220+
#observeAnchorRemoval(anchor: Element): void {
221+
if (anchor.parentElement === null) {
222+
return;
223+
}
224+
if (this.#anchorObserver) {
225+
this.#anchorObserver.disconnect();
226+
}
227+
228+
this.#anchorObserver = new MutationObserver(mutations => {
229+
for (const mutation of mutations) {
230+
if (mutation.type === 'childList' && [...mutation.removedNodes].includes(anchor)) {
231+
if (this.#timeout) {
232+
window.clearTimeout(this.#timeout);
233+
}
234+
this.hidePopover();
235+
}
236+
}
237+
});
238+
this.#anchorObserver.observe(anchor.parentElement, {childList: true});
239+
}
216240
}
217241

218242
export function closestAnchor(tooltip: Element, selector: string): Element|null {

front_end/ui/components/tooltip/tooltip.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
padding: 0;
1414
overflow: visible;
1515
position-area: bottom;
16+
position-visibility: anchors-visible;
1617
justify-self: anchor-center;
1718
position-try-fallbacks: flip-inline, flip-block, flip-inline flip-block;
1819

0 commit comments

Comments
 (0)