diff --git a/app/modifiers/tooltip.js b/app/modifiers/tooltip.js index 97a6123..abeca75 100644 --- a/app/modifiers/tooltip.js +++ b/app/modifiers/tooltip.js @@ -16,17 +16,30 @@ export default modifier((element) => { const rect = element.getBoundingClientRect(); const tipRect = tooltipEl.getBoundingClientRect(); - const margin = 6; - let top = rect.top - tipRect.height - margin; - if (top < margin) top = rect.bottom + margin; + 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( - margin, - Math.min(left, window.innerWidth - tipRect.width - margin) + 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 = () => { diff --git a/app/styles/app.css b/app/styles/app.css index ea36721..54c8e6a 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -728,6 +728,26 @@ body { animation: tooltip-fade-in 0.15s ease; } +.tooltip::after { + content: ''; + position: absolute; + width: 0; + height: 0; + border: 6px solid transparent; + left: var(--arrow-left, 50%); + transform: translateX(-50%); +} + +.tooltip[data-placement='top']::after { + top: 100%; + border-top-color: var(--body-text-color); +} + +.tooltip[data-placement='bottom']::after { + bottom: 100%; + border-bottom-color: var(--body-text-color); +} + @keyframes tooltip-fade-in { from { opacity: 0; diff --git a/tests/integration/components/app-menu/settings/nostr-test.gjs b/tests/integration/components/app-menu/settings/nostr-test.gjs index de13829..dbe59b7 100644 --- a/tests/integration/components/app-menu/settings/nostr-test.gjs +++ b/tests/integration/components/app-menu/settings/nostr-test.gjs @@ -293,9 +293,30 @@ module('Integration | Component | app-menu/settings/nostr', function (hooks) { removeBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); await settled(); + const tooltip = document.body.querySelector('.tooltip'); assert.dom('.tooltip', document.body).exists('tooltip appears on hover'); assert.dom('.tooltip', document.body).hasText('Remove relay'); + // Verify arrow is rendered + assert.ok(tooltip.dataset.placement, 'tooltip has placement attribute'); + const arrowStyle = getComputedStyle(tooltip, '::after'); + assert.strictEqual(arrowStyle.content, '""', 'arrow pseudo-element exists'); + + // Verify arrow points in correct direction based on placement + if (tooltip.dataset.placement === 'top') { + assert.notStrictEqual( + arrowStyle.borderTopColor, + 'rgba(0, 0, 0, 0)', + 'arrow points down when tooltip is above trigger' + ); + } else { + assert.notStrictEqual( + arrowStyle.borderBottomColor, + 'rgba(0, 0, 0, 0)', + 'arrow points up when tooltip is below trigger' + ); + } + removeBtn.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); await settled();