Skip to content

Commit

Permalink
Support click to open devtools-tooltip
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
Marcel Pütz authored and Devtools-frontend LUCI CQ committed Feb 21, 2025
1 parent 87bddfd commit b0e0d4c
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 21 deletions.
12 changes: 6 additions & 6 deletions front_end/ui/components/docs/tooltip/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Lit.render(
<devtools-tooltip id="simple-tooltip">Simple content</devtools-tooltip>
</div>
<div style="position: relative; z-index: 0;">
<button aria-details="rich-tooltip" style="position: absolute; left: 16px; top: 116px;">
Rich
</button>
<devtools-tooltip id="rich-tooltip" variant="rich">
<span aria-details="rich-tooltip" style="position: absolute; left: 16px; top: 116px; border: 1px solid black;">
Non-button click trigger
</span>
<devtools-tooltip id="rich-tooltip" variant="rich" use-click>
<p>Rich tooltip</p>
<button>Action</button>
</devtools-tooltip>
Expand All @@ -47,8 +47,8 @@ programmaticTooltip.append('Text content');
anchor.appendChild(programmaticTooltip);

// Make the buttons draggable, so that we can experiment with the position of the tooltip.
container.querySelectorAll('button').forEach(draggable);
function draggable(element: HTMLElement|null) {
container.querySelectorAll('button,span').forEach(anchor => draggable(anchor as HTMLElement));
function draggable(element: HTMLElement|null): void {
if (!element) {
return;
}
Expand Down
37 changes: 32 additions & 5 deletions front_end/ui/components/tooltip/Tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,25 @@ const {
const {html, Directives} = Lit;
const {ref, createRef} = Directives;

function renderTooltip(
{variant = 'simple',
attribute = 'aria-describedby'}: {variant?: TooltipVariant, attribute?: 'aria-describedby'|'aria-details'} = {}) {
interface RenderProps {
variant?: TooltipVariant;
attribute?: 'aria-describedby'|'aria-details';
useClick?: boolean;
}

function renderTooltip({
variant = 'simple',
attribute = 'aria-describedby',
useClick = false,
}: RenderProps = {}) {
const container = document.createElement('div');
// clang-format off
Lit.render(html`
${attribute === 'aria-details' ?
html`<button aria-details="tooltip-id">Button</button>` :
html`<button aria-describedby="tooltip-id">Button</button>`
}
<devtools-tooltip id="tooltip-id" variant=${variant}>
<devtools-tooltip id="tooltip-id" variant=${variant} ?use-click=${useClick}>
${variant === 'rich' ? html`<p>Rich content</p>` : 'Simple content'}
</devtools-tooltip>
`, container);
Expand Down Expand Up @@ -58,7 +66,26 @@ describe('Tooltip', () => {
button?.dispatchEvent(new MouseEvent('mouseenter'));

await checkForPendingActivity();
assert.isFalse(container.querySelector('devtools-tooltip')?.hidden);
assert.isTrue(container.querySelector('devtools-tooltip')?.open);
});

it('should not open on hover if use-click is set', async () => {
const container = renderTooltip({useClick: true});

const button = container.querySelector('button');
button?.dispatchEvent(new MouseEvent('mouseenter'));

await checkForPendingActivity();
assert.isFalse(container.querySelector('devtools-tooltip')?.open);
});

it('should open with click if use-click is set', () => {
const container = renderTooltip({useClick: true});

const button = container.querySelector('button');
button?.click();

assert.isTrue(container.querySelector('devtools-tooltip')?.open);
});

const eventsNotToPropagate = ['click', 'mouseup'];
Expand Down
61 changes: 51 additions & 10 deletions front_end/ui/components/tooltip/Tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,34 @@ export interface TooltipProperties {
* @attr hover-delay - Hover length in ms before the tooltip is shown and hidden.
* @attr variant - Variant of the tooltip, `"simple"` for strings only, inverted background,
* `"rich"` for interactive content, background according to theme's surface.
* @attr use-click - If present, the tooltip will be shown on click instead of on hover.
* @prop {String} id - reflects the `"id"` attribute.
* @prop {Number} hoverDelay - reflects the `"hover-delay"` attribute.
* @prop {String} variant - reflects the `"variant"` attribute.
* @prop {Boolean} useClick - reflects the `"click"` attribute.
*/
export class Tooltip extends HTMLElement {
static readonly observedAttributes = ['id', 'variant'];

readonly #shadow = this.attachShadow({mode: 'open'});
#anchor: HTMLElement|null = null;
#timeout: number|null = null;
#closing = false;

get open(): boolean {
return this.matches(':popover-open');
}

get useClick(): boolean {
return this.hasAttribute('use-click') ?? false;
}
set useClick(useClick: boolean) {
if (useClick) {
this.setAttribute('use-click', '');
} else {
this.removeAttribute('use-click');
}
}

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

toggle = (): void => {
// We need this check because clicking on the anchor while the tooltip is open will trigger both
// the click event on the anchor and the toggle event from the backdrop of the tooltip.
if (!this.#closing) {
this.togglePopover();
}
};

#setAttributes(): void {
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'tooltip');
}
this.setAttribute('popover', 'manual');
}

#preventDefault(event: Event): void {
event.preventDefault();
this.setAttribute('popover', this.useClick ? 'auto' : 'manual');
}

#stopPropagation(event: Event): void {
event.stopPropagation();
}

#setClosing = (event: Event): void => {
if ((event as ToggleEvent).newState === 'closed') {
this.#closing = true;
}
};

#resetClosing = (event: Event): void => {
if ((event as ToggleEvent).newState === 'closed') {
this.#closing = false;
}
};

#registerEventListeners(): void {
if (this.#anchor) {
this.#anchor.addEventListener('mouseenter', this.showTooltip);
this.#anchor.addEventListener('mouseleave', this.hideTooltip);
// By default the anchor with a popovertarget would toggle the popover on click.
this.#anchor.addEventListener('click', this.#preventDefault);
if (this.useClick) {
this.#anchor.addEventListener('click', this.toggle);
} else {
this.#anchor.addEventListener('mouseenter', this.showTooltip);
this.#anchor.addEventListener('mouseleave', this.hideTooltip);
this.addEventListener('mouseleave', this.hideTooltip);
}
}
this.addEventListener('mouseleave', this.hideTooltip);
// Prevent interaction with the parent element.
this.addEventListener('click', this.#stopPropagation);
this.addEventListener('mouseup', this.#stopPropagation);
this.addEventListener('beforetoggle', this.#setClosing);
this.addEventListener('toggle', this.#resetClosing);
}

#removeEventListeners(): void {
if (this.#anchor) {
this.#anchor.removeEventListener('click', this.toggle);
this.#anchor.removeEventListener('mouseenter', this.showTooltip);
this.#anchor.removeEventListener('mouseleave', this.hideTooltip);
}
this.removeEventListener('mouseleave', this.hideTooltip);
this.removeEventListener('click', this.#stopPropagation);
this.removeEventListener('mouseup', this.#stopPropagation);
this.removeEventListener('beforetoggle', this.#setClosing);
this.removeEventListener('toggle', this.#resetClosing);
}

#attachToAnchor(): void {
Expand Down

0 comments on commit b0e0d4c

Please sign in to comment.