UNPKG

chrome-devtools-frontend

Version:
147 lines (124 loc) 4.65 kB
// Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Lit from '../../lit/lit.js'; import tooltipStyles from './tooltip.css.js'; const {html} = Lit; export type TooltipVariant = 'simple'|'rich'; /** * @attr id - Id of the tooltip. Used for searching an anchor element with aria-describedby. * @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. * @prop {String} id - reflects the `"id"` attribute. * @prop {Number} hoverDelay - reflects the `"hover-delay"` attribute. * @prop {String} variant - reflects the `"variant"` 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; get hoverDelay(): number { return this.hasAttribute('hover-delay') ? Number(this.getAttribute('hover-delay')) : 200; } set hoverDelay(delay: number) { this.setAttribute('hover-delay', delay.toString()); } get variant(): TooltipVariant { return this.getAttribute('variant') === 'rich' ? 'rich' : 'simple'; } set variant(variant: TooltipVariant) { this.setAttribute('variant', variant); } attributeChangedCallback(name: string): void { if (name === 'id') { this.#removeEventListeners(); this.#attachToAnchor(); } } connectedCallback(): void { this.#attachToAnchor(); if (!this.hasAttribute('role')) { this.setAttribute('role', 'tooltip'); } this.setAttribute('popover', 'manual'); // clang-format off Lit.render(html` <style>${tooltipStyles.cssContent}</style> <!-- Wrapping it into a container, so that the tooltip doesn't disappear when the mouse moves from the anchor to the tooltip. --> <div class="container"> <slot></slot> </div> `, this.#shadow, {host: this}); // clang-format on } disconnectedCallback(): void { this.#removeEventListeners(); } showTooltip = (): void => { if (this.#timeout) { window.clearTimeout(this.#timeout); } this.#timeout = window.setTimeout(() => { this.showPopover(); }, this.hoverDelay); }; hideTooltip = (event: MouseEvent): void => { if (this.#timeout) { window.clearTimeout(this.#timeout); } // Don't hide a rich tooltip when hovering over the tooltip itself. if (this.variant === 'rich' && event.relatedTarget === this) { return; } this.#timeout = window.setTimeout(() => { this.hidePopover(); }, this.hoverDelay); }; #preventDefault(event: Event): void { event.preventDefault(); } #removeEventListeners(): void { if (this.#anchor) { this.#anchor.removeEventListener('mouseenter', this.showTooltip); this.#anchor.removeEventListener('mouseleave', this.hideTooltip); this.#anchor.removeEventListener('click', this.#preventDefault); this.removeEventListener('mouseleave', this.hideTooltip); } } #attachToAnchor(): void { const id = this.getAttribute('id'); if (!id) { throw new Error('<devtools-tooltip> must have an id.'); } const anchor = (this.getRootNode() as Element).querySelector(`[aria-describedby="${id}"]`); if (!anchor) { throw new Error(`No anchor for tooltip with id ${id} found.`); } if (!(anchor instanceof HTMLElement)) { throw new Error('Anchor must be an HTMLElement.'); } const anchorName = `--${id}-anchor`; anchor.style.anchorName = anchorName; anchor.setAttribute('popovertarget', id); this.style.positionAnchor = anchorName; this.#anchor = 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); this.addEventListener('mouseleave', this.hideTooltip); } } customElements.define('devtools-tooltip', Tooltip); declare global { interface HTMLElementTagNameMap { 'devtools-tooltip': Tooltip; } // Remove this once the CSSStyleDeclaration interface is updated in the TypeScript standard library. interface CSSStyleDeclaration { anchorName: string; positionAnchor: string; } }