UNPKG

@ussebastian/kitdigital

Version:

Kit Digital de la Universidad San Sebastián

292 lines (264 loc) 8.66 kB
import { computePosition, autoPlacement, flip, shift, offset, arrow, autoUpdate, } from '@floating-ui/dom'; export class Tooltip { constructor(element, key) { this.key = key; this.element = element; this.setupInitialState(); } setupInitialState() { this.tooltip = null; this.arrow = null; this.cleanup = () => {}; this.delay = 350; this.placement = 'top'; this.isOpen = false; this.isOverTrigger = false; this.shiftPadding = 0; this.autoPlacement = false; this.startOpen = false; this.validPlacements = [ 'top', 'right', 'bottom', 'left', 'top-start', 'top-end', 'right-start', 'right-end', 'bottom-start', 'bottom-end', 'left-start', 'left-end', ]; } setAriaAttributes() { this.element.setAttribute('aria-describedby', this.key); this.tooltip.setAttribute('id', this.key); this.tooltip.setAttribute('role', 'tooltip'); } mount() { this.handleDataSet(); this.buildTooltipElement(); this.setAriaAttributes(); this.updateTooltipPosition(); this.setEventListeners(); this.hideTooltip(true); } handleDataSet() { const { ussTooltipDelay, ussTooltipPlacement, ussTooltipShiftPadding, ussTooltipAutoPlacement, ussTooltipStartOpen, } = this.element.dataset; const validPlacement = this.validPlacements.includes(ussTooltipPlacement); if (ussTooltipPlacement && !validPlacement) { throw new Error( `El valor de data-uss-tooltip-placement: ${ussTooltipPlacement} no es válido.`, ); } this.delay = ussTooltipDelay ? Number(ussTooltipDelay) : this.delay; this.placement = ussTooltipPlacement || this.placement; this.shiftPadding = Number(ussTooltipShiftPadding) || this.shiftPadding; this.autoPlacement = Boolean(ussTooltipAutoPlacement) || this.autoPlacement; this.startOpen = Boolean(ussTooltipStartOpen) || this.startOpen; } buildTooltipElement() { const contentElementId = this.element.getAttribute('data-uss-tooltip-content-element-id'); const contentText = this.element.getAttribute('data-uss-tooltip-content'); this.validateContent(contentText, contentElementId); this.tooltip = this.findOrCreateTooltip(contentElementId, contentText); this.cannotHaveButtonsOrLinks(); this.buildArrow(); } // eslint-disable-next-line class-methods-use-this validateContent(contentText, contentElementId) { if (!contentText && !contentElementId) throw new Error('El Tooltip debe tener contenido o un elemento asociado.'); } findOrCreateTooltip(contentElementId, contentText) { if (contentElementId) { const tooltipElements = document.querySelectorAll( `[data-uss-tooltip-content-id="${contentElementId}"]`, ); if (tooltipElements.length !== 1) { throw new Error( `Elemento con data-uss-tooltip-content-id: ${contentElementId} ${ tooltipElements.length > 1 ? 'no es único.' : 'no existe.' }`, ); } return tooltipElements[0]; } if (contentText.includes('<') || contentText.includes('>')) { throw new Error("El attributo 'data-uss-tooltip-content' no puede contener HTML."); } const tooltip = document.createElement('div'); tooltip.innerHTML = contentText; this.element.parentNode.insertBefore(tooltip, this.element.nextSibling); tooltip.setAttribute('data-uss-tooltip-content-id', this.key); this.element.removeAttribute('data-uss-tooltip-content'); return tooltip; } cannotHaveButtonsOrLinks() { const buttons = this.tooltip.querySelectorAll('button'); const links = this.tooltip.querySelectorAll('a'); const inputs = this.tooltip.querySelectorAll('input'); const tabindex = this.tooltip.querySelectorAll('[tabindex]'); if (buttons.length || links.length || inputs.length || tabindex.length) { throw new Error( 'El tooltip no puede contener botones, inputs, links ni nada que lleve tabindex. Evaluar otras soluciones', ); } } buildArrow() { this.arrow = document.createElement('div'); this.arrow.setAttribute('data-uss-tooltip-arrow', this.key); this.tooltip.appendChild(this.arrow); } getAntiOverflowStrategy() { return this.autoPlacement ? autoPlacement : flip; } updateTooltipPosition() { computePosition(this.element, this.tooltip, { placement: this.placement, middleware: [ offset(12), this.getAntiOverflowStrategy()({ padding: 10, }), shift({ padding: this.shiftPadding }), arrow({ element: this.arrow }), ], }).then(({ x, y, placement, middlewareData }) => { this.positionTooltip({ x, y }); this.positionArrow(middlewareData.arrow, placement); }); } positionTooltip({ x, y }) { Object.assign(this.tooltip.style, { left: `${x}px`, top: `${y}px` }); } positionArrow({ x: arrowX, y: arrowY }, placement) { const staticSide = this.getStaticSide(placement); Object.assign(this.arrow.style, this.getArrowStyle(arrowX, arrowY, staticSide, placement)); } // eslint-disable-next-line class-methods-use-this getStaticSide(placement) { return { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[placement.split('-')[0]]; } getArrowStyle(arrowX, arrowY, staticSide, placement) { const side = placement.split('-')[0]; const borderStyle = this.getArrowBorderStyle(side); const x = arrowX ? `${arrowX}px` : 'auto'; const y = arrowY ? `${arrowY}px` : 'auto'; return { left: x, top: y, right: '', bottom: '', [staticSide]: '-4px', ...borderStyle, }; } // eslint-disable-next-line class-methods-use-this getArrowBorderStyle(side) { const styles = { top: { borderTop: 'none', borderRight: 'inherit', borderBottom: 'inherit', borderLeft: 'none', }, bottom: { borderTop: 'inherit', borderRight: 'none', borderBottom: 'none', borderLeft: 'inherit', }, left: { borderTop: 'inherit', borderRight: 'inherit', borderBottom: 'none', borderLeft: 'none', }, right: { borderTop: 'none', borderRight: 'none', borderBottom: 'inherit', borderLeft: 'inherit', }, }; return styles[side]; } setEventListeners() { this.element.addEventListener('mouseenter', this.handleMouseEnter.bind(this)); this.element.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); this.element.addEventListener('focus', () => this.showTooltip(true)); this.element.addEventListener('blur', () => this.hideTooltip()); this.element.addEventListener('keydown', this.handleKeydown.bind(this)); } handleMouseEnter() { this.isOverTrigger = true; setTimeout(() => { if (this.isOverTrigger) this.showTooltip(); }, this.delay); } handleMouseLeave() { this.isOverTrigger = false; setTimeout(() => { if (!this.isOverTrigger) this.hideTooltip(); }, this.delay / 2); } handleKeydown(event) { if (['Escape', 'Enter', ' '].includes(event.key)) { this.hideTooltip(); } } showTooltip(isFocus = false) { if (!isFocus) { this.tooltip.addEventListener('mouseenter', () => { this.isOverTrigger = true; }); this.tooltip.addEventListener('mouseleave', () => { this.isOverTrigger = false; setTimeout(() => { if (!this.isOverTrigger) this.hideTooltip(); }, this.delay / 4); }); setTimeout(() => { if (!this.isOverTrigger) this.hideTooltip(); }, this.delay / 4); this.tooltip.addEventListener('mouseenter', () => { this.isOverTrigger = true; }); this.tooltip.addEventListener('mouseleave', () => { this.isOverTrigger = false; }); setTimeout(() => { if (!this.isOverTrigger) this.hideTooltip(); }, this.delay / 4); } document.addEventListener('keydown', this.hideOnEscape.bind(this)); document.body.append(this.tooltip); this.cleanup = autoUpdate(this.element, this.tooltip, () => this.updateTooltipPosition()); this.isOpen = true; } hideTooltip(noCleanup = false) { document.removeEventListener('keydown', this.hideOnEscape.bind(this)); if (this.tooltip) this.tooltip.remove(); this.isOpen = false; if (!noCleanup) this.cleanup(); } hideOnEscape(e) { if (e.key === 'Escape') this.hideTooltip(); } }