63 lines
1.9 KiB
JavaScript
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();
|
|
};
|
|
});
|