/* eslint-disable class-methods-use-this */ import { html, LitElement, TemplateResult, nothing } from 'lit'; import { property, state } from 'lit/decorators.js'; import { computePosition, Strategy, Placement, autoUpdate, autoPlacement, AutoPlacementOptions, offset, OffsetOptions, } from '@floating-ui/dom'; import './templates/ix-simple-tooltip.js'; export class IxTooltip extends LitElement { static _placement: Placement | undefined = undefined; static _strategy: Strategy = 'fixed'; static _tagName: string = 'ix-tooltip'; static _offsetOptions: OffsetOptions = { mainAxis: 10 }; static _hidingDuration: number = 500; // milliseconds static _showTooltip: string = 'show-tooltip'; static _hideTooltip: string = 'hide-tooltip'; // eslint-disable-next-line lit/no-native-attributes @property({ type: String, reflect: true }) id: string = 'tooltip'; // eslint-disable-next-line lit/no-native-attributes @property({ type: String, reflect: true }) role: string = 'tooltip'; @property({ type: String, reflect: true }) placement: Placement | undefined = IxTooltip._placement; @property({ type: String, reflect: true }) strategy: Strategy = IxTooltip._strategy; @state() tooltip: TemplateResult | undefined = undefined; @state() target: HTMLElement | undefined = undefined; @state() hidding: boolean = false; autoPlacementOptions: AutoPlacementOptions | undefined = undefined; offsetOptions: OffsetOptions = IxTooltip._offsetOptions; hidingDuration: number = IxTooltip._hidingDuration; hideTimeout: ReturnType | undefined = undefined; cleanup: () => void = () => undefined; connectedCallback(): void { super.connectedCallback(); document.addEventListener(IxTooltip._showTooltip, this.show); document.addEventListener(IxTooltip._hideTooltip, this.hide); document.addEventListener('keydown', this.handleEscapeKey); document.addEventListener('focusin', this.handleShow); document.addEventListener('focusout', this.handleHide); document.addEventListener('mouseover', this.handleShow); document.addEventListener('mouseout', this.handleHide); } disconnectedCallback(): void { super.disconnectedCallback(); document.removeEventListener(IxTooltip._showTooltip, this.show); document.removeEventListener(IxTooltip._hideTooltip, this.hide); document.removeEventListener('keydown', this.handleEscapeKey); document.removeEventListener('focusin', this.handleShow); document.removeEventListener('focusout', this.handleHide); document.removeEventListener('mouseover', this.handleShow); document.removeEventListener('mouseout', this.handleHide); this.clean(); } handleEscapeKey = (e: KeyboardEvent) => { if (this.tooltip && e.key === 'Escape') { this.clean(); } }; handleShow = (e: Event) => { const target = e.composedPath()[0]; // I hate the shadowDOM const eventTarget = e.target; if ( eventTarget && eventTarget.tagName === IxTooltip._tagName.toUpperCase() ) { clearTimeout(this.hideTimeout); } if (target && target.hasAttribute('data-tooltip')) { this.clearHideTimeout(); document.dispatchEvent( new CustomEvent('show-tooltip', { detail: { target: e.target, tooltip: html`${target.getAttribute('data-tooltip')}`, placement: target.getAttribute('data-tooltip-placement'), strategy: target.getAttribute('data-tooltip-strategy'), }, }), ); } }; clearHideTimeout() { if (this.hideTimeout) { this.hidding = false; clearTimeout(this.hideTimeout); } } handleHide = (e: Event) => { if ( this.tooltip && ((e.type === 'focusout' && this.shadowRoot?.activeElement !== null) || e.type === 'mouseout') ) { this.hide(); } }; hide = () => { this.clearHideTimeout(); this.hidding = true; this.hideTimeout = setTimeout(() => { this.clean(); }, this.hidingDuration); }; show = (e: any) => { const { placement, strategy, target, tooltip, autoPlacementOptions, offsetOptions, hidingDuration, } = e.detail; this.clearHideTimeout(); this.target = target; this.placement = placement || IxTooltip._placement; this.strategy = strategy || IxTooltip._strategy; this.tooltip = tooltip; this.autoPlacementOptions = autoPlacementOptions; this.offsetOptions = offsetOptions || IxTooltip._offsetOptions; this.hidingDuration = hidingDuration !== undefined ? hidingDuration : IxTooltip._hidingDuration; this.cleanup = autoUpdate(target, this, this.updatePosition); this.hidding = false; }; clean = () => { this.cleanup(); this.placement = IxTooltip._placement; this.strategy = IxTooltip._strategy; this.offsetOptions = IxTooltip._offsetOptions; this.hidingDuration = IxTooltip._hidingDuration; this.target = undefined; this.autoPlacementOptions = undefined; this.tooltip = undefined; this.clearHideTimeout(); }; getAutoPlacementOptions() { const autoPlacementOptions = { ...this.autoPlacementOptions, }; if (this.placement) { autoPlacementOptions.allowedPlacements = [this.placement]; } return autoPlacementOptions; } updatePosition = () => { if (this.target) { computePosition(this.target, this, { strategy: this.strategy, placement: this.placement, middleware: [ autoPlacement(this.getAutoPlacementOptions()), offset(this.offsetOptions), ], }).then(({ x, y }) => { Object.assign(this.style, { left: `${x}px`, top: `${y}px`, position: this.strategy, }); }); } else { this.clean(); } }; handleMouseEnter = () => { this.clearHideTimeout(); }; handleMouseLeave = () => { this.hide(); }; render() { return html`
${this.tooltip || nothing}
`; } }