Files
marco/app/modifiers/tooltip.js
T
2026-08-27 08:11:02 -06:00

63 lines
1.9 KiB
JavaScript

import { modifier } from 'ember-modifier';
export default modifier((element) => {
let tooltipEl = null;
const portal = document.getElementById('modal-portal') || document.body;
const show = () => {
const text = element.getAttribute('aria-description');
if (!text || tooltipEl) return;
tooltipEl = document.createElement('div');
tooltipEl.className = 'tooltip';
tooltipEl.setAttribute('role', 'tooltip');
tooltipEl.textContent = text;
portal.appendChild(tooltipEl);
const rect = element.getBoundingClientRect();
const tipRect = tooltipEl.getBoundingClientRect();
const arrowSize = 6;
// Vertical placement: above trigger, flip below if no room
let placement = 'top';
let top = rect.top - tipRect.height - arrowSize;
if (top < arrowSize) {
placement = 'bottom';
top = rect.bottom + arrowSize;
}
// Horizontal: center on trigger, clamp to viewport
let left = rect.left + rect.width / 2 - tipRect.width / 2;
left = Math.max(
arrowSize,
Math.min(left, window.innerWidth - tipRect.width - arrowSize)
);
// Arrow always points at trigger center, even when clamped
const arrowLeft = rect.left + rect.width / 2 - left;
tooltipEl.dataset.placement = placement;
tooltipEl.style.top = `${top}px`;
tooltipEl.style.left = `${left}px`;
tooltipEl.style.setProperty('--arrow-left', `${arrowLeft}px`);
};
const hide = () => {
tooltipEl?.remove();
tooltipEl = null;
};
element.addEventListener('mouseenter', show);
element.addEventListener('mouseleave', hide);
element.addEventListener('focus', show);
element.addEventListener('blur', hide);
return () => {
element.removeEventListener('mouseenter', show);
element.removeEventListener('mouseleave', hide);
element.removeEventListener('focus', show);
element.removeEventListener('blur', hide);
hide();
};
});