Skip to content

Commit b0e0d4c

Browse files
Marcel PützDevtools-frontend LUCI CQ
authored andcommitted
Support click to open devtools-tooltip
Demo: https://screencast.googleplex.com/cast/NTY3Mjc1ODE3MTE0MDA5NnxhMTkyYmY5MS0zMA Change-Id: I25b77d07412ba6688d1112d876464a117ca7de06 Bug: 397967873 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6291047 Reviewed-by: Ergün Erdoğmuş <[email protected]> Reviewed-by: Danil Somsikov <[email protected]> Commit-Queue: Marcel Pütz <[email protected]>
1 parent 87bddfd commit b0e0d4c

File tree

3 files changed

+89
-21
lines changed

3 files changed

+89
-21
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +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-
<button aria-details="rich-tooltip" style="position: absolute; left: 16px; top: 116px;">
29-
Rich
30-
</button>
31-
<devtools-tooltip id="rich-tooltip" variant="rich">
28+
<span aria-details="rich-tooltip" style="position: absolute; left: 16px; top: 116px; border: 1px solid black;">
29+
Non-button click trigger
30+
</span>
31+
<devtools-tooltip id="rich-tooltip" variant="rich" use-click>
3232
<p>Rich tooltip</p>
3333
<button>Action</button>
3434
</devtools-tooltip>
@@ -47,8 +47,8 @@ programmaticTooltip.append('Text content');
4747
anchor.appendChild(programmaticTooltip);
4848

4949
// Make the buttons draggable, so that we can experiment with the position of the tooltip.
50-
container.querySelectorAll('button').forEach(draggable);
51-
function draggable(element: HTMLElement|null) {
50+
container.querySelectorAll('button,span').forEach(anchor => draggable(anchor as HTMLElement));
51+
function draggable(element: HTMLElement|null): void {
5252
if (!element) {
5353
return;
5454
}

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@ const {
1717
const {html, Directives} = Lit;
1818
const {ref, createRef} = Directives;
1919

20-
function renderTooltip(
21-
{variant = 'simple',
22-
attribute = 'aria-describedby'}: {variant?: TooltipVariant, attribute?: 'aria-describedby'|'aria-details'} = {}) {
20+
interface RenderProps {
21+
variant?: TooltipVariant;
22+
attribute?: 'aria-describedby'|'aria-details';
23+
useClick?: boolean;
24+
}
25+
26+
function renderTooltip({
27+
variant = 'simple',
28+
attribute = 'aria-describedby',
29+
useClick = false,
30+
}: RenderProps = {}) {
2331
const container = document.createElement('div');
2432
// clang-format off
2533
Lit.render(html`
2634
${attribute === 'aria-details' ?
2735
html`<button aria-details="tooltip-id">Button</button>` :
2836
html`<button aria-describedby="tooltip-id">Button</button>`
2937
}
30-
<devtools-tooltip id="tooltip-id" variant=${variant}>
38+
<devtools-tooltip id="tooltip-id" variant=${variant} ?use-click=${useClick}>
3139
${variant === 'rich' ? html`<p>Rich content</p>` : 'Simple content'}
3240
</devtools-tooltip>
3341
`, container);
@@ -58,7 +66,26 @@ describe('Tooltip', () => {
5866
button?.dispatchEvent(new MouseEvent('mouseenter'));
5967

6068
await checkForPendingActivity();
61-
assert.isFalse(container.querySelector('devtools-tooltip')?.hidden);
69+
assert.isTrue(container.querySelector('devtools-tooltip')?.open);
70+
});
71+
72+
it('should not open on hover if use-click is set', async () => {
73+
const container = renderTooltip({useClick: true});
74+
75+
const button = container.querySelector('button');
76+
button?.dispatchEvent(new MouseEvent('mouseenter'));
77+
78+
await checkForPendingActivity();
79+
assert.isFalse(container.querySelector('devtools-tooltip')?.open);
80+
});
81+
82+
it('should open with click if use-click is set', () => {
83+
const container = renderTooltip({useClick: true});
84+
85+
const button = container.querySelector('button');
86+
button?.click();
87+
88+
assert.isTrue(container.querySelector('devtools-tooltip')?.open);
6289
});
6390

6491
const eventsNotToPropagate = ['click', 'mouseup'];

front_end/ui/components/tooltip/Tooltip.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,34 @@ export interface TooltipProperties {
2121
* @attr hover-delay - Hover length in ms before the tooltip is shown and hidden.
2222
* @attr variant - Variant of the tooltip, `"simple"` for strings only, inverted background,
2323
* `"rich"` for interactive content, background according to theme's surface.
24+
* @attr use-click - If present, the tooltip will be shown on click instead of on hover.
2425
* @prop {String} id - reflects the `"id"` attribute.
2526
* @prop {Number} hoverDelay - reflects the `"hover-delay"` attribute.
2627
* @prop {String} variant - reflects the `"variant"` attribute.
28+
* @prop {Boolean} useClick - reflects the `"click"` attribute.
2729
*/
2830
export class Tooltip extends HTMLElement {
2931
static readonly observedAttributes = ['id', 'variant'];
3032

3133
readonly #shadow = this.attachShadow({mode: 'open'});
3234
#anchor: HTMLElement|null = null;
3335
#timeout: number|null = null;
36+
#closing = false;
37+
38+
get open(): boolean {
39+
return this.matches(':popover-open');
40+
}
41+
42+
get useClick(): boolean {
43+
return this.hasAttribute('use-click') ?? false;
44+
}
45+
set useClick(useClick: boolean) {
46+
if (useClick) {
47+
this.setAttribute('use-click', '');
48+
} else {
49+
this.removeAttribute('use-click');
50+
}
51+
}
3452

3553
get hoverDelay(): number {
3654
return this.hasAttribute('hover-delay') ? Number(this.getAttribute('hover-delay')) : 200;
@@ -110,42 +128,65 @@ export class Tooltip extends HTMLElement {
110128
}, this.hoverDelay);
111129
};
112130

131+
toggle = (): void => {
132+
// We need this check because clicking on the anchor while the tooltip is open will trigger both
133+
// the click event on the anchor and the toggle event from the backdrop of the tooltip.
134+
if (!this.#closing) {
135+
this.togglePopover();
136+
}
137+
};
138+
113139
#setAttributes(): void {
114140
if (!this.hasAttribute('role')) {
115141
this.setAttribute('role', 'tooltip');
116142
}
117-
this.setAttribute('popover', 'manual');
118-
}
119-
120-
#preventDefault(event: Event): void {
121-
event.preventDefault();
143+
this.setAttribute('popover', this.useClick ? 'auto' : 'manual');
122144
}
123145

124146
#stopPropagation(event: Event): void {
125147
event.stopPropagation();
126148
}
127149

150+
#setClosing = (event: Event): void => {
151+
if ((event as ToggleEvent).newState === 'closed') {
152+
this.#closing = true;
153+
}
154+
};
155+
156+
#resetClosing = (event: Event): void => {
157+
if ((event as ToggleEvent).newState === 'closed') {
158+
this.#closing = false;
159+
}
160+
};
161+
128162
#registerEventListeners(): void {
129163
if (this.#anchor) {
130-
this.#anchor.addEventListener('mouseenter', this.showTooltip);
131-
this.#anchor.addEventListener('mouseleave', this.hideTooltip);
132-
// By default the anchor with a popovertarget would toggle the popover on click.
133-
this.#anchor.addEventListener('click', this.#preventDefault);
164+
if (this.useClick) {
165+
this.#anchor.addEventListener('click', this.toggle);
166+
} else {
167+
this.#anchor.addEventListener('mouseenter', this.showTooltip);
168+
this.#anchor.addEventListener('mouseleave', this.hideTooltip);
169+
this.addEventListener('mouseleave', this.hideTooltip);
170+
}
134171
}
135-
this.addEventListener('mouseleave', this.hideTooltip);
136172
// Prevent interaction with the parent element.
137173
this.addEventListener('click', this.#stopPropagation);
138174
this.addEventListener('mouseup', this.#stopPropagation);
175+
this.addEventListener('beforetoggle', this.#setClosing);
176+
this.addEventListener('toggle', this.#resetClosing);
139177
}
140178

141179
#removeEventListeners(): void {
142180
if (this.#anchor) {
181+
this.#anchor.removeEventListener('click', this.toggle);
143182
this.#anchor.removeEventListener('mouseenter', this.showTooltip);
144183
this.#anchor.removeEventListener('mouseleave', this.hideTooltip);
145184
}
146185
this.removeEventListener('mouseleave', this.hideTooltip);
147186
this.removeEventListener('click', this.#stopPropagation);
148187
this.removeEventListener('mouseup', this.#stopPropagation);
188+
this.removeEventListener('beforetoggle', this.#setClosing);
189+
this.removeEventListener('toggle', this.#resetClosing);
149190
}
150191

151192
#attachToAnchor(): void {

0 commit comments

Comments
 (0)